多態與虛函數:
什麼是虛函數:
用virtual關鍵字聲明的函數都是虛函數。虛函數存在的唯一目的,就是爲了實現多態(動態綁定/運行時綁定)。
virtual 關鍵字只在類定義中的成員函數聲明處使用,不能在類外部寫成員函數體時使用。所有派生類中具有覆蓋關係的同名函數都將自動成爲虛函數。(派生類轉化爲的虛函數,最好也寫上virtual,清晰麼。)
靜態成員函數不能是虛函數。
再說簡單點:有virtual聲明的函數都是虛函數。如果沒有virtual,那麼派生類中的同名函數會把基類中的同名函數隱藏了。如果有,那麼派生類的同名函數(同參同返回)會在虛函數表中將基類的同名函數覆蓋掉。
什麼是多態:
多態性可以簡單概括爲“一個接口,多種行爲”。
動態綁定(運行階段)是多態的基礎。
基類指針或引用,指向一系列的派生類對象, 調用派生類對象的同名覆蓋方法(也就是那個與基類虛函數同名同參同返回的函數),指針指向誰,就會調用誰的方法。
多態分爲兩種:
(1)編譯時多態(也叫靜態的多態):主要通過函數的重載和模板來實現。
(2)運行時多態(也叫動態的多態):主要通過虛函數來實現。
覆蓋:
基類的某個成員函數爲虛函數,派生類又定義一個成員函數,函數名、形參、返回類型都與基類的成員函數相同。那麼就會用派生類的函數覆蓋掉基類的虛函數。
說一下多態是如何實現的:
在代碼編譯階段產生一張虛函數表vftable。運行的時候,會載入內存,加載到數據段(rodata段,只讀),在程序的整個聲生命週期都存在。一個類的虛函數表中列出了該類的全部虛函數地址。
如果成員裏有虛函數,在成員變量裏只會多一個虛函數指針(對象的前4個字節),指向虛函數表vftable()存放虛函數地址。虛函數的個數只會影響表的大小。不會影響對象的大小。
基類有虛函數,派生類如果有同名同參數列表同返回值的函數,派生類的函數會自動變成虛函數,在虛函數表中派生類會覆蓋掉原有的函數。
補充:在虛函數表中還有一項RTTI(運行時類型) 指針。
打印類型 #include <typeinfo>
cout << typeid(p).name() << endl; 該函數打印p的類型,因爲有RTTI才能在繼承派生中實現
舉個栗子:
#include <iostream>
using namespace std;
class A
{
public:
int i;
virtual void func() {}
virtual void func2() {}
};
class B : public A
{
int j;
void func() {}
};
int main()
{
cout << sizeof(A) << ", " << sizeof(B);
return 0;
}
設 pa 的類型是 A*,則 pa->func() 這條語句的執行過程如下:
1) 取出 pa 指針所指位置的前 4 個字節,也就是虛函數指針。如果 pa 指向的是類 A 的對象,則這個地址就是類 A 的虛函數表的地址;類 B 同
2) 根據虛函數指針找到虛函數表,在其中查找要調用的虛函數的地址。
如果 pa 指向的是類 A 的對象,自然就會在類 A 的虛函數表中查出 A::func 的地址;類 B 同
類 B 沒有自己的 func2 函數,因此在類 B 的虛函數表中保存的是 A::func2 的地址,這樣,即便 pa 指向類 B 的對象,pa->func2();
這條語句在執行過程中也能在類 B 的虛函數表中找到 A::func2 的地址。
3) 根據找到的虛函數的地址調用虛函數。
由以上過程可以看出,只要是通過基類指針或基類引用調用虛函數的語句,就一定是多態的,也一定會執行上面的查表過程,哪怕這個虛函數僅在基類中有,在派生類中沒有。
在虛函數表中還有一個:RTTI 運行時的類型信息
Base *p = new Derive();
cout<<typeid(*p).name()<<endl;
動態綁定與靜態綁定:
綁定就是函數調用。
在使用的時候,用一個基類的指針指向了一個派生類的對象,如果調用這個虛函數,會先找虛函數指針,再找虛函數表,再再找虛函數地址。而這個綁定過程就是動態綁定(運行時期)。
如果你不用指針調用,而是用對象本身調用函數,不論是否是虛函數,都是靜態綁定(編譯時期)。
用指針調用如果是虛函數,指針識別的就是運行時期的類型;如果調用的是一般的函數,指針識別就是在編譯時期。
- 沒有virtual -> 靜態綁定
- 有 virtual 用引用或指針 -> 動態綁定
- 有 virtual 但用對象調用-> 動態綁定
純虛函數:
一般情況下,基類是不希望定義對象的。基類只是爲了將共有的屬性統一起來。
爲了實現這一目的:在基類提供的一個虛函數,爲所有派生類提供統一的虛函數接口,具體實現讓派生類自己去重寫的。
virtual void show() = 0; // 在虛函數後面加 = 0 就是純虛函數,不用去實現。
純虛函數實際上是不存在的,引入純虛函數就是爲了便於實現多態。
擁有純虛函數的類叫做抽象類。抽象類不能實例化。一般基類都應該實現爲抽象類。
當不知道用哪個函數定義爲純虛函數的時候,我們可以將析構函數定義爲純虛函數,但需要注意的是,析構函數成純虛函數了,它在類內就不能實現了。當然編譯也就沒法通過了。解決辦法:類內不能實現,我類外實現啊。
還有一點,如果你基類寫了純虛函數,但在派生類中沒有寫對應基類純虛函數,那麼由於繼承的關係,會導致派生類也成爲純虛函數。
那麼問題來了:
1、基類在沒有更多方法的時候,把誰實現成純虛函數呢?--------> 析構函數
首先明確一個函數想要成爲虛函數 1、它得有地址,有地址才能放入虛函數表;2、得依賴對象,有對象纔會有指針,有指針才能找到虛函數表。
1)構造函數能不能是虛函數?
構造函數不依賴對象,構造函數執行完纔有對象,有對象纔有虛函數指針,所以不能是虛函數。
2)static成員方法能不能是虛函數? virtual static
靜態函數也不依賴對象,可以直接使用類名調用,也不能是虛函數。
3)inline函數能不能是虛函數? => virtual inline
內聯函數直接在程序內展開,沒有地址,無法往虛函數表放。也不能是虛函數。
4)析構函數能不能實現成虛函數?
析構函數依賴對象,有地址。可以寫成虛函數。我們知道如果將一個函數寫爲虛函數,那麼其派生類會有一個同名的函數也成爲虛函數,兩者成覆蓋關係。但是析構函數的函數名是類名前加~。所以名字是不同的,但其實這是可以的,因爲一個類只會有一個虛函數。
2、什麼時候必須將析構函數定義爲虛函數呢?
當基類指針,指向堆上的派生類對象時。
int main()
{
Base *p = new Derive(20);
p->show();
delete p; 如果析構函數不是虛函數的話,調用的時候由於p是基類指針,所以只會調用基類的析構函數,
而不會調用派生類的析構函數。導致資源泄露。所以必須將基類的析構函數聲明爲虛函數。
return 0;
}
3、坑1
class Base
{
public:
Base(int data) :ma(data){ cout << "Base()" << endl; }
virtual ~Base() = 0;
virtual void show(int i=10)
{
cout << "Base::show" << endl;
}
private:
int ma;
};
Base::~Base()
{
cout << "~Base()" << endl;
}
///////////////////////////////////////////////////////////
class Derive : public Base
{
public:
Derive(int data) :mb(data), Base(data)
{
cout << "Derive()" << endl;
}
~Derive(){ cout << "~Derive()" << endl; }
void show(int i=20)
{
cout << "Derive::show i:" << i<<endl;
}
private:
int mb;
};
int main()
{
const int a = 10; 用const進行類比,我們知道const定義的變量的值是不能修改的,直接修改會報錯。
int *q = (int*)&a; 但通過指針修改地址的值,還是可以將其更改。
*q = 20; 原因就是,編譯的時候確實沒有檢測出來修改,但運行的時候就可以將他改了。
//////////////////////////////////////////////////////////////////
Base *p = new Derive(20);
p->show();
結果:打印出來的是Derive::show i:10。誒呦奇怪了,我派生類定義的明明是20,爲啥調的是派生類的函數,打印出來的卻是10?
解釋:由於參數的壓棧,是編譯階段確定的。具體調用哪個方法,是經過動態綁定,運行時才確定。
所以可能導致使用的是基類的參數,而調用的是派生類的函數。(所以虛函數最好不要寫默認值,寫的話就寫成一樣的)
delete p;
return 0;
}
在問個問題:如何調用派生類的私有成員函數?
跟const是一樣的。由於方法的訪問權限是在編譯階段確定的。所以如果將基類對應的函數寫爲虛函數,那麼在使用方法時是動態綁定,在運行時確定使用哪個方法,所以由此可以調用派生類的私有成員函數。
4、坑2
class Animal
{
public:
Animal(string name) :_name(name){}
virtual void bark() = 0;
protected:
string _name;
};
class Cat : public Animal
{
public:
Cat(string name) :Animal(name){}
virtual void bark()
{
cout << _name << " miao miao!" << endl;
}
};
class Dog : public Animal
{
public:
Dog(string name) :Animal(name){}
virtual void bark()
{
cout << _name << " wang wang!" << endl;
}
};
int main()
{
Animal *p1 = new Cat("貓");
Animal *p2 = new Dog("狗");
int *pp1 = (int*)p1;
int *pp2 = (int*)p2;
int tmp = pp1[0];將兩個虛函數指針交換
pp1[0] = pp2[0];
pp2[0] = tmp;
p1->bark(); 貓 vfptr -> 狗vftable
p2->bark(); 導致貓叫出了狗的聲音
delete p1;
delete p2;
return 0;
}
5、在構造函數中,調用虛函數,是靜態綁定還是動態綁定?
析構函數和構造函數內部都不會發生動態綁定(多態)。前面提到了調用虛函數一定要有對象,而對象的生命週期在構造函數結束後一直到析構函數開始前。所以在構造與析構內部不會發生動態綁定。
6、在看一個題:
class Base
{
public:
Base(int data) :ma(data)
{
// 1. 棧幀開闢 2.棧幀初始化 3.vftable=》vfptr裏面
cout << "Base()" << endl;
clear();
this->show();
}
virtual ~Base()
{
this->show();
cout << "~Base()" << endl;
}
void clear()
{
memset(this, 0, sizeof(*this));
}
virtual void show(int i=10)
{
cout << "Base::show" << endl;
}
private:
int ma;
};
///////////////////////////////////////////////////////////
class Derive : public Base
{
public:
Derive(int data) :mb(data), Base(data)
{
cout << "Derive()" << endl;
}
~Derive()
{
cout << "~Derive()" << endl;
}
void show(int i=10)
{
cout << "Derive::show i:" << i<<endl;
}
private:
int mb;
};
int main()
{
幀的開闢,棧幀的初始化 將虛函數表的地址寫入虛函數指針中。都是進入構造函數一開始時進行的,
如果清空了虛函數指針,在動態綁定的時候指向基類虛函數對象的指針會報錯。
Base *p1 = new Base(10);
p1->show();
delete p1;
Base *p2 = new Derive(10);
p2->show();
delete p2;
// 繼承結構中,每一層構造函數都會把自己類型的虛函數表的地址,寫到vfptr裏面
return 0;
}
棧幀的開闢,棧幀的初始化 將虛函數表的地址寫入虛函數指針中。都是進入構造函數一開始時進行的,如果清空了虛函數指針,在動態綁定的時候指向基類虛函數對象的指針會報錯。
寫的有點多了,但還沒寫完,後續內容在下一篇裏。