函數指針,回調函數的定義和使用

 

一、函數指針

定義:函數指針是指向函數的指針變量,即本質是一個指針變量。

int (*f) (int x); /* 聲明一個函數指針 */

f=func; /* 將func函數的首地址賦給指針f */

指向函數的指針包含了函數的地址,可以通過它來調用函數。聲明格式如下:  類型說明符 (*函數名)(參數)
其實這裏不能稱爲函數名,應該叫做指針的變量名。這個特殊的指針指向一個返回整型值的函數。指針的聲明筆削和它指向函數的聲明保持一致。
指針名和指針運算符外面的括號改變了默認的運算符優先級。如果沒有圓括號,就變成了一個返回整型指針的函數的原型聲明。
例如:
  void(*fptr)();
應用:把函數的地址賦值給函數指針,可以採用下面兩種形式:
  fptr=&Function;
  fptr=Function;
取地址運算符&不是必需的,因爲單單一個函數標識符就標號表示了它的地址,如果是函數調用,還必須包含一個圓括號括起來的參數表。
可以採用如下兩種方式來通過指針調用函數:
  x=(*fptr)();
  x=fptr();

下面舉個函數指針的使用例子:

#include<stdio.h>

int f(int x,int y)
{
    int z;
    z=(x>y)?x:y;
    return z;
}

int main()
{
    int f();
    int i,a,b;
    int (*p)();//定義函數指針
    scanf("%d",&a);
    p=f;//給函數指針p賦值,使它指向函數
    for(i=1;i<9;i++)
    {
        scanf("%d",&b);
        a=(*p)(a ,b);//通過指針p調用函數f
    }
    printf("The max number is:%d\n",a);
    system("pause");
    return 0;
}

 運行結果如下:

wKioL1csqHTiraNEAAAd0omZ5bM720.png

 

二、回調函數

1、什麼是回調函數?
回調函數就是一個通過函數指針調用的函數。如果你把函數的指針(地址)作爲參數傳遞給另一個函數,當這個指針被用爲調用它所指向的函數時,我們就說這是回調函數。

2、爲什麼要使用回調函數?
因爲可以把調用者與被調用者分開。調用者不關心誰是被調用者,所有它需知道的,只是存在一個具有某種特定原型、某些限制條件(如返回值爲int)的被調用函數。
如果想知道回調函數在實際中有什麼作用,先假設有這樣一種情況,我們要編寫一個庫,它提供了某些排序算法的實現,如冒泡排序、快速排序、shell排序、shake排序等等,但爲使庫更加通用,不想在函數中嵌入排序邏輯,而讓使用者來實現相應的邏輯;或者,想讓庫可用於多種數據類型(int、float、string),此時,該怎麼辦呢?可以使用函數指針,並進行回調。
回調可用於通知機制,例如,有時要在程序中設置一個計時器,每到一定時間,程序會得到相應的通知,但通知機制的實現者對我們的程序一無所知。而此時,就需有一個特定原型的函數指針,用這個指針來進行回調,來通知我們的程序事件已經發生。實際上,SetTimer() API使用了一個回調函數來通知計時器,而且,萬一沒有提供回調函數,它還會把一個消息發往程序的消息隊列。
另一個使用回調機制的API函數是EnumWindow(),它枚舉屏幕上所有的頂層窗口,爲每個窗口調用一個程序提供的函數,並傳遞窗口的處理程序。如果被調用者返回一個值,就繼續進行迭代,否則,退出。EnumWindow()並不關心被調用者在何處,也不關心被調用者用它傳遞的處理程序做了什麼,它只關心返回值,因爲基於返回值,它將繼續執行或退出。
不管怎麼說,回調函數是繼續自C語言的,因而,在C++中,應只在與C代碼建立接口,或與已有的回調接口打交道時,才使用回調函數。除了上述情況,在C++中應使用虛擬方法或函數符(functor),而不是回調函數。

3、C語言回調函數的實現(函數指針的使用)

 程序員常常需要實現回調。下面將討論函數指針的基本原則並說明如何使用函數指針實現回調。注意這裏針對的是普通的函數,不包括完全依賴於不同語法和語義規則的類成員函數。

聲明函數指針

