深入淺出剖析C語言函數(shù)指針與回調(diào)函數(shù)
微信公眾號:楊源鑫
如果你覺得本文對你有幫助,歡迎留言探討!
一、C語言回調(diào)函數(shù)
什么是回調(diào)函數(shù)?
百度的權(quán)威解釋如下:
回調(diào)函數(shù)就是一個通過函數(shù)指針調(diào)用的函數(shù)。如果你把函數(shù)的指針(地址)作為參數(shù)傳遞給另一個函數(shù),當這個指針被用來調(diào)用其所指向的函數(shù)時,我們就說這是回調(diào)函數(shù)?;卣{(diào)函數(shù)不是由該函數(shù)的實現(xiàn)方直接調(diào)用,而是在特定的事件或條件發(fā)生時由另外的一方調(diào)用的,用于對該事件或條件進行響應(yīng)。
1#include
2void print();
3int main(void)
4{
5 void (*fuc)();
6 fuc = print ;
7 fuc();
8}
9void print()
10{
11 printf("hello world!\n");
12}
運行結(jié)果:
從這個例子可以看到,我們首先定義了一個函數(shù)指針fuc ,這個函數(shù)指針的返回值為void型,然后我們給函數(shù)指針賦值,賦值為print,也就是print函數(shù)的首地址,此時fuc獲得了print的地址,fuc的地址等于print的地址,所以最終調(diào)用fuc();也就相當于調(diào)用了print();那么我寫的這個例子明顯和百度解釋的不符合啊?定義是如果你把函數(shù)的指針(地址)作為參數(shù)傳遞給另一個函數(shù),當這個指針被用來調(diào)用其所指向的函數(shù)時,我們就說這是回調(diào)函數(shù),確實,有所不同,但道理是一樣的,我們接下來再來看一個例子。
1#include
2
3int add_ret() ;
4
5int add(int a , int b , int (*add_value)())
6{
7 return (*add_value)(a,b);
8}
9
10int main(void)
11{
12 int sum = add(3,4,add_ret);
13 printf("sum:%d\n",sum);
14 return 0 ;
15}
16
17int add_ret(int a , int b)
18{
19 return a+b ;
20}
運行結(jié)果:
從這個例子里,我們看到:
這樣子不就符合我們的定義了嘛?我們把函數(shù)的指針(地址),這里也就是add_ret,作為參數(shù)int add(int a , int b , int (add_value)()) , 這里的參數(shù)就是int(add_value)() , 這個名字可以隨便取,但是要符合C語言的命名規(guī)范。當這個指針被用來調(diào)用其所指向的函數(shù)時,我們就說這是回調(diào)函數(shù)。 我們看到add函數(shù)內(nèi)部,return (add_value)(a,b) ; 這個(add_value)(a,b)相當于對指針進行了簡引用,我們在main函數(shù)中,傳入具體要實現(xiàn)功能的函數(shù),add_ret,這個函數(shù)很簡單,就是實現(xiàn)兩數(shù)相加并返回,這里剛剛好,簡引用,相當于取出指針返回地址里的值,這個值就是return a+b,也就是我們傳入a和b兩數(shù)相加的結(jié)果。
那么,回調(diào)函數(shù)究竟有什么作用呢?
說到這里,就有了用戶和開發(fā)者之間的概念,假設(shè),用戶是實現(xiàn)add_ret這個函數(shù),而開發(fā)者是實現(xiàn)add這個函數(shù),現(xiàn)在的需求是,用戶將add_ret這個函數(shù)以參數(shù)的形式傳入開發(fā)者實現(xiàn)的add函數(shù),add函數(shù)就會返回一個數(shù)字給用戶,開發(fā)者沒必要告訴用戶他實現(xiàn)了什么東西,用戶也并不知道開發(fā)者是怎么實現(xiàn)的,用戶只需要傳入自己寫的函數(shù),便可以得到開發(fā)者實現(xiàn)的函數(shù)的返回值,開發(fā)者可以將內(nèi)容封裝起來,將頭文件以及庫文件提供給用戶。
接下來,我們用Linux來演示下這個結(jié)果,我們在目錄下創(chuàng)建三個文件main.c,vendor.c,vendor.h
main.c是用戶開發(fā)的。
vendor.c和vendor.h是開發(fā)者實現(xiàn)的。
在main.c中,代碼如下:
1#include
2#include "vendor.h"
3
4int add_ret(int a , int b)
5{
6 return a + b ;
7}
8
9int main(void)
10{
11 int sum = add(3,4,add_ret);
12 printf("sum:%d\n",sum);
13 return 0 ;
14}
vendor.c,代碼如下:
1#include "vendor.h"
2int add(int a , int b , int (*add_value)())
3{
4 return (*add_value)(a,b);
5}
vendor.h,代碼如下:
1#ifndef __VENDOR_H
2#define __VENDOR_H
3
4int add(int a , int b , int (*add_value)());
5
6#endif
接下來,我們制作一個動態(tài)鏈接庫,最終開發(fā)者把vendor.c的內(nèi)容封起來,把vendor.h提供給用戶使用。
1#include
2#include "vendor.h"
3
4int add_ret(int a , int b)
5{
6 return a + b ;
7}
8
9int main(void)
10{
11 int sum = add(3,4,add_ret);
12 printf("sum:%d\n",sum);
13 return 0 ;
14}
在linux下制作動態(tài)鏈接庫,將vendor.c和vendor.h打包成一個動態(tài)鏈接庫
先明白以下幾個命令是什么意思:
生成動態(tài)庫:
gcc -shared -fPIC dvendor.c -o libvendor.so
參數(shù)含義:
-shared : 生成動態(tài)庫;
-fPIC : 生成與位置無關(guān)代碼;
-o :指定生成的目標文件;
使用動態(tài)庫:
gcc main.c -L . –lvendor -o main
-L : 指定庫的路徑(編譯時); 不指定就使用默認路徑(/usr/lib/lib)
-lvendor : 指定需要動態(tài)鏈接的庫是誰;
代碼運行時需要加載動態(tài)庫:
./main 加載動態(tài)庫 (默認加載路徑:/usr/lib /lib ./ …)
./main
我們將編譯動態(tài)庫生成的libvendor.so拷貝到/usr/lib后,現(xiàn)在就不需要vendor.c了,此時我們將vendor.c移除,也可以正常的編譯并且執(zhí)行main函數(shù)的結(jié)果,這就是回調(diào)函數(shù)的作用之一。
操作流程如下:
二、回調(diào)函數(shù)在Linux內(nèi)核中的應(yīng)用
回調(diào)函數(shù)在Linux內(nèi)核里得到了廣泛的應(yīng)用,接下來,我將引用Linux內(nèi)核中文件操作結(jié)構(gòu)體來詳細的說明。
我們首先來看到這個結(jié)構(gòu)體,這段代碼位于linux內(nèi)核的include/linux/fs.h中,由于代碼眾多,我只截取幾個最基本的例子:
File_operations文件操作結(jié)構(gòu)體:
1struct file_operations {
2 struct module *owner;
3 loff_t (*llseek) (struct file *, loff_t, int);
4 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
5 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
6 ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
7 ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
8 int (*readdir) (struct file *, void *, filldir_t);
9 unsigned int (*poll) (struct file *, struct poll_table_struct *);
10 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
11 long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
12 int (*mmap) (struct file *, struct vm_area_struct *);
13 int (*open) (struct inode *, struct file *);
14 int (*flush) (struct file *, fl_owner_t id);
15 int (*release) (struct inode *, struct file *);
16 int (*fsync) (struct file *, loff_t, loff_t, int datasync);
17 int (*aio_fsync) (struct kiocb *, int datasync);
18 int (*fasync) (int, struct file *, int);
19 int (*lock) (struct file *, int, struct file_lock *);
20 ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
21 unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
22 int (*check_flags)(int);
23 int (*flock) (struct file *, int, struct file_lock *);
24 ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
25 ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
26 int (*setlease)(struct file *, long, struct file_lock **);
27 long (*fallocate)(struct file *file, int mode, loff_t offset,
28 loff_t len);
29};
這段代碼中,利用結(jié)構(gòu)體的封裝思想,將函數(shù)指針封裝在一個file_operations結(jié)構(gòu)體里,然后,在具體實現(xiàn)驅(qū)動的時候,實現(xiàn)具體的函數(shù),再賦值給結(jié)構(gòu)體里的函數(shù)指針做好初始化操作,我們來看看友善之臂的led驅(qū)動就明白了。
以下這段代碼截取友善之臂提供的linux內(nèi)核中的tiny4412_led.c
1static struct file_operations tiny4412_led_dev_fops = {
2 .owner = THIS_MODULE,
3 .unlocked_ioctl = tiny4412_leds_ioctl,
4};
5
6static struct miscdevice tiny4412_led_dev = {
7 .minor = MISC_DYNAMIC_MINOR,
8 .name = DEVICE_NAME,
9 .fops = &tiny4412_led_dev_fops,
10};
首先,先是定義了一個結(jié)構(gòu)體變量,并對結(jié)構(gòu)體變量進行初始化,在這個驅(qū)動中,只實現(xiàn)了ioctl函數(shù),對照著上面的結(jié)構(gòu)體,ulocked_ioctl就是結(jié)構(gòu)體中的這個函數(shù)指針。
long (*unlocked_ioctl) (struct file *,unsigned int, unsigned long);
再來看看友善實現(xiàn)的adc驅(qū)動里,也是這么來做,這里看到 : 也是C語言結(jié)構(gòu)體的一種初始化方式,也是合理的。
1static struct file_operations adc_dev_fops = {
2 owner: THIS_MODULE,
3 open: exynos_adc_open,
4 read: exynos_adc_read,
5 unlocked_ioctl: exynos_adc_ioctl,
6 release: exynos_adc_release,
7};
8static struct miscdevice misc = {
9 .minor = MISC_DYNAMIC_MINOR,
10 .name = "adc",
11 .fops = &adc_dev_fops,
12};
在內(nèi)核中,有很多這樣的函數(shù)指針,所以,當我們了解了這樣的套路以后,再去學(xué)習(xí)linux內(nèi)核,我們的思想就會清晰很多了。
再來看看回調(diào)函數(shù)在linux內(nèi)核里的基本應(yīng)用。
從上節(jié)我們了解到,回調(diào)函數(shù)的本質(zhì)其實也就是函數(shù)指針,只不過定義有所區(qū)別。它的定義就是:你把函數(shù)的指針(地址)作為參數(shù)傳遞給另一個函數(shù),當這個指針被用來調(diào)用其所指向的函數(shù)時,我們就說這是回調(diào)函數(shù)。
接下來我們來看一個例子:
這段代碼摘自友善之臂的button驅(qū)動:
1static int tiny4412_buttons_open(struct inode *inode, struct file *file)
2{
3 int irq;
4 int i;
5 int err = 0;
6
7 for (i = 0; i < ARRAY_SIZE(buttons); i++) {
8 if (!buttons[i].gpio)
9 continue;
10
11 setup_timer(&buttons[i].timer, tiny4412_buttons_timer,
12 (unsigned long)&buttons[i]);
13
14 irq = gpio_to_irq(buttons[i].gpio);
15 err = request_irq(irq, button_interrupt, IRQ_TYPE_EDGE_BOTH,
16 buttons[i].name, (void *)&buttons[i]);
17 if (err)
18 break;
19 }
20
21 if (err) {
22 i--;
23 for (; i >= 0; i--) {
24 if (!buttons[i].gpio)
25 continue;
26
27 irq = gpio_to_irq(buttons[i].gpio);
28 disable_irq(irq);
29 free_irq(irq, (void *)&buttons[i]);
30
31 del_timer_sync(&buttons[i].timer);
32 }
33
34 return -EBUSY;
35 }
36
37 ev_press = 1;
38 return 0;
39}
我們在tiny4412_buttons_open函數(shù)里看到
1err = request_irq(irq, button_interrupt, IRQ_TYPE_EDGE_BOTH,
2 buttons[i].name,(void *)&buttons[i]);
我們來看看request_irq這個函數(shù):
1static inline int __must_check
2request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
3 const char *name, void *dev)
4{
5 return request_threaded_irq(irq, handler, NULL, flags, name, dev);
6}
到這里我們就明白了,第二個參數(shù)是一個用typedef重新定義的一個新類型的函數(shù)指針。
那么也就是說一旦執(zhí)行了tiny4412的open函數(shù),就會通過request_irq去通過回調(diào)函數(shù)去執(zhí)行按鍵中斷,并返回一個中斷句柄。這個回調(diào)函數(shù),其實就是一個中斷服務(wù)函數(shù)。
1static irqreturn_t button_interrupt(int irq, void *dev_id)
2{
3 struct button_desc *bdata = (struct button_desc *)dev_id;
4
5 mod_timer(&bdata->timer, jiffies + msecs_to_jiffies(40));
6
7 return IRQ_HANDLED;
8}
回調(diào)函數(shù)在內(nèi)核中就是這么來使用的,當然,還有其它的,比如我們在tiny4412的open函數(shù)里面還看到:
1 setup_timer(&buttons[i].timer,tiny4412_buttons_timer,
2 (unsignedlong)&buttons[i]);
這個函數(shù)的作用是注冊一個定時器,通過回調(diào)函數(shù)tiny4412_buttons_timer來進行觸發(fā)。
如果你不看它的定義,你可能以為它是一個普通函數(shù),其實它是一個宏封裝的。
1#define setup_timer(timer, fn, data) \
2 do { \
3 static struct lock_class_key __key; \
4 setup_timer_key((timer), #timer, &__key, (fn), (data));\
5 } while (0)
這個宏函數(shù)通過調(diào)用setup_timer_key這個函數(shù)來實現(xiàn)定時器的注冊:
1static inline void setup_timer_key(struct timer_list * timer,
2 const char *name,
3 struct lock_class_key *key,
4 void (*function)(unsigned long),
5 unsigned long data)
6{
7 timer->function = function;
8 timer->data = data;
9 init_timer_key(timer, name, key);
10}
通過這個例子,我們更加了解到回調(diào)函數(shù)在Linux內(nèi)核中的應(yīng)用,為學(xué)習(xí)Linux內(nèi)核,分析linux內(nèi)核源代碼打下了基礎(chǔ)。
三、回調(diào)函數(shù)在Posix應(yīng)用API中的使用
其實,在Posix應(yīng)用編程里,我們也能用到回調(diào)函數(shù),我們來看看多線程編程中,經(jīng)常使用的pthread_create函數(shù):
我們先來看看它的原型:
1int pthread_create((pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg)
參數(shù)說明:
(1) thread:表示線程的標識符
(2) attr:表示線程的屬性設(shè)置
(3) start_routine:表示線程函數(shù)的起始地址
(4) arg:表示傳遞給線程函數(shù)的參數(shù)
函數(shù)的返回值為:
(1) success:返回0
(2) fair:返回-1
看到這個函數(shù)的第三個參數(shù),這不就是一個函數(shù)指針,同時也是一個回調(diào)函數(shù)嘛!這就是函數(shù)指針和回調(diào)函數(shù)在UNIX環(huán)境多線程編程中的應(yīng)用。
我們在windows的dev C++上寫一個測試程序來看看:
1#include
2#include
3
4
5void *function(void *args)
6{
7 while(1)
8 {
9 printf("hello world1!\n");
10 sleep(1);
11 }
12}
13int main(void)
14{
15 pthread_t tid ;
16 tid = pthread_create(&tid , NULL , function , NULL);
17 while(1)
18 {
19 printf("hello world!\n");
20 sleep(1);
21 }
22 return 0 ;
23}
運行結(jié)果:
我們會看到在main函數(shù)里的打印語句和在線程回調(diào)函數(shù)void *function(void *args)里打印語句在同時打印。
關(guān)于這個函數(shù)的如何使用,網(wǎng)上文章有很多講得非常的詳細,這里僅僅只是寫函數(shù)指針和回調(diào)函數(shù)的應(yīng)用,詳細可以參考這篇文章,了解進程和線程。
http://blog.csdn.net/tommy_wxie/article/details/8545253
當然,應(yīng)用里還有其它的API通用運用到了回調(diào)函數(shù),期待大家在實踐中去發(fā)掘。
另外,推薦一下韋東山老師的嵌入式課程:
移動互聯(lián)網(wǎng),人工智能,物聯(lián)網(wǎng)等技術(shù)正在飛速發(fā)展,這都離不開嵌入式技術(shù)的支持,如果您想創(chuàng)業(yè)自己開發(fā)產(chǎn)品,韋東山老師的視頻一定可以幫助您完成創(chuàng)客夢想。
[分享]韋東山嵌入式linux 第1期 ARM裸機實戰(zhàn)
https://j.youzan.com/zRcvW9
[分享]韋東山嵌入式linux第2期 驅(qū)動大全
https://j.youzan.com/o3nvW9
[分享]韋東山嵌入式linux 第3期 項目實戰(zhàn)
https://j.youzan.com/z0cvW9
[分享]韋東山第4期 Android系統(tǒng)視頻
https://j.youzan.com/ifMlW9
[分享]韋東山嵌入式Linux arm 不帶s3c2440開發(fā)板java入門視頻
https://j.youzan.com/1VnvW9
[分享]韋東山嵌入式linux視頻不帶ARM9開發(fā)板c++入門 手把手指導(dǎo)
https://j.youzan.com/NdnvW9
[分享]韋東山嵌入式Linux SPI arm視頻不帶s3c2440開發(fā)板 手把手指導(dǎo)
https://j.youzan.com/xHcvW9
[分享]錄制完畢韋東山嵌入式Linux視頻JZ2440s3c2440開發(fā)板設(shè)備樹詳解
https://j.youzan.com/lmnvW9
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!