寫在前面
先說說我的狀态吧,五一假期五天假,這些天都在玩,很少學習,我不是後悔,也沒必要,本來假期就是為了讓自己放松.我唯一要反思看到别人在學,我心裡也想學但是卻做不到,這是我的缺點,後面我會克服的.盡量快點和大家分享知識.今天和大家分享的是C++關于函數重載的關系,我們不僅僅需要學會重載的使用,更要了解C++為什麼支援函數重載.
函數重載
我們可能對函數很是熟悉,但是重載又是什麼意思呢?我們先來用一個具體的場景來分享.
一天,張三的老闆要你寫一個兩位數相加的函數,張三心想這不很簡單嗎?手指一動,結果就出來了,挺簡單的嘛.
int add(int x, int y) { return x + y; }
現在老闆看張三的代碼立馬火了,你是怎麼想的,要是我想12,10.9相加呢?你這個就隻能兩個整型相加,回去修改!!!張三聽到老闆的話不由得反駁道:這怎麼改,總不能再寫一個add1,add2…吧.老闆聽到了張三的嘟囔,生氣道,你沒有學過函數重載嗎?看看下面的代碼,回去好好學習學習,基礎都不紮實.
張三看到代碼,不由大吃一驚,C++還可以這麼寫?好神奇啊,我要好好看看書.
int add(int x, int y) { return x + y; } double add(double x, int y) { return x + y; } double add(int x, double y) { return x + y; }
我們可不希望張三這種事發生在我們身上,先來看看函數重載的定義
函數重載:是函數的一種特殊情況,C++允許在同一作用域中聲明幾個功能類似的同名函數,這些同名函數的
形參清單(參數個數 或 類型 或 順序)必須不同,常用來處理實作功能類似資料類型不同的問題 .
可能大家不喜歡看定義,我這裡給一個總結.
函數重載要滿足下面的要求.
- 函數名相同
- 參數的類型,個數,順序有一個不同就可以了
- 傳回類型不做要求
函數重載的原理
一般情況下,我們知道了函數重載到會應用就可以了,但是對于我們來說需要我們看看他們的原理,為什麼C語言不支援重載,C++支援重載?這些都是問題.
為何C++可以支援重載
我們先用C++的編譯器簡單的看看如何執行程式,下面是我在Linux環境下使用g++來完成的,大家要是不太懂,可以先不管,直接了解C++的原理.
我們先來看看現象,發現C++可以精準的找到需要比對的函數,這是我們所疑惑的.
// test.h
#pragma once
#include <iostream>
#include <stdio.h>
using std::cout;
void func(int a, double b);
void func(double a, int b);
//test.cpp
#include "test.h"
//寫兩個函數 函數形成重載
void func(int a, double b)
{
printf("%d %lf", a, b);
}
void func(double a, int b)
{
printf("%lf %d", a, b);
}
//Mian.cpp
#include "test.h"
int main()
{
func(10, 2.20);
return 0;
}
程式的編譯連結
關于這一點,我們先簡單的說說,之前我們詳細的談過.一個檔案變成一個可執行程式需要經過下面4個步驟.
- 預處理 宏替換 頭檔案展開 注釋替換 main.cpp -> main.i test.cpp -> test.i
- 編譯 檢查文法 ,代碼變換成彙編語言 main.i -> main.s test.i -> test.s
- 彙編 彙編語言變成二進制語言,各個檔案變成目标檔案 main.s -> main.o test.s -> test.o
- 連結 多個目标檔案+連結庫發生連結
這裡我們需要重點談談連結,這是我們今天最重要的一部分
連結就僅僅隻是目标檔案的合并嗎?不是的,它要完成的任務很多,其中最重要的就是找到函數的位址,連結對應上,合并到一起
當我們進行過頭檔案的展開後,Main.cpp中有func函數的聲明和調用.在編譯和彙編的過程中存在一個符号表,這個符号表記錄了函數的定義以及相應的映射.這是很重要的.符号表裡面包含了函數名和函數的位址.
每一個目标檔案(.o)都包含一個符号表和一系列指令,我們看看入和完成函數連結.
現在到mian.o的指令這裡了,前面的一些列指令都正常經行,直到它遇到了func這個點,要是看過C語言的彙編語言的朋友們可能對下面的比較熟悉.
到了func這裡,編譯器開始call (func: ?),編譯器不知道func的位址,但是前面頭檔案的的展開中func函數已經聲明了,是以編譯器知道了func是一個函數.就先給它一個無效的位址.當程式進行連結時,編譯器一看它是一個無效位址,會拿函數名和其他的.o檔案裡面的符号表去碰,碰到了就填上,找不到就會報連接配接錯誤.
C語言為何不支援重載
到這裡就可以明白了,當我們拿函數名去碰的時候,符号表裡面存在多個相同的函數名,編譯器就不會識别該用哪個.更何況存在相同函數名的.c檔案有時都不可能編譯過.
gcc對函數名都不會做任何處理,這也是C語言不支援函數重載的原因.
C++為何可以支援函數重載
到這裡我們就可以得到了結果,既然在連結的時候無效的函數會拿函數名去其他的符号表裡面去碰,那麼隻要我們看看重載的函數名像不像同就可以了,大家可能會有些疑惑,重載的函數名不是相同的嗎?是的,但是C++編譯器會做一定的處理.這裡每個編譯器都有自己的函數名修飾規則 這就是C++ 支援重載的原理.
這就是C++可以支援重載的原因,g++的函數修飾後變成【_Z+函數名長度+函數名+類型首字母1+類型首字母2…】,也是我們隻對參數清單做了要求,對傳回值不做要求的原因.
C++和C語言互相調用
我們都知道C++支援C語言的大部分文法,C++和C語言可以互相調用嗎?實際上是可以的,在一個大型程式中,有的部門可能使用的是C寫的的函數,有的部門可能用的C++,要是他們不能互相使用那就打臉了.
建立靜态庫
我們可以把自己寫的代碼編譯成一個靜态庫或者動态庫,這裡我以靜态庫舉例,看看如何在VS中中建立一個靜态庫.
C++調用C
我們已經有了一個C語言的靜态庫,現在有一個C++的項目需要使用這個靜态庫,我們該如何使用呢?需要分為下面幾個步驟
下面這兩張圖檔都是修改環境的設定,我使用的是VS2013,其他的大概應該差不多,大家依次來修改就可以了.
到這裡我們就可以調用C語言的靜态庫了,讓我們來看看結果吧.
#include "../../Heap/Heap/heap.h" //相對路徑
int main()
{
MyHeap myHeap;
InitMyHeap(&myHeap);
HeapPush(&myHeap, 1);
HeapPush(&myHeap, 2);
HeapPush(&myHeap, 3);
Display(&myHeap);
return 0;
}
這為什麼報錯?我們不是已經設定好了靜态庫了嗎?實際上這種錯誤是很容易分析出來的,當C++去調用C語言的函數時,C++會自動修改函數名,當時C語言不會啊,是以他們就不會碰到一起,連結就會出錯.
extern “C”
既然編譯器不能自動識别C語言的函數名,我們告訴編譯器一下不就可以了嗎.extern “C” 就是這種作用.
有時候在C++工程中可能需要将某些函數按照 C 的風格來編譯,在函數前加 extern “C” ,意思是告訴編譯器,
将該函數按照 C 語言規則來編譯。比如:tcmalloc是google用C++實作的一個項目,他提供tcmallc()和tcfree
兩個接口來使用,但如果是C項目就沒辦法使用,那麼他就使用extern “C”來解決
extern "C" // 告知這是C語言的函數聲明
{
#include "../../Heap/Heap/heap.h"
}
int main()
{
MyHeap myHeap;
InitMyHeap(&myHeap);
HeapPush(&myHeap, 1);
HeapPush(&myHeap, 2);
HeapPush(&myHeap, 3);
Display(&myHeap);
return 0;
}
extern “C” 原理
我們需要來看看extern “C” 的原理,使用了extern “C” 後,在C++在進行編譯的時候函數名字就依據C語言的方法來修改了,不在變成C++ 的規則.extern "C"可以單獨修飾函數,也可以修飾一系列函數,使用代碼塊.
// test.h
#pragma once
#include <iostream>
#include <stdio.h>
extern "C" void func(int a, double b);
//test.cpp
#include "test.h"
//寫兩個函數 函數形成重載
void func(int a, double b)
{
printf("%d %lf", a, b);
}
//Mian.cpp
#include "test.h"
int main()
{
func(10, 2.20);
return 0;
}
C語言調用C++
那麼C語言可以調用C++ 的嗎?可以了,不過也需要一些段來完成.如何讓C語言去識别C++的規則呢?這是我們需要考慮的.
我們已經把庫改成的了C++的靜态庫了.
#include "../../Heap/Heap/heap.h"
int main()
{
MyHeap myHeap;
InitMyHeap(&myHeap);
HeapPush(&myHeap, 1);
HeapPush(&myHeap, 2);
HeapPush(&myHeap, 3);
Display(&myHeap);
return 0;
}
我們無法讓C語言的編譯器去識别C++ 的函數的命名,那麼我們是不是可以在函數一編譯的時候就完成函數名依照C語言來說.這就很簡單了.
但是即使是這樣,C語言仍舊會報錯,原因在于在頭檔案展開的時候,C語言根本不識别extern “C”,是以我們就需要條件編譯了.
使用條件編譯來修改的靜态庫的方法如下,需要再次編譯.
//方法一
#ifdef __cplusplus // C++獨有的
#define EXTERNC extern "C"
#else
#define EXTERNC
#endif
EXTERNC extern void InitMyHeap(MyHeap * pHeap);
EXTERNC extern void HeapPush(MyHeap* pHeap, HPDataType x);
EXTERNC extern bool IsFull(MyHeap* pHeap);
EXTERNC extern bool IsEmpty(MyHeap* pHeap);
EXTERNC extern int HeapSize(MyHeap* pHeap);
EXTERNC extern void adjustDown(MyHeap* pHeap);
EXTERNC extern void adjustUp(MyHeap* pHeap);
EXTERNC extern void Display(MyHeap* pHeap);
EXTERNC extern HPDataType HeapTop(MyHeap* pHeap);
EXTERNC extern void HeapPop(MyHeap* pHeap);
//方法 二
#ifdef __cplusplus
extern "C"
{
#endif
extern void InitMyHeap(MyHeap * pHeap);
extern void HeapPush(MyHeap* pHeap, HPDataType x);
extern bool IsFull(MyHeap* pHeap);
extern bool IsEmpty(MyHeap* pHeap);
extern int HeapSize(MyHeap* pHeap);
extern void adjustDown(MyHeap* pHeap);
extern void adjustUp(MyHeap* pHeap);
extern void Display(MyHeap* pHeap);
extern HPDataType HeapTop(MyHeap* pHeap);
extern void HeapPop(MyHeap* pHeap);
#ifdef __cplusplus
}
#endif
這樣就解決了.
注意,這裡有一點需要注意的,當我們C語言調用C++靜态庫的時候,最起碼我們實際需要的的那部分代碼在extern "C"修飾的函數中不能發生重載.
C++ 注意事項
這個注意事項主要是依據extern "C"來談的,有些比較偏僻的内容需要關注下.
## extern "C"修飾的函數和一個函數完全一樣
在extern "C"修飾的函數子產品外面存在了一個完全一摸一樣的的函數,這個編譯器不會給通過的.
#ifdef __cplusplus
extern "C"
{
#endif
void func(int a, int b)
{
printf("C : %d %d\n", a, b);
}
#ifdef __cplusplus
}
#endif
//完全一樣
void func(int a, int b)
{
printf("C : %d %d\n", a, b);
}
extern "C"修飾的函數和一個函數構成重載
在extern "C"修飾的函數子產品外面一個函數構成重載這種編譯器可以通過的,但是extern "C"修飾的命名方法仍舊還是按照C語言的方式,構成重載的是C++的方式.
#include <iostream>
using namespace std;
#ifdef __cplusplus
extern "C"
{
#endif
void func(int a, int b)
{
printf("C : %d %d\n", a, b);
}
#ifdef __cplusplus
}
#endif
void func(double a, int b)
{
printf("C++: %lf %d\n", a, b);
}
int main()
{
func(1, 2);
func(1.11, 2);
return 0;
}