解密C++多态:一篇文章掌握精髓
目录
1. 什么是多态?
1.1 C++中的多态分为两类
1.2 生活中的多态例子
2. 多态的实现条件
2.1 虚函数与重写
2.1.1 虚函数(Virtual Function)
2.1.2 虚函数的重写(Override)
2.2 多态调用示例
3. 多态的实现原理
3.1 虚函数表指针(vptr)
4.2虚函数表
4. 动态绑定与静态绑定
5. 特殊情况与关键字
5.1 协变(Covariant)
5.2 析构函数的重写
5.3 override 和 final 关键字
6. 重载、重写、隐藏的对比
7. 纯虚函数与抽象类
8. 总结
1. 什么是多态?
多态(Polymorphism) 是面向对象编程的三大特性之一(另外两个是封装和继承)。通俗来讲,多态就是“一个接口,多种实现”。即同一个行为或函数,在不同对象上表现出不同的形态。
1.1 C++中的多态分为两类
-
编译时多态(静态多态):包括函数重载和模板。在编译阶段就确定了调用哪个函数。
-
运行时多态(动态多态):通过虚函数机制实现,在程序运行时根据对象的实际类型来决定调用哪个函数。
本文将重点讲解运行时多态。
1.2 生活中的多态例子
-
买票行为:普通人全价,学生半价,军人优先。
-
动物叫声:猫发出“喵”,狗发出“汪汪”。
2. 多态的实现条件
要实现运行时多态,必须满足以下两个条件:
-
必须使用基类的指针或引用来调用虚函数
-
被调用的函数必须是虚函数,并且在派生类中完成了重写(覆盖)
说明:要实现多态效果,第一必须是基类的指针或引用,因为只有基类的指针或引用才能既指向基类对象又指向派生类对象;第二派生类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派生类之间才能有不同的函数,多态的不同形态效果才能达到。
2.1 虚函数与重写
2.1.1 虚函数(Virtual Function)
在成员函数前加上 virtual 关键字,该函数就成为虚函数。注意非成员函数不能加virtual修饰。
class Person {
public:virtual void BuyTicket() {cout << "买票-全价" << endl;}
};
2.1.2 虚函数的重写(Override)
派生类中定义一个与基类虚函数完全相同(函数名、参数列表、返回值相同)的虚函数,称为重写或覆盖。
class Student : public Person {
public:virtual void BuyTicket() {cout << "买票-半价" << endl;}
};
2.2 多态调用示例
多态是面向对象编程的重要特性之一,它允许我们通过基类指针或引用来调用派生类的函数。下面是一个完整的多态调用示例:
class Shape {
public:virtual double area() const = 0; // 纯虚函数
};class Circle : public Shape {double radius;
public:Circle(double r) : radius(r) {}double area() const override {return 3.14159 * radius * radius;}
};class Rectangle : public Shape {double width, height;
public:Rectangle(double w, double h) : width(w), height(h) {}double area() const override {return width * height;}
};void printArea(const Shape& shape) {cout << "面积: " << shape.area() << endl;
}int main() {Circle c(5.0);Rectangle r(4.0, 6.0);printArea(c); // 输出圆的面积printArea(r); // 输出矩形的面积return 0;
}
这个示例展示了:
-
纯虚函数和抽象类
-
通过引用实现多态
-
带参数的多态方法调用
3. 多态的实现原理
3.1 虚函数表指针(vptr)
每个含有虚函数的类(或有虚函数的基类)都会有一个虚函数表指针,指向该类的虚函数表。虚表中存放的是该类所有虚函数的地址。
class Base {
public:virtual void func1() {}virtual void func2() {}int a = 1;
};int main() {Base b;cout << sizeof(b) << endl; // 输出可能是12(32位系统)return 0;
}
上面代码运行结果12bytes,除了_b和_ch成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关)。对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中至少都有一个虚函数表指针,因为一个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。
4.2虚函数表
-
基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共用同一张虚表,不同类型的对象各自有独立的虚表,所以基类和派生类有各自独立的虚表。
-
派生类由两部分构成,继承下来的基类和自己的成员,一般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但是要注意的这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个,就像基类对象的成员和派生类对象中的基类对象成员也是独立的。
-
派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。
-
派生类的虚函数表中包含:(1)基类的虚函数地址,(2)派生类重写的虚函数地址完成覆盖,派生类自己的虚函数地址三个部分。
-
虚函数表本质是一个存虚函数指针的指针数组,一般情况下这个数组最后面放了一个0x00000000标记。(这个C++并没有进行规定,各个编译器自行定义的,vs系列编译器会再后面放个0x00000000标记,g++系列编译不会放)
-
虚函数存在哪的?虚函数和普通函数一样,编译好后是一段指令,都是存在代码段的,只是虚函数的地址又存到了虚表中。
-
虚函数表存在哪的?这个问题严格说并没有标准答案,C++标准并没有规定,我们写下面的代码可以对比验证一下。vs下是存在代码段(常量区)

