C语言深处

    返回首页    发表留言
本文作者:李德强
          第一节 回调函数
 
 

一、回调函数

        在上一章结束时我们学习了如何使用一个通用的指针作为函数的参数,从而实现一些更“通用”的功能函数。在数组复制的例子中我们只是实现了一个可以复制数组的函数array_copy函数,而显示数组中的内容还是实现了3遍,也就是说为了显示array_1、array_3、array_5这3个数组我们写了3遍循环数组的功能。这样的代码并不是我们提倡的,我们希望能够实现一些更通用的函数,于是我们接下来要利用通用指针来实现一个可以显示“任意类型”数组的函数。实现的过程很简单,但是需要用到一个很有意思的机制——函数指针。

        首先定义3个函数,用于显示不同数据类型的值,这里利用了通用指针做参数来分别显示char、int、double类型的数据:

void print_char(void *val);
void print_int(void *val);
void print_double(void *val);

        再来实现一个用于显示数组的功能函数叫array_show:

void array_show(void *array, int element_size, int array_size, void (*p_print_value)(void *));

        这个函数的最后一个参数是本节的知识的关键,它是一个函数指针,这个指针指向一个功能函数,这个函数的返回值为void,参数为void *。我们可以通过指针参数p_print_value来调用一个用于“显示数值”的函数,那么这个被调用的函数究竟是什么函数呢?这个其实在array_show函数内部时我们并不关心,总之这是一个可以“显示值”的函数,我们直接调用就可以了,至于在main函数中调用array_show时给它传入哪个函数的地址它就是哪个函数。最后来看一下完整的的实现:

#include <stdio.h>

#define SIZE 5

void print_char(void *val)
{
        printf("%c ", *(char *) val);
}

void print_int(void *val)
{
        printf("%d ", *(int *) val);
}

void print_double(void *val)
{
        printf("%f ", *(double *) val);
}

void array_copy(void *src, void *tar, int element_size, int array_size)
{
        for (int i = 0; i < element_size * array_size; i++)
        {
                *(char *) tar = *(char *) src;
                tar++;
                src++;
        }
}

void array_show(void *array, int element_size, int array_size, void (*p_print_value)(void *))
{
        for (int i = 0; i < array_size; i++)
        {
                p_print_value(array);
                array += element_size;
        }
        printf("\n");
}

int main(int argc, char **args)
{
        char array_0[SIZE] =
        { 'H', 'e', 'l', 'l', 'o' };
        char array_1[SIZE];

        int array_2[SIZE] =
        { 1, 2, 3, 4, 5 };
        int array_3[SIZE];

        double array_4[SIZE] =
        { 12.3, 23.4, 34.5, 45.6, 56.7 };
        double array_5[SIZE];

        array_copy(array_0, array_1, sizeof(char), SIZE);
        array_copy(array_2, array_3, sizeof(int), SIZE);
        array_copy(array_4, array_5, sizeof(double), SIZE);

        array_show(array_1, sizeof(char), SIZE, &print_char);
        array_show(array_3, sizeof(int), SIZE, &print_int);
        array_show(array_5, sizeof(double), SIZE, &print_double);
}

       在main函数调用array_show时,3次调用传入了3个不用的参数:&printf_char、&print_int和&print_double。于是在array_show内部通用函数指针调用时就执行了3个不同的函数。运行结果如下:

H e l l o 
1 2 3 4 5 
12.300000 23.400000 34.500000 45.600000 56.700000

 

二、多态

        要知道C语言是一个很老的高级编程语言,它是面向过程的,在C语言中并没有所谓的象回调函数和面向对象的说法,但通过指针C语言能够非常灵活的完成回调函数和面向对象的功能。多态是指一个对象在不同情况下所表现出来的多种形态。在C++和Java中分别有不同的机制来实现对象的多态,当然在底层实现上原理都是相同的。

        先来看一下在C++中定一的一个类:

class Animal
{
        public:
                char name[20];
                void say_name();
};

