C再学习 05 - 函数指针

C再学习 05 - 函数指针

竟然还有函数指针,上次在看C语言现代方法第二版的时候压根没有留意到有函数指针。这次要好好看看,感觉有了struct,再有函数指针,一个对象的雏形好像就出来了。 这次是用一个常见的高级语言里的filter函数式编程的方法,给filter函数传递一个函数(所谓传递函数,就是传递一段代码用于执行)来进行过

竟然还有函数指针,上次在看C语言现代方法第二版的时候压根没有留意到有函数指针。这次要好好看看,感觉有了struct,再有函数指针,一个对象的雏形好像就出来了。 这次是用一个常见的高级语言里的filter函数式编程的方法,给filter函数传递一个函数(所谓传递函数,就是传递一段代码用于执行)来进行过滤。

普通过滤函数

一个普通的过滤函数用来过滤满足一定条件的数据。比如有一个字符串数组如下:
int NUM_ADS = 7;
char *ADS[] = {
    "William: SBM GSOH likes sports, TV, dining",
    "Matt: SWM NS likes art, movies, theater",
    "Luis: SLM ND likes books, theater, art",
    "Mike: DWM DS likes trucks, sports and bieber",
    "Peter: SAM likes chess, working out and art",
    "Josh: SJM likes sports, movies and theater",
    "Jed: DBM likes theater, books and dining"
};
如果要过滤出其中喜欢运动但不喜欢bieber(这么不受欢迎啊)的人,可以用一个简单的方法来打印出来:
void find(char **strlist, int length){
    int i = 0;
    for (; i < length; i++) {

        if (strstr(strlist[i], "sports") && !strstr(strlist[i], "bieber")) {
            printf("%s\n", strlist[i]);
        }
    }
}
但是这个函数有个很大的缺点,就是写死了过滤方法。如果要换一种过滤方法,就要重新编写一个函数。然而这些函数之间只有过滤的条件不同。 如果可以给这个函数传入一个函数,传入的函数专门用来供这个函数调用,判断是否符合条件,这样每次只要传入不同的函数就可以进行不同的过滤,而不需要写很多过滤函数了。 函数名称就是一种指针,然而不能直接把函数名传进来,必须使用特殊的函数指针语法。

函数指针

函数名其实就是一个指向函数的指针,调用的时候写函数名(参数)就是在调用函数。 但要注意的是,函数名和指针变量还是有所区别,函数名是一个左值,而指针变量的值是右值。函数名称不能使用++,--等运算。 在C语言里也没有函数类型。需要针对函数的参数和返回值,采用特殊的语法声明函数指针。 对于这样一个函数:
int filter_by_sports_bieber(char* string){
    return strstr(string, "sports") && !strstr(string, "bieber");
}
对应的函数指针是:int (*filt) (char *); 红色的部分表示函数的返回值,蓝色的部分就是新声明的指针名称,而橙色的部分就是函数的参数。 在声明之后,就可以把filter_by_sports_bieber函数名赋给filt变量。
filt = filter_by_sports_bieber;
之后使用,就可以像使用函数名称一样,可以试验一下:
    printf("%d\n", filt(ADS[0]));
    printf("%d\n", filt(ADS[1]));
    printf("%d\n", filt(ADS[2]));
    printf("%d\n", filt(ADS[3]));
    printf("%d\n", filt(ADS[4]));
    printf("%d\n", filt(ADS[5]));
    printf("%d\n", filt(ADS[6]));
可以看到,只有索引为0和5的结果返回值是1,说明这两个元素符合条件,和刚才我们的过滤函数的结果类似。 现在就可以来改造过滤函数,传入这个指针了:
void find(char **strlist, int length, int (*match)(char *)){
    int i = 0;
    for (; i < length; i++) {

        if (match(strlist[i])) {
            printf("%s\n", strlist[i]);
        }
    }
}
然后在头文件中声明:
void find(char **strlist, int length, int (*match)(char *));
在main文件中导入头文件,然后来使用这个函数:
int filter_by_sports_bieber(char* string){
    return strstr(string, "sports") && !strstr(string, "bieber");
}

int main(int argc, char *argv[]){

    int (*filt)(char *);

    filt = filter_by_sports_bieber;

    find(ADS, NUM_ADS, filt);
}
可以看到完成了和原来filt的同样结果,如果想修改一下过滤条件,比如过滤出喜欢movies的,只修改过滤函数就可以了,主体函数无需改动。
int filter_by_movies(char * string){
    return strstr(string, "movies") != NULL;
}