回調函數是一個程序員不能顯式調用的函數;通過將回調函數的地址傳給調用者從而實現調用。要實現回調,必須首先定義函數指針。儘管定義的語法有點不可思議,但如果你熟悉函數聲明的一般方法,便會發現函數指針的聲明與函數聲明非常類似。請看下面的例子:

void f();//函數原型

上面的語句聲明瞭一個函數,沒有輸入參數並返回void。那麼函數指針的聲明方法如下:

void (*) ();

分析一下,左邊圓括弧中的星號是函數指針聲明的關鍵。另外兩個元素是函數的返回類型(void)和由邊圓括弧中的入口參數。注意本例中還沒有創建指針變量-只是聲明瞭變量類型。目前可以用這個變量類型來創建類型定義名及用sizeof表達式獲得函數指針的大小:

// 獲得函數指針的大小
unsigned psize = sizeof (void (*) ());

// 爲函數指針聲明類型定義
typedef void (*pf) ();

pf是一個函數指針,它指向的函數沒有輸入參數,返回類行爲void。使用這個類型定義名可以隱藏複雜的函數指針語法。

指針變量應該有一個變量名:

void (*p) (); //p是指向某函數的指針

p是指向某函數的指針,該函數無輸入參數,返回值的類型爲void。左邊圓括弧裏星號後的就是指針變量名。有了指針變量便可以賦值,值的內容是署名匹配的函數名和返回類型。例如:

void func()
{

}
p = func;

p的賦值可以不同,但一定要是函數的地址,並且署名和返回類型相同。

傳遞迴調函數的地址給調用者

現在可以將p傳遞給另一個函數(調用者)- caller(),它將調用p指向的函數,而此函數名是未知的:

voidcaller(void(*ptr)())
{
ptr();
}
void func();
int main()
{
p = func;
caller(p);
}

如果賦了不同的值給p(不同函數地址),那麼調用者將調用不同地址的函數。賦值可以發生在運行時,這樣使你能實現動態綁定。

調用規範

到目前爲止,我們只討論了函數指針及回調而沒有去注意ANSI C/C++的編譯器規範。許多編譯器有幾種調用規範。如在Visual C++中,可以在函數類型前加_cdecl,_stdcall或者_pascal來表示其調用規範(默認爲_cdecl)。C++ Builder也支持_fastcall調用規範。調用規範影響編譯器產生的給定函數名,參數傳遞的順序(從右到左或從左到右),堆棧清理責任(調用者或者被調用者)以及參數傳遞機制(堆棧,CPU寄存器等)。

將調用規範看成是函數類型的一部分是很重要的;不能用不兼容的調用規範將地址賦值給函數指針。例如:

// 被調用函數是以int爲參數,以int爲返回值
__stdcall int callee(int);

// 調用函數以函數指針爲參數
void caller( __cdecl int(*ptr)(int));

// 在p中企圖存儲被調用函數地址的非法操作
__cdecl int(*p)(int) = callee; // 出錯


指針p和callee()的類型不兼容,因爲它們有不同的調用規範。因此不能將被調用者的地址賦值給指針p,儘管兩者有相同的返回值和參數列。

4、函數指針&回調函數&linux中的signal函數

函數指針在linux中的應用signal函數

在Unix/Linux中signal函數是比較複雜的一個,其定義原型如下:

void (*signal(intsigno,void (*func)(int))) (int)

signal(int signo,void(*func)(int))是signal函數的主體.

需要兩個參數:int型的signo,以及一個指向函數的指針.

void (*func)(int).

這個函數中,最外層的函數體

void (* XXX )(int)表明其返回值是一個指針(函數指針),指向一個函數XXX的指針,XXX所代表的函數需要一個int型的參數,返回void。

正是由於其複雜性,在[Plauger 1992]用typedef來對其進行簡化

typedef voidSigfuc(int);//這裏可以看成一個返回值 .

再對signal函數進行簡化就是這樣的了

Sigfunc*signal(int,Sigfuc *);

在signal.h頭文件中還有以下幾個定義

#define SIG_ERR(void (*)())-1

#define SIG_DFL(void (*)())0

#define SIG_IGN(void (*)())1

