跳到主要内容 C++ 多态详解 | 极客日志
C++ 算法
C++ 多态详解 C++ 多态分为编译时多态和运行时多态。运行时多态需满足继承关系、基类指针或引用调用虚函数、派生类重写虚函数三个条件。核心机制依赖虚函数表(vtable)和虚函数表指针(_vfptr),实现动态绑定。纯虚函数定义抽象类,强制子类实现接口。析构函数建议设为虚函数以防止内存泄漏。override 和 final 关键字用于辅助重写检查和禁止重写。
微码行者 发布于 2026/3/30 更新于 2026/4/13 1 浏览一、多态的概念与分类
多态(polymorphism)即'多种形态',在 C++ 中分为两类:
编译时多态(静态多态) :
典型代表:函数重载、函数模板
特点:在编译阶段,根据实参类型匹配到对应的函数地址,属于静态绑定
运行时多态(动态多态) :
典型代表:通过虚函数实现的多态
特点:在运行阶段,根据指针/引用指向的实际对象类型,调用对应的虚函数,属于动态绑定
例子:
买票:普通人全价、学生半价/75 折、军人优先
动物叫:猫'喵'、狗'汪汪'
二、多态的定义及实现
2.1 多态的构成条件
多态发生在继承关系下,用基类的指针或引用调用虚函数,产生不同行为。
必须同时满足两个条件:
必须是基类的指针或引用调用虚函数
被调用的函数必须是虚函数,并且在派生类中完成了虚函数重写/覆盖
说明:
只有基类指针/引用才能既指向基类对象,又指向派生类对象
派生类必须对基类虚函数完成重写,才能在运行时表现出不同行为
代码示例:
#include <iostream>
using namespace std;
class Person {
public :
virtual void BuyTicket () {
cout << "买票 - 全价" << endl;
}
};
class Student : public Person {
public :
void BuyTicket () override {
cout << "买票 - 打折" << endl;
}
};
void Func (Person& ptr) {
ptr. ();
}
{
Person p;
Student s;
(p);
(s);
;
}
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 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
BuyTicket
int main ()
Func
Func
return
0
虚函数重写 :
Person 中的 BuyTicket 是 virtual 虚函数。
Student 中的 BuyTicket 虽然没有写 virtual,但因为继承了基类的虚函数属性,所以依然构成重写(Override)。加上 override 关键字是更好的编程习惯,可以让编译器帮你检查是否真的重写了基类函数。
多态的触发条件 :
这里使用了基类的引用 Person& ptr 作为函数参数。
当 ptr 引用 Person 对象时,调用 Person::BuyTicket()。
当 ptr 引用 Student 对象时,调用 Student::BuyTicket()。
这种在运行时根据对象实际类型来决定调用哪个函数的机制,就是动态绑定,也是多态的核心。
底层原理 :
当类中存在虚函数时,编译器会为该类生成一张虚函数表(vtable),表中存放着所有虚函数的地址。
每个对象都会包含一个隐藏的虚函数表指针(_vfptr),指向所属类的虚函数表。
当通过基类引用调用虚函数时,程序会通过对象的 _vfptr 找到对应的虚函数表,再从表中查找并调用正确的函数地址。
2.1.1 虚函数
定义 :在类的成员函数前加 virtual 修饰,该函数即为虚函数
注意 :非成员函数不能加 virtual
class Person {
public :
virtual void BuyTicket () {
cout << "买票 - 全价" << endl;
}
};
2.1.2 虚函数的重写/覆盖
定义 :派生类中有一个跟基类完全相同的虚函数(返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。
注意 :派生类虚函数不加 virtual 时,也可构成重写(因为继承后基类虚函数属性被保留),但写法不规范,不推荐。考试选择题常故意设置此'坑',用于判断是否构成多态。
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 ;
}
#include <iostream>
using namespace std;
class Animal {
public :
virtual void talk () const {}
};
class Dog : public Animal {
public :
virtual void talk () const override {
std::cout << "汪汪" << std::endl;
}
};
class Cat : public Animal {
public :
virtual void talk () const override {
std::cout << "(>^ω^<) 喵" << std::endl;
}
};
void letsHear (const Animal& animal) {
animal.talk ();
}
int main () {
Cat cat;
Dog dog;
letsHear (cat);
letsHear (dog);
return 0 ;
}
多态的体现 :
letsHear 函数接收一个 const Animal& 类型的引用。
当传入 Cat 对象时,调用 Cat::talk();传入 Dog 对象时,调用 Dog::talk()。
同一个函数 letsHear,在运行时根据传入对象的实际类型,表现出不同的行为,这就是多态。
关键机制 :
虚函数:Animal 中的 talk() 被声明为 virtual,这是实现多态的基础。
重写(Override):Dog 和 Cat 类中重新定义了 talk(),与基类的虚函数签名完全一致,构成重写。
动态绑定:通过基类的引用(或指针)调用虚函数时,会在运行时查找对象的虚函数表,找到并调用正确的函数版本。
代码规范 :在派生类的重写函数后加上 override 关键字是一个好习惯,它能让编译器检查你是否真的重写了一个存在的虚函数,避免因拼写错误等导致的'隐藏'而非'重写'的问题。
基类指针/引用 = 可以指向/引用 父类对象 或 子类对象
void letsHear (const Animal& animal) {
animal.talk ();
}
这里:animal 是 基类 Animal 的引用;但它既可以引用猫,也可以引用狗
letsHear(cat); // animal 引用了猫
letsHear(dog); // animal 引用了狗
然后你调用:animal.talk();
这就叫:✅ 通过基类的引用,调用虚函数
void func (Animal* ptr) {
ptr->talk ();
}
Animal* p1 = new Cat;
Animal* p2 = new Dog;
p1->talk ();
p2->talk ();
重点:为什么一定要「基类指针/引用」?因为只有基类指针/引用,才能同时接收父类和子类。
Animal& animal = cat; // ✅ 可以
Animal& animal = dog; // ✅ 可以
如果不用基类指针/引用,就不是多态:
Cat c;
c.talk(); // 这是普通调用,不是多态
总结 :多态 = 基类指针/引用 + 调用虚函数;指针/引用是谁不重要;它指向/引用的对象是谁,就调用谁的函数
2.1.3 多态场景选择题解析 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 ();
p->func ();
return 0 ;
}
① p->test(); p 是 B* ;B 自己没有写 test() ;所以去调用 父类 A::test()
virtual void test () {
func ();
}
这里的 this 是谁?this 是 A* 类型,但指向的是 B 对象
func() 是虚函数,满足多态条件 → 调用 B::func()
③ 调用 B::func(int val = 0)
这里是最坑的地方:
规则 -> 函数体:运行时动态决议(多态) 默认参数:编译期静态决议(看指针/引用类型)
在 A::test() 里:this 是 A*,编译阶段就把默认参数定为 A 里的 val = 1,不会用 B 里的 0
所以 -> 调用的是:B::func(1),输出:B->1
p->func();
执行过程:p 是 B*,直接调用 B::func(),没有多态,就是普通调用,默认参数用 B 自己的:val = 0
test 找不到 → 调用 A::test
test 里的 func 是虚函数 → 多态调用 B::func
默认参数看当前指针类型(A)→ 用 A 的默认值 1
在 test() 里调用 func:函数 → B,默认参数 → A 的 1
直接 p->func():函数 → B,默认参数 → B 的 0
函数体看对象,默认参数看类型。
2.1.4 虚函数重写的其他问题
定义 :派生类重写基类虚函数时,返回值类型不同,但基类返回基类对象指针/引用,派生类返回派生类对象指针/引用,这种情况称为协变
特点 :实际意义不大,仅作了解
#include <iostream>
using namespace std;
class A {
public :
virtual void show () {
cout << "I am A" << endl;
}
};
class B : public A {
public :
void show () override {
cout << "I am B" << endl;
}
};
class Person {
public :
virtual A* BuyTicket () {
cout << "买票 - 全价" << endl;
return new A;
}
};
class Student : public Person {
public :
B* BuyTicket () override {
cout << "买票 - 打折" << endl;
return new B;
}
};
void Func (Person* ptr) {
A* p = ptr->BuyTicket ();
p->show ();
delete p;
}
int main () {
Person ps;
Student st;
Func (&ps);
cout << "--------" << endl;
Func (&st);
return 0 ;
}
「协变」;子类重写虚函数时,返回值可以是父类返回值的「派生类指针/引用」。
类 A 和 类 B(用来演示协变):A 是父类;B 继承 A,是 A 的子类;它们有一个同名虚函数 show(),构成重写;这一对 A 和 B 就是为了满足:返回值类型是父子关系。
Person 类(父类):有一个虚函数 BuyTicket(),返回值类型:A*
Student 类(子类,重点!协变):Student 继承 Person,重写了虚函数 BuyTicket(),返回值是 B*,而 B 是 A 的子类 👉 这就是 C++ 协变:子类重写虚函数时,返回值可以是父类返回值的派生类指针/引用。
多态调用函数 Func:ptr 是 Person*,可以指向 Person 或 Student;ptr->BuyTicket() 是多态调用:指向 Person → 调用 Person::BuyTicket;指向 Student → 调用 Student::BuyTicket
main 函数执行流程:
第一次调用:Func(&ps)
调用 Person::BuyTicket() ;输出:买票 - 全价 ;返回 new A ;调用 A::show() → 输出 I am A
第二次调用:Func(&st)
调用 Student::BuyTicket() ;输出:买票 - 打折 ;返回 new B ;调用 B::show() → 输出 I am B
协变的规则(必须记住):必须是 虚函数 重写;返回值必须是 指针 或 引用;子类返回值类型 必须是 父类返回值类型的 派生类。满足这三条,编译器就认为是正确的重写。
总结(超精简):协变 = 虚函数重写 + 返回值是父子类指针/引用
规则 :如果基类的析构函数为虚函数,派生类析构函数只要定义,无论是否加 virtual,都与基类析构函数构成重写
原因 :编译器对析构函数名称做了特殊处理,统一处理成 destructor
重要性 :若基类析构函数不是虚函数,delete 基类指针指向派生类对象时,只会调用基类析构函数,导致派生类资源泄漏
#include <iostream>
using namespace std;
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 ;
}
~A ()
~B ()->delete:0000021 E45AFB420
~A ()
第一行输出:~A() 对应代码:A* p1 = new A; delete p1;
p1 指向 new A,是纯 A 对象;delete p1 调用 A 的析构函数;打印:~A()
第二行输出:~B()->delete:0x... 对应代码:A* p2 = new B; delete p2;
p2 是父类指针 A*,但指向子类 B 对象;因为 ~A() 是 虚析构,所以 delete p2 会多态调用 ~B();因为父析构 是虚析构,delete p2 会 多态调用,先调用 ~B(),再自动调用 ~A(),子类、父类都被正确销毁,没有内存泄漏。先执行 B 的析构:打印:~B()->delete:地址;执行 delete _p; 释放 B 内部的数组。
第三行输出:~A() 子类析构执行完后,会自动调用父类析构,所以再打印:~A()
析构函数是虚函数 → 构成多态
虚析构 = 保证「父类指针删子类对象」时,能删干净。
只要满足:有继承,父类指针指向子类对象,子类有动态内存 / 需要清理,父类析构函数,一律写成虚析构!只要子类里有动态申请的资源,父类析构必须是虚函数。
父类指针指向子类对象,delete 时,先调用子类析构,再自动调用父类析构,这样才不会内存泄漏。
2.1.5 override 和 final 关键字 C++11 引入,用于辅助虚函数重写的检查和控制:
override :
作用:显式标记派生类函数是重写基类虚函数,编译器会检查是否真的重写了基类方法
若未重写,编译报错,避免拼写错误等导致的'假重写'
final :
作用:修饰虚函数,表示该函数不能被派生类重写;修饰类,表示该类不能被继承
class Car {
public :
virtual void Drive () {}
};
class Benz : public Car {
public :
virtual void Drive () override {
cout << "Benz-舒适" << endl;
}
};
class Car {
public :
virtual void Drive () final {}
};
class Benz : public Car {
public :
virtual void Drive () {
cout << "Benz-舒适" << endl;
}
};
override 是干嘛的?
作用:检查你有没有写对重写。
virtual void Drive() override; 告诉编译器:我这个函数,是要重写父类的虚函数!
如果写错了(比如名字拼错、参数不对),编译器直接报错。
不加 override,写错了编译器不报错,只会当成新函数,你还不知道错在哪。
举例:
父类:virtual void Drive() {}
子类写成:virtual void Dirve() override; // 拼错了加了 override → 直接报错,马上就知道写错了。
final 是干嘛的?
作用:禁止子类再重写我。
virtual void Drive() final {} 意思就是:到此为止,不许子类再改我这个函数!谁再重写,就编译报错
一句话总结
override:帮你检查重写是否正确,防止写错。
final:禁止子类重写,断了继承的路。
final 放在类后面:禁止继承
class Car final
{ };
class Benz : public Car { };
class 类名 final;谁都不能继承它;子类都写不出来,直接编译报错
总结 :final 写在类后面:禁止被继承;final 写在虚函数后面:禁止被重写
2.1.6 重载/重写/隐藏的对比 特性 重载 (Overload) 重写/覆盖 (Override) 隐藏 (Hide) 作用域 同一作用域 继承体系的父类和子类(不同作用域) 继承体系的父类和子类(不同作用域) 函数名 相同 相同 相同 参数列表 不同(类型、个数、顺序) 完全相同 可相同或不同 返回值 可相同或不同 必须相同(协变例外) 可相同或不同 virtual 关键字 无关 必须是虚函数 无关 本质 编译器多态 运行期多态 名字隐藏,编译期确定
三、纯虚函数和抽象类
纯虚函数 :在虚函数声明后加 = 0,如 virtual void Drive() = 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 ;
}
什么是 纯虚函数? 写法:virtual 函数 = 0; 只有声明,没有实现;作用:规定子类必须重写这个函数
什么是 抽象类? 包含 至少一个纯虚函数 的类;不能创建对象!Car car; // 报错!抽象类不能实例化
子类必须做什么? 子类必须重写纯虚函数;不重写 → 子类也变成抽象类,也不能创建对象
main 函数里做了什么? 父类指针 指向子类对象;调用 Drive();发生 多态,调用对应子类的函数
纯虚函数:virtual void Drive() = 0;
抽象类:包含纯虚函数,不能创建对象
作用:制定接口/规则,强制子类实现
子类必须重写纯虚函数,否则也不能实例化
依然支持 多态调用
四、多态的原理
4.1 虚函数表指针(_vfptr) 当类中包含虚函数时,编译器会在对象布局中插入一个虚函数表指针(_vfptr)
该指针指向一个虚函数表(vtable),表中存放该类所有虚函数的地址
32 位程序中,_vfptr 占 4 字节;64 位程序中占 8 字节
class Base {
public :
virtual void Func1 () {
cout << "Func1()" << endl;
}
virtual void Func2 () {
cout << "Func2()" << endl;
}
void Func3 ()
{
cout << "Func3()" << endl;
}
protected :
int _b = 1 ;
char _ch = 'x' ;
};
int main () {
Base b;
cout << sizeof (b) << endl;
return 0 ;
}
关键点 :只要有 虚函数,对象里就多一个 虚表指针
只要类里有 至少一个 virtual 函数,对象就会多一个:vfptr 虚函数表指针
32 位平台:指针大小 4 字节,64 位平台:指针大小 8 字节
成员变量
int _b;
char _ch;
内存对齐(重点)
规则:整体对齐到 最大成员类型的大小;这里最大是 int → 对齐到 4 字节
计算 -> vfptr:4 _b:4 _ch:1 总和 = 4 + 4 + 1 = 9 对齐到 4 的倍数 → 12 字节
总结 :有虚函数 → 多 4 字节 虚表指针;成员变量:int(4) + char(1);内存对齐 → 最终大小 12
普通成员函数(Func3)不占对象大小!只有:成员变量、虚表指针(有虚函数才存在),才算进 sizeof(对象);虚函数:也不存到对象里!只多一个 8 字节(64 位)或 4 字节(32 位)的指针 指向虚表 👉 函数本身,永远不算进对象大小!
4.2 多态的实现原理
4.2.1 动态绑定过程
通过对象的 _vfptr 找到对应的虚函数表
在虚函数表中查找要调用的虚函数地址
根据实际对象类型,调用对应版本的虚函数
代码示例:加 virtual → 有多态(各自调用自己)
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 ;
}
父类加了 virtual → 变成虚函数
子类函数构成重写
用 Person* 调用时:看指向的对象,不看指针类型;指向谁,就调用谁的函数
对比代码:不加 virtual → 没有多态(全调用父类)
#include <iostream>
#include <string>
using namespace std;
class Person {
public :
void BuyTicket () {
cout << "买票 - 全价" << endl;
}
protected :
string _name;
};
class Student : public Person {
public :
void BuyTicket () {
cout << "买票 - 打折" << endl;
}
protected :
int _id;
};
class Soldier : public Person {
public :
void BuyTicket () {
cout << "买票 - 优先" << endl;
}
protected :
string _codename;
};
void Func (Person* ptr) {
ptr->BuyTicket ();
}
int main () {
Person ps;
Student st;
Soldier sr;
Func (&ps);
Func (&st);
Func (&sr);
return 0 ;
}
父类 BuyTicket 不是虚函数,子类加不加 virtual 都没用,子类的 BuyTicket 不叫重写,叫隐藏
什么叫「同名隐藏」?子类有个函数,名字和父类一样;但不是重写;用父类指针调用时,只看父类,不看子类
Func (&ps);
Func (&st);
Func (&sr);
用 Person* 指针调用时:编译器只看指针类型,不看指向对象
指针是 Person* → 一律调用 Person::BuyTicket
不加 virtual:看指针类型
加 virtual:看指向对象
这就是多态的本质。
父类不加 virtual:指针是谁,就调用谁 → 全调用父类
父类加 virtual:指针指向谁,就调用谁 → 多态生效
4.2.2 动态绑定 vs 静态绑定
静态绑定 :不满足多态条件的函数调用,编译期确定函数地址
例子:普通函数调用、非虚函数调用、对象直接调用函数
动态绑定 :满足多态条件的函数调用,运行期通过虚函数表确定函数地址
ptr->BuyTicket ();
ptr->BuyTicket ();
4.2.3 虚函数表(vtable) 虚函数表是一个数组,存放类中所有虚函数的地址,最后通常以 0x00000000 作为结束标记(不同编译器实现略有差异)
基类虚函数表:存放基类虚函数地址
派生类虚函数表:
先拷贝基类虚函数表的内容
若派生类重写了基类虚函数,用派生类虚函数地址覆盖对应位置
新增的派生类虚函数地址追加到表中
虚函数表存放在代码段/常量区,虚函数本身也存放在代码段
#include <iostream>
using namespace std;
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 b1;
Base b2;
Derive d;
return 0 ;
}
先看类结构
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::func3" << endl; }
void func4 () { cout << "Derive::func4" << endl; }
protected :
int b = 2 ;
};
先讲最重要:虚函数表(虚表)
(1)Base 类的虚表
Base 有 2 个虚函数:func1、func2
所以 Base 对象里会有:vfptr 虚表指针(4/8 字节)、成员变量 int a
64 位下 sizeof(Base) = 12(8 指针 + 4 int)
(2)Derive 类的虚表
Derive 继承 Base,会继承虚表。
发生两件事:
func1 被重写 → 虚表中 func1 被替换成 Derive::func1
新的虚函数 func3 → 加到 Derive 自己的虚表里
最终 Derive 虚表内容:func1 → Derive::func1、func2 → Base::func2、func3 → Derive::func3
Derive 对象内容:vfptr(8 字节)、Base::a(4)、Derive::b(4);sizeof(Derive) = 16
哪些是重写?哪些不是?
✅ 构成重写:Base::func1() virtual Derive::func1() virtual
❌ 不构成重写:func2:子类没重写;func3:子类新虚函数,父类没有;func4、func5:普通成员函数,和多态无关
普通函数 vs 虚函数
虚函数(virtual):进虚表,对象存指针,支持多态
普通函数(func4、func5):不进虚表,不占对象大小,不支持多态
main 里的对象
int main () {
Base b1;
Base b2;
Derive d;
return 0 ;
}
b1 和 b2 是两个不同对象,各有一套成员变量;但它们共用同一张虚表 (所有 Base 对象共享一张虚表)
d 是子类对象,有自己的虚表
总结 :有虚函数 → 对象多一个 vfptr 虚表指针;子类重写虚函数 → 虚表中对应函数地址被替换;普通函数 不算进对象大小,不进虚表;同类对象 共享同一张虚表;子类会继承并改写虚表
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::func3" << endl;
}
void func4 () {
cout << "Derive::func4" << endl;
}
protected :
int b = 2 ;
};
int main () {
Base b;
Derive d;
printf ("Person 虚表地址:%p\n" , *(int *)&b);
printf ("Student 虚表地址:%p\n" , *(int *)&d);
printf ("虚函数地址:%p\n" , &Base::func1);
printf ("普通函数地址:%p\n" , &Base::func5);
return 0 ;
}
解释 :&b:取对象 b 的地址 (int*)&b:把对象地址强转成 int 指针 (int )&b:解引用,取出对象最前面 4 字节的值 ;对象最前面 4 字节 → 就是虚表指针 vfptr → 也就是虚表地址!
子类对象 d 最前面也是 虚表指针,但它指向的是 Derive 自己的虚表,所以打印出来的地址 和 Base 不一样
&Base::func1:取虚函数的地址;这个地址 存在虚表里面
普通函数地址 直接存在代码段,不进虚表,不占对象空间
Base 虚表地址:0xXXXXXXXX
Derive 虚表地址:0xYYYYYYYY
虚函数地址:0xZZZZZZZZ
普通函数地址:0xWWWWWWWW
Base 和 Derive 虚表地址不一样,各自有自己的虚表
虚函数、普通函数地址完全不同 -> 虚函数:走虚表;普通函数:直接调用
总结 :有虚函数 → 对象里有虚表指针;每个类一张虚表;子类重写虚函数 → 改写自己虚表;普通函数不进虚表,不占对象空间 (int )&对象 就是在 取虚表地址
3. 子类不重写任何虚函数 → 虚表地址依然不一样!但 虚函数的地址会一样。
虚表地址((int)&b 和 (int)&d) 永远不一样!
Base 有自己的虚表,Derive 有自己的虚表,只要是两个类,虚表就是两个不同的数组,所以它们的地址一定不同。
Base 虚表地址:0x123
Derive 虚表地址:0x456 ← 一定不同
虚函数地址(比如 func1、func2) 如果子类没有重写 → 地址完全一样!
Base::func1
Derive::func1(继承过来,没重写)它们是同一个函数,所以:虚表里面存的函数地址是一样的
普通函数地址:本来就和虚表无关,永远一样
虚表地址:Base 虚表地址 ≠ Derive 虚表地址;不管有没有重写,都不一样
虚函数地址:子类不重写 → 父子虚函数地址 相同;子类重写 → 父子虚函数地址 不同
虚表:每个类一张,地址永远不同;虚函数:重写才变,不重写就共用父类的
#include <cstdio>
using namespace std;
int main () {
int i = 0 ;
static int j = 1 ;
int * p1 = new int ;
const char * p2 = "xxxxxxxx" ;
printf ("栈:%p\n" , &i);
printf ("静态区:%p\n" , &j);
printf ("堆:%p\n" , p1);
printf ("常量区:%p\n" , p2);
return 0 ;
}
int i = 0; 存放在:栈(stack);局部变量,函数一结束就自动销毁;取地址 &i 就是栈上地址
static int j = 1;
存放在:静态区(全局/静态区);程序整个运行期间都存在;只初始化一次;取地址 &j 是静态区地址
int* p1 = new int; p1 本身在栈,new int 申请的空间在堆(heap),p1 存放的就是堆地址
const char* p2 = "xxxxxxxx";
字符串常量 "xxxxxxxx" 存放在常量区,p2 本身在栈,p2 的值就是常量区地址
这 4 个地址的特点(重点):你运行后会看到类似这样(地址只是示例)
栈:0x7ffee3b5c8ac
静态区:0x4040a0
堆:0x800003240
常量区:0x4020a4
规律一眼看懂:1. 栈地址最高 (接近 0x7fff…) 2. 堆 在中间 3. 静态区、常量区 很低 (靠近程序代码区)
总结 :局部变量 → 栈;static / 全局 → 静态区;new / malloc → 堆;字符串常量 → 常量区
五、总结
多态是什么:父类指针/引用指向子类对象;调用同一个函数,不同对象表现不同行为
多态成立的 3 个条件:有 继承;子类 重写 父类 虚函数;父类指针/引用调用虚函数
虚函数 virtual void func() {}:允许子类重写,支持多态
重写(覆盖):函数名、参数、返回值完全相同;父类必须带 virtual
协变(特殊重写):返回值是父子类指针/引用,父虚函数返回父类指针,子虚函数返回子类指针
virtual A* f () {}
virtual B* f () {}
override:检查重写 void Drive() override; 作用:必须重写成功,否则报错;防止拼写错、参数错
final:禁止重写/继承
virtual void f () final {}
class A final {};
虚析构函数 virtual ~A() {}
父类指针指向子类对象 delete 时;必须用 虚析构;否则子类析构不调用 → 内存泄漏
纯虚函数 & 抽象类 virtual void Drive() = 0;
包含纯虚函数 → 抽象类;抽象类 不能实例化;子类必须重写,否则还是抽象类
继承虚函数,重写加指针,多态就成立。父指子类象,析构必须虚,否则会泄漏。纯虚是接口,子类必须写,抽象不实例。override 检查,final 禁止改。
多态的核心是运行时根据对象类型调用对应函数,依赖虚函数和虚函数表实现
虚函数表是实现多态的底层机制,每个含虚函数的类都有一张表,对象通过 _vfptr 访问
抽象类(含纯虚函数)用于定义接口,强制派生类实现具体功能
基类析构函数应设为虚函数,避免内存泄漏