跳到主要内容 C++ 多态详解 | 极客日志
C++ 算法
C++ 多态详解 本文详解 C++ 多态机制。多态指同一接口在不同对象下呈现不同行为。构成条件包括:基类指针或引用调用虚函数,且派生类重写虚函数。虚函数由 virtual 修饰,支持协变和析构函数重写。C++11 引入 override 和 final 关键字辅助管理重写。抽象类含纯虚函数,不可实例化。多态原理基于虚函数表(vtable)和虚表指针(vfptr),实现运行时动态绑定。单继承和多继承场景下虚表结构有所不同。
Kubernet 发布于 2026/3/30 更新于 2026/4/13 1 浏览多态
多态:多态就是函数调用的多种形态,调用函数更加灵活。具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
class Person {
public :
virtual void BuyTicket () { cout << "买票 - 全价" << endl; }
};
class Student : public Person {
public :
virtual void BuyTicket () { cout << "买票 - 打折" << endl; }
};
void Func (Person* ptr) {
ptr->BuyTicket ();
}
int main () {
Person ps;
Student st;
Func (&ps);
Func (&st);
return 0 ;
}
我们可以看到上面的代码构成了多态,那么是怎么形成多态的呢?主要有两点:
1、子类重写父类的虚函数
class Person {
public :
virtual void BuyTicket { cout << << endl; }
};
: Person {
:
{ cout << << endl; }
};
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown 转 HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
HTML 转 Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
()
"买票 - 全价"
class
Student
public
public
void BuyTicket ()
"买票 - 打折"
void Func (Person* ptr) {
ptr->BuyTicket ();
}
void Func (Person& ptr) {
ptr.BuyTicket ();
}
那么什么是虚函数?多态的原理又是怎么样的?上面为什么可以满足多态的条件?
静态的多态:函数重载,调用同一个函数,传不同的参数,就有不同的行为/形态
动态的多态:父类指针或引用调用重写虚函数,不同的对象去调用,会有不同的行为/状态,父类指针或者引用指向父类,调用的就是父类的虚函数,父类指针或引用指向那个子类,调用的就是子类的虚函数
多态的构成条件 **多态是在不同继承关系的类对象,去调用同一个函数,产生了不同的行为。比如 Student 继承了 Person。**Person 对象买的是全价票,Student 对象买的是打折票。
必须通过基类的指针或者引用调用虚函数
被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
虚函数 虚函数:就是被 virtual 修饰的类成员函数称为虚函数
class Person {
public :
virtual void BuyTicket () { cout << "买票 - 全价" << endl; }
};
**√**只有类的非静态成员函数才可以加 virtual
**√**虚函数这里 virtual 和虚继承中用的 virtual 是同一个关键字,但是他们都没有关系,这里的虚函数是为了实现多态,虚继承是为了解决菱形继承的数据冗余性和二义性
虚函数的重写 虚函数的重写 (覆盖):派生类中有一个跟基类完全相同的虚函数 (及派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同) ,我们叫做派生类的虚函数重写了基类的虚函数。
class Person {
public :
virtual void BuyTicket () { cout << "买票 - 全价" << endl; }
};
class Student : public Person {
public :
void BuyTicket () { cout << "买票 - 打折" << endl; }
};
class Person {
public :
virtual void BuyTicket () { cout << "买票 - 全价" << endl; }
};
class Student : public Person {
public :
void BuyTicket () { cout << "买票 - 打折" << endl; }
};
void Func (Person* ptr) {
ptr->BuyTicket ();
}
void Func (Person& ptr) {
ptr.BuyTicket ();
}
int main () {
Person ps;
Student st;
Func (&ps);
Func (&st);
Func (ps);
Func (st);
return 0 ;
}
必须通过基类的指针或者引用调用虚函数
被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
正常的虚函数重写,要求虚函数的函数名、参数、返回值都要相同,但是协变除外。
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或者引用的时候,叫做协变。
class A {};
class B : public A {};
class Person {
public :
virtual A* BuyTicket () { cout << "买票 - 全价" << endl; return nullptr ; }
};
class Student : public Person {
public :
virtual B* BuyTicket () { cout << "买票 - 打折" << endl; return nullptr ; }
};
void Func (Person* ptr) {
ptr->BuyTicket ();
}
int main () {
Person ps;
Student st;
Func (&ps);
Func (&st);
return 0 ;
}
2、析构函数的重写 (基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加 virtual 关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同,也就是函数名不相同,看起来违背了重写的规则。实际上,这里可以理解为编译器对析构函数的名字做了特殊的处理,编译后析构函数的名称同意处理成 destructor。
class A {
public :
~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;
return 0 ;
}
因为 delete 时底层会去调用该对象类的析构函数和 operator delete,不是虚函数时,他们构成隐藏,因为 p1 和 p2 都是父类指针,所以他们都是去调用父类的析构函数
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;
return 0 ;
}
只有派生类 Student 的析构重写了 Person 的析构函数,下面的 delete 对象调用了析构函数,才能构成多态,才能保证 p1 和 p2 指向的对象正确的调用析构函数
注意:如果A(),不加 virtual,那么 deletep2 时只调⽤的 A 的析构函数,没有调⽤ B 的析构函数,就会导致内存泄漏问题,因为B() 中在释放资源。
C++11 override 和 final
C++ 对虚函数重写的要求⽐较严格,但是有些情况下由于疏忽,⽐如函数名写错参数 写错等导致⽆法构成重写,⽽这种错误在编译期间是不会报出的,只有在程序运⾏时没有得到预期结 果才来 debug 会得不偿失,因此 C++11 提供了 override,可以帮助⽤⼾检测是否重写。如果我们不想让 派⽣类重写这个虚函数,那么可以⽤ final 去修饰
如果不想虚函数被重写,那么就在虚函数后面加关键字 final:
class Car {
public :
virtual void Dirve () final {}
};
class Benz :public Car {
public :
virtual void Drive () { cout << "Benz-舒适" << endl; }
};
override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写就发生报错
class Car {
public :
virtual void Dirve () {}
};
class Benz :public Car {
public :
virtual void Drive () override { cout << "Benz-舒适" << endl; }
};
重载、覆盖 (重写)、隐藏的对比
多态:调用一个函数时,展现出多种形态 (通过调用不同的函数,完成不同的行为)。
多态分为静态的多态和动态的多态:
静态的多态:
函数重载就是静态的多态,在编译时确定地址。
动态的多态:
1、子类继承父类,完成虚函数重写
2、父类的指针或引用去调用这个重写的虚函数
父类的指针或引用指向父类对象,调用的是父类的虚函数
父类的指针或引用指向子类对象,调用的是子类的虚函数
虚函数重写条件:1、要是虚函数 2、函数名、参数、返回值都相等
例外:
1、协变 (返回值不一样,父类的虚函数返回的是基类对象指针和引用,子类的虚函数返回的是子类对象指针和引用)
2、析构函数
3、子类中的重写的虚函数可以不加 virtual 关键字 (建议加上)
抽象类
概念
在虚函数的后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类 (也叫接口类),抽象类不能实例化对象。派生类继承后也不能实例化对象,只有重写纯虚函数,派生类才能实例化对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现了接口继承。
class Car {
public :
virtual void Drive () = 0 ;
};
int main () {
Car cc;
return 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 ;
}
要注意和 override 区分,override 检查子类虚函数是否完成重写。纯虚函数是强制子类去重写虚函数,如果不重写,继承下来还是纯虚函数,照样无法实例化出对象。
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
多态的原理
虚函数表 这里常考一道笔试题:sizeof(Base)是多少?
class Base {
public :
virtual void Func1 () { cout << "Func1()" << endl; }
private :
int _b = 1 ;
char _ch = 'a' ;
};
int main () {
cout << sizeof (Base) << endl;
return 0 ;
}
为什么是 12 呢?根据内存对齐应该是 8 呀,这里为什么会是 12。
是因为只要包含虚函数的类,该类的对象就包含一个虚函数表指针 (简称虚表指针),这个虚函数表指针就是用来实现多态的:
这个虚表指针指向一个数组,这个数组的元素是函数指针,这里面的函数指针指向该类中的虚函数。
虚函数被编译成指令后,还是和普通函数一样,存在代码段,只是它的地址放在虚表中。
这里跟虚继承那里是不一样的,他们虽然都用了 virtual 关键字,但是他们的使用场景完全不一样,解决的也是不一样的问题,他们之间没有关联,虚继承产生的是虚基表,虚基表里面存的是距离虚基类的偏移量。
class Person {
public :
virtual void BuyTicket () { cout << "买票 - 全价" << endl; }
virtual void Func1 () { cout<<"Person::Func1()" <<endl; }
};
class Student : public Person {
public :
virtual void BuyTicket () { cout << "买票 - 半价" << endl; }
};
void Func (Person& p) {
p.BuyTicket ();
}
int main () {
Person Mike;
Func (Mike);
Student Johnson;
Func (Johnson);
return 0 ;
}
父子类无论是否完成虚函数重写,都有各自的独立虚表,一个类的所有对象共享一个虚表。
满足多态条件以后,构成多态:指针或引用在调用虚函数时,不是在编译时确定,是在运行时到指针或引用指向的对象的虚表中去找对应的虚函数调用,如果指向的时父类对象,则调用的就是父类的虚函数,指向的是子类对象,调用的就是子类的虚函数。需要注意的是,如果不构成多态,那么这里调用的时候就是编译时确定的调用哪个函数,主要看的 p 的类型,调用的就是 Person 和 Buyticket,跟传上面类型对象过来没有关系。
构成多态,指向谁,调用谁的虚函数,跟对象有关;不构成多态,对象类型是什么,调用那个对象的函数,跟类型有关。
为什么多态的条件之一必须是父类的指针或引用去调用虚函数时才会发生多态,父类对象却不行?
父类的指针和引用,在切片时,指向或者引用父类对象 或者 指向或引用子类对象中切出来的父类那一部分。vfptr 在对象的前四个字节保存,指向父类看到的是父类的虚表,指向子类看到的是子类的虚表
如果为父类对象时,切片只会拷贝成员变量过去,不会拷贝 vfptr 过去,因为拷贝过去不合理,如果可以拷贝过去,因为一个类共享一个虚表,在创建一个父类对象,这个父类对象的虚表时子类的虚表,这样不合理。
我们通过汇编代码分析,可以看出满足多态的函数调用不是在编译时确定的,是运行起来以后到对象中去找的,不满足多态的函数调用是编译或者链接时确认好的:
动态绑定和静态绑定 静态绑定又叫前期绑定 (早绑定),在程序编译期间确定了程序的行为,也称为静态多态
动态绑定又叫后期绑定 (晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也成为多态动态
普通函数的调用,编译 (当在一个文件当中时在编译阶段确定) 链接 (当声明和定义分离时,在链接时确定) 时确定地址,多态的调用是运行时确定地址,如何确定?去指向对象的虚函数表中找到虚函数地址。
对象中虚表指针是在什么阶段初始化的呢?虚表又是在什么阶段生成的呢?
对象中虚表指针是在构造函数初始化列表进行初始化,虚表是在编译时就生成好了。
可以看到调用了构造函数后虚表指针依旧进行初始化了。
虚函数放在虚表里面的,这句话对吗?
这句话不准确,虚表里面放的是虚函数地址,虚函数跟普通函数一样,编译完成后,都是放在代码段。
一个类中所有的虚函数地址,都会放在虚表中。
这句话是正确的,虽然可能大家有时候在调式的监视窗口看不到某个虚函数,这是因为编译器进行了优化,其实在内存中是可以看到有的,这些虚函数的地址都会放在虚表当中。
虚函数的重写,也叫做虚函数的覆盖,原因是子类刚开始是拷贝父类的虚函数过来,如果重写了哪个虚函数,就会将该虚函数拿过来进行覆盖从父类拷贝过来的虚函数。
vs 下会在虚表结束位置放一个空指针表示虚表结束了。
在面试中,面试官可能会问虚表是存在哪里的?想办法写一段程序,论证一下虚表存在哪个区域的?那么怎么论证呢?我们定义各个区域的变量或者常量,通过看地址的方式看哪个地址和虚表指针的内容相近:
class Person {
public :
virtual void BuyTicket () { cout << "买票 - 全价" << endl; }
};
class Student : public Person {
public :
virtual void BuyTicket () { cout << "买票 - 半价" << endl; }
};
void Func (Person& p) {
p.BuyTicket ();
}
int j = 0 ;
int main () {
Person p;
Person* pp = &p;
printf ("vftptr:%p\n" ,*((int *)pp));
int i;
printf ("栈上地址:%p\n" ,&i);
printf ("数据段地址:%p\n" ,&j);
int *k = new int ;
printf ("堆地址:%p\n" ,k);
char * cp = "hello world" ;
printf ("代码段地址:%p\n" ,cp);
return 0 ;
}
printf ("vftptr:%p\n" ,*((int *)pp));
这个代码就打印出来了虚表指针,为什么呢?pp 指向整个对象 p,首先将 pp 强转为 int ,此时 pp 指向 p 对象的前四个字节,对它解引用就拿到了前四个字节,这前四个字节就是虚表指针。 *
可以看到它是更接近代码段地址的,所以虚表是存在代码段的。虚函数编译出来函数指令跟普通函数一样,存在代码段,虚函数地址又被放到虚函数表中
单继承和多继承关系的虚函数表
单继承的虚函数表 class Base {
public :
virtual void func1 () { cout << "Base::func1" << endl; }
virtual void func2 () { cout << "Base::func2" << endl; }
private :
int a;
};
class Derive :public Base {
public :
virtual void func1 () { cout << "Derive::func1" << endl; }
virtual void func3 () { cout << "Derive::func3" << endl; }
virtual void func4 () { cout << "Derive::func4" << endl; }
private :
int b;
};
int main () {
Base b;
Derive d;
return 0 ;
}
我们通过调试发现监视窗口看不到子类自己的虚函数 fun3 和 funn4。
我们可以通过写一个程序打印一下虚表,通过调用虚表中的虚函数,确定上面两个就是我们说的 fun3 和 fun4 的地址:
class Base {
public :
virtual void func1 () { cout << "Base::func1" << endl; }
virtual void func2 () { cout << "Base::func2" << endl; }
private :
int a;
};
class Derive :public Base {
public :
virtual void func1 () { cout << "Derive::func1" << endl; }
virtual void func3 () { cout << "Derive::func3" << endl; }
virtual void func4 () { cout << "Derive::func4" << endl; }
private :
int b;
};
typedef void (*VFunc) () ;
void PrintVFT (VFunc ptr[])
{
for (int i = 0 ;ptr[i]!=nullptr ;++i) {
printf ("VFT[%d]:%p\n" ,i,ptr[i]);
ptr[i]();
}
printf ("\n" );
}
int main () {
Base b;
PrintVFT ((VFunc*)(*(int *)&b));
Derive d;
PrintVFT ((VFunc*)(*(int *)&d));
return 0 ;
}
多继承的虚函数表 class Base1 {
public :
virtual void func1 () {cout << "Base1::func1" << endl;}
virtual void func2 () {cout << "Base1::func2" << endl;}
private :
int b1;
};
class Base2 {
public :
virtual void func1 () {cout << "Base2::func1" << endl;}
virtual void func2 () {cout << "Base2::func2" << endl;}
private :
int b2;
};
class Derive : public Base1, public Base2 {
public :
virtual void func1 () {cout << "Derive::func1" << endl;}
virtual void func3 () {cout << "Derive::func3" << endl;}
private :
int d1;
};
typedef void (*VFunc) () ;
void PrintVFT (VFunc* ptr)
{
for (int i = 0 ;ptr[i]!=nullptr ;++i) {
printf ("VFT[%d]:%p\n" ,i,ptr[i]);
ptr[i]();
}
printf ("\n" );
}
int main () {
Base1 b1;
Base2 b2;
Derive d;
PrintVFT ((VFunc*)(*(int *)&d));
PrintVFT ((VFunc*)(*(int *)((char *)&d+sizeof (Base1))));
return 0 ;
}
可以看到多继承中,Derive 既继承了 Base2,Derive 就有两种虚表。
我们看到 Derive 中自己的虚函数 func3 在监视窗口并没有,那么怎么证明它是存在的呢?和上面其实是一样的,只不过打印第二张虚表有些不一样:
typedef void (*VFunc) () ;
void PrintVFT (VFunc ptr[])
{
for (int i = 0 ;ptr[i]!=nullptr ;++i) {
printf ("VFT[%d]:%p\n" ,i,ptr[i]);
ptr[i]();
}
printf ("\n" );
}
int main () {
Base1 b1;
Base2 b2;
Derive d;
PrintVFT ((VFunc*)(*(int *)&d));
PrintVFT ((VFunc*)(*(int *)((char *)&d+sizeof (Base1))));
return 0 ;
}
PrintVFT ((VFunc*)(*(int *)&d));
因为 d 对象中起始位置为第一张虚表的虚表指针,需要强转为 VFunc*,因为虚表指针指向的类型是函数指针数组,这个和前面验证单继承没有区别,但是打印第二张虚表就有些不一样了:
PrintVFT ((VFunc*)*((int *)((char *)&d+sizeof (Base1))));
首先将取地址 d 将他转为 char类型,加上 Base1 的大小就到了 Base2,Base2 的前四个字节是虚表指针,所以再强转为 int ,然后解引用拿到这四个字节,最后强转为 VFunc*