- 定义:在基类中使用 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 函数时,实际调用的是对象所对应的派生类中的函数版本,从而实现了多态性。
- 定义:在基类中声明的没有函数体,并且初始化为 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++ 的虚继承机制就是为了解决这类问题,它通过引入虚基类指针等方式,确保虚函数表在复杂继承结构中的正确维护。
评论前必须登录!
注册