在 C++ 面向对象编程的三大核心特性(封装、继承、多态)中,多态无疑是最具灵活性和扩展性的特性。它允许不同类的对象对同一消息做出不同响应,让代码更具通用性和可维护性,是实现设计模式、框架开发的基础。很多开发者在初学多态时,往往只能掌握表面用法,对其底层原理和进阶细节理解不深。本文将从概念定义出发,逐步深入多态的实现条件、核心机制、底层原理,结合大量实战代码和面试高频考点,全面解析 C++ 多态的方方面面,帮助大家真正吃透这一核心特性。下面就让我们正式开始吧!
多态(polymorphism),字面意思是 “多种形态”。在编程语境中,指的是同一个行为(函数调用),作用于不同的对象,会产生不同的执行结果。生活中处处可见多态的影子:


这种 “一个接口,多种实现” 的思想,正是多态的核心价值 —— 它屏蔽了不同对象之间的差异,让开发者可以通过统一的方式调用不同对象的方法,极大简化了代码逻辑。
C++ 中的多态分为两大类:编译时多态(静态多态) 和运行时多态(动态多态),二者的核心区别在于 “行为确定的时机” 不同。
编译时多态是指在编译阶段就确定了函数的调用关系,行为结果在编译时已经明确。它的实现方式主要有两种:
示例:函数重载实现静态多态
#include <iostream>
using namespace std;
// 函数重载:参数类型不同
int Add(int a, int b) {
cout << "int Add: ";
return a + b;
}
double Add(double a, double b) {
cout << "double Add: ";
return a + b;
}
// 函数重载:参数个数不同
int Add(int a, int b, int c) {
cout << "int Add(3 params): ";
return a + b + c;
}
int main() {
cout << Add(1, 2) << endl; // 调用int Add(int, int)
cout << Add(1.5, 2.5) << endl; // 调用double Add(double, double)
cout << Add(1, 2, 3) << endl; // 调用int Add(int, int, int)
return 0;
}运行结果:
int Add: 3
double Add: 4
int Add(3 params): 6静态多态的特点是效率高(编译时确定调用地址,无运行时开销),但灵活性差(必须在编译时明确所有可能的行为,无法适应运行时动态变化的场景)。
运行时多态是指在程序运行阶段才确定函数的调用关系,行为结果取决于运行时的对象类型。它是 C++ 多态的核心,也是本文重点讲解的内容。
示例:运行时多态的直观体现
#include <iostream>
using namespace std;
// 基类:人
class Person {
public:
// 虚函数:买票
virtual void BuyTicket() {
cout << "普通人买票:全价" << endl;
}
};
// 派生类:学生(继承自Person)
class Student : public Person {
public:
// 重写基类虚函数
virtual void BuyTicket() {
cout << "学生买票:半价(硬座)/75折(高铁二等座)" << endl;
}
};
// 派生类:军人(继承自Person)
class Soldier : public Person {
public:
// 重写基类虚函数
virtual void BuyTicket() {
cout << "军人买票:优先购票" << endl;
}
};
// 统一接口:调用买票行为
void DoBuyTicket(Person& people) {
people.BuyTicket(); // 同一调用语句,不同对象表现不同
}
int main() {
Person p;
Student s;
Soldier sol;
DoBuyTicket(p); // 输出:普通人买票:全价
DoBuyTicket(s); // 输出:学生买票:半价(硬座)/75折(高铁二等座)
DoBuyTicket(sol); // 输出:军人买票:优先购票
return 0;
}运行结果:
普通人买票:全价
学生买票:半价(硬座)/75折(高铁二等座)
军人买票:优先购票 在这个示例中,DoBuyTicket函数接收Person类型的引用,但传入不同的派生类对象时,会执行对应的BuyTicket方法。这种 “同一接口,多种实现” 的效果,正是运行时多态的核心体现。它的特点是灵活性高(支持动态扩展,新增派生类无需修改原有接口代码),但有轻微运行时开销(需要在运行时查找函数地址)。
想要实现 C++ 运行时多态,必须满足三个核心条件,缺一不可。很多开发者在使用多态时出现问题,本质上都是没有完全满足这三个条件。
多态必须建立在类的继承体系之上,即存在基类(父类)和派生类(子类)的继承关系。派生类通过继承基类,获得基类的接口(虚函数),并可以根据自身需求重写该接口。
需要注意:

