C 語言回撥函式詳解

安全劍客發表於2020-04-23
回撥函式就是一個透過函式指標呼叫的函式。如果你把函式的指標(地址)作為引數傳遞給另一個函式,當這個指標被用來呼叫其所指向的函式時,我們就說這是回撥函式。
1. 什麼是回撥函式?

回撥函式,光聽名字就比普通函式要高大上一些,那到底什麼是回撥函式呢?恕我讀得書少,沒有在那本書上看到關於回撥函式的定義。我在百度上搜了一下,發現眾說紛紜,有很大一部分都是使用類似這麼一個場景來說明:A君去B君店裡買東西,恰好缺貨,A君留下號碼給B君,有貨時通知A君。感覺這個讓人更容易想到的是非同步操作,而不是回撥。另外還有兩句英文讓我印象深刻:1) If you call me, I will call you back; 2) Don't call me, I will call you. 看起來好像很有道理,但是仔細一想,普通函式不也可以做到這兩點嗎?所以,我覺得這樣的說法都不是很妥當,因為我覺得這些說法都沒有把回撥函式的特點表達出來,也就是都看不到和普通函式到底有什麼差別。不過,百度百科的解析我覺得還算不錯(雖然經常吐槽百度搜尋...):回撥函式就是一個透過函式指標呼叫的函式。如果你把函式的指標(地址)作為引數傳遞給另一個函式,當這個指標被用來呼叫其所指向的函式時,我們就說這是回撥函式。

下面先說說我的看法。我們可以先在字面上先做個分解,對於"回撥函式",中文其實可以理解為這麼兩種意思:1) 被回撥的函式;2) 回頭執行呼叫動作的函式。那這個回頭呼叫又是什麼鬼?

先來看看來自維基百科的對回撥(Callback)的解析:In computer programming, a callback is any executable code that is passed as an argument to other code, which is expected to call back (execute) the argument at a given time. This execution may be immediate as in a synchronous callback, or it might happen at a later time as in an asynchronous callback. 也就是說,把一段可執行的程式碼像引數傳遞那樣傳給其他程式碼,而這段程式碼會在某個時刻被呼叫執行,這就叫做回撥。如果程式碼立即被執行就稱為同步回撥,如果在之後晚點的某個時間再執行,則稱之為非同步回撥。關於同步和非同步,這裡不作討論,請查閱相關資料。

再來看看來自Stack Overflow某位大神簡潔明瞭的表述:A "callback" is any function that is called by another function which takes the first function as a parameter。 也就是說,函式 F1 呼叫函式 F2 的時候,函式 F1 透過引數給 函式 F2 傳遞了另外一個函式 F3 的指標,在函式 F2 執行的過程中,函式F2 呼叫了函式 F3,這個動作就叫做回撥(Callback),而先被當做指標傳入、後面又被回撥的函式 F3 就是回撥函式。到此應該明白回撥函式的定義了吧?

2. 為什麼要使用回撥函式?

很多朋友可能會想,為什麼不像普通函式呼叫那樣,在回撥的地方直接寫函式的名字呢?這樣不也可以嗎?為什麼非得用回撥函式呢?有這個想法很好,因為在網上看到解析回撥函式的很多例子,其實完全可以用普通函式呼叫來實現的。要回答這個問題,我們先來了解一下回到函式的好處和作用,那就是解耦,對,就是這麼簡單的答案,就是因為這個特點,普通函式代替不了回撥函式。所以,在我眼裡,這才是回撥函式最大的特點。來看看維基百科上面我覺得畫得很好的一張圖片。
C 語言回撥函式詳解C 語言回撥函式詳解

下面以一段不完整的 C 語言程式碼來呈現上圖的意思:

例項

#include#include// 包含Library Function所在讀得Software library庫的標頭檔案
int Callback() // Callback Function
{
    // TODO
    return 0;
}
int main() // Main program
{
    // TODO
    Library(Callback);
    // TODO
    return 0;
}

