小白入門C++ 繼承 多態 函數重載

概念:繼承和多態

繼承可以把父類的所有功能都直接拿過來,這樣就不必重零做起,子類只需要新增自己特有的方法,也可以把父類不適合的方法覆蓋重寫;
有了繼承,纔能有多態。在調用類實例方法的時候,儘量把變量視作父類類型,這樣,所有子類類型都可以正常被接收;
虛函數是面向對象編程實現多態的基本手段。它是面向對象程序設計中的一個重要的概念。只能適用於指針和參考的計算機工程運算。當從父類中繼承的時候,虛函數和被繼承的函數具有相同的簽名。但是在運行過程中,運行系統將根據對象的類型,自動地選擇適當的具體實現運行。

例如,一個基類 Animal 有一個虛函數 eat。子類 Fish 要實做一個函數 eat(),這個子類 Fish 與子類 Wolf 是完全不同的,但是你可以引用類別 Animal 底下的函數 eat() 定義,而使用子類 Fish 底下函數 eat() 的進程。
using namespace std;
class Animal
{
public:
virtual void eat() const { cout << “I eat like a generic Animal.” << endl; }
virtual ~Animal() {}
};

class Wolf : public Animal
{
public:
void eat() const { cout << “I eat like a wolf!” << endl; }
};

class Fish : public Animal
{
public:
void eat() const { cout << “I eat like a fish!” << endl; }
};

函數重載

函數重載是指在同一作用域內,可以有一組具有相同函數名,不同參數列表的函數,這組函數被稱爲重載函數。重載函數通常用來命名一組功能相似的函數,這樣做減少了函數名的數量,避免了名字空間的污染,對於程序的可讀性有很大的好處。

1、聲明/定義重載函數時,是如何解決命名衝突的?(拋開函數重載不談,using就是一種解決命名衝突的方法,解決命名衝突還有很多其它的方法,這裏就不論述了)
2、當我們調用一個重載的函數時,又是如何去解析的?(即怎麼知道調用的是哪個函數呢).

這兩個問題是任何支持函數重載的語言都必須要解決的問題!帶着這兩個問題,我們開始本文的探討。本文的主要內容如下:

1、例子引入(現象)
什麼是函數重載(what)?
爲什麼需要函數重載(why)?
2、編譯器如何解決命名衝突的?
函數重載爲什麼不考慮返回值類型
3、重載函數的調用匹配
模凌兩可的情況
4、編譯器是如何解析重載函數調用的?
根據函數名確定候選函數集
確定可用函數
確定最佳匹配函數
5、總結

1、例子引入(現象)

1.1、什麼是函數重載(what)?

函數重載是指在同一作用域內,可以有一組具有相同函數名,不同參數列表的函數,這組函數被稱爲重載函數。重載函數通常用來命名一組功能相似的函數,這樣做減少了函數名的數量,避免了名字空間的污染,對於程序的可讀性有很大的好處。

看下面的一個例子,來體會一下:實現一個打印函數,既可以打印int型、也可以打印字符串型。在C++中,我們可以這樣做:

#include<iostream>
using namespace std;

void print(int i)
{
        cout<<"print a integer :"<<i<<endl;
}

void print(string str)
{
        cout<<"print a string :"<<str<<endl;
}

