C++中的虚函数是实现运行时多态的核心机制之一。当父类指针指向子类对象时,如果通过该指针调用的是虚函数,程序可以根据对象的实际类型选择对应的子类方法,而不是简单按照指针声明类型调用父类方法。这也是C++面向对象编程中“父类指针调用子类方法”的典型实现方式。
一、什么是C++虚函数
普通成员函数的调用通常在编译阶段就能够确定。例如:
C++class Animal { public: void speak() { std::cout << "Animal speak" << std::endl; } }; class Dog : public Animal { public: void speak() { std::cout << "Dog speak" << std::endl; } };
如果使用父类指针:
C++Animal* animal = new Dog(); animal->speak();
此时输出的是:
Animal speak
原因在于 speak() 是普通成员函数,调用关系主要根据指针的静态类型 Animal* 在编译阶段确定。
如果希望父类指针能够根据实际对象类型调用子类版本,就需要将父类中的函数声明为 virtual。
C++class Animal { public: virtual void speak() { std::cout << "Animal speak" << std::endl; } }; class Dog : public Animal { public: void speak() override { std::cout << "Dog speak" << std::endl; } };
再次执行:
C++Animal* animal = new Dog(); animal->speak();
输出:
Dog speak
这就是C++虚函数实现父类指针调用子类方法的基本原理。
二、父类指针调用子类方法的完整示例
下面通过一个更加完整的例子理解运行时多态。
C++#includeclass Animal { public: virtual void speak() { std::cout << "Animal is speaking" << std::endl; } virtual ~Animal() = default; }; class Dog : public Animal { public: void speak() override { std::cout << "Dog is barking" << std::endl; } }; class Cat : public Animal { public: void speak() override { std::cout << "Cat is meowing" << std::endl; } }; int main() { Animal* animal1 = new Dog(); Animal* animal2 = new Cat(); animal1->speak(); animal2->speak(); delete animal1; delete animal2; return 0; }
运行结果:
Dog is barking Cat is meowing
这里有两个变量都是 Animal* 类型,但它们实际分别指向 Dog 和 Cat 对象。
C++Animal* animal1 = new Dog(); Animal* animal2 = new Cat();
调用:
C++animal1->speak(); animal2->speak();
程序会根据对象实际类型分别执行:
C++Dog::speak() Cat::speak()
这种根据运行时对象类型决定调用哪个函数的机制,就是运行时多态。
三、为什么父类指针能够调用子类方法
理解这个问题,需要区分两个概念:静态类型和动态类型。
例如:
C++Animal* animal = new Dog();
这里:
-
animal的静态类型是Animal* -
animal实际指向的对象类型是Dog
对于普通函数:
C++animal->speak();
编译器通常根据 Animal* 确定调用哪个函数。
对于虚函数,C++会保留运行时动态绑定的能力。程序运行过程中,会根据当前对象的实际类型找到对应的虚函数实现。
因此:
C++Animal* animal = new Dog(); animal->speak();
最终可以调用:
C++Dog::speak()
而不是:
C++Animal::speak()
这也是“父类指针调用子类方法”成立的关键。
四、virtual关键字的作用
父类函数必须声明为虚函数。
例如:
C++class Animal { public: virtual void speak() { std::cout << "Animal" << std::endl; } };
其中:
C++virtual void speak()
表示 speak() 是虚函数。
子类可以重新实现:
C++class Dog : public Animal { public: void speak() override { std::cout << "Dog" << std::endl; } };
C++11以后推荐使用 override。
override 并不是实现多态所必需的,但可以帮助编译器检查函数是否正确重写了父类虚函数。
例如不小心写成:
C++class Dog : public Animal { public: void speak(int type) override { std::cout << "Dog" << std::endl; } };
由于参数列表不同,这并没有正确重写父类函数,编译器会直接报错。
因此,使用:
C++void speak() override
能够减少函数重写过程中的隐蔽错误。
五、虚函数与普通函数的区别
可以通过一个简单例子直观比较。
普通成员函数
C++class Parent { public: void show() { std::cout << "Parent" << std::endl; } }; class Child : public Parent { public: void show() { std::cout << "Child" << std::endl; } }; Parent* p = new Child(); p->show();
输出:
Parent
虚函数
C++class Parent { public: virtual void show() { std::cout << "Parent" << std::endl; } }; class Child : public Parent { public: void show() override { std::cout << "Child" << std::endl; } }; Parent* p = new Child(); p->show();
输出:
Child
两段代码最关键的区别就是父类的 show() 是否使用 virtual。
六、虚函数表是如何工作的
C++标准并没有规定必须使用“虚函数表”实现虚函数,但主流C++编译器通常采用虚函数表,也就是 vtable,配合虚函数指针实现动态绑定。
简单理解,可以把它看成一个函数地址表。
例如:
C++class Animal { public: virtual void speak() { std::cout << "Animal" << std::endl; } };
编译器通常会为包含虚函数的类建立相应的虚函数表。
子类:
C++class Dog : public Animal { public: void speak() override { std::cout << "Dog" << std::endl; } };
其虚函数表中对应的 speak 项通常会指向:
Dog::speak()
当执行:
C++Animal* animal = new Dog(); animal->speak();
程序会通过对象关联的虚函数机制找到 Dog::speak(),从而实现运行时多态。
需要注意的是,vtable、vptr属于常见实现方式,并不是C++语言标准强制规定的对象布局。因此编写跨平台代码时,不应该依赖某个编译器具体的虚表内存布局。
七、父类指针为什么是多态编程的重要形式
假设有多个动物类:
C++class Animal { public: virtual void speak() = 0; virtual ~Animal() = default; }; class Dog : public Animal { public: void speak() override { std::cout << "Dog" << std::endl; } }; class Cat : public Animal { public: void speak() override { std::cout << "Cat" << std::endl; } };
此时可以使用统一的父类指针:
C++Animal* animals[] = { new Dog(), new Cat() }; for (Animal* animal : animals) { animal->speak(); }
不需要分别判断:
C++if (animal is Dog) { // ... } if (animal is Cat) { // ... }
而是直接调用统一接口:
C++animal->speak();
最终由对象自身决定执行哪个版本。
这种设计能够降低代码之间的耦合,特别适合插件系统、图形界面、游戏开发、支付系统、数据库驱动等需要统一接口、多种实现的场景。
八、纯虚函数实现抽象父类
如果父类本身不需要提供具体实现,可以定义纯虚函数:
C++class Animal { public: virtual void speak() = 0; virtual ~Animal() = default; };
其中:
C++virtual void speak() = 0;
就是纯虚函数。
包含纯虚函数的类属于抽象类,不能直接创建对象:
C++Animal animal; // 错误
但可以使用父类指针指向子类对象:
C++Animal* animal = new Dog(); animal->speak(); delete animal;
这种设计非常适合定义统一接口。
例如:
C++class Shape { public: virtual double area() const = 0; virtual ~Shape() = default; };
不同图形分别实现:
C++class Circle : public Shape { public: double area() const override { return 3.14159 * 10 * 10; } }; class Rectangle : public Shape { public: double area() const override { return 20 * 10; } };
调用代码只需要:
C++Shape* shape = new Circle(); std::cout << shape->area() << std::endl; delete shape;
并不需要关心 shape 当前具体是哪一种图形。
九、虚析构函数非常重要
使用父类指针删除子类对象时,父类析构函数通常应该声明为虚函数。
例如:
C++class Parent { public: virtual ~Parent() { std::cout << "Parent destructor" << std::endl; } };
子类:
C++class Child : public Parent { public: ~Child() { std::cout << "Child destructor" << std::endl; } };
然后:
C++Parent* p = new Child(); delete p;
能够按照正确的析构顺序执行子类和父类析构函数。
通常会看到:
Child destructor Parent destructor
如果父类析构函数不是虚函数:
C++class Parent { public: ~Parent() {} };
再执行:
C++Parent* p = new Child(); delete p;
通过父类指针删除实际指向的派生类对象会产生未定义行为。
因此,只要一个类准备作为多态基类使用,通常就应该考虑提供虚析构函数:
C++virtual ~Parent() = default;
十、使用智能指针管理多态对象
现代C++中,不建议频繁使用裸指针配合 new 和 delete 管理对象。
可以使用 std::unique_ptr:
C++#include#include class Animal { public: virtual void speak() { std::cout << "Animal" << std::endl; } virtual ~Animal() = default; }; class Dog : public Animal { public: void speak() override { std::cout << "Dog" << std::endl; } }; int main() { std::unique_ptr<Animal> animal = std::make_unique<Dog>(); animal->speak(); return 0; }
输出:
Dog
程序结束时,unique_ptr 会自动释放对象资源。
如果需要保存多种不同子类对象,可以使用:
C++std::vector<std::unique_ptr<Animal>> animals; animals.push_back(std::make_unique<Dog>()); animals.push_back(std::make_unique<Cat>()); for (const auto& animal : animals) { animal->speak(); }
这是一种更加符合现代C++实践的多态对象管理方式。
十一、哪些情况下父类指针不会调用子类方法
并不是所有子类方法都可以通过父类指针调用。
例如:
C++class Parent { public: virtual void show() { std::cout << "Parent show" << std::endl; } }; class Child : public Parent { public: void show() override { std::cout << "Child show" << std::endl; } void test() { std::cout << "Child test" << std::endl; } };
下面的代码:
C++Parent* p = new Child(); p->show();
可以调用:
C++Child::show()
但是:
C++p->test();
无法编译。
原因是 test() 并没有定义在 Parent 接口中。虽然实际对象是 Child,但表达式 p 的静态类型仍然是 Parent*。
如果确实需要访问子类特有功能,可以使用类型转换,但应谨慎设计:
C++Child* child = dynamic_cast<Child*>(p); if (child != nullptr) { child->test(); }
如果代码中大量依赖这种向下转型,通常说明类层次结构或接口设计可能需要重新考虑。
十二、构造函数和析构函数中的虚函数需要特别注意
不要把普通运行时多态的理解简单套用到构造函数和析构函数中。
例如:
C++class Parent { public: Parent() { show(); } virtual void show() { std::cout << "Parent" << std::endl; } }; class Child : public Parent { public: void show() override { std::cout << "Child" << std::endl; } };
创建:
C++Child child;
构造 Parent 部分时,虚函数调用不会像普通运行阶段那样分派到 Child::show()。
这是因为此时派生类对象部分尚未完成构造。
析构阶段也存在类似规则。执行父类析构函数时,派生类部分已经完成析构,因此不能把它当作完整的派生类对象进行普通虚函数分派。
实际开发中,应尽量避免在构造函数和析构函数中依赖虚函数实现多态行为。
十三、父类指针调用子类方法的核心条件
要实现稳定的运行时多态,一般需要满足以下条件:
-
父类函数使用
virtual声明。 -
子类提供正确的函数重写。
-
通过父类指针或父类引用访问该虚函数。
-
父类与子类之间存在继承关系。
-
多态对象的生命周期必须正确管理。
-
多态基类通常应提供虚析构函数。
典型结构可以概括为:
C++class Base { public: virtual void run() { std::cout << "Base" << std::endl; } virtual ~Base() = default; }; class Derived : public Base { public: void run() override { std::cout << "Derived" << std::endl; } };
然后:
C++Base* ptr = new Derived(); ptr->run(); delete ptr;
最终调用:
Derived
这就是C++虚函数实现父类指针调用子类方法的完整基本模型。
十四、常见错误总结
1. 忘记使用virtual
C++class Base { public: void run() {} };
这种情况下通过 Base* 调用时不会产生真正的运行时多态。
2. 子类函数签名写错
例如父类:
C++virtual void run(int value);
子类却写成:
C++void run(double value);
这并不是重写,而是定义了另外一个函数。
推荐使用:
C++void run(int value) override;
让编译器帮助检查。
3. 父类析构函数不是virtual
如果通过父类指针管理子类对象,非虚析构函数可能导致未定义行为。
推荐:
C++virtual ~Base() = default;
4. 把“父类指针”理解成“可以访问所有子类成员”
C++Base* ptr = new Derived();
并不意味着 ptr 可以直接访问 Derived 独有的方法。
父类指针只能直接访问父类接口中可见的成员,而其中的虚函数可以根据实际对象类型发生动态绑定。
5. 滥用dynamic_cast
如果业务代码频繁出现:
C++dynamic_cast<Derived*>(ptr)
往往意味着没有充分利用多态接口。更好的方式通常是把共同操作抽象到基类的虚函数中。
十五、C++虚函数与运行时多态的实际意义
虚函数真正解决的问题,并不是单纯“让父类指针调用子类方法”,而是让调用者依赖抽象接口,而不必依赖具体实现。
例如设计一个支付接口:
C++class Payment { public: virtual void pay(double amount) = 0; virtual ~Payment() = default; };
不同支付方式实现统一接口:
C++class Alipay : public Payment { public: void pay(double amount) override { std::cout << "Alipay: " << amount << std::endl; } }; class WeChatPay : public Payment { public: void pay(double amount) override { std::cout << "WeChatPay: " << amount << std::endl; } };
业务代码只需要:
C++void processPayment(Payment& payment) { payment.pay(100); }
调用:
C++Alipay alipay; WeChatPay wechat; processPayment(alipay); processPayment(wechat);
同一个:
C++processPayment()
就可以处理不同的具体支付实现。
这就是面向对象设计中多态的价值:统一接口、隐藏实现、降低耦合、提高扩展能力。
十六、总结
C++虚函数是实现运行时多态的重要机制。通过在父类中使用 virtual 声明函数,并由子类使用 override 正确重写,就可以让父类指针或父类引用指向不同子类对象时,根据对象实际类型调用对应的子类方法。
最典型的代码结构是:
C++class Base { public: virtual void show() { std::cout << "Base" << std::endl; } virtual ~Base() = default; }; class Derived : public Base { public: void show() override { std::cout << "Derived" << std::endl; } }; int main() { Base* ptr = new Derived(); ptr->show(); delete ptr; }
其中最关键的三个知识点是:
virtual → 开启虚函数动态绑定 override → 明确检查子类重写 virtual ~Base() → 保证多态对象正确析构
理解这套机制之后,就能进一步掌握C++中的抽象类、纯虚函数、接口设计、运行时多态以及面向对象设计原则。