C++的两个参考文档
非官方文档: 官方文档:
C++ 多态通过虚函数和抽象类实现运行时绑定。纯虚函数定义接口,派生类必须重写才能实例化。底层依赖虚函数表(vtable)和虚函数指针(vfptr)。对象内存布局包含 vfptr,指向存储虚函数地址的数组。静态绑定在编译期确定地址,动态绑定在运行期通过 vfptr 查找。VS 编译器下虚函数表位于代码段(常量区)。理解内存分区与汇编调用有助于掌握多态原理。

非官方文档: 官方文档:
在虚函数的后面写上 = 0,则这个函数为纯虚函数。纯虚函数不需要定义实现(实现没啥意义因为要被派生类重写,但是语法上可以实现),只要声明即可。
包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象。如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写就实例化不出对象。
#include <iostream>
using namespace std;
// 抽象类
class Car {
public:
virtual void Drive() = 0; // 纯虚函数
};
class Benz : public Car {
public:
virtual void Drive() { cout << "Benz - 舒适" << endl; }
};
class BMW : public Car {
virtual void Drive() { cout << "BMW - 操纵" << endl; }
};
class Ferrari : public Car {
virtual void Drive() { cout << "Ferrari - 极致性能" << endl; }
};
class Base {
public:
virtual void Func1() { cout << "Func1()" << endl; }
virtual void Func2() { cout << "Func2()" << endl; }
virtual void Func3() { cout << "Func3()" << endl; }
protected:
int _b = 1;
char _ch = 'x';
};
int main() {
// Car car; // 编译报错:无法实例化抽象类
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
Car* pFerrari = new Ferrari;
pFerrari->Drive();
Base b;
cout << sizeof(b) << endl; // 32 位环境:12, 64 位环境:16
return 0;
}
Car 就是一个抽象类,这个实践验证了抽象类确实无法实例化出对象。
多态允许使用基类(如 Car)的指针来引用派生类(如 Benz、BMW、Ferrari)的对象,并调用在派生类中重写的虚函数。
以 Benz 为例:
Car* pBenz = new Benz;
pBenz->Drive(); // 调用 Benz::Drive(),如果 Drive() 是虚函数
其他两个派生类也是同样的道理。这需要基类 Car 中声明 Drive() 为虚函数(使用 virtual 关键字),然后在派生类中重写该方法。这样,通过基类指针调用 Drive() 时,会根据实际对象的类型动态决定调用哪个派生类的实现。
(此处省略调试截图,实际运行时输出各车型信息)
下面编译为 32 位程序的运行结果是什么? A. 编译报错 B. 运行报错 C. 8 D. 12
class Base {
public:
virtual void Func1() { cout << "Func1()" << endl; }
virtual void Func2() { cout << "Func2()" << endl; }
virtual void Func3() { cout << "Func3()" << endl; }
protected:
int _b = 1;
char _ch = 'x';
};
int main() {
Base b;
cout << sizeof(b) << endl; // 32 位环境:12, 64 位环境:16
return 0;
}
正确答案:选项 D。 32 位环境下运行结果是 12,所以选择 D 选项。
上面题目运行结果 12bytes,除了 _b 和 _ch 成员,还多一个 _vfptr 放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关)。对象中的这个指针我们叫做虚函数表指针(v 代表 virtual,f 代表 function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为一个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。
监视窗口会特殊处理,监视窗口看不到的话,内存窗口可以勉强一看。监视窗口会进行一些特殊处理,方便程序员看和调试,但是这导致窗口里面的东西不一定是真实的,不过地址是对的。
验证有两种方式:
(此处省略调试截图,显示 vfptr 位于对象起始位置)
转到反汇编可以获得比监视窗口更加明了的信息——通过汇编我们可以理解代码的底层行为。如下图,我们通过对比两种调用方式的 call 指令,可以明显地感受到两者的区别:满足多态时,运行时到指向对象的虚函数表中找到对应的虚函数进行调用;不满足多态的时候,编译时变成调用特定作用域的函数,普通调用直接去调用 Person 作用域的函数了,并且普通调用是在编译的时候确定地址的;而运行时的多态调用则是到指向的对象找地址。
这里我们打开反汇编,可以看一下普通调用的汇编,对比一下多态的汇编。 像上图中多态调用的这个 call 指令就是真实的调用。
我们可以转到反汇编看一下。确实,有时候转到反汇编可以获得比监视窗口更加明了的信息——通过汇编我们可以理解代码的底层行为。但是,汇编是很偏底层的,如果理想的岗位是算法岗、研发岗、测试开发岗、测试岗位的话,对汇编的需求就只是了解即可。
从底层的角度 Func 函数中 ptr->BuyTicket(),是如何作为 ptr 指向 Person 对象调用到 Person::BuyTicket,ptr 指向 Student 对象调用 Student::BuyTicket 的呢?通过下图我们可以看到,满足多态条件后,底层不再是编译时通过调用对象确定函数的地址,而是运行时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。
// ===============买票=============
class Person {
public:
virtual void BuyTicket() { cout << "买票 - 全价" << endl; }
private:
string _name;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票 - 打折" << endl; }
private:
string _id;
};
void Func(Person ptr) {
// 这里可以看到虽然都是 Person 指针 ptr 在调用 BuyTicket
// 但是跟 ptr 没关系,而是由 ptr 指向的对象决定的。
ptr.BuyTicket();
}
int main() {
Person ps;
Student st;
Func(ps);
Func(st);
Person p1;
Person p2;
Person p3;
return 0;
}
上面的这张图,ptr 指向的 Person 对象,调用的是 Person 的虚函数。 下面这张图,ptr 指向 Student 对象,调用的是 Student 的虚函数。
// 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)
0x00000000 标记(这个 C++ 并没有进行规定,各个编译器自行定义的,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::func3" << endl; }
void func4() { cout << "Derive::func4" << endl; }
protected:
int b = 2;
};
int main() {
Base b;
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;
};
int main() {
int i = 0;
static int j = 1;
int* p1 = new int;
const char* p2 = "xxxxxxxxxxxx";
printf("栈:%p\n", &i);
printf("静态区:%p\n", &j);
printf("堆:%p\n", p1);
printf("常量区:%p\n", p2);
Base b;
Derive d;
printf("Base 虚函数地址:%p\n", *(int*)&b);
printf("Derive 虚函数表地址:%p\n", *((int*)&d));
printf("虚函数地址:%p\n", &Base::func1);
printf("普通函数地址:%p\n", &Base::func5);
return 0;
}
从运行结果可以看出,虚函数地址和常量区的地址十分接近,由此可知:
VS 下,虚函数地址是存在代码段(常量区)里面的。
在本文的最后,艾莉丝会对比 x64 环境和 x86 环境的几个地址。
(此处省略对比截图)
// ======================多态语法层剩余 + 原理层======================
#include <iostream>
#include <string>
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::func3" << endl; }
void func4() { cout << "Derive::func4" << endl; }
protected:
int b = 2;
};
int main() {
Base b;
Derive d;
return 0;
}

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online