什么是虛函數
在我看來,虛函數是在基類中聲明并由派生類重新定義的成員函數(如果不是純虛函數也可以不覆蓋)。當用基類的指針或者引用
訪問虛函數時會發生「動態綁定」找到真正的函數地址。
一個基本的例子就是這樣:
#include <iostream>
using namespace std;
class base {
public:
virtual void print()
{
cout << "print base class" << endl;
}
void show()
{
cout << "show base class" << endl;
}
};
class derived : public base {
public:
void print()
{
cout << "print derived class" << endl;
}
void show()
{
cout << "show derived class" << endl;
}
};
int main()
{
base* bptr;
derived d;
bptr = &d;
// virtual function, binded at runtime
bptr->print();
// Non-virtual function, binded at compile time
bptr->show();
}
注意:
如果我們在基類中定義了虛函數,函數在派生類中自動被視為虛函數。比如我們看這個代碼:
#include <iostream>
using namespace std;
class base {
private:
public:
virtual void print() const {
cout << "base" << '\n';
}
};
class A : public base {
public:
void print() const {
cout << "A" << '\n';
}
};
class C : public A {
public:
void print() const {
cout << "C" << '\n';
}
};
int main() {
base *b;
b = new A();
b->print();
b = new C();
b->print();
return 0;
}
結果:
A
C
A 中沒定義 print 是虛函數,但是還是調用到了 C 的 print。證明 C 這個類中有一個「虛函數表」,也就證明了 print 還是一個虛函數。
虛函數的實現
之前提到了虛函數表,什么是虛函數表(VTABLE)?以及虛函數怎么實現的?
如果一個類包含一個虛函數,那么編譯器本身會做兩件事:
- 如果創建了該類的對象,則將虛擬指針(VPTR)作為該類的數據成員指向該類的 VTABLE。對于創建的每一個新對象都會插入一個新的虛擬指針作為該類的數據成員。
- 無論對象是否創建,一個類的 VTABLE 都會存在,本質上就是一個靜態的函數指針數組,數組的每個單元包含該類中的每個
虛函數
的地址。
什么時候編譯器會選擇使用虛表
我覺得就兩個條件:
- 該類的某一個父類的指針或者引用
- 調用虛函數
我理解的虛函數為什么會讓代碼變慢的原因
以一個經典的 ARM 五級流水線為例子,一條指令的執行有五個過程:1. Fetch 2. Decode 3. Excute 4. Memory 5. Write
在編譯器確定函數的情況下,在執行跳轉的時候,函數的指令就已經可以放在流水線里面了。但是虛函數必須得等到從虛表中拿到函數地址才能繼續執行,不能提前運行函數的指令。所以就需要空出幾個 CPU 周期,所以會變慢。
但是真正的瓶頸還是 IO 這一點消耗應該也不算什么。
虛析構函數有什么作用
我們來看這個例子:
#include<iostream>
using namespace std;
class base {
public:
base(){
cout<<"Constructing base \n";
}
~base() {
cout<<"Destructing base \n";
}
};
class derived: public base {
public:
derived() {
cout<<"Constructing derived \n";
}
~derived() {
cout<<"Destructing derived \n";
}
};
int main() {
derived *d = new derived();
base *b = d;
delete b;
return 0;
}
結果是:
Constructing base
Constructing derived
Destructing base
沒有正確釋放派生類的對象。
如果給基類的西溝函數加上 virtual 就會正確釋放 派生類的對象。所以為了避免內存泄漏,任何時候在類中有虛函數時,都應該立即添加虛析構函數(即使它什么都不做)。
#include<iostream>
using namespace std;
class base {
public:
base(){
cout<<"Constructing base \n";
}
virtual ~base() {
cout<<"Destructing base \n";
}
};
class derived: public base {
public:
derived() {
cout<<"Constructing derived \n";
}
~derived() {
cout<<"Destructing derived \n";
}
};
int main() {
derived *d = new derived();
base *b = d;
delete b;
return 0;
}