class Derive : public Base {
public:virtual void func1() override { cout << "Derive::func1"; }virtual void func3() { cout << "Derive::func3"; }
};
4. 动态绑定与静态绑定
-
静态绑定:在编译时确定函数地址,如普通函数调用、重载函数。
-
动态绑定:在运行时通过虚函数表查找函数地址,实现多态。
// ptr是指针+BuyTicket是虚函数满⾜多态条件。// 这⾥就是动态绑定,编译在运⾏时到ptr指向对象的虚函数表中确定调⽤函数地址
ptr->BuyTicket();
00EF2001 mov eax,dword ptr [ptr]
00EF2004 mov edx,dword ptr [eax]
00EF2006 mov esi,esp
00EF2008 mov ecx,dword ptr [ptr]
00EF200B mov eax,dword ptr [edx]
00EF200D call eax// BuyTicket不是虚函数,不满⾜多态条件。// 这⾥就是静态绑定,编译器直接确定调⽤函数地址ptr->BuyTicket();
00EA2C91 mov ecx,dword ptr [ptr]
00EA2C94 call Student::Student (0EA153Ch)
5. 特殊情况与关键字
5.1 协变(Covariant)
允许派生类虚函数的返回值类型为基类虚函数返回类型的派生类指针或引用。
class A {};
class B : public A {};class Person {
public:virtual A* BuyTicket() { return nullptr; }
};class Student : public Person {
public:virtual B* BuyTicket() override { return nullptr; }
};
5.2 析构函数的重写
建议将基类的析构函数声明为虚函数,否则通过基类指针删除派生类对象时,只会调用基类析构函数,导致内存泄漏。
class A {
public:virtual ~A() { cout << "~A()"; }
};class B : public A {
public:~B() { cout << "~B()"; delete[] _p; }
private:int* _p = new int[10];
};// 使用:
A* p = new B;
delete p; // 正确调用 ~B() 和 ~A()
5.3 override 和 final 关键字
从上面可以看出,C++对虚函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错、参数写错等,导致无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运⾏时没有得到预期结果才来debug会得不偿失,因此C++11提供了override,可以帮助用户检测是否重写。如果我们不想让派生类重写这个虚函数,那么可以用final去修饰。
-
override 显式声明重写,编译器会检查是否成功重写。
-
final 禁止后续派生类重写该虚函数。
class Car {
public:virtual void Drive() final {} // 禁止重写
};class Benz : public Car {
public:virtual void Drive() override {} // 错误:无法重写final函数
};
6. 重载、重写、隐藏的对比
类型 | 作用域 | 函数要求 | 关键字 |
---|---|---|---|
重载 | 同一类 | 函数名相同,参数不同 | 无 |
重写 | 基类与派生类 | 函数名、参数、返回值相同 | virtual |
隐藏 | 基类与派生类 | 函数名相同,不构成重写 | 无 |
7. 纯虚函数与抽象类
在虚函数的后面写上 =0,则这个函数为纯虚函数,纯虚函数不需要定义实现。((实现没什么意义,因为要被派生类重写,但是语法上可以实现)只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承若不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写就实例化不出对象。
class Car {
public:virtual void Drive() = 0; // 纯虚函数
};class Benz : public Car {
public:virtual void Drive() override {cout << "Benz-舒适" << endl;}
};// Car car; // 错误:抽象类不能实例化
Car* p = new Benz; // 正确:多态使用
8. 总结
特性 | 说明 |
---|---|
多态类型 | 编译时多态(重载、模板) vs 运行时多态(虚函数) |
实现条件 | 基类指针/引用 + 虚函数重写 |
底层机制 | 虚函数表(vtable) + 虚函数表指针(vptr) |
关键字 | virtual 、override 、final |
应用场景 | 接口抽象、动态行为选择、资源安全释放 |
参考资料:
-
C++ Primer, 5th Edition
-
Effective C++, Scott Meyers