1、什么是对象模型
C++对象模型可以概括为以下2部分:
(1)语言中直接支持面向对象程序设计的部分;
(2)对于各种支持的底层实现机制。
语言中直接支持面向对象程序设计的部分,包括了构造函数、析构函数、多态、虚函数等。 对象模型的底层实现机制并未标准化,不同的编译器有一定的自由来设计对象模型的实现细节。
C++的类有2种成员变量:static, nonstatic,3种成员函数:static, nonstatic, virtual。
在C++对象模型中:
(1)nonstatic 成员变量被放置到对象内部;
(2)static成员变量、static and nonstatic成员函数均被放到对象之外。
(3)对虚函数的支持分为两步:
a)每个class的对象都会添加一个指针(vptr),指向相关的虚函数表(vtbl)。
b)每个class会为每个虚函数生成一个指针,这些指针统一放在虚函数表中(vtbl)
(4)另外,虚函数表地址的前面设置了一个指向type_info类的指针。C++提供了一个type_info类来获取对象类型信息。
假如有这么一个类:
using namespace std;
class Base {
public:
Base(){}
void print() { cout << "调用了普通成员函数Base::print()" << endl; }
static void print_s() { cout << "调用了静态成员函数Base::print_s()" << endl; }
virtual void print_v() { cout << "调用了虚函数Base::print_v()" << endl; }
virtual ~Base() {}
public:
char base_c[8];
float base_f;
int base_i;
static int base_s; //变量声明
};
int Base::base_s = 0; //变量定义
这个类在计算机中,该怎么存储它的数据和函数呢?
我这里先给出结论,其实它的C++对象模型是这样的:
2、对象模型验证
下面,我分别用3种不同的方法来验证这个类的对象模型。我这里用VS2019来调试代码(选择X86环境)。
在main()函数里加上下面的代码:
Base base;
std::cout << sizeof(base) << std::endl;
memset(base.base_c, 0, 8);
strcpy(base.base_c, "Peter");
base.base_f = 170.5;
base.base_i = 9;
Base::base_s = 11;
return 0;
(1)方法1:通过快速监视查看
我们把Debug断点设在第10行:return 0的位置。运行后打开VS2019的快速"监视窗口"。
输入:&base
可以看到如下结果:
其实这就是对象base的数据布局。从上到下按顺序分别是:虚函数表指针vptr、base_c、base_f和base_i。但是没有base_s变量。
(2)方法2:用cl /d1命令查看
打开Developer Command Prompt for VS2019,在命令行中输入:cl /d1 reportSingleClassLayoutBase ch01.cpp
(3)方法3:通过读取二进制流进行确认
我们往原来的main()函数继续添加代码,改成下面这样:
int main() {
Base base;
std::cout << sizeof(base) << std::endl;
memset(base.base_c, 0, 8);
strcpy(base.base_c, "Peter");
base.base_f = 170.5;
base.base_i = 9;
Base::base_s = 11;
//下面通过代码来验证模型:
//我们直接通过对象地址来读取对应的值
//1.调用虚函数
//1.1.对象的开头保存的是虚函数指针,这里用long是因为x86中虚函数指针长度是4个字节
long* pvptr = (long*)&base;
//1.2.把虚函数指针指向的虚函数表地址取出来
long* vptr = (long*)(*pvptr);
//1.3.定义函数指针
typedef void (*Func)(void);
//1.4.调用虚函数
Func f1 = (Func)vptr[0];
f1();
//2.调用char字符串
char* pbase_c = (char*)(((char*)&base) + 4);
cout << pbase_c << endl;
//3.调用base_f
float* pbase_f = (float*)(((char*)&base) + 12);
cout << *pbase_f << endl;
//4.调用base_i
int* pbase_i = (int*)(((char*)&base) + 16);
cout << *pbase_i << endl;
return 0;
}
输出结果:
我简单地讲解一下代码:
(1)long* pvptr = (long*)&base;
把存储在base首地址的虚函数指针读取出来。
(2)long* vptr = (long*)(*pvptr);
虚函数表指针指向的是虚函数表,所以*pvptr就是虚函数表地址,用long指针vptr指向虚函数表的首地址。
(3)Func f1 = (Func)vptr[0];
vptr[0]代表虚函数表里的第一个虚函数,也就是virtual void print_v()。
(4)char* pbase_c = (char*)(((char*)&base) + 4);
因为虚函数表指针占了4个字节,所以要从base首地址往下移动4个字节。这里要注意,你不能用&base + 4,必须得这样写((char*)&base) + 4。
(5)float* pbase_f = (float*)(((char*)&base) + 12);
因为base_c占了8个字节,所以要从base首地址往下移动4+8 = 12个字节才是base_f的首地址。