C++对象模型

Davids 2025-08-10 05:05:12
Categories: > Tags:

实例化对象

影响因素

虚函数
由虚表指针指向,所有对象共享。
非虚函数
所有对象共享。
非静态数据成员
对象大小的决定因素
静态函数
所有对象共享。
静态数据成员

内存对齐
可以手动指定。32位、64位不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Abstract {
public:
Abstract(float value) : m_Value(value) {}
// 虚函数
virtual ~Abstract() {}
// 非虚函数
float GetValue() { return m_Value; }
// 静态函数
Static Abstract* GetInstance();
private:
// 非静态数据成员
float m_Value;
// 静态数据成员
static Abstract* m_Instance;
};
Abstract abstract;
printf("%d", sizeof(abstract));
// 内存布局
// vptr
// float
// pad (或有)
// 64位 16 = 4(float) + 8(虚表指针) + 4(对齐)
// 32位 8 = 4(float) + 4(虚表指针)

一个实例对象,包含 非静态数据成员、虚表指针、为内存对齐而必须的填充静态数据成员、函数独立于实例对象。

空对象

1
2
3
4
5
6
7
class Empty{
};
Empty empty;
printf("%d", sizeof(empty)); // 1
Empty arr[3]; // 若sizeof(Empty)=0,则arr[0]、arr[1]、arr[2]地址相同
// 内存布局
// 1

地址唯一性要求​​ C++规定每个对象必须有唯一的地址。空类对象大小至少为1字节,只是为了区分实例对象。 空类大小为0会导致数组元素无法正确分隔。

空基类

1
2
3
4
5
6
7
8
class Derived : public Empty { 
public:
int x;
};
Derived derived;
printf("%d", sizeof(derived)); // 4,空基类不占用额外空间
// 内存布局
// int

​**​空基类优化(Empty Base Optimization, EBO)**派生类的内存布局中,空基类与首个数据成员共享地址空间。

内存对齐

手动指定

1
2
3
4
5
6
7
8
9
10
#pragma pack(n) // n 字节对齐
#pragma pack() // 恢复默认

#pragma pack(1) // 1 字节对齐
struct NetworkPacket {
uint8_t type;
uint32_t data; // 紧邻 type,无填充
};
#pragma pack() // 恢复默认对齐
sizeof(NetworkPacket) = 5

手动修改内存对齐,作用域级别

1
2
3
4
5
6
7
8
9
10
11
12
13
__attribute__((aligned(n)))  // 单独设置 n 字节对齐,GCC/Clang 特有(非标准 C++)
alignas(n) // C++11 标准提供

// 结构体整体按 16 字节对齐
struct Vector4f __attribute__((aligned(16))) {
float x, y, z, w;
};
// 单个成员按 8 字节对齐
struct Data {
char id;
int value __attribute__((aligned(8)));
};
`Vector4f`地址为 16 的倍数;`value`在 `Data`中的偏移为 8 的倍数

手动修改内存对齐,声明级别

非静态数据成员声明顺序与内存对齐

相同访问控制级别(public protected private),更 靠后 的成员有更 的地址。
即内存布局和声明顺序一致。
对于不同访问控制级别,未作规定,由编译器自行实现。但实际上编译器也是按声明顺序进行处理。

变量顺序会直接影响 内存对齐

不含虚表的继承

单继承

1
2
3
4
5
6
7
8
9
10
11
class A {
float x;
short y;
};
class B : public A {
short z;
};
sizeof(B); // 12 = A(4 + 2 + 2(对齐)) + B(2 + 2(对齐))
// 顺序
// A
// B

类之间单独计算内存对齐。
C++ 保证,出现在 derived class 中的 base class subobject 有其 完整原样性
否则,大的 object 赋值给小的 object,会引发 object 切割,将 大的object 的 subobject 赋值给 小的 object。

多继承

1
2
3
4
5
6
7
class A {};
class B {};
class C : public A, public B {};
// 内存布局
// A
// B
// C

多重继承

1
2
3
4
5
6
7
8
9
10
11
class A {};
class B : public A {};
class C : public A {};
class D : public B, public C {};
// 内存布局
// B::A
// B
// C::A
// C
// D
// 菱形继承(特例),访问A需要指明,但与多重继承一致

含虚表的继承

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
26
class A {
virtual void DoSomething();
virtual void DoNothing();
void NoVirtual();
virtual ~A();
};
// 虚表结构
// vptr -> 虚表
// A_type_info
// A::DoSomething
// A::~A (类中声明的)
// A::~A (编译器合成的,会调用上面的)
// A::DoNothing
class B : public A {
virtual void DoSomething() override;
};
// 虚表结构
// vptr -> 虚表
// B_type_info
// B::DoSomething
// B::~A (类中声明的)
// B::~A (编译器合成的,会调用上面的)
// A::DoNothing
// 虚表调用过程
(*ptr->vptr[n])(ptr)
// 菱形继承(特例),访问A需要指明,但与多重继承一致

构造函数语义

默认构造生成规则

一些情况下,编译会定义 默认构造、拷贝构造、移动构造、拷贝复制运算符、移动复制运算符、析构成员函数

以下条件满足都满足时,编译器不会提供 默认构造

基类和数据成员的初始化时机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A {
A(){}
};
class B : public A {
B(){
A::A(this); //由编译器扩充
A::A(&a); //由编译器扩充
std::string(&m_Str); //由编译器扩充
m_Str = "m_Str"; //函数自定义
}
B(const std::string& str) : m_Str(str) {
A::A(this); //由编译器扩充
A::A(&a); //由编译器扩充,按顺序(初始化成员列表的顺序问题)
m_Str(std::string(&str)); //由编译器扩充
}
A a;
std::string m_Str;
};

构造是,从 低地址 -> 高地址 去构造

拷贝构造生成规则

以下条件均满足时,编译器不会实现 拷贝构造、移动构造

析构函数语义

析构函数生成规则

以下条件满足都满足时,编译器不会提供 默认析构

数据成员 析构执行的顺序,与数据成员的 声明顺序 相反。
析构是,从 高地址 -> 低地址 去析构。

1
2
3
4
5
6
7
8
9
10
class A { ~A(){} };
class B { ~B(){} };
class C { ~C(){
A a,
B b,
} };
// 顺序
// C::~C
// B::~B
// B::~B

函数调用原理

全局函数

step1 参数传递
step2 调用函数
step3 处理返回值

非静态非虚成员函数

1
2
3
class A { A(...){} }
// 实际执行逻辑
A::A(&a, ...) // 隐式传递 this 指针

step1 参数传递
step2 把对象的首地址作为参数,调用函数
step3 处理返回值

构造函数、拷贝构造函数、析构函数、虚函数

step1 参数传递
step2 虚表跳转
step3 把对象的首地址作为参数,调用函数
step4 处理返回值