1. 程式人生 > >函數重載(七)

函數重載(七)

C++ 函數重載

今天我們來看下函數重載,那麽什麽是函數重載呢?我們先來看看自然語言中的上下文,比如:洗字。在不同的場景便會有不同的意義,比如:洗衣服、洗車、洗臉、洗馬桶、洗腦。每個洗字搭配的詞匯的意義都不一樣,這便有了重載(overload)的概念。

重載是指同一個標識符在不同的上下文有不同的意義,重載在自然語言中是隨處可見的,那麽在程序設計中是否也有重載呢?C++ 是一門面向對象的語言,當然要支持函數重載了。C++ 中的函數重載表現為:a> 用同一個函數名定義不同的函數;b> 當函數名和不同的參數搭配時函數的含義不同

下來我們以代碼為例進行分析

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

int func(int a)
{
    return a;
}

int func(int a, int b)
{
    return a + b;
}

int func(const char* s)
{
    return strlen(s);
}

int main()
{
    printf("%d\n", func(1));
    printf("%d\n", func(1, 2));
    printf("%d\n", func("hello"));
    
    return 0;
}

我們看到用同一個函數名定義了 3 個函數,在 C 語言中這樣是不行的。我們看看在 C++ 中是否支持呢?

技術分享圖片

我們看到編譯通過並且也完成函數的功能了。那麽怎樣就能稱之為函數重載呢?它至少得滿足下面的一個條件:a> 參數個數不同;b> 參數類型不同;c> 參數順序不同;那麽下面的函數可以稱之為函數重載嗎?

int func(int a, const char* s)
{
    return a;
}

int func(const char* s, int a)
{
    return strlen(s);
}

它們顯然是函數重載了,因為它們滿足上面的第 3 個條件。當函數默認參數遇上函數重載會發生什麽呢?下來我們再來看一份示例代碼

#include <stdio.h>

int func(int a, int b, int c = 0)
{
    return a * b * c;
}

int func(int a, int b)
{
    return a + b;
}

int main()
{
    printf("%d\n", func(1, 2));
    
    return 0;
}

我們看到定義了兩個 func 函數,第一個的最後一個是默認參數,這樣的函數可以稱之為函數重載嗎?編譯器究竟是否知道它該調用那個函數呢?我們看看編譯結果

技術分享圖片

