C++的继承与多态是面向对象编程的核心特性,虚函数则是实现多态的关键机制,它允许派生类重写基类的函数,并且在运行时根据对象的实际类型来调用对应的函数实现,而不是在编译时就确定调用的函数版本。

虚函数的基本定义与语法
在基类中,使用virtual关键字修饰的成员函数就是虚函数,虚函数可以在派生类中被重写,重写时需要保证函数名、参数列表、返回值类型都和基类中的虚函数完全一致(协变返回类型除外)。
下面是一个简单的虚函数定义示例:
#include <iostream>
using namespace std;
// 基类
class Base {
public:
// 定义虚函数
virtual void show() {
cout << "这是基类的show方法" << endl;
}
// 虚析构函数,避免派生类对象析构时内存泄漏
virtual ~Base() {}
};
继承体系中的虚函数重写与多态实现
当派生类继承包含虚函数的基类时,可以重写基类的虚函数,此时通过基类的指针或引用指向派生类对象,调用虚函数时会自动调用派生类中重写的版本,这就是动态多态的体现。
来看一个完整的继承与多态示例:
#include <iostream>
using namespace std;
// 基类
class Animal {
public:
// 虚函数,定义动物的叫声
virtual void makeSound() {
cout << "动物发出叫声" << endl;
}
virtual ~Animal() {}
};
// 派生类:狗
class Dog : public Animal {
public:
// 重写基类的虚函数
void makeSound() override {
cout << "狗汪汪叫" << endl;
}
};
// 派生类:猫
class Cat : public Animal {
public:
// 重写基类的虚函数
void makeSound() override {
cout << "猫喵喵叫" << endl;
}
};
int main() {
// 基类指针指向派生类对象
Animal* animal1 = new Dog();
Animal* animal2 = new Cat();
// 调用虚函数,运行时根据实际对象类型调用对应方法
animal1->makeSound(); // 输出:狗汪汪叫
animal2->makeSound(); // 输出:猫喵喵叫
// 释放内存
delete animal1;
delete animal2;
return 0;
}
上面的示例中,Dog和Cat都继承了Animal类,并且重写了makeSound虚函数。通过Animal类型的指针指向不同的派生类对象,调用makeSound时就会执行对应派生类的实现,这就是多态的效果。
注意override关键字是C++11引入的,用来显式标记函数是重写基类的虚函数,如果重写不符合规则,编译器会直接报错,避免写错函数签名导致隐藏基类虚函数的问题。
纯虚函数与抽象类
如果基类中的虚函数没有实际的实现,需要派生类必须重写,就可以把该虚函数声明为纯虚函数。纯虚函数的语法是在函数声明的后面加上= 0,包含纯虚函数的类称为抽象类,抽象类不能实例化对象,只能作为基类被继承。
纯虚函数的示例:
#include <iostream>
using namespace std;
// 抽象基类,包含纯虚函数
class Shape {
public:
// 纯虚函数,计算面积
virtual double getArea() = 0;
// 纯虚函数,计算周长
virtual double getPerimeter() = 0;
virtual ~Shape() {}
};
// 派生类:圆形
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
// 重写纯虚函数
double getArea() override {
return 3.14 * radius * radius;
}
double getPerimeter() override {
return 2 * 3.14 * radius;
}
};
int main() {
// Shape s; // 错误,抽象类不能实例化
Shape* shape = new Circle(5.0);
cout << "圆形面积:" << shape->getArea() << endl;
cout << "圆形周长:" << shape->getPerimeter() << endl;
delete shape;
return 0;
}
虚函数使用的注意事项
- 虚函数的调用需要在运行时确定,因此会带来一定的性能开销,不过在绝大多数场景下这个开销可以忽略。
- 基类的析构函数最好声明为虚函数,这样当用基类指针删除派生类对象时,会先调用派生类的析构函数,再调用基类的析构函数,避免内存泄漏。
- 静态成员函数不能是虚函数,因为静态成员函数不属于某个对象,没有this指针,无法实现动态绑定。
- 构造函数不能是虚函数,因为构造对象时对象还没有完全创建,无法确定对象的实际类型。
- 如果派生类没有重写基类的虚函数,那么调用时会执行基类的虚函数实现。
虚函数实现原理简单说明
编译器会为每个包含虚函数的类生成一个虚函数表(vtable),表中存储该类所有虚函数的地址。每个该类的对象都会包含一个指向对应虚函数表的指针(vptr),当调用虚函数时,会通过对象的vptr找到对应的虚函数表,再从表中找到要调用的函数地址,从而实现动态绑定。
这个机制也解释了为什么构造函数不能是虚函数:对象构造时vptr还没有被正确初始化,无法找到对应的虚函数表。