浅浅探究一下c++对象内存模型

最近做 cmu15445 project2 实现 B+ Tree,有一个操作涉及到将一个从 buffer_pool_manager 获取的缓存页通过 reinterpret_cast直接转成一个类对象指针,并调用了其成员函数。第一次见这样的操作还挺无法理解的,因为这相当于将一段全为 0 的内存直接当作一个类对象去访问,听起来感觉很离经叛道,毕竟这个对象根本没有初始化过。顿感对 c++ 对象模型的了解太少,于是查了一些资料。更深入的细节还是得系统地看看 《深度探索c++对象模型》。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Base {
public:
void print() {
std::cout << this << " " << "Base::print()" << std::endl;
}
static void sPrint() {
std::cout << "Base::sPrint()" << std::endl;
}

virtual void virtualPrint() {
std::cout << "Base::virtualPrint()" << std::endl;
}
char x;
static int s;
};

int Base::s = 10;

内存模型如下:

p1

占用类对象内存的只有虚函数表指针和非静态成员变量,非虚成员函数的存储其实和普通函数相同,都是存储在代码区,其地址在编译期已经确定,只不过调用时额外传入 this 指针作参数罢了。静态成员变量在全局区,不占用对象空间。所以对象没有初始化对于调用非虚成员函数,静态成员函数,访问静态成员变量没有影响。测试如下:

1
2
3
4
5
6
7
8
9
10
11
int main() {
std::cout << sizeof(Base) << std::endl;
char data[16];

Base* b = reinterpret_cast<Base*>(data);
cout << b << endl;
// b指向未初始化的内存,但不影响以下操作
b->print();
b->sPrint();
cout << b->s << endl;
}

输出

1
2
3
4
5
16
0x7fff03157d00
0x7fff03157d00 Base::print()
Base::sPrint()
10

但是如果调用 b->virtualPrint()就会崩溃,因为要访问虚函数表,而虚函数表指针的内存没有初始化过。但是我们可以“组装”一个带虚函数的对象出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void hack(Base* b) {
cout << b << " " << "hack()" << endl;
cout << b->x << endl;
}

int main() {

std::cout << sizeof(Base) << std::endl;
char data[16];

// “组装”虚函数表
void* vtbl[] = {reinterpret_cast<void*>(hack)};
void* vtbl_ptr = &vtbl;
memcpy(data, &vtbl_ptr, 8);
data[8] = 'A';

Base* b = reinterpret_cast<Base*>(data);
cout << b << endl;
b->print();
b->sPrint();
cout << b->s << endl;

cout << endl;
b->virtualPrint();
}

输出

1
2
3
4
5
6
7
8
16
0x7ffdc5d2b670
0x7ffdc5d2b670 Base::print()
Base::sPrint()
10

0x7ffdc5d2b670 hack()
A

hhh还挺有意思的。

更复杂的继承以及多继承、虚继承下的内存模型暂时还不了解,以后有空再更新吧。

参考链接:

一个很好用的画图网页: VisionOn