alarm(設置信號傳送鬧鐘)
相關函數 signal,sleep

表頭文件 #include

定義函數 unsigned int alarm(unsigned int seconds);

函數說明 alarm()用來設置信號SIGALRM在經過參數seconds指定的秒數後傳送給目前的進程。如果參數seconds 爲0,則之前設置的鬧鐘會被取消,並將剩下的時間返回。返回值返回之前鬧鐘的剩餘秒數,如果之前未設鬧鐘則返回0。
kill(傳送信號給指定的進程)
相關函數 raise,signal

表頭文件 #include
#include

定義函數 int kill(pid_t pid,int sig);

函數說明 kill()可以用來送參數sig指定的信號給參數pid指定的進程。參數pid有幾種情況:
pid>0 將信號傳給進程識別碼爲pid 的進程。
pid=0 將信號傳給和目前進程相同進程組的所有進程
pid=-1 將信號廣播傳送給系統內所有的進程
pid<0 將信號傳給進程組識別碼爲pid絕對值的所有進程
參數sig代表的信號編號可參考附錄D

返回值 執行成功則返回0,如果有錯誤則返回-1。

錯誤代碼 EINVAL 參數sig 不合法
ESRCH 參數pid 所指定的進程或進程組不存在
EPERM 權限不夠無法傳送信號給指定進程


pause
(讓進程暫停直到信號出現)
相關函數 kill,signal,sleep

表頭文件 #include

定義函數 int pause(void);

函數說明 pause()會令目前的進程暫停(進入睡眠狀態),直到被信號(signal)所中斷。

返回值 只返回-1。

錯誤代碼 EINTR 有信號到達中斷了此函數。


sigaction
(查詢或設置信號處理方式)
相關函數 signal,sigprocmask,sigpending,sigsuspend

表頭文件 #include

定義函數 int sigaction(int signum,const struct sigaction *act ,structsigaction *oldact);

函數說明 sigaction()會依參數signum指定的信號編號來設置該信號的處理函數。參數signum可以指定SIGKILL和SIGSTOP以外的所有信號。
如參數結構sigaction定義如下
struct sigaction
{
void (*sa_handler) (int);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer) (void);
}
sa_handler此參數和signal()的參數handler相同,代表新的信號處理函數,其他意義請參考signal()。
sa_mask 用來設置在處理該信號時暫時將sa_mask 指定的信號擱置。
sa_restorer 此參數沒有使用。
sa_flags 用來設置信號處理的其他相關操作,下列的數值可用。
OR 運算(|)組合
A_NOCLDSTOP : 如果參數signum爲SIGCHLD,則當子進程暫停時並不會通知父進程
SA_ONESHOT/SA_RESETHAND:當調用新的信號處理函數前,將此信號處理方式改爲系統預設的方式。
SA_RESTART:被信號中斷的系統調用會自行重啓
SA_NOMASK/SA_NODEFER:在處理此信號未結束前不理會此信號的再次到來。
如果參數oldact不是NULL指針,則原來的信號處理方式會由此結構sigaction 返回。

返回值 執行成功則返回0,如果有錯誤則返回-1。

錯誤代碼 EINVAL 參數signum 不合法,或是企圖攔截SIGKILL/SIGSTOPSIGKILL信號
EFAULT 參數act,oldact指針地址無法存取。
EINTR 此調用被中斷

sigaddset(增加一個信號至信號集)
相關函數 sigemptyset,sigfillset,sigdelset,sigismember

表頭文件 #include

定義函數 int sigaddset(sigset_t *set,int signum);

函數說明 sigaddset()用來將參數signum 代表的信號加入至參數set 信號集裏。

返回值執行成功則返回0,如果有錯誤則返回-1。

錯誤代碼 EFAULT 參數set指針地址無法存取
EINVAL 參數signum非合法的信號編號


sigdelset
(從信號集裏刪除一個信號)
相關函數 sigemptyset,sigfillset,sigaddset,sigismember

表頭文件 #include

定義函數 int sigdelset(sigset_t * set,int signum);

函數說明 sigdelset()用來將參數signum代表的信號從參數set信號集裏刪除。

返回值 執行成功則返回0,如果有錯誤則返回-1。