我們看到編譯報錯了,它說這個函數重載是模糊的,它不知道該調用那個函數。雖然 C++ 相對於 C 添加了很多特性,顯然這個就是一個不好的特性,在後續的高級語言(如 Java、C#等)中都取消這些相互矛盾的特性。所以這樣的函數重載是不可取的,那麽編譯器調用重載函數時有哪些準則呢?A、將所有的同名函數作為候選者;B、嘗試尋找可行的候選函數:a> 精確匹配實參。 b> 通過默認參數能夠匹配實參 。 c> 通過默認類型轉換匹配實參;C、匹配失敗:a> 最終尋找到的候選函數不唯一,則出現二義性,編譯失敗。 b> 無法匹配所有候選者,函數未定義,編譯失敗

在函數重載時應註意的事項:a> 重載函數在本質上是相互獨立的不同函數;b> 重載函數的函數類型不同;c> 函數返回值不嫩作為函數重載的依據;函數重載是由函數名和參數列表決定的!!!

下來我們通過一份示例代碼來看看函數重載的本質

#include <stdio.h>

int add(int a, int b)    // int(int, int)
{
    return a + b;
}

int add(int a, int b, int c)    // int(int, int, int)
{
    return a + b + c;
}

int main()
{
    printf("%p\n", (int(*)(int, int))add);
    printf("%p\n", (int(*)(int, int, int))add);
    
    return 0;
}

我們在之前學過函數名其實就是函數的入口地址,我們通過函數的類型來打印它的地址。我們來看看編譯結果

技術分享圖片

我們看到打印的是不同的地址,也就是說這兩個重載函數是不同的。我們再在 C 語言編譯器中編譯試下,看看結果是什麽

技術分享圖片

我們看到它報錯了,說 add 函數已經定義了。在 C 語言中,只要函數名相同,它便認為這是同一個函數。我們再來看看重載與指針,還是以代碼為例進行分析

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

int func(int a)
{
    return a;
}

int func(int a, int b)
{
    return a + b;
}

int func(const char* s)
{
    return strlen(s);
}

typedef int(*PFUNC)(int a);

int main(int argc, char *argv[])
{
    int c = 0;

    PFUNC p = func;
        
    c = p(1);   
    
    printf("c = %d\n", c);
    
    return 0;
}

我們在第 19 行定義了一個函數指針 PFUNC,那麽我們在第 25 行將它指向 func 函數,這時它會知道自己指向的是哪個函數嗎?我們來看看編譯結果

技術分享圖片

我們看到編譯通過,並完美運行。很明顯它指向的是第一個 func 函數,因為它的類型為 int(int);所以它匹配到了第一個 func 函數。

當函數重載遇上函數指針,將重載函數名賦值給函數指針時:a> 根據重載規則挑選與函數指針參數列表一致的候選者;b> 嚴格匹配候選者的函數類型與函數指針的函數類型。註意:1、函數重載必然發生在同一個作用域中;2、編譯器需要用參數列表或函數類型進行函數選擇;3、無法直接通過函數名得到重載函數的入口地址

在實際的工程中 C++ 和 C 代碼間的相互調用時不可避免的,C++ 編譯器能夠兼容 C 語言的編譯方式。但 C++ 編譯器會優先使用 C++ 編譯的方式,extern 關鍵字能夠強制讓 C++ 編譯器進行 C 方式的編譯。下來我們來看個示例代碼


add.h 源碼

int add(int a, int b);


add.c 源碼

#include "add.h"

int add(int a, int b)
{
    return a + b;
}


test.cpp 源碼

#include <stdio.h>

extern "C"
{
#include "add.h"
}

int main(int argc, char *argv[])
{
    int c = add(1, 2);
    
    printf("c = %d\n", c);
    
    return 0;
}

我們先將 add.c 編譯成 add.o 文件,再編譯 test.cpp 文件,看看結果是否如我們所想的那樣

技術分享圖片

我們看到結果已經實現了。我們如果將 test.cpp 裏面的代碼復制到 test.c 裏面,以 C 的方式編譯,能否通過呢?

技術分享圖片

我們發現它不認識 extern "C",那麽問題來了,我們如何保證一段 C 代碼只會以 C 的方式被編譯呢?__cplusplus 是 C++ 編譯器內置的標準宏定義,我們可以利用這個宏來確保 C 代碼以統一的 C 方式被編譯成目標文件。我們將上面 test.cpp 中的 extern "C" 這一段代碼改成下面這樣,我們再次試試看呢

#ifdef __cplusplus
extern "C" {
#endif

#include "add.h"

#ifdef __cplusplus
}
#endif

這樣便保證了在 C++ 編譯器中以 C 的方式進行編譯,如果是在 C 編譯器中,extern "C" 便不起作用。我們編譯下看看結果

技術分享圖片

我們看到在 C 編譯器下也能正常編譯。在這塊有兩個註意事項:A、C++ 編譯器不能以 C 的方式編譯重載函數;B、編譯方式決定函數名被編譯後的目標名:a> C++ 編譯方式將函數名和參數列表編譯成目標名。 b> C 編譯方式只將函數名作為目標名進行編譯。

通過對函數重載的學習,總結如下:1、函數重載是 C++ 中引入的概念,函數重載用於模擬自然語言中的詞匯搭配;2、函數重載使得 C++ 具有更豐富的語義表達能力,它的本質為相互獨立的不同函數;3、C++ 中通過函數名和函數參數確定函數調用;4、函數重載是 C++ 對 C 的一個重要升級;5、函數重載通過函數參數列表區分不同的同名函數;6、extern 關鍵字能夠實現 C 和 C++ 的相互調用;7、編譯方式決定符號表中的函數名的最終目標名。


歡迎大家一起來學習 C++ 語言,可以加我QQ:243343083

函數重載(七)