int main(int argc, char *argv[]){

    int (*filt)(char *);

    filt = filter_by_movies;

    find(ADS, NUM_ADS, filt);

}
这就非常方便了。 通过函数指针调用的时候,可以在前边加*,也可以不加,就像数组变量一样,编译器知道它们就是指针。用&也可以取得函数的地址,而&function_namefunction_name是同一个东西。 C标准库中有一些排序函数比如qsort就接受函数指针,这个函数指针只要根据两个数比较结果返回大于,等于和小于0的数就可以。 库函数经常接受带有*void指针签名的函数指针,但是在使用前必须要转换成具体类型的指针。 通常就是用int a = * (*int) void_poinnter从指针中取具体的值。

函数指针数组

指针生万物,数组放万物。既然我们知道有指针数组的存在,而函数指针也是指针类型的一种,则很显然也需要有函数指针数组了。 声明方法很简单:int (*functions[])(param),很显然,一个函数指针数组中也必须存放一组签名相同的函数指针。 所以我们就可以用一个函数指针数组来存放不同的过滤函数,然后只要根据需要传一下就可以了。
int filter_by_sports_bieber(char* string){
    return strstr(string, "sports") && !strstr(string, "bieber");
}

int filter_by_movies(char * string){
    return strstr(string, "movies") != NULL;
}

int filter_by_books(char * string){
    return strstr(string, "books") != NULL;
}

int main(int argc, char *argv[]){
    int (*filt[])(char *) = {filter_by_sports_bieber, filter_by_movies, filter_by_books};

    find(ADS, NUM_ADS, filt[0]);
    find(ADS, NUM_ADS, filt[1]);
    find(ADS, NUM_ADS, filt[2]);

}
可以看到只需要传入不同的数组元素也就是函数指针,就相当于传入了不同的函数。 这个数组方式的声明比较方便,直接传入函数名即可。不像单独声明函数指针,还需要用一个指针变量名称接受一下函数名。

可变参函数

C里边的可变参函数,必须使用stdarg.h库。然后有特殊的写法。来编写一个指定打印几个int的函数,第一个参数是固定参数,表示其后的参数数量。
#include "params.h"
#include <stdarg.h>
#include <stdio.h>

void print_ints(int args, ...){

    //必须使用宏声明一个特殊的变量,实际上就是这个函数接受的所有参数组成的数组
    va_list ap;
    //表示可变参数从哪里开始,第一个参数是上边的参数列表,第二个是指最后一个固定参数
    va_start(ap,args);

    int i;
    for (i = 0; i < args; i++) {
        //va_arg用于从ap中取出指定的类型的参数,每一次循环就会自动往后取一次
        printf("arguments: %i\n", va_arg(ap, int));
    }
    //最后要对va_list调用va_end来销毁这个参数列表
    va_end(ap);
}
va_开头的都是样板代码,会用就可以了。

实现原始的对象

现在有了struct,还有了函数指针,想到了什么。貌似可以实现一个简单的对象了。尝试了一下还真的自己写了出来。 假设我们创建一个带有字符串姓名,short 年龄,和一个说出自己名字和年龄的函数。 先来创建结构:
typedef struct object {
    char *name;
    short age;
    struct object *self;
    void(*sayhello)(struct object *);
} Person;
在其中设置了一个指针叫做self,还有一个sayhello名称的函数指针,其中需要传入一个Person指针。 之后先来写出sayhello函数:
void show(struct object * obj){
    printf("My name is %s\n", obj->name);
    printf("My age is %d\n", obj->age);
}
然后是创建这个struct的函数,动态分配地址,然后返回指向这个struct的指针:
struct object * create_obj(char*name, short age){
    void (*showself)(struct object *);
    showself = show;
    struct object * new_object = malloc(sizeof(struct object));
    new_object->name = name;
    new_object->age=age;
    new_object->self = new_object;
    new_object->sayhello = showself;
}
这个创建函数类似构造函数,设置了名字和年龄,将指针指向自己,然后设置了函数指针。 之后就可以来生成对象和调用方法了。高级语言里会说绑定对象的方法默认参数是传入了当前对象。这里我们也显式的传一下:
int main(int argc, char *argv[]){

    Person * new_obj = create_obj("cony",5);

    new_obj->sayhello(new_obj);

}
哇哈哈哈,看起来已经是一个最简单的类了。而且函数指针都指向同一个函数,说明方法也属于类。在调用的时候根据传入的具体对象不同而不同。
LICENSED UNDER CC BY-NC-SA 4.0
Comment