錯誤代碼 EFAULT 參數set指針地址無法存取
EINVAL 參數signum非合法的信號編號


sigemptyset
(初始化信號集)
相關函數 sigaddset,sigfillset,sigdelset,sigismember

表頭文件 #include

定義函數 int sigemptyset(sigset_t *set);

函數說明 sigemptyset()用來將參數set信號集初始化並清空。

返回值 執行成功則返回0,如果有錯誤則返回-1。

錯誤代碼 EFAULT 參數set指針地址無法存取


sigfillset
(將所有信號加入至信號集)
相關函數 sigempty,sigaddset,sigdelset,sigismember

表頭文件 #include

定義函數 int sigfillset(sigset_t * set);

函數說明 sigfillset()用來將參數set信號集初始化,然後把所有的信號加入到此信號集裏。

返回值 執行成功則返回0,如果有錯誤則返回-1。

附加說明 EFAULT 參數set指針地址無法存取


sigismember
(測試某個信號是否已加入至信號集裏)
相關函數 sigemptyset,sigfillset,sigaddset,sigdelset

表頭文件 #include

定義函數 int sigismember(const sigset_t *set,int signum);

函數說明 sigismember()用來測試參數signum 代表的信號是否已加入至參數set信號集裏。如果信號集裏已有該信號則返回1,否則返回0。

返回值信號集已有該信號則返回1,沒有則返回0。如果有錯誤則返回-1。

錯誤代碼 EFAULT 參數set指針地址無法存取
EINVAL 參數signum 非合法的信號編號


signal
(設置信號處理方式)
相關函數 sigaction,kill,raise

表頭文件 #include

定義函數 void (*signal(int signum,void(* handler)(int)))(int);

函數說明 signal()會依參數signum 指定的信號編號來設置該信號的處理函數。當指定的信號到達時就會跳轉到參數handler指定的函數執行。如果參數handler不是函數指針,則必須是下列兩個常數之一:
SIG_IGN 忽略參數signum指定的信號。
SIG_DFL 將參數signum 指定的信號重設爲核心預設的信號處理方式。
關於信號的編號和說明,請參考附錄D

返回值返回先前的信號處理函數指針,如果有錯誤則返回SIG_ERR(-1)。

附加說明在信號發生跳轉到自定的handler處理函數執行後,系統會自動將此處理函數換回原來系統預設的處理方式,如果要改變此操作請改用sigaction()。


sigpending
(查詢被擱置的信號)
相關函數 signal,sigaction,sigprocmask,sigsuspend

表頭文件 #include

定義函數 int sigpending(sigset_t *set);

函數說明 sigpending()會將被擱置的信號集合由參數set指針返回。

返回值執 行成功則返回0,如果有錯誤則返回-1。

錯誤代碼 EFAULT 參數set指針地址無法存取
EINTR 此調用被中斷。


sigprocmask
(查詢或設置信號遮罩)
相關函數 signal,sigaction,sigpending,sigsuspend

表頭文件 #include

定義函數 int sigprocmask(int how,const sigset_t *set,sigset_t * oldset);

函數說明 sigprocmask()可以用來改變目前的信號遮罩,其操作依參數how來決定
SIG_BLOCK 新的信號遮罩由目前的信號遮罩和參數set 指定的信號遮罩作聯集
SIG_UNBLOCK 將目前的信號遮罩刪除掉參數set指定的信號遮罩
SIG_SETMASK 將目前的信號遮罩設成參數set指定的信號遮罩。
如果參數oldset不是NULL指針,那麼目前的信號遮罩會由此指針返回。

返回值 執行成功則返回0,如果有錯誤則返回-1。

錯誤代碼 EFAULT 參數set,oldset指針地址無法存取。
EINTR 此調用被中斷


sleep
(讓進程暫停執行一段時間)
相關函數 signal,alarm

表頭文件 #include

定義函數 unsigned int sleep(unsigned int seconds);

函數說明 sleep()會令目前的進程暫停,直到達到參數seconds 所指定的時間,或是被信號所中斷。

返回值 若進程暫停到參數seconds 所指定的時間則返回0,若有信號中斷則返回剩餘秒數。

 


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章