虚函数是多态的 “开关”,在基类的成员函数前加上virtual关键字,该函数就成为虚函数。
语法格式:
class 基类名 {
public:
virtual 返回值类型 函数名(参数列表) {
// 函数实现
}
};注意事项:
virtual关键字仅需在基类声明时添加,派生类重写时可加可不加,但建议加上,能够提高代码可读性;static修饰)、构造函数不能声明为虚函数;虚函数的重写(也叫覆盖)是指:派生类中有一个与基类虚函数完全相同的函数,即满足 “三同” 原则:
示例:虚函数重写的正确实现
#include <iostream>
using namespace std;
class Animal {
public:
// 基类虚函数
virtual void Talk() const {
cout << "动物发出声音" << endl;
}
};
class Cat : public Animal {
public:
// 重写基类虚函数:三同原则
virtual void Talk() const {
cout << "(>^ω^<)喵~" << endl;
}
};
class Dog : public Animal {
public:
// 重写基类虚函数:三同原则(派生类可省略virtual,但不推荐)
void Talk() const { // 仍构成重写,因为继承了基类虚函数属性
cout << "汪汪汪!" << endl;
}
};
// 统一接口
void LetHear(const Animal& animal) {
animal.Talk();
}
int main() {
Cat cat;
Dog dog;
LetHear(cat); // 输出:(>^ω^<)喵~
LetHear(dog); // 输出:汪汪汪!
return 0;
}常见错误:不满足三同原则,导致重写失败
// 错误示例1:参数列表不同
class Animal {
public:
virtual void Eat(string food) { // 参数为string类型
cout << "动物吃" << food << endl;
}
};
class Rabbit : public Animal {
public:
virtual void Eat(const char* food) { // 参数为const char*类型,不满足三同
cout << "兔子吃" << food << endl;
}
};
// 错误示例2:返回值类型不同(非协变)
class Animal {
public:
virtual int GetAge() { // 返回int类型
return 0;
}
};
class Bird : public Animal {
public:
virtual double GetAge() { // 返回double类型,不满足三同(非协变)
return 1.5;
}
};在上述错误的示例中,派生类的函数与基类虚函数不满足 “三同” 原则,无法构成重写,因此也就无法实现多态。
多态的触发必须通过基类的指针或基类的引用来调用虚函数,直接通过对象本身调用虚函数无法触发多态。
示例:不同调用方式的对比
#include <iostream>
using namespace std;
class Person {
public:
virtual void BuyTicket() {
cout << "普通人买票:全价" << endl;
}
};
class Student : public Person {
public:
virtual void BuyTicket() {
cout << "学生买票:半价" << endl;
}
};
int main() {
Person p;
Student s;
// 1. 直接通过对象调用:无多态
p.BuyTicket(); // 输出:普通人买票:全价
s.BuyTicket(); // 输出:学生买票:半价(这是直接调用派生类函数,非多态)
// 2. 基类指针调用:触发多态
Person* p1 = &p;
Person* p2 = &s;
p1->BuyTicket(); // 输出:普通人买票:全价
p2->BuyTicket(); // 输出:学生买票:半价(多态生效)
// 3. 基类引用调用:触发多态
Person& r1 = p;
Person& r2 = s;
r1.BuyTicket(); // 输出:普通人买票:全价
r2.BuyTicket(); // 输出:学生买票:半价(多态生效)
return 0;
}为什么必须用基类指针 / 引用?
结合以上三个条件,下面给出一个完整的多态实现示例,涵盖继承、虚函数重写、基类指针 / 引用调用三个核心要素:
#include <iostream>
#include <string>
using namespace std;
// 基类:交通工具
class Vehicle {
public:
Vehicle(string name) : _name(name) {}
// 虚函数:行驶
virtual void Run() {
cout << _name << ":正在行驶" << endl;
}
// 虚函数:鸣笛
virtual void Honk() {
cout << _name << ":鸣笛警告" << endl;
}
protected:
string _name; // 交通工具名称
};
// 派生类:汽车
class Car : public Vehicle {
public:
Car(string name) : Vehicle(name) {}
// 重写Run函数
virtual void Run() {
cout << _name << ":在公路上匀速行驶,速度60km/h" << endl;
}
// 重写Honk函数
virtual void Honk() {
cout << _name << ":嘀嘀嘀~" << endl;
}
};
// 派生类:飞机
class Plane : public Vehicle {
public:
Plane(string name) : Vehicle(name) {}
// 重写Run函数
virtual void Run() {
cout << _name << ":在蓝天上飞行,高度10000米" << endl;
}
// 重写Honk函数
virtual void Honk() {
cout << _name << ":呜呜呜~(航空警报)" << endl;
}
};
// 派生类:轮船
class Ship : public Vehicle {
public:
Ship(string name) : Vehicle(name) {}
// 重写Run函数
virtual void Run() {
cout << _name << ":在海面上航行,航向正东" << endl;
}
// 重写Honk函数
virtual void Honk() {
cout << _name << ":嘟嘟嘟~(雾笛)" << endl;
}
};
// 统一接口:控制交通工具运行和鸣笛
void ControlVehicle(Vehicle* vehicle) {
vehicle->Run(); // 多态调用Run函数
vehicle->Honk(); // 多态调用Honk函数
cout << "------------------------" << endl;
}
int main() {
// 创建不同交通工具对象
Car car("家用轿车");
Plane plane("民航客机");
Ship ship("远洋货轮");
// 通过基类指针调用,触发多态
ControlVehicle(&car);
ControlVehicle(&plane);
ControlVehicle(&ship);
return 0;
}运行结果如下:
家用轿车:在公路上匀速行驶,速度60km/h
家用轿车:嘀嘀嘀~
------------------------
民航客机:在蓝天上飞行,高度10000米
民航客机:呜呜呜~(航空警报)
------------------------
远洋货轮:在海面上航行,航向正东
远洋货轮:嘟嘟嘟~(雾笛)
------------------------ 从运行结果可以看出,ControlVehicle函数通过基类指针Vehicle*接收不同的派生类对象,却能正确调用各自的Run和Honk方法,实现了 “一个接口,多种实现” 的多态效果。如果后续需要新增 “高铁”“自行车” 等交通工具,只需新增派生类并重写虚函数,无需修改ControlVehicle函数,这大大提高了代码的可扩展性。
在实际开发和面试中,虚函数重写还有很多容易混淆的进阶细节,比如协变、析构函数重写、override 和 final 关键字等。这些细节既是重点,也是高频考点,大家也需要深入理解。
我在前面提到,虚函数重写需要满足 “三同” 原则,其中返回值类型必须相同。但存在一种特殊情况 ——协变,允许派生类虚函数的返回值类型与基类虚函数不同。
协变是指:基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或引用(派生类是基类的子类)。这种情况下,即使返回值类型不同,也构成虚函数重写。
示例:协变的实现
#include <iostream>
using namespace std;
// 基类:A
class A {};
// 派生类:B(继承自A)
class B : public A {};
// 基类:Person
class Person {
public:
// 基类虚函数:返回A*
virtual A* BuyTicket() {
cout << "普通人买票:全价" << endl;
return new A();
}
};
// 派生类:Student
class Student : public Person {
public:
// 派生类虚函数:返回B*(B是A的子类),构成协变
virtual B* BuyTicket() {
cout << "学生买票:半价" << endl;
return new B();
}
};
int main() {
Person p;
Student s;
Person* p1 = &p;
Person* p2 = &s;
delete p1->BuyTicket(); // 输出:普通人买票:全价
delete p2->BuyTicket(); // 输出:学生买票:半价(多态生效)
return 0;
}析构函数的重写是面试中最高频的考点之一。很多开发者在使用多态时,容易忽略析构函数的虚函数声明,导致派生类对象的资源无法释放,引发内存泄漏。
下面通过一个问题场景来引入对析构函数重写的介绍——未声明虚析构函数:
#include <iostream>
using namespace std;
class Base {
public:
Base() {
cout << "Base::构造函数" << endl;
_p = new int[10]; // 动态分配内存
}
// 非虚析构函数
~Base() {
cout << "Base::析构函数" << endl;
delete[] _p; // 释放内存
}
private:
int* _p;
};
class Derive : public Base {
public:
Derive() {
cout << "Derive::构造函数" << endl;
_q = new char[100]; // 动态分配内存
}
~Derive() {
cout << "Derive::析构函数" << endl;
delete[] _q; // 释放内存
}
private:
char* _q;
};
int main() {
Base* p = new Derive(); // 基类指针指向派生类对象
delete p; // 释放对象
return 0;
}运行结果:
Base::构造函数
Derive::构造函数
Base::析构函数问题分析:
delete p时,由于基类Base的析构函数不是虚函数,编译器根据基类指针的静态类型(Base*)调用基类的析构函数;Derive的析构函数没有被调用,导致_q指向的内存无法释放,引发内存泄漏。解决方案:声明虚析构函数
将基类的析构函数声明为虚函数,派生类的析构函数会自动构成重写,从而触发多态析构。
#include <iostream>
using namespace std;
class Base {
public:
Base() {
cout << "Base::构造函数" << endl;
_p = new int[10];
}
// 虚析构函数
virtual ~Base() {
cout << "Base::析构函数" << endl;
delete[] _p;
}
private:
int* _p;
};
class Derive : public Base {
public:
Derive() {
cout << "Derive::构造函数" << endl;
_q = new char[100];
}
// 派生类析构函数:自动重写基类虚析构
~Derive() {
cout << "Derive::析构函数" << endl;
delete[] _q;
}
private:
char* _q;
};
int main() {
Base* p = new Derive();
delete p;
return 0;
}运行结果:
Base::构造函数
Derive::构造函数
Derive::析构函数
Base::析构函数原理说明:
destructor;virtual后,派生类析构函数会自动重写该虚函数;delete p时,通过基类指针调用虚析构函数,触发多态,先调用派生类析构函数(释放派生类资源),再调用基类析构函数(释放基类资源),避免内存泄漏。因此,只要类可能被继承,并且可能通过基类指针删除派生类对象,就必须将基类的析构函数声明为虚函数;若类不会被继承,或不会通过基类指针删除派生类对象,可不用声明虚析构函数(因为虚函数会增加对象内存开销)。
C++11 引入了override和final两个关键字,用于解决虚函数重写中的 “隐式错误”,增强代码的可读性和安全性。
override关键字用于派生类的虚函数,表示该函数意图重写基类的虚函数。编译器会检查是否满足重写条件,若不满足则编译报错。
示例:override 的使用
#include <iostream>
using namespace std;
class Car {
public:
// 基类虚函数:注意函数名是Drive(拼写正确)
virtual void Drive() {
cout << "汽车:基本行驶功能" << endl;
}
};
class Benz : public Car {
public:
// 意图重写Drive函数,但拼写错误(Dirve)
virtual void Dirve() override { // 编译报错:没有重写任何基类方法
cout << "奔驰:舒适行驶" << endl;
}
};
int main() {
Benz benz;
return 0;
}VS 编译器下的编译错误信息:
error C3668: “Benz::Dirve”: 包含重写说明符“override”的方法没有重写任何基类方法作用:
final关键字用于基类的虚函数,表示该函数禁止被任何派生类重写;也可用于类,表示该类禁止被继承。
示例 1:禁止虚函数被重写
#include <iostream>
using namespace std;
class Car {
public:
// final修饰虚函数:禁止派生类重写
virtual void Drive() final {
cout << "汽车:基本行驶功能" << endl;
}
};
class BMW : public Car {
public:
// 试图重写被final修饰的函数,编译报错
virtual void Drive() {
cout << "宝马:操控性行驶" << endl;
}
};
int main() {
BMW bmw;
return 0;
}VS 编译器下的编译错误信息:
error C3248: “Car::Drive”: 声明为“final”的函数无法被“BMW::Drive”重写示例 2:禁止类被继承
#include <iostream>
using namespace std;
// final修饰类:禁止被继承
class Car final {
public:
virtual void Drive() {
cout << "汽车:基本行驶功能" << endl;
}
};
// 试图继承被final修饰的类,编译报错
class Audi : public Car {
public:
virtual void Drive() {
cout << "奥迪:科技感行驶" << endl;
}
};
int main() {
Audi audi;
return 0;
}VS 编译器下的编译错误信息:
error C3246: “Audi”: 无法从“Car”继承,因为它已被声明为“final”作用:
C++ 中函数的重载、重写、隐藏是三个容易混淆的概念,面试中经常会以选择题或简答题的形式考察。通过下面这张图来详细对比三者的区别:

#include <iostream>
using namespace std;
class Base {
public:
// 1. 重载:同一作用域,参数列表不同
void Func(int a) {
cout << "Base::Func(int): " << a << endl;
}
void Func(double a) {
cout << "Base::Func(double): " << a << endl;
}
// 虚函数:用于重写
virtual void Show() {
cout << "Base::Show()" << endl;
}
// 普通函数:用于隐藏
void Display() {
cout << "Base::Display()" << endl;
}
};
class Derive : public Base {
public:
// 2. 重写:基类虚函数,三同原则
virtual void Show() {
cout << "Derive::Show()" << endl;
}
// 3. 隐藏:与基类同名,不满足重写条件
void Display() {
cout << "Derive::Display()" << endl;
}
// 隐藏:与基类Func同名,但参数列表不同(非重载,因为作用域不同)
void Func(const char* str) {
cout << "Derive::Func(const char*): " << str << endl;
}
};
int main() {
Derive d;
// 测试重载:调用Base类的重载函数
d.Func(10); // 输出:Base::Func(int): 10
d.Func(3.14); // 输出:Base::Func(double): 3.14
// 测试隐藏:调用Derive类的Func(隐藏基类Func)
d.Func("hello"); // 输出:Derive::Func(const char*): hello
// 测试重写:基类指针指向派生类,多态调用
Base* p = &d;
p->Show(); // 输出:Derive::Show()(重写,多态)
// 测试隐藏:基类指针调用基类Display,派生类对象调用派生类Display
p->Display(); // 输出:Base::Display()(隐藏,静态绑定)
d.Display(); // 输出:Derive::Display()(隐藏,静态绑定)
return 0;
}运行结果:
Base::Func(int): 10
Base::Func(double): 3.14
Derive::Func(const char*): hello
Derive::Show()
Base::Display()
Derive::Display()结论:
在实际开发中,有些基类只需要定义接口,不需要实现具体功能,具体功能由派生类实现。这时就需要用到纯虚函数和抽象类。
纯虚函数是指在基类中声明的、没有具体实现的虚函数,语法格式为在虚函数声明后加上=0。
语法格式:
class 基类名 {
public:
virtual 返回值类型 函数名(参数列表) = 0; // 纯虚函数
};需要注意以下两点:
抽象类是一种特殊的类,具有以下核心特性:
示例:纯虚函数和抽象类的使用
#include <iostream>
#include <string>
using namespace std;
// 抽象类:形状(包含纯虚函数)
class Shape {
public:
Shape(string name) : _name(name) {}
// 纯虚函数:计算面积(仅声明,无实现)
virtual double CalculateArea() = 0;
// 纯虚函数:计算周长(仅声明,无实现)
virtual double CalculatePerimeter() = 0;
// 普通成员函数:显示形状名称
void ShowName() {
cout << "形状:" << _name << endl;
}
protected:
string _name;
};
// 派生类:圆形(重写所有纯虚函数)
class Circle : public Shape {
public:
Circle(string name, double radius) : Shape(name), _radius(radius) {}
// 重写纯虚函数:计算面积(圆面积=πr²)
virtual double CalculateArea() {
return 3.14159 * _radius * _radius;
}
// 重写纯虚函数:计算周长(圆周长=2πr)
virtual double CalculatePerimeter() {
return 2 * 3.14159 * _radius;
}
private:
double _radius; // 半径
};
// 派生类:矩形(重写所有纯虚函数)
class Rectangle : public Shape {
public:
Rectangle(string name, double length, double width)
: Shape(name), _length(length), _width(width) {}
// 重写纯虚函数:计算面积(矩形面积=长×宽)
virtual double CalculateArea() {
return _length * _width;
}
// 重写纯虚函数:计算周长(矩形周长=2×(长+宽))
virtual double CalculatePerimeter() {
return 2 * (_length + _width);
}
private:
double _length; // 长
double _width; // 宽
};
// 派生类:三角形(未重写所有纯虚函数,仍为抽象类)
class Triangle : public Shape {
public:
Triangle(string name, double a, double b, double c)
: Shape(name), _a(a), _b(b), _c(c) {}
// 仅重写一个纯虚函数,另一个未重写
virtual double CalculatePerimeter() {
return _a + _b + _c;
}
private:
double _a, _b, _c; // 三边长
};
// 统一接口:计算并显示形状的面积和周长
void ShowShapeInfo(Shape* shape) {
shape->ShowName();
cout << "面积:" << shape->CalculateArea() << endl;
cout << "周长:" << shape->CalculatePerimeter() << endl;
cout << "------------------------" << endl;
}
int main() {
// 1. 抽象类不能实例化对象(编译报错)
// Shape shape("未知形状");
// 2. 派生类(重写所有纯虚函数)可以实例化
Circle circle("圆形", 5.0);
Rectangle rect("矩形", 4.0, 6.0);
// 3. 未重写所有纯虚函数的派生类不能实例化(编译报错)
// Triangle tri("三角形", 3.0, 4.0, 5.0);
// 4. 抽象类指针指向派生类对象,实现多态
ShowShapeInfo(&circle);
ShowShapeInfo(&rect);
return 0;
}运行结果:
形状:圆形
面积:78.5397
周长:31.4159
------------------------
形状:矩形
面积:24
周长:20
------------------------抽象类的核心价值是定义统一接口,强制派生类实现特定功能,常见应用场景包括:
例如,在图形处理软件中,抽象类Shape定义了所有图形必须具备的 “计算面积” 和 “计算周长” 接口,后续新增 “正方形”“椭圆形” 等图形时,只需继承Shape并重写纯虚函数,即可无缝集成到现有代码中,无需修改原有接口逻辑。
很多人在使用多态的时候,只知道 “怎么用”,却不知道 “为什么能这样用”。理解多态的底层原理,不仅能帮助我们更灵活地使用多态,也是面试中的核心考点(比如 “虚函数表是什么?”“动态绑定的过程是怎样的?”等)。
在 C++ 中,当一个类包含虚函数时,编译器会为该类的每个对象添加一个隐藏的成员变量 ——虚函数表指针(virtual function table pointer),简写为__vfptr。
__vfptr是一个指针,指向一个存储虚函数地址的数组,这个数组称为虚函数表(virtual function table),简称虚表(vtable);下面通过示例验证包含虚函数的类的对象大小(以 32 位系统为例):
#include <iostream>
using namespace std;
// 普通类(无虚函数)
class Base1 {
public:
void Func() {}
private:
int _a = 1;
char _ch = 'x';
};
// 包含虚函数的类
class Base2 {
public:
virtual void Func() {}
private:
int _a = 1;
char _ch = 'x';
};
int main() {
cout << "Base1对象大小:" << sizeof(Base1) << endl; // 输出:8(int占4字节,char占1字节,内存对齐为8字节)
cout << "Base2对象大小:" << sizeof(Base2) << endl; // 输出:12(4字节__vfptr + 4字节int + 1字节char,内存对齐为12字节)
return 0;
}运行结果(32 位系统):
Base1对象大小:8
Base2对象大小:12分析:
Base1无虚函数,对象大小由成员变量决定(int 4 字节 + char 1 字节,内存对齐为 8 字节);Base2包含虚函数,对象大小 = 虚函数表指针(4 字节) + 成员变量大小(8 字节),内存对齐后为 12 字节。这证明了包含虚函数的类的对象中,确实存在一个隐藏的虚函数表指针。
虚函数表是一个存储虚函数地址的数组,其结构取决于类的继承关系和虚函数重写情况。
基类包含虚函数时,编译器会为其生成一张虚表,虚表中存储基类所有虚函数的地址。
示例:基类虚表结构
#include <iostream>
using namespace std;
class Base {
public:
virtual void Func1() { cout << "Base::Func1" << endl; }
virtual void Func2() { cout << "Base::Func2" << endl; }
private:
int _a = 1;
};
int main() {
Base b;
// 通过内存查看虚表结构(32位系统)
// b的内存布局:__vfptr(4字节) + _a(4字节)
// __vfptr指向虚表,虚表中存储Func1和Func2的地址,末尾可能有nullptr标记(编译器相关)
return 0;
}基类虚表的结构示意如下:
Base的虚表(vtable for Base):
[0] → &Base::Func1
[1] → &Base::Func2
[2] → nullptr(VS编译器标记,g++无此标记)派生类继承基类后,会继承基类的虚表指针和虚表。当派生类重写基类的虚函数时,会将虚表中对应基类虚函数的地址替换为派生类重写函数的地址;同时,派生类新增的虚函数会被添加到虚表的末尾。
示例:派生类虚表结构
#include <iostream>
using namespace std;
class Base {
public:
virtual void Func1() { cout << "Base::Func1" << endl; }
virtual void Func2() { cout << "Base::Func2" << endl; }
private:
int _a = 1;
};
class Derive : public Base {
public:
// 重写基类Func1
virtual void Func1() { cout << "Derive::Func1" << endl; }
// 新增虚函数Func3
virtual void Func3() { cout << "Derive::Func3" << endl; }
private:
int _b = 2;
};
int main() {
Derive d;
// d的内存布局:__vfptr(4字节) + 继承的_a(4字节) + 自身的_b(4字节)
// __vfptr指向派生类的虚表,虚表中Func1被替换为Derive::Func1,新增Func3
return 0;
}派生类虚表的结构示意如下:
Derive的虚表(vtable for Derive):
[0] → &Derive::Func1(重写,替换基类Func1地址)
[1] → &Base::Func2(未重写,继承基类Func2地址)
[2] → &Derive::Func3(新增,添加到虚表末尾)
[3] → nullptr(VS编译器标记)虚函数表存储在代码段(常量区),而非堆或栈中。可以通过以下代码验证:
#include <iostream>
using namespace std;
class Base {
public:
virtual void Func1() {}
};
class Derive : public Base {
public:
virtual void Func1() {}
};
int main() {
// 栈:局部变量
int i = 0;
// 静态区:静态变量
static int j = 1;
// 堆:动态分配内存
int* pHeap = new int;
// 常量区:字符串常量
const char* pConst = "hello";
Base b;
Derive d;
Base* pBase = &b;
Derive* pDerive = &d;
// 输出各区域地址
cout << "栈地址:" << &i << endl;
cout << "静态区地址:" << &j << endl;
cout << "堆地址:" << pHeap << endl;
cout << "常量区地址:" << (void*)pConst << endl;
cout << "Base虚表地址:" << *(int*)pBase << endl; // 虚表指针指向的地址即虚表地址
cout << "Derive虚表地址:" << *(int*)pDerive << endl;
delete pHeap;
return 0;
}运行结果(32 位系统):
栈地址:0x0019FF3C
静态区地址:0x0041D000
堆地址:0x0042D740
常量区地址:0x0041ABA4
Base虚表地址:0x0041AB44
Derive虚表地址:0x0041AB84我们也可以通过VS中的监视窗口和内存窗口进行观察:


分析:
多态的实现本质是动态绑定,而普通函数调用是静态绑定。二者的核心区别在于函数地址的确定时机。
静态绑定是指在编译阶段就确定函数的调用地址,直接生成函数调用指令。
适用场景:
示例:静态绑定的汇编代码分析
#include <iostream>
using namespace std;
class Base {
public:
void Func() { cout << "Base::Func" << endl; } // 普通函数
};
int main() {
Base b;
b.Func(); // 静态绑定
return 0;
}对应的汇编代码(VS 编译器,32 位)如下:
0041141E lea ecx,[b]
00411421 call Base::Func (04110ACh) // 直接调用Func的地址04110AC 分析:编译时直接确定Func的地址(04110AC),生成call指令调用该地址,无运行时开销。
动态绑定是指在运行阶段通过虚函数表指针查找虚函数地址,再调用函数。
适用场景:通过基类指针 / 引用调用虚函数(满足多态条件)。
示例:动态绑定的汇编代码分析
#include <iostream>
using namespace std;
class Base {
public:
virtual void Func() { cout << "Base::Func" << endl; } // 虚函数
};
class Derive : public Base {
public:
virtual void Func() { cout << "Derive::Func" << endl; } // 重写虚函数
};
void CallFunc(Base* p) {
p->Func(); // 动态绑定
}
int main() {
Derive d;
CallFunc(&d);
return 0;
}对应的汇编代码(VS 编译器,32 位)如下:
void CallFunc(Base* p) {
00411450 push ebp
00411451 mov ebp,esp
00411453 push esi
00411454 mov esi,dword ptr [p] // esi = p(基类指针)
p->Func();
00411457 mov eax,dword ptr [esi] // eax = p->__vfptr(虚表指针)
00411459 mov edx,dword ptr [eax] // edx = 虚表[0](Func的地址)
0041145B mov ecx,dword ptr [p] // ecx = p(this指针)
0041145E call edx // 调用edx中的地址(Derive::Func)
00411460 pop esi
00411461 pop ebp
00411462 ret
}动态绑定的过程如下:
p指向的对象中的虚表指针__vfptr(eax = *p);edx = *eax,即虚表第 0 个元素);call edx);p指向Derive对象,虚表中存储的是Derive::Func的地址,因此最终调用Derive::Func。这就是多态的底层实现原理:通过虚函数表指针和虚表,在运行时动态查找函数地址,实现 “同一接口,多种实现”。
多态的灵活性是以轻微的性能开销为代价的,主要体现在两个方面:
__vfptr → 虚表 → 虚函数地址)。但在大多数场景下,这种开销是可以忽略不计的。只有在对性能要求极高的核心模块(如高频调用的函数),才需要考虑是否使用多态。
多态是 C++ 面试的重中之重,以下整理了常见的面试题及详细解析,帮助读者应对面试。
题目:以下程序的输出结果是什么?( )
#include <iostream>
using namespace std;
class A {
public:
virtual void func(int val = 1) { cout << "A->" << val << endl; }
virtual void test() { func(); }
};
class B : public A {
public:
void func(int val = 0) { cout << "B->" << val << endl; }
};
int main() {
B* p = new B;
p->test();
delete p;
return 0;
}选项:A. A->0 B. B->1 C. A->1 D. B->0 E. 编译出错 F. 以上都不正确
解析:
A::func是虚函数,B::func与A::func满足三同原则(函数名、参数列表、返回值相同),构成重写;p是B*类型,调用test()函数(继承自A),test()中调用func();func()是虚函数,通过this指针(B*类型)调用,触发多态,调用B::func;A::func的默认参数是 1,因此val的值为 1;B->1。因此最终答案为:B
题目:C++ 中运行时多态的实现需要满足哪些条件?
解析:
virtual修饰);题目:什么是虚函数表?虚函数表存储在内存的哪个区域?
解析:
__vfptr),指向该类的虚表;题目:为什么基类的析构函数建议声明为虚函数?如果不声明会有什么问题?
解析:
题目:利用 C++ 多态实现一个计算器,支持加法、减法、乘法、除法运算,要求可以灵活扩展新的运算(如取模、平方)。
解析:
Calculator,声明纯虚函数Calculate(统一接口);Add、Subtract、Multiply、Divide,分别重写Calculate函数,实现对应运算;Calculate,无需修改原有代码。实现代码:
#include <iostream>
#include <stdexcept>
using namespace std;
// 抽象基类:计算器
class Calculator {
public:
Calculator(double a, double b) : _a(a), _b(b) {}
virtual ~Calculator() {}
// 纯虚函数:计算接口
virtual double Calculate() const = 0;
// 获取操作数
double GetA() const { return _a; }
double GetB() const { return _b; }
protected:
double _a; // 操作数1
double _b; // 操作数2
};
// 派生类:加法
class Add : public Calculator {
public:
Add(double a, double b) : Calculator(a, b) {}
virtual double Calculate() const {
return GetA() + GetB();
}
};
// 派生类:减法
class Subtract : public Calculator {
public:
Subtract(double a, double b) : Calculator(a, b) {}
virtual double Calculate() const {
return GetA() - GetB();
}
};
// 派生类:乘法
class Multiply : public Calculator {
public:
Multiply(double a, double b) : Calculator(a, b) {}
virtual double Calculate() const {
return GetA() * GetB();
}
};
// 派生类:除法
class Divide : public Calculator {
public:
Divide(double a, double b) : Calculator(a, b) {
if (b == 0) {
throw invalid_argument("除数不能为0");
}
}
virtual double Calculate() const {
return GetA() / GetB();
}
};
// 派生类:取模(新增运算,无需修改原有代码)
class Mod : public Calculator {
public:
Mod(int a, int b) : Calculator(a, b) {
if (b == 0) {
throw invalid_argument("模数不能为0");
}
}
virtual double Calculate() const {
return static_cast<int>(GetA()) % static_cast<int>(GetB());
}
};
// 统一接口:执行计算并输出结果
void DoCalculate(const Calculator& calc, const string& opName) {
cout << calc.GetA() << " " << opName << " " << calc.GetB() << " = " << calc.Calculate() << endl;
}
int main() {
try {
Add add(10, 5);
Subtract sub(10, 5);
Multiply mul(10, 5);
Divide div(10, 5);
Mod mod(10, 3);
DoCalculate(add, "+"); // 输出:10 + 5 = 15
DoCalculate(sub, "-"); // 输出:10 - 5 = 5
DoCalculate(mul, "*"); // 输出:10 * 5 = 50
DoCalculate(div, "/"); // 输出:10 / 5 = 2
DoCalculate(mod, "%"); // 输出:10 % 3 = 1
// 测试除数为0的异常
// Divide div2(10, 0);
} catch (const exception& e) {
cout << "错误:" << e.what() << endl;
}
return 0;
}运行结果:
10 + 5 = 15
10 - 5 = 5
10 * 5 = 50
10 / 5 = 2
10 % 3 = 1多态是 C++ 面向对象编程的灵魂,掌握多态的概念、实现和原理,不仅能写出更灵活、可维护的代码,也是成为高级 C++ 开发者的必备技能。建议大家结合本文的示例代码反复练习,深入理解每个细节,尤其是虚函数表和动态绑定的底层逻辑,应对面试时才能游刃有余。我们下期再见!