int main()
{
        print(12);
        print("hello world!");
        return 0;

通過上面代碼的實現,可以根據具體的print()的參數去調用print(int)還是print(string)。上面print(12)會去調用print(int),print(“hello world”)會去調用print(string),如下面的結果:(先用g++ test.c編譯,然後執行)
1.2、爲什麼需要函數重載(why)?

試想如果沒有函數重載機制,如在C中,你必須要這樣去做:爲這個print函數取不同的名字,如print_int、print_string。這裏還只是兩個的情況,如果是很多個的話,就需要爲實現同一個功能的函數取很多個名字,如加入打印long型、char*、各種類型的數組等等。這樣做很不友好!
類的構造函數跟類名相同,也就是說:構造函數都同名。如果沒有函數重載機制,要想實例化不同的對象,那是相當的麻煩!
操作符重載,本質上就是函數重載,它大大豐富了已有操作符的含義,方便使用,如+可用於連接字符串等!
通過上面的介紹我們對函數重載,應該喚醒了我們對函數重載的大概記憶。下面我們就來分析,C++是如何實現函數重載機制的。

2、編譯器如何解決命名衝突的?

爲了瞭解編譯器是如何處理這些重載函數的,我們反編譯下上面我們生成的執行文件,看下彙編代碼(全文都是在Linux下面做的實驗,Windows類似,你也可以參考《一道簡單的題目引發的思考》一文,那裏既用到Linux下面的反彙編和Windows下面的反彙編,並註明了Linux和Windows彙編語言的區別)。我們執行命令objdump -d a.out >log.txt反彙編並將結果重定向到log.txt文件中,然後分析log.txt文件。

發現函數void print(int i) 編譯之後爲:(注意它的函數簽名變爲——_Z5printi)

我們可以發現編譯之後,重載函數的名字變了不再都是print!這樣不存在命名衝突的問題了,但又有新的問題了——變名機制是怎樣的,即如何將一個重載函數的簽名映射到一個新的標識?我的第一反應是:函數名+參數列表,因爲函數重載取決於參數的類型、個數,而跟返回類型無關。但看下面的映射關係:

void print(int i) –> _Z5printi
void print(string str) –> _Z5printSs

進一步猜想,前面的Z5表示返回值類型,print函數名,i表示整型int,Ss表示字符串string,即映射爲返回類型+函數名+參數列表。最後在main函數中就是通過_Z5printi、_Z5printSs來調用對應的函數的:

80489bc: e8 73 ff ff ff call 8048934 <_Z5printi>
……………
80489f0: e8 7a ff ff ff call 804896f <_Z5printSs>

我們再寫幾個重載函數來驗證一下猜想,如:

void print(long l) –> _Z5printl
void print(char str) –> _Z5printc
可以發現大概是int->i,long->l,char->c,string->Ss….基本上都是用首字母代表,現在我們來現在一個函數的返回值類型是否真的對函數變名有影響,如:

#include<iostream>
using namespace std;

int max(int a,int b)
{
        return a>=b?a:b;
}

double max(double a,double b)
{
        return a>=b?a:b;
}
int main()
{
        cout<<"max int is: "<<max(1,3)<<endl;
        cout<<"max double is: "<<max(1.2,1.3)<<endl;
        return 0;
}

int max(int a,int b) 映射爲_Z3maxii、double max(double a,double b) 映射爲_Z3maxdd,這證實了我的猜想,Z後面的數字代碼各種返回類型。更加詳細的對應關係,如那個數字對應那個返回類型,哪個字符代表哪重參數類型,就不去具體研究了,因爲這個東西跟編譯器有關,上面的研究都是基於g++編譯器,如果用的是vs編譯器的話,對應關係跟這個肯定不一樣。但是規則是一樣的:“返回類型+函數名+參數列表”。

既然返回類型也考慮到映射機制中,這樣不同的返回類型映射之後的函數名肯定不一樣了,但爲什麼不將函數返回類型考慮到函數重載中呢?——這是爲了保持解析操作符或函數調用時,獨立於上下文(不依賴於上下文),看下面的例子

float sqrt(float);
double sqrt(double);

void f(double da, float fla)
{
float fl=sqrt(da);//調用sqrt(double)
double d=sqrt(da);//調用sqrt(double)

  fl=sqrt(fla);//調用sqrt(float)

d=sqrt(fla);//調用sqrt(float)
}
如果返回類型考慮到函數重載中,這樣將不可能再獨立於上下文決定調用哪個函數。

至此似乎已經完全分析清楚了,但我們還漏了函數重載的重要限定——作用域。上面我們介紹的函數重載都是全局函數,下面我們來看一下一個類中的函數重載,用類的對象調用print函數,並根據實參調用不同的函數。
void print(int i) –> _ZN4test5printEi

void print(char c) –> _ZN4test5printEc

注意前面的N4test,我們可以很容易猜到應該表示作用域,N4可能爲命名空間、test類名等等。這說明最準確的映射機制爲:作用域+返回類型+函數名+參數列表

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