多态
多态(polymorphism)是面向对象编程的灵魂之一。它让同一个接口在不同对象上表现出不同的行为,从而大大提升了代码的复用性、扩展性与灵活性。本文将深入理解多态的核心原理,从概念、实现条件、虚函数、重写规则,到虚函数表与动态绑定机制,逐步揭开多态背后的运行逻辑。
一、多态的概念
通俗来说,多态就是'多种形态'。分为编译时多态(静态多态)和运行时多态(动态多态)。编译时多态主要通过函数重载和模板实现,参数匹配在编译期完成;运行时多态则是通过基类指针或引用调用虚函数,根据实际对象类型决定执行哪个函数。比如同样是'买票'行为,普通人全价,学生优惠,军人优先。
二、多态的定义及实现
多态发生在继承体系下,基类指针或引用调用同一函数产生不同行为。例如 Student 继承 Person,Person 买票全价,Student 买票半价。
2.1 多态的构成条件
要实现多态,必须同时满足两个条件:
- 必须是基类的指针或者引用调用虚函数。
- 被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖。
解释一下:只有基类的指针或引用才能既指向基类对象又指向派生类对象;而派生类必须对基类的虚函数完成重写,这样两者之间才有不同的函数实现,多态效果才能显现。
2.1.2 虚函数
类成员函数前加 virtual 修饰即为虚函数。注意非成员函数不能加 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 在调用 BuyTicket,但结果由 p 指向的实际对象决定。
2.1.4 多态场景的一个选择题
以下程序输出结果是什么? A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
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;
}
这道题有些难度。对于虚函数重写,我们理解为只重写实现(函数体)。所以派生类可以不写 virtual。
但在 p->test() 时,this 指针实际上是 A* 类型。
func 函数是否构成重写?由于默认参数不一致,且 val 参数不同,这里存在陷阱。
最终输出应该是 B->1。
2.1.5 虚函数重写的一些其他问题
- 协变:派生类重写基类虚函数时,若基类返回基类指针/引用,派生类返回派生类指针/引用,称为协变。
- 析构函数的重写:基类析构函数为虚函数时,派生类析构函数无论是否加
virtual都构成重写。这很重要,否则delete基类指针时只会调用基类析构函数,导致内存泄漏。
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; // 只有基类析构函数为虚函数,这里才能正确调用 B 的析构函数
return 0;
}
一句话:为了防止通过基类指针删除派生类对象时,只调用基类析构函数而不调用派生类析构函数,导致内存泄漏。
2.1.6 override 和 final 关键字
C++11 提供了 override 帮助检测是否重写成功,避免拼写错误导致的静默失败。如果想禁止派生类继续重写,可用 final 修饰。
class Car {
public:
virtual void Drive() {}
};
class Benz : public Car {
public:
virtual void Drive() override { cout << "Benz 舒适" << endl; }
};
若拼写错误,编译器会报错。若基类加了 final,派生类再 override 也会报错。
2.1.7 重载/重写/隐藏的对比
这是面试常考点。重载是同一作用域同名不同参;重写是继承关系中同名同参;隐藏是派生类屏蔽基类同名函数(无论参数是否相同)。
三、纯虚函数和抽象类
虚函数后写 =0 即为纯虚函数。包含纯虚函数的类叫抽象类,不能实例化。派生类若不重写纯虚函数,也是抽象类。这强制了派生类实现接口。
class Car {
public:
virtual void Drive() = 0;
};
class Benz : public Car {
public:
virtual void Drive() { cout << "Benz 舒适" << endl; }
};
class BMW : public Car {
public:
virtual void Drive() { cout << "BMW 操控" << endl; }
};
int main() {
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->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;
}
虚函数表本质是函数指针数组。
4.2 多态的原理
4.2.1 多态是如何实现的
底层不再是编译时确定函数地址,而是运行时到指向对象的虚表中查找对应虚函数的地址。
class Person {
public:
virtual void BuyTicket() { cout << "买票全价" << endl; }
private:
string _name;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票打折" << endl; }
private:
string _id;
};
class Soldier : public Person {
public:
virtual void BuyTicket() { cout << "买票优先" << endl; }
private:
string _codename;
};
void Func(Person* ptr) { ptr->BuyTicket(); }
int main() {
Person ps;
Student st;
Soldier sr;
Func(&ps);
Func(&st);
Func(&sr);
return 0;
}
ptr 指向谁,就去谁的虚表里找 BuyTicket 的地址。
4.2.2 动态绑定与静态绑定
- 静态绑定:不满足多态条件(如普通函数调用),编译时确定地址。
- 动态绑定:满足多态条件(指针/引用 + 虚函数),运行时查虚表确定地址。
涉及汇编层面,简单理解即可:静态绑定直接 call 函数地址;动态绑定先取 vptr,再取 vtable 中的函数地址,最后 call。
4.2.3 虚函数表
- 基类所有虚函数的地址。同类型对象共用同一张虚表。
- 派生类虚表包含:(1) 基类虚函数地址,(2) 派生类重写的虚函数地址(覆盖),(3) 派生类自己的虚函数地址。
- 虚函数表一般存放在常量区。VS 编译器会在虚表末尾放
0x00000000标记,g++ 通常不放。
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
void func5() { cout << "Base::func5" << endl; }
protected:
int a = 1;
};
class Derive : public Base {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func1" << endl; }
void func4() { cout << "Derive::func4" << endl; }
protected:
int b = 2;
};
int main() {
Base b;
Derive d;
// 调试时可查看虚表地址
return 0;
}
总结
多态让'同一接口'在'不同对象'上拥有'不同实现',是 C++ 面向对象思想真正落地的关键。掌握其底层机制有助于写出更健壮、高效的代码。


