- 虛函數(Virtual Function)
- 定義:在基類中使用
virtual
關鍵字聲明的成員函數,允許在派生類中被重新定義(覆蓋,override)。其目的是實現多態性,即通過基類指針或引用調用函數時,根據對象的實際類型來決定調用哪個類的函數版本。
- 定義:在基類中使用
#include <iostream>class Animal {
public:virtual void speak() {std::cout << "Animal speaks" << std::endl;}
};class Dog : public Animal {
public:void speak() override {std::cout << "Dog barks" << std::endl;}
};class Cat : public Animal {
public:void speak() override {std::cout << "Cat meows" << std::endl;}
};int main() {Animal* animal1 = new Dog();Animal* animal2 = new Cat();animal1->speak();animal2->speak();delete animal1;delete animal2;return 0;
}
- 在上述代碼中,
Animal
類中的speak
函數被聲明為虛函數。Dog
和Cat
類從Animal
類派生,并覆蓋了speak
函數。在main
函數中,通過基類指針調用speak
函數時,實際調用的是對象所對應的派生類中的函數版本,從而實現了多態性。
- 純虛函數(Pure Virtual Function)
- 定義:在基類中聲明的沒有函數體,并且初始化為
0
的虛函數。包含純虛函數的類稱為抽象類,抽象類不能實例化對象,只能作為基類被派生類繼承。派生類必須實現純虛函數,否則派生類也將成為抽象類。
- 定義:在基類中聲明的沒有函數體,并且初始化為
#include <iostream>class Shape {
public:virtual double area() = 0;
};class Circle : public Shape {
public:Circle(double r) : radius(r) {}double area() override {return 3.14159 * radius * radius;}
private:double radius;
};class Rectangle : public Shape {
public:Rectangle(double w, double h) : width(w), height(h) {}double area() override {return width * height;}
private:double width, height;
};int main() {Shape* shape1 = new Circle(5.0);Shape* shape2 = new Rectangle(4.0, 6.0);std::cout << "Circle area: " << shape1->area() << std::endl;std::cout << "Rectangle area: " << shape2->area() << std::endl;delete shape1;delete shape2;return 0;
}
Shape
類中的area
函數是純虛函數。Circle
和Rectangle
類繼承自Shape
類,并實現了area
函數。通過這種方式,強制派生類提供自己的area
計算方法,同時利用基類指針實現多態調用。
-
虛函數實現機制
- 虛函數表(Virtual Table,簡稱 vtable):當一個類中包含虛函數時,編譯器會為該類創建一個虛函數表。虛函數表是一個存儲類成員虛函數指針的數組。每個包含虛函數的類都有自己的虛函數表。
- 虛指針(Virtual Pointer,簡稱 vptr):每個包含虛函數的對象都包含一個指向其所屬類的虛函數表的指針,即虛指針。當對象被創建時,虛指針被初始化,指向該對象所屬類的虛函數表。
- 調用過程:當通過基類指針或引用調用虛函數時,首先根據對象的虛指針找到對應的虛函數表,然后在虛函數表中查找與被調用函數對應的指針,最后通過該指針調用實際的函數。例如,在上述
Animal
類及其派生類的例子中,animal1
和animal2
對象都有自己的虛指針,分別指向Dog
和Cat
類的虛函數表。當調用animal1->speak()
時,通過animal1
的虛指針找到Dog
類的虛函數表,再從虛函數表中找到speak
函數的指針并調用。
-
虛函數表的細節
- 布局:虛函數表中的函數指針按照虛函數在類中聲明的順序排列。如果派生類覆蓋了基類的虛函數,虛函數表中相應位置的指針會被替換為派生類中該虛函數的實現地址。
- 多重繼承:在多重繼承的情況下,一個對象可能有多個虛指針,分別指向不同基類的虛函數表。這是因為不同基類可能有不同的虛函數集合。例如,當一個類從多個包含虛函數的基類派生時,每個基類的虛函數表都需要被正確管理,以確保虛函數調用的正確性。
- 運行時開銷:虛函數機制帶來了運行時的額外開銷,主要包括存儲虛指針的空間開銷以及通過虛指針和虛函數表查找函數指針的時間開銷。然而,這種開銷在大多數情況下是可以接受的,并且為C++ 提供了強大的多態性支持。
-
虛函數與純虛函數
- 析構函數的虛屬性:
- 重要性:當基類指針指向派生類對象,并且通過該指針刪除對象時,如果基類析構函數不是虛函數,那么只會調用基類的析構函數,派生類的析構函數不會被調用,這可能導致內存泄漏。例如:
- 析構函數的虛屬性:
#include <iostream>class Base {
public:~Base() {std::cout << "Base destructor" << std::endl;}
};class Derived : public Base {
public:~Derived() {std::cout << "Derived destructor" << std::endl;}
};int main() {Base* basePtr = new Derived();delete basePtr;return 0;
}
- **輸出**:只會輸出 “Base destructor”。但如果將 `Base` 類的析構函數聲明為虛函數 `virtual ~Base()`,則會先調用 `Derived` 類的析構函數,再調用 `Base` 類的析構函數,確保資源正確釋放。
- 純虛析構函數:純虛析構函數是一種特殊情況,一個類可以有純虛析構函數,但必須在類外提供函數體。例如:
class AbstractClass {
public:virtual ~AbstractClass() = 0;
};AbstractClass::~AbstractClass() {std::cout << "AbstractClass destructor" << std::endl;
}class ConcreteClass : public AbstractClass {
public:~ConcreteClass() override {std::cout << "ConcreteClass destructor" << std::endl;}
};
- 虛函數與模板:模板元編程中,虛函數的使用需要特別注意。模板是在編譯期進行實例化的,而虛函數是運行時多態的基礎。當模板類與虛函數結合時,由于模板的實例化機制,可能會導致一些不易察覺的問題。例如,模板類中的虛函數可能不會像預期那樣在派生類中被正確覆蓋,因為模板的實例化是基于不同的模板參數,每個實例化可能會有不同的虛函數表布局。
- 虛函數實現機制與虛函數表
- 虛函數表與動態綁定:動態綁定是指在運行時根據對象的實際類型來確定調用哪個虛函數的過程。虛函數表是實現動態綁定的關鍵。編譯器在編譯時生成虛函數表,運行時通過虛指針和虛函數表進行函數調用。但在一些優化場景下,如在編譯期能夠確定對象的實際類型(例如通過
constexpr
條件判斷等),編譯器可能會進行靜態綁定,直接調用相應的函數,而不通過虛函數表機制,以提高效率。 - 虛函數表與內存對齊:由于虛指針的存在,對象的內存布局可能會受到影響。虛指針的大小通常與機器的指針大小相同(例如在 64 位系統中為 8 字節)。為了滿足內存對齊的要求,對象的大小可能會增加。例如,一個類只有一個
int
成員變量(通常 4 字節),但如果它包含虛函數,加上 8 字節的虛指針,并且按照 8 字節對齊,對象的大小就會變為 8 字節,而不是 4 + 8 = 12 字節(因為內存對齊會補齊)。 - 虛函數表的維護與繼承層次:在復雜的繼承層次結構中,虛函數表的維護變得更加復雜。當有多層繼承和虛函數的覆蓋時,編譯器需要確保虛函數表的一致性。例如,在菱形繼承(一個派生類從兩個基類繼承,而這兩個基類又從同一個基類繼承)的情況下,虛函數表的布局需要精心設計,以避免二義性和重復調用等問題。C++ 的虛繼承機制就是為了解決這類問題,它通過引入虛基類指針等方式,確保虛函數表在復雜繼承結構中的正確維護。
- 虛函數表與動態綁定:動態綁定是指在運行時根據對象的實際類型來確定調用哪個虛函數的過程。虛函數表是實現動態綁定的關鍵。編譯器在編譯時生成虛函數表,運行時通過虛指針和虛函數表進行函數調用。但在一些優化場景下,如在編譯期能夠確定對象的實際類型(例如通過