C++对象模型:g++的实现(一)

刚看完了《深度探索C++对象模型》第三章,这里做一下总结,也写一下我自己在g++ 7.5.0上的验证。
本文中所有的源文件都可以在这里拿到(百度网盘链接)。
注意,这里所说的“对象”是指在C++中使用classstruct关键字创建的类的实例。

1. 无继承情况下的C++对象内存布局

首先当然是从最基础的情况来讲,在没有继承的情况下的C++对象内存布局是什么样的?这又分为两种:无虚函数和有虚函数。

1.1 无虚函数

C++类内成员变量分为两类:static成员变量和非static成员变量。static成员变量不在类的实例的内部,在整个内存中只有一份,只需要使用类名即可访问;而非static成员变量在类的实例内部,需要为其分配空间。
在这种情况下C++的对象和C的结构体是一样的,毕竟要实现和C的兼容,主要就是结构体/类内成员变量的对齐。
其一般规则总结如下:

  1. 所有成员按照在类内的声明顺序在内存中排列;
// test00.cpp
#include <iostream>
int main();
class Test00 {
friend int main();
public:
int i1;
private:
int i2;
public:
int i3;
};
#define showOffset(ClassName, memberName) (reinterpret_cast<unsigned long>( &(static_cast<ClassName*>(nullptr)->memberName)))
int main() {
std::cout << showOffset(Test00, i1) << std::endl;
std::cout << showOffset(Test00, i2) << std::endl;
std::cout << showOffset(Test00, i3) << std::endl;
}
// Output:
// 0
// 4
// 8
  1. 任一非static成员变量的偏移(offset)要是其大小的倍数;
// test01.cpp
#include <iostream>
struct Test01 {
char c;
int i; // 如果紧凑排列,则i的偏移为1,但i的size为4,偏移要是4的倍数,因此i的偏移为4
};
#define showOffset(ClassName, memberName) (reinterpret_cast<unsigned long>( &(static_cast<ClassName*>(nullptr)->memberName)))
int main() {
std::cout << showOffset(struct Test01, i) << std::endl;
}
// Output:
// 4
  1. 结构体整体的size需要为最大非static成员变量size的倍数;
// test02.cpp
#include <iostream>
// 如果紧凑排,Test02_1的size应为9,
// 但要与int(size为4)对其,所以其size为12
struct Test02_1 {
char c1; // Offset: 0
int i; // Offset: 4
char c2; // Offset: 8
};
// Test02_2成员和Test02_1相同,但顺序不同,
// 受规则2和3影响,其size为8
struct Test02_2 {
char c1; // Offset: 0
char c2; // Offset: 1
int i; // Offset: 4
};
int main() {
std::cout << "sizeof Test02_1: " << sizeof(Test02_1) << std::endl;
std::cout << "sizeof Test02_2: " << sizeof(Test02_2) << std::endl;
}
// Output:
// sizeof Test02_1: 12
// sizeof Test02_2: 8
  1. 空对象的size为1,为了保证每个对象都有唯一的内存位置(memory location)
// test03.cpp
#include <iostream>
struct Test03 {}; // Empty class
int main() {
Test03 a, b;
std::cout << "sizeof Test03: " << sizeof(Test03) << std::endl;
if (&a == &b)
std::cerr << " Error! &a == &b, at " << static_cast<void*>(&a) << std::endl;
else
std::cout << "a and b has different address, &a = " << static_cast<void*>(&a) << " and &b = " << static_cast<void*>(&b) << std::endl;
}
// Output:
// sizeof Test03: 1
// a and b has different address, &a = 0x7fffe62e8486 and &b = 0x7fffe62e8487
  1. 当类(class)/结构体(struct) A 作为一个类B的内部成员变量时,其对齐要求为类A内部最大的对齐要求;
// test04.cpp
#include <iostream>
// 规则2中的类,size为8,对齐要求为4
struct Test01{
char c;
int i;
};
struct Test04 {
char c; // Offset: 1, size 1
Test01 t; // Offset: 4, size 8
};
#define showOffset(ClassName, memberName) (reinterpret_cast<unsigned long>( &(static_cast<ClassName*>(nullptr)->memberName)))
int main() {
std::cout << "Offset of t in struct Test04: " << showOffset(Test04, t) << std::endl;
std::cout << "sizeof Test04: " << sizeof(Test04) << std::endl;
}
// Output:
// Offset of t in struct Test04: 4
// sizeof Test04: 12
  1. 空的类(empty class)A作为作为一个类B的成员变量时,类A占用一个字节的空间,对其要求也为1;
// test05.cpp
#include <iostream>
// 规则4中的类,空类,size = 1
struct Test03 {};
struct Test05 {
char c; // Offset: 0, size: 1
Test03 t; // Offset: 1, size: 1
};
#define showOffset(ClassName, memberName) (reinterpret_cast<unsigned long>( &(static_cast<ClassName*>(nullptr)->memberName)))
int main() {
std::cout << "Offset of t in struct Test04: " << showOffset(Test05, t) << std::endl;
std::cout << "sizeof Test05: " << sizeof(Test05) << std::endl;
}
// Output:
// Offset of t in struct Test04: 1
// sizeof Test05: 2

1.2 有虚函数

