深入浅出剖析C语言函数指针与回调函数

简介: 深入浅出剖析C语言函数指针与回调函数

一、C语言回调函数

什么是回调函数?

百度的权威解释如下:

   回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

#include <stdio.h>
void print();
int main(void)
{
void (*fuc)(); 
fuc = print ; 
fuc();  
} 
void print()
{
printf("hello world!\n");
}

运行结果:

640.png

   从这个例子可以看到,我们首先定义了一个函数指针fuc ,这个函数指针的返回值为void型,然后我们给函数指针赋值,赋值为print,也就是print函数的首地址,此时fuc获得了print的地址,fuc的地址等于print的地址,所以最终调用fuc();也就相当于调用了print();那么我写的这个例子明显和百度解释的不符合啊?定义是如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数,确实,有所不同,但道理是一样的,我们接下来再来看一个例子。

#include <stdio.h>
 int add_ret() ;
 int add(int a , int b , int (*add_value)())
 {
     return (*add_value)(a,b);
 }
int main(void)
{
    int sum = add(3,4,add_ret);
    printf("sum:%d\n",sum);
    return 0 ;
} 
int add_ret(int a , int b)
{
   return a+b ;
}

运行结果:

640.jpg

从这个例子里,我们看到:

   这样子不就符合我们的定义了嘛?我们把函数的指针(地址),这里也就是add_ret,作为参数int add(int a , int b , int (add_value)()) , 这里的参数就是int(add_value)() , 这个名字可以随便取,但是要符合C语言的命名规范。当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。       我们看到add函数内部,return (add_value)(a,b) ; 这个(add_value)(a,b)相当于对指针进行了简引用,我们在main函数中,传入具体要实现功能的函数,add_ret,这个函数很简单,就是实现两数相加并返回,这里刚刚好,简引用,相当于取出指针返回地址里的值,这个值就是return a+b,也就是我们传入a和b两数相加的结果。

   那么,回调函数究竟有什么作用呢?

  说到这里,就有了用户和开发者之间的概念,假设,用户是实现add_ret这个函数,而开发者是实现add这个函数,现在的需求是,用户将add_ret这个函数以参数的形式传入开发者实现的add函数,add函数就会返回一个数字给用户,开发者没必要告诉用户他实现了什么东西,用户也并不知道开发者是怎么实现的,用户只需要传入自己写的函数,便可以得到开发者实现的函数的返回值,开发者可以将内容封装起来,将头文件以及库文件提供给用户。

   接下来,我们用Linux来演示下这个结果,我们在目录下创建三个文件main.c,vendor.c,vendor.h

main.c是用户开发的。

vendor.c和vendor.h是开发者实现的。

在main.c中,代码如下:

#include <stdio.h>
 #include "vendor.h"
 int add_ret(int a , int b)
 {
         return a + b ;
 }
 int main(void)
{
    int sum = add(3,4,add_ret);
    printf("sum:%d\n",sum);
    return 0 ;
}

vendor.c,代码如下:

#include "vendor.h"
int add(int a , int b , int (*add_value)())
{
        return (*add_value)(a,b);
}

vendor.h,代码如下:

#ifndef __VENDOR_H
#define __VENDOR_H
int add(int a , int b , int (*add_value)());
#endif

接下来,我们制作一个动态链接库,最终开发者把vendor.c的内容封起来,把vendor.h提供给用户使用。

#include <stdio.h>
#include "vendor.h"
int add_ret(int a , int b)
{
         return a + b ;
}
int main(void)
{
   int sum = add(3,4,add_ret);
   printf("sum:%d\n",sum);
   return 0 ;
}

   在linux下制作动态链接库,将vendor.c和vendor.h打包成一个动态链接库

先明白以下几个命令是什么意思:

生成动态库:

gcc -shared -fPIC dvendor.c -o libvendor.so

参数含义:

-shared : 生成动态库;

