多态
在 C++ 的世界里,多态是面向对象编程的灵魂之一。它让同一个接口在不同对象上表现出不同的行为,从而大大提升了代码的复用性、扩展性与灵活性。本文将带你深入理解多态的核心原理,从概念、实现条件、虚函数、重写规则,到虚函数表与动态绑定机制,逐步揭开多态背后的运行逻辑。
一、多态的概念
通俗来说,就是'多种形态'。多态分为编译时多态(静态多态)和运行时多态(动态多态)。编译时多态主要指函数重载和模板,通过参数匹配在编译期完成;运行时多态则是重点,即调用同一函数时,根据传入对象的不同类型执行不同行为。比如买票,普通人全价,学生优惠,军人优先;再比如动物叫,猫叫喵,狗叫汪。
二、多态的定义及实现
多态发生在继承关系下,基类指针或引用调用虚函数产生不同行为。例如 Student 继承 Person,Person 买票全价,Student 买票半价。
2.1 多态的构成条件
- 必须是基类的指针或者引用调用虚函数。
- 被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖。
解释:只有基类的指针或引用才能既指向基类对象又指向派生类对象;派生类必须对基类的虚函数完成重写,否则无法体现不同形态。
2.1.2 虚函数
类成员函数前加 virtual 修饰即为虚函数。非成员函数不能加。
class Person {
public:
virtual void BuyTicket() { cout << "买票全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票半价" << endl; }
};
这样,调用同一函数就能产生不同行为。
2.1.3 虚函数的重写/覆盖
派生类中有跟基类完全相同的虚函数(返回值、名字、参数列表相同),称为重写。注意:派生类重写时不加 virtual 关键字也可以构成重写(因为继承了虚属性),但规范写法建议加上。
#include <iostream>
using namespace std;
class Person {
public:
virtual void BuyTicket() { cout << "买票全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票半价" << endl; }
};
void fun(Person& p) { p.BuyTicket(); }
void test() {
Person p; fun(p);
Student s; fun(s);
}
int main() {
test();
return 0;
}
输出显示,虽然 fun 函数接收的是 Person 引用,但实际行为由 p 指向的对象决定。
2.1.4 多态场景的一个选择题
下面程序输出结果是什么?
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(int argc, char* argv[]) {
B* p = new B; p->test();
return 0;
}
这道题有些难度。对于虚函数重写,本质是重写实现(函数体)。但在默认参数问题上容易踩坑。
在 p->test() 时,this 指针实际上是 A* 类型。func 函数是否构成重写?由于参数列表不同(默认值不同导致签名差异),实际上并未构成严格的重写,而是隐藏。因此调用 test() 时,使用的是 A::func,默认参数为 1。
所以输出应该是 B->1。
2.1.5 虚函数重写的一些其他问题
- 协变:派生类重写基类虚函数时,若返回类型为基类对象的指针或引用,派生类可返回派生类对象的指针或引用。
- 析构函数的重写:基类析构函数设为虚函数后,派生类析构函数无论是否加
virtual都构成重写。这是为了防止通过基类指针删除派生类对象时只调用基类析构函数导致内存泄漏。
class A {
public:
virtual ~A() { cout << "~A()" << endl; }
};
class B : public A {
public:
~B() { cout << "~B()->delete:" << _p << endl; delete _p; }
protected:
int* _p = new int[10];
};
int main() {
A* p1 = new A; A* p2 = new B;
delete p1; delete p2; // 确保 p2 能正确调用 B 的析构函数
return 0;
}
2.1.6 override 和 final 关键字
C++11 提供了 override 帮助检测是否重写成功,避免拼写错误导致未重写。final 用于禁止后续类继续重写该函数。
2.1.7 重载/重写/隐藏的对比
重载发生在同一作用域,重写发生在继承体系,隐藏发生在同名函数但不满足重写条件时。
三、纯虚函数和抽象类
虚函数后写 =0 为纯虚函数。包含纯虚函数的类叫抽象类,不能实例化。强制派生类重写。
class Car {
public:
virtual void Drive() = 0;
};
class Benz : public Car {
public:
virtual void Drive() { cout << "Benz 舒适" << endl; }
};
int main() {
Car* pBenz = new Benz; pBenz->Drive();
return 0;
}
四、多态的原理
4.1 虚函数表指针
含有虚函数的类对象中至少有一个虚函数表指针(vptr)。虚函数表存储虚函数地址。
class Base {
public:
virtual void Func1() { cout << "Func1()" << endl; }
protected:
int _b = 1; char _ch = 'x';
};
int main() {
Base b; cout << sizeof(b) << endl; // 通常大于成员变量之和,含 vptr
return 0;
}
32 位系统下通常是 12 字节(4 字节 vptr + 4 字节 int + 4 字节 char + padding)。
4.2 多态的原理
4.2.1 多态是如何实现的
底层不再是编译时确定地址,而是运行时通过 vptr 找到 vtable,再查表获取函数地址。
4.2.2 动态绑定与静态绑定
- 静态绑定:不满足多态条件(如普通函数调用),编译期确定地址。
- 动态绑定:满足多态条件(指针/引用 + 虚函数),运行时查表确定地址。
4.2.3 虚函数表
- 同类型对象共用同一张虚表。
- 派生类虚表包含基类虚函数地址(被重写则覆盖)、派生类新增虚函数地址。
- 虚表通常存在常量区,末尾可能有 0x00000000 标记(编译器优化)。
总结
多态让'同一接口'在'不同对象'上拥有'不同实现',是让 C++ 面向对象思想真正落地的关键。