C++使用虚函数来实现多态,非虚函数不展现多态性,当调用非虚函数时,只要调用一个写死的地址即可,无论是使用对象调用还是使用指针/引用调用;而当使用指针/引用调用虚函数需要视其绑定到的实际对象来调用对应的虚函数,以展现多态性(用对象调用虚函数不展现多态性)。
而C++实现虚函数用到的便是虚表。所谓虚表,就是保存该类所有虚函数地址的一张表,一个类的某个确定的虚函数在虚表的确定位置,而类实例中有一个虚表指针指向该虚表,当出现类继承并覆写(override)了该虚函数时,只需要将虚表指针指向另一张虚表,该虚表中对应位置的函数指针换为新的函数即可。另外,一个类的所有对象共享同一张虚表,因此不会带来大的内存消耗。该虚表由编译器生成。
这里只是对于虚函数和虚表进行了简单的描述,详细可查询网络资源,这里不再赘述。
就像上面所说,相比于没有虚函数的类,由虚函数的类的实例只是多了一个指向虚表的指针,其放在类的开头或者结尾(g++将其放在类的开头),大小和对其要求视平台而定,在x86-64平台上,虚表指针大小和对其要求为8字节。

// test06.cpp
class Point {
public:
Point(int x)
:m_x(x)
{}
virtual
int getX()
{ return m_x; }
private:
int m_x;
};
int main() {
Point p(1);
int x = p.getX();
}

gdb调试图片1
使用gdb观察,可以看到Point类实例p的size为16,包括size为8的虚表指针和size为4的int类型的成员变量m_x,同时,由于虚表指针的对其要求为8,所以Point的size必须是8的倍数,所以其size为16。
同时查看p的内存布局,可以看到虚表指针被放置于类实例的头部,占用8个字节,后面紧跟4个字节的int类型的成员变量m_i,最后填充了4个字节以使类Point的size为8的倍数。
gdb调试图片2
我们在查看一下虚表指针指向的内存,我这里使用的是64位系统和程序,所以函数指针是8位大小,虚表指针指向的虚表的第一个表项是地址0x080007b2,同时查看反汇编,因为我们使用对象来调用虚函数,不展现多态性,这里直接call了Point::getX()的地址,可以看到其地址为0x080007b2,正好是前面虚表的第一个表项。
还有就是Point类型对应的typeinfo对象的地址,在《深度探索C++对象模型》中提到其位于虚表的第一个表项,但前面我们看到虚表第一个表项存放的是虚函数,那typeinfo的地址放在哪里呢?我们来找一下。

// file test07.cpp
#include <typeinfo>
2 class Point {
3 public:
4 Point(int x)
5 :m_x(x)
6 {}
7
8 virtual
9 int getX()
10 { return m_x; }
11
12 private:
13 int m_x;
14 };
15
16 int main() {
17 Point p(1);
18 auto& ti = typeid(p);
19 int x = p.getX();
20 }

gbd调试图片3
可以看到反汇编中保存了0x8200da8这一地址到栈上,再结合我们的源码,很可能gdb所提示的<_ZTI5Point>这一对象就是Point类的typeinfo对象,我们使用工具c++filt来看_ZTI5Point这个被修饰过的符号是什么含义,不出所料,正是Point类对应的typeinfo对象。

liuyun@DESKTOP-Q5AT31V:/tmp/test/cppObjectModel/chap03/blog$ c++filt _ZTI5Point
typeinfo for Point

既然Point对应的typeinfo对象的地址为0x8200da8,我们查看虚表附近的地址,发现虚表指针指向的地址的前面的一个QWORD的内容正好是typeinfo的地址,那是不是虚表指针指向的并不是虚表的开头,而是第一个虚函数所在的地址,而在虚表中,第一个虚函数这一表项前面便是该类对应的typeinfo的地址?
g++使用-fdump-class-hierarchy选项生成类信息
在查阅资料的时候,《C++虚函数之二:虚函数表与虚函数调用》这篇博客提到g++支持-fdump-class-hierarchy这一编译选项,可以生成一个名为{source_file_name}.002t.class的文件,文件中详细记录了各个类的信息,包括其虚表信息。
正如我们所想,如果我们使用vptr指代虚表指针,那么vptr[0]就是第一个虚函数的地址,vptr[-1]则是该类对应的typeinfo的地址,而在最前面,g++还填充了一个空的表项。

最后还有一个问题,再没有虚函数的时候,编译器为了让每一个对象都有自己独一无二的地址,会在对象中插入一个字节占位,而在有虚函数的时候类中会有一个原生的虚表指针vptr,从而至少占8字节大小(x86-64上),那么是否就不需要再插入一个字节了呢?事实正如我们所想,Test08类的size为8而不是16。

// test08.cpp
#include <iostream>
class Test08 {
public:
virtual
int getNumber() { return s_i++; }
private:
static int s_i;
};
int Test08::s_i = 0;
int main() {
std::cout << "sizeof Test08: " << sizeof(Test08) << std::endl;
Test08 t;
int i = t.getNumber();
}
// Output:
// sizeof Test08: 8

这一篇博客就先写到这里,下一篇再谈谈在继承体系下g++是如何实现C++对象的内存布局的。

posted @   流云cpp  阅读(259)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」
点击右上角即可分享
微信分享提示