在C++中,虚函数是对多态性(Polymorphism)的一种支持。当基类指针或引用被用来调用派生类对象的成员函数时,可以通过虚函数来实现动态绑定,即根据运行时类型确定要调用的函数。
下面是使用虚函数的两个典型场景:
- 实现多态
如果你需要在不同的子类上调用相同名字的函数,可以将该函数定义为虚函数。这样就可以使得程序在运行时按照其具体类型来选择调用哪个版本的函数,并实现多态性。
考虑如下代码:
class Animal {
public:virtual void speak() {cout << "This animal makes a sound." << endl;}
};class Dog : public Animal {
public:void speak() override {cout << "The dog barks." << endl;}
};class Cat : public Animal {
public:void speak() override {cout << "The cat meows." << endl;}
};int main() {Animal* animal1 = new Dog();Animal* animal2 = new Cat();animal1->speak(); // Output: The dog barks.animal2->speak(); // Output: The cat meows.delete animal1;delete animal2;return 0;
}
上述代码中,Animal
类是一个基类,其中speak()
函数被声明为虚函数。Dog
和Cat
类都从Animal
类派生而来,重新实现了speak()
函数。在主函数中,我们分别用类型为Animal*
的指针指向不同的子类对象,并调用了虚函数speak()
。因为speak()
是虚函数,所以调用的实际函数版本在运行时被动态地确定,从而实现了多态性。
- 实现动态绑定
当被重载的函数在程序运行时才能确定需要调用哪个版本时,使用虚函数非常有用。这种情况下,通过基类指针或引用调用该函数,编译器会根据其动态类型来选择调用正确的版本,也就是实现动态绑定的功能。
考虑如下代码:
class Car {
public:virtual void run() = 0;
};class GasCar : public Car {
public:void run() override {cout << "Gas car runs by burning gasoline." << endl;}
};class ElectricCar : public Car {
public:void run() override {cout << "Electric car runs by using battery power." << endl;}
};int main() {GasCar myGasCar;ElectricCar myElectricCar;Car* p1 = &myGasCar;Car& r1 = myElectricCar;p1->run(); // Output: Gas car runs by burning gasoline.r1.run(); // Output: Electric car runs by using battery power.return 0;
}
上述代码中,Car
类是一个抽象基类,其中run()
函数被声明为纯虚函数。GasCar
和ElectricCar
类都从Car
类派生而来,重新实现了run()
函数。在主函数中,我们分别创建了两个不同的子类对象,并将它们的地址存储到类型为基类Car*
和Car&
的指针和引用中。通过这些指针和引用我们调用了虚函数run()
。因为run()
是纯虚函数,所以编译器会根据其动态类型来选择调用正确版本的函数。
除了上述场景,还有一些其他的情况下适合使用虚函数:
- 实现接口
在C++中,可以通过纯虚函数来定义接口规范,然后让子类来实现这些函数。使用纯虚函数定义接口可以使得代码更加清晰和易于维护。
考虑如下代码:
class Shape {
public:virtual double getArea() const = 0;
};class Rectangle : public Shape {
private:double width_;double height_;
public:Rectangle(double width, double height) :width_(width), height_(height) {}double getArea() const override {return width_ * height_;}
};class Circle : public Shape {
private:double radius_;
public:Circle(double radius) : radius_(radius) {}double getArea() const override {return 3.14159 * radius_ * radius_;}
};int main() {Shape* shapes[2] = { new Rectangle(4, 3), new Circle(2) };cout << "The areas of the shapes are:" << endl;for (int i = 0; i < 2; ++i) {cout << "Shape " << i + 1 << ": " << shapes[i]->getArea() << endl;delete shapes[i];}return 0;
}
上述代码中,Shape
类是一个抽象基类,其中getArea()
函数被声明为纯虚函数。Rectangle
和Circle
类都从Shape
类派生而来,并重新实现了该纯虚函数。在主函数中,我们创建了一个包含两个不同形状对象的数组,并调用它们的虚函数getArea()
,计算它们的面积。
- 实现析构函数
当类中涉及到继承时,子类中可能会定义一些新的资源。为了确保在释放基类时也能够正确地释放这些资源,可以将基类的析构函数定义为虚函数。这样,在删除通过指向基类指针或引用指向其派生类的对象时,会首先调用其派生类的析构函数,然后再调用基类的析构函数。
考虑如下代码:
class Base {
public:virtual ~Base() { cout << "Base destructor." << endl; }
};class Derived : public Base{
private:int* ptr_;
public:Derived(int value) : ptr_(new int {value}) {}~Derived() override {delete ptr_;cout << "Derived destructor." << endl;}
};int main() {Base* base = new Derived(42);delete base;return 0;
}
上述代码中,Base
类是一个简单的基类,其中的析构函数被声明为虚函数。Derived
类从Base
类派生而来,重载了该析构函数。在主函数中,我们使用类型为指向基类Base*
的指针指向类型为派生类Derived
的对象,并对其进行删除操作。由于Base
的析构函数被声明为虚函数,因此在删除派生类对象时会先调用其析构函数,然后再调用Base
类的析构函数。
需要注意的是,虚函数存在虚表(Virtual Table)和虚指针(Virtual Pointer),这些数据结构占用了额外的内存空间。如果在程序中定义过多的虚函数或虚函数的层级比较深,将可能导致性能下降。因此,在使用虚函数时也要注意其对程序性能的影响。