-fPIC  : 生成与位置无关代码;

-o               :指定生成的目标文件;

使用动态库:

gcc main.c -L . –lvendor -o main

-L : 指定库的路径(编译时); 不指定就使用默认路径(/usr/lib/lib)

-lvendor : 指定需要动态链接的库是谁;

代码运行时需要加载动态库:

./main 加载动态库 (默认加载路径:/usr/lib /lib ./ …)
./main

   我们将编译动态库生成的libvendor.so拷贝到/usr/lib后,现在就不需要vendor.c了,此时我们将vendor.c移除,也可以正常的编译并且执行main函数的结果,这就是回调函数的作用之一。

操作流程如下:

640.jpg

二、回调函数在Linux内核中的应用

   回调函数在Linux内核里得到了广泛的应用,接下来,我将引用Linux内核中文件操作结构体来详细的说明。

   我们首先来看到这个结构体,这段代码位于linux内核的include/linux/fs.h中,由于代码众多,我只截取几个最基本的例子:

File_operations文件操作结构体:

struct file_operations {
     struct module *owner;
     loff_t (*llseek) (struct file *, loff_t, int);
     ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
     ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
     ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
     ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
     int (*readdir) (struct file *, void *, filldir_t);
     unsigned int (*poll) (struct file *, struct poll_table_struct *);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    int (*open) (struct inode *, struct file *);
    int (*flush) (struct file *, fl_owner_t id);
    int (*release) (struct inode *, struct file *);
    int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    int (*aio_fsync) (struct kiocb *, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **);
    long (*fallocate)(struct file *file, int mode, loff_t offset,
              loff_t len);
};

   这段代码中,利用结构体的封装思想,将函数指针封装在一个file_operations结构体里,然后,在具体实现驱动的时候,实现具体的函数,再赋值给结构体里的函数指针做好初始化操作,我们来看看友善之臂的led驱动就明白了。

以下这段代码截取友善之臂提供的linux内核中的tiny4412_led.c

static struct file_operations tiny4412_led_dev_fops = {
     .owner          = THIS_MODULE,
     .unlocked_ioctl = tiny4412_leds_ioctl,
 };
 static struct miscdevice tiny4412_led_dev = {
     .minor          = MISC_DYNAMIC_MINOR,
     .name           = DEVICE_NAME,
     .fops           = &tiny4412_led_dev_fops,
};

   首先,先是定义了一个结构体变量,并对结构体变量进行初始化,在这个驱动中,只实现了ioctl函数,对照着上面的结构体,ulocked_ioctl就是结构体中的这个函数指针。

long (*unlocked_ioctl) (struct file *,unsigned int, unsigned long);

   再来看看友善实现的adc驱动里,也是这么来做,这里看到 : 也是C语言结构体的一种初始化方式,也是合理的。

static struct file_operations adc_dev_fops = {
     owner:  THIS_MODULE,
     open:   exynos_adc_open,
     read:   exynos_adc_read,    
     unlocked_ioctl: exynos_adc_ioctl,
     release:    exynos_adc_release,
 };
 static struct miscdevice misc = {
     .minor  = MISC_DYNAMIC_MINOR,
     .name   = "adc",
     .fops   = &adc_dev_fops,
};

   在内核中,有很多这样的函数指针,所以,当我们了解了这样的套路以后,再去学习linux内核,我们的思想就会清晰很多了。

再来看看回调函数在linux内核里的基本应用。

从上节我们了解到,回调函数的本质其实也就是函数指针,只不过定义有所区别。它的定义就是:你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。

   接下来我们来看一个例子:

   这段代码摘自友善之臂的button驱动:

static int tiny4412_buttons_open(struct inode *inode, struct file *file)
 {
     int irq;
     int i;
     int err = 0;
     for (i = 0; i < ARRAY_SIZE(buttons); i++) {
         if (!buttons[i].gpio)
             continue;
        setup_timer(&buttons[i].timer, tiny4412_buttons_timer,
                (unsigned long)&buttons[i]);
        irq = gpio_to_irq(buttons[i].gpio);
        err = request_irq(irq, button_interrupt, IRQ_TYPE_EDGE_BOTH, 
                buttons[i].name, (void *)&buttons[i]);
        if (err)
            break;
    }
    if (err) {
        i--;
        for (; i >= 0; i--) {
            if (!buttons[i].gpio)
                continue;
            irq = gpio_to_irq(buttons[i].gpio);
            disable_irq(irq);            
            free_irq(irq, (void *)&buttons[i]);
            del_timer_sync(&buttons[i].timer);
        }
        return -EBUSY;
    }
    ev_press = 1;
    return 0;
}

我们在tiny4412_buttons_open函数里看到

err = request_irq(irq, button_interrupt, IRQ_TYPE_EDGE_BOTH,
                                 buttons[i].name,(void *)&buttons[i]);

我们来看看request_irq这个函数:

static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
        const char *name, void *dev)
{
    return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

   到这里我们就明白了,第二个参数是一个用typedef重新定义的一个新类型的函数指针。

   那么也就是说一旦执行了tiny4412的open函数,就会通过request_irq去通过回调函数去执行按键中断,并返回一个中断句柄。这个回调函数,其实就是一个中断服务函数。

static irqreturn_t button_interrupt(int irq, void *dev_id)
{
    struct button_desc *bdata = (struct button_desc *)dev_id;
    mod_timer(&bdata->timer, jiffies + msecs_to_jiffies(40));
    return IRQ_HANDLED;
}

   回调函数在内核中就是这么来使用的,当然,还有其它的,比如我们在tiny4412的open函数里面还看到:

   setup_timer(&buttons[i].timer,tiny4412_buttons_timer,
             (unsignedlong)&buttons[i]);

   这个函数的作用是注册一个定时器,通过回调函数tiny4412_buttons_timer来进行触发。

如果你不看它的定义,你可能以为它是一个普通函数,其实它是一个宏封装的。

#define setup_timer(timer, fn, data)                    \
    do {                                \
        static struct lock_class_key __key;         \
        setup_timer_key((timer), #timer, &__key, (fn), (data));\
    } while (0)

这个宏函数通过调用setup_timer_key这个函数来实现定时器的注册:

static inline void setup_timer_key(struct timer_list * timer,
                 const char *name,
                 struct lock_class_key *key,
                 void (*function)(unsigned long),
                 unsigned long data)
{
     timer->function = function;
     timer->data = data;
     init_timer_key(timer, name, key);
}

   通过这个例子,我们更加了解到回调函数在Linux内核中的应用,为学习Linux内核,分析linux内核源代码打下了基础。

三、回调函数在Posix应用API中的使用  

   其实,在Posix应用编程里,我们也能用到回调函数,我们来看看多线程编程中,经常使用的pthread_create函数:

我们先来看看它的原型:

int  pthread_create((pthread_t *thread,  pthread_attr_t  *attr,  void  *(*start_routine)(void *),  void  *arg)

参数说明:

(1)    thread:表示线程的标识符

(2)    attr:表示线程的属性设置

(3)    start_routine:表示线程函数的起始地址

(4)    arg:表示传递给线程函数的参数

函数的返回值为:

(1)    success:返回0

(2)    fair:返回-1

   看到这个函数的第三个参数,这不就是一个函数指针,同时也是一个回调函数嘛!这就是函数指针和回调函数在UNIX环境多线程编程中的应用。

我们在windows的dev C++上写一个测试程序来看看:

#include <stdio.h>
 #include <pthread.h>
 void *function(void *args)
 {
     while(1)
     {
         printf("hello world1!\n");
        sleep(1);
    }
}
int main(void)
{
    pthread_t tid ;
    tid = pthread_create(&tid , NULL , function , NULL);
    while(1)
    {
        printf("hello world!\n");
        sleep(1);
    }
    return 0 ;
}

运行结果:

640.png

   我们会看到在main函数里的打印语句和在线程回调函数void *function(void *args)里打印语句在同时打印。

关于这个函数的如何使用,网上文章有很多讲得非常的详细,这里仅仅只是写函数指针和回调函数的应用,详细可以参考这篇文章,了解进程和线程。

http://blog.csdn.net/tommy_wxie/article/details/8545253

   当然,应用里还有其它的API通用运用到了回调函数,期待大家在实践中去发掘。

目录
相关文章
|
29天前
|
C语言
【c语言】指针就该这么学(1)
本文详细介绍了C语言中的指针概念及其基本操作。首先通过生活中的例子解释了指针的概念,即内存地址。接着,文章逐步讲解了指针变量的定义、取地址操作符`&`、解引用操作符`*`、指针变量的大小以及不同类型的指针变量的意义。此外,还介绍了`const`修饰符在指针中的应用,指针的运算(包括指针加减整数、指针相减和指针的大小比较),以及野指针的概念和如何规避野指针。最后,通过具体的代码示例帮助读者更好地理解和掌握指针的使用方法。
48 0
|
28天前
|
C语言
【c语言】指针就该这么学(3)
本文介绍了C语言中的函数指针、typedef关键字及函数指针数组的概念与应用。首先讲解了函数指针的创建与使用,接着通过typedef简化复杂类型定义,最后探讨了函数指针数组及其在转移表中的应用,通过实例展示了如何利用这些特性实现更简洁高效的代码。
17 2
|
28天前
|
C语言
如何避免 C 语言中的野指针问题?
在C语言中,野指针是指向未知内存地址的指针,可能引发程序崩溃或数据损坏。避免野指针的方法包括:初始化指针为NULL、使用完毕后将指针置为NULL、检查指针是否为空以及合理管理动态分配的内存。
|
28天前
|
C语言
C语言:哪些情况下会出现野指针
C语言中,野指针是指指向未知地址的指针,通常由以下情况产生:1) 指针被声明但未初始化;2) 指针指向的内存已被释放或重新分配;3) 指针指向局部变量,而该变量已超出作用域。使用野指针可能导致程序崩溃或不可预测的行为。
|
1月前
|
存储 C语言
C语言32位或64位平台下指针的大小
在32位平台上,C语言中指针的大小通常为4字节;而在64位平台上,指针的大小通常为8字节。这反映了不同平台对内存地址空间的不同处理方式。
|
1月前
|
存储 算法 C语言
C语言:什么是指针数组,它有什么用
指针数组是C语言中一种特殊的数据结构,每个元素都是一个指针。它用于存储多个内存地址,方便对多个变量或数组进行操作,常用于字符串处理、动态内存分配等场景。
|
1月前
|
存储 C语言
C语言指针与指针变量的区别指针
指针是C语言中的重要概念,用于存储内存地址。指针变量是一种特殊的变量,用于存放其他变量的内存地址,通过指针可以间接访问和修改该变量的值。指针与指针变量的主要区别在于:指针是一个泛指的概念,而指针变量是具体的实现形式。
|
1月前
|
C语言
C语言指针(3)
C语言指针(3)
14 1
|
1月前
|
C语言
C语言指针(2)
C语言指针(2)
15 1
|
28天前
|
编译器 C语言
【c语言】指针就该这么学(2)
本文详细介绍了指针与数组的关系,包括指针访问数组、一维数组传参、二级指针、指针数组和数组指针等内容。通过具体代码示例,解释了数组名作为首元素地址的用法,以及如何使用指针数组模拟二维数组和传递二维数组。文章还强调了数组指针与指针数组的区别,并通过调试窗口展示了不同类型指针的差异。最后,总结了指针在数组操作中的重要性和应用场景。
19 0