乍一看,回撥似乎只是函式間的呼叫,和普通函式呼叫沒啥區別,但仔細一看,可以發現兩者之間的一個關鍵的不同:在回撥中,主程式把回撥函式像引數一樣傳入庫函式。這樣一來,只要我們改變傳進庫函式的引數,就可以實現不同的功能,這樣有沒有覺得很靈活?並且絲毫不需要修改庫函式的實現,這就是解耦。再仔細看看,主函式和回撥函式是在同一層的,而庫函式在另外一層,想一想,如果庫函式對我們不可見,我們修改不了庫函式的實現,也就是說不能透過修改庫函式讓庫函式呼叫普通函式那樣實現,那我們就只能透過傳入不同的回撥函式了,這也就是在日常工作中常見的情況。現在再把main()、Library()和Callback()函式套回前面 F1、F2和F3函式里面,是不是就更明白了?

明白了回撥函式的特點,是不是也可以大概知道它應該在什麼情況下使用了?沒錯,你可以在很多地方使用回撥函式來代替普通的函式呼叫,但是在我看來,如果需要降低耦合度的時候,更應該使用回撥函式。

3. 怎麼使用回撥函式?

知道了什麼是回撥函式,瞭解了回撥函式的特點,那麼應該怎麼使用回撥函式?下面來看一段簡單的可以執行的同步回撥函式程式碼。

例項

#includeint Callback_1() // Callback Function 1
{
    printf("Hello, this is Callback_1 ");
    return 0;
}
int Callback_2() // Callback Function 2
{
    printf("Hello, this is Callback_2 ");
    return 0;
}
int Callback_3() // Callback Function 3
{
    printf("Hello, this is Callback_3 ");
    return 0;
}
int Handle(int (*Callback)())
{
    printf("Entering Handle Function. ");
    Callback();
    printf("Leaving Handle Function. ");
}
int main()
{
    printf("Entering Main Function. ");
    Handle(Callback_1);
    Handle(Callback_2);
    Handle(Callback_3);
    printf("Leaving Main Function. ");
    return 0;
}

執行結果:

Entering Main Function.
Entering Handle Function.
Hello, this is Callback_1
Leaving Handle Function.
Entering Handle Function.
Hello, this is Callback_2
Leaving Handle Function.
Entering Handle Function.
Hello, this is Callback_3
Leaving Handle Function.
Leaving Main Function.

可以看到,Handle()函式里面的引數是一個指標,在main()函式里呼叫Handle()函式的時候,給它傳入了函式Callback_1()/Callback_2()/Callback_3()的函式名,這時候的函式名就是對應函式的指標,也就是說,回撥函式其實就是函式指標的一種用法。現在再讀一遍這句話:A "callback" is any function that is called by another function which takes the first function as a parameter,是不是就更明白了呢?

4. 怎麼使用帶引數的回撥函式?

眼尖的朋友可能發現了,前面的例子裡面回撥函式是沒有引數的,那麼我們能不能回撥那些帶引數的函式呢?答案是肯定的。那麼怎麼呼叫呢?我們稍微修改一下上面的例子就可以了:

例項

#includeint Callback_1(int x) // Callback Function 1
{
    printf("Hello, this is Callback_1: x = %d ", x);
    return 0;
}
int Callback_2(int x) // Callback Function 2
{
    printf("Hello, this is Callback_2: x = %d ", x);
    return 0;
}
int Callback_3(int x) // Callback Function 3
{
    printf("Hello, this is Callback_3: x = %d ", x);
    return 0;
}
int Handle(int y, int (*Callback)(int))
{
    printf("Entering Handle Function. ");
    Callback(y);
    printf("Leaving Handle Function. ");
}
int main()
{
    int a = 2;
    int b = 4;
    int c = 6;
    printf("Entering Main Function. ");
    Handle(a, Callback_1);
    Handle(b, Callback_2);
    Handle(c, Callback_3);
    printf("Leaving Main Function. ");
    return 0;
}

執行結果:

Entering Main Function.
Entering Handle Function.
Hello, this is Callback_1: x = 2
Leaving Handle Function.
Entering Handle Function.
Hello, this is Callback_2: x = 4
Leaving Handle Function.
Entering Handle Function.
Hello, this is Callback_3: x = 6
Leaving Handle Function.
Leaving Main Function.

可以看到,並不是直接把int Handle(int (*Callback)()) 改成 int Handle(int (*Callback)(int)) 就可以的,而是透過另外增加一個引數來儲存回撥函式的引數值,像這裡 int Handle(int y, int (*Callback)(int)) 的引數 y。同理,可以使用多個引數的回撥函式。

原文地址:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31559985/viewspace-2687928/,如需轉載,請註明出處,否則將追究法律責任。

相關文章