void Animal::say_name()
{
        cout << "My name is " << this->name << endl;
}
int main(int argc, char **args)
{
    Animal a;
    a.say_name();

    return 0;
}

        当这段C++代码被编译成可执行的程序之后,say_name()这个函数其实被编译编译为另外一个全局函数,并且这个函数的名字被修改成_ZN6Animal8say_nameEv。这是为了让C++可以实现方法重载的功能。现在我们不去关心它的名字,而是要关心它的参数。这函数的原型为void say_name();但是在编译后变为void _ZN6Animal8say_nameEv(void *p);来看一下编译后的伪代码:

class Animal
{
        public:
                char name[20];
};

void _ZN6Animal8say_nameEv(void *p)
{
        cout << "Say name " << ((Animal *)p)->name << endl;
}

int main(int argc, char **args)
{
        Animal a;
        say_name(&a);

        return 0;
}

        可以看到C++类中的say_name方法已经被修改成了C语言的形式,在类方法中使用this指针的方式被修改成了“对象地址做参数”的方式。其实C++中面向对象部分的本质实现与C语言中的函数没有任何区别,只不过C++的编译器在面向对象方面为我们做了很多工作而已。

        下面我们就利用C语言的回调函数来实现在面向对象语言的“多态”机制,准确的说这并不是什么多态,这是C语言特有的实现方式:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct
{
        char name[20];
        void (*say_name)(void*);
} Animal;

void init_animal(Animal *a, char *name, void *say_name)
{
        memcpy(a->name, name, strlen(name));
        a->say_name = say_name;
}

void cat_say_name(Animal *cat)
{
        printf("I'am a mouse, my name's %s.\n", cat->name);
}

void mouse_say_name(Animal *mouse)
{
        printf("I'am a mouse, my name's %s.\n", mouse->name);
}

int main(int argc, char **args)
{
        Animal *tom = malloc(sizeof(Animal));
        Animal *jerry = malloc(sizeof(Animal));

        init_animal(tom, "Tom", &cat_say_name);
        init_animal(jerry, "Jerry", &mouse_say_name);

        printf("%s\n", jerry->name);
        tom->say_name(tom);
        jerry->say_name(jerry);

        free(tom);
        free(jerry);

        return 0;
}

        运行结果:

I'am a mouse, my name's Tom.
I'am a mouse, my name's Jerry.

        上面例子中的核心部分就是结构体中的函数指针say_name,这个函数指针在不同对象初始化时,被赋值成了不同的函数。于是同样是Animal的指针变量在调用say_name函数时,所产生的结果却是不一样的。

    返回首页    返回顶部
#1楼  逸想天开  于 2017年04月18日22:46:07 发表
 
由于是array是void*类型,因此 array += element_size;这句不能编译通过呀?
#2楼  李德强  于 2017年04月21日06:34:29 发表
 
可以的,编译器在对通用指针进行编译时,认为void*指向的元素大小都是1个字节。
#3楼  李德强  于 2017年10月18日15:52:30 发表
 
一些C语言的编译器不允许对void*的指针变量做算数运算,因为编译器不知道void*型的指针变量所指向的是一个什么类型的变量 ,所以对其做算数运算时,编译器无法确定应该加减多少个字节的地址。

但在C11标准中,为了让所有的指针都能做算数运算,所以规定了void*型指针在++或--时只做一个字节的增减,在其它算数运算时与char*型指针一样处理,这就是为什么指定了了C11标准后的程序允许void*型指针做算数运算。

但二级指针void**在不指定C11标准也是可以直接做算数运算的,因为二级指针void**它是指向了一个一级指针void*的地址,也就是说编译器知道这个二级指针所指向变量的类型,也就可以确定它所指向的地址长度,所以对二级指针++就是增加4个字节(64位系统里是8个字节)。

在编译程序时指定C99或C11标准即可,另外如果不指定C标准也可以这样处理:

在array_copy这个函数开始的地方

char *p_s = (char*)tar;

char *p_t = (char*)src;

在下面的循环里用p_s和p_t就可以了

*p_s = *p_t;

p_s++;

p_t++;
#4楼  小飞侠  于 2020年02月27日17:55:43 发表
 
这个函数指针在java里是要查表的,这里直接传进来了。哈哈
  看不清?点击刷新

 

  Copyright © 2015-2023 问渠网 辽ICP备15013245号