第16章 类的内存

类的内存布局

类的内存布局是指类的成员变量和成员函数在内存中的存储方式。理解类的内存布局对于优化代码性能、理解对象模型和调试问题都非常重要。

基本概念

  1. 成员变量:存储在对象的内存空间中,每个对象都有自己的成员变量副本
  2. 成员函数:存储在代码段中,所有对象共享同一个成员函数副本
  3. 静态成员:存储在静态存储区中,所有对象共享同一个静态成员
  4. 虚函数表:存储在只读数据段中,包含虚函数的地址
  5. 虚指针:存储在对象的内存空间中,指向虚函数表

类的大小计算

类的大小由以下因素决定:

  1. 成员变量的大小:所有非静态成员变量的大小之和
  2. 内存对齐:为了提高访问效率,编译器会对成员变量进行内存对齐
  3. 虚指针:如果类包含虚函数,会额外增加一个虚指针的大小
  4. 继承:派生类会包含基类的成员变量
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
27
28
// 空类的大小
class EmptyClass { };

// 一个空类的大小是1字节,而不是0字节
// 这是为了确保每个对象都有唯一的内存地址
std::cout << "Size of EmptyClass: " << sizeof(EmptyClass) << " bytes" << std::endl; // 输出: 1

// 包含成员变量的类
class SimpleClass {
private:
char c; // 1字节
int i; // 4字节
double d; // 8字节
};

// 由于内存对齐,大小不是1+4+8=13字节
std::cout << "Size of SimpleClass: " << sizeof(SimpleClass) << " bytes" << std::endl; // 输出: 16

// 包含虚函数的类
class ClassWithVirtual {
private:
int x; // 4字节
public:
virtual void doSomething() { } // 虚函数
};

// 包含虚指针,大小为4(或8) + 4 = 8(或16)字节
std::cout << "Size of ClassWithVirtual: " << sizeof(ClassWithVirtual) << " bytes" << std::endl; // 输出: 8 (32位) 或 16 (64位)

内存对齐

内存对齐是指变量存储的起始地址必须是某个值的倍数,这个值称为对齐值。内存对齐的目的是提高内存访问效率。

默认对齐规则

  1. 基本类型的对齐值通常是其大小

    • char:1字节
    • short:2字节
    • intfloat:4字节
    • double:8字节
  2. 结构体/类的对齐值是其成员中最大的对齐值

  3. 每个成员的起始地址必须是其对齐值的倍数

  4. 整个结构体/类的大小必须是其对齐值的倍数

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
27
28
29
30
31
32
33
34
35
36
37
// 内存对齐示例
class AlignmentExample {
private:
char c; // 1字节,对齐值1
int i; // 4字节,对齐值4
double d; // 8字节,对齐值8
short s; // 2字节,对齐值2
};

// 内存布局分析:
// - c: 偏移0 (1字节)
// - 填充3字节 (使i的起始地址为4的倍数)
// - i: 偏移4 (4字节)
// - 填充4字节 (使d的起始地址为8的倍数)
// - d: 偏移16 (8字节)
// - s: 偏移24 (2字节)
// - 填充6字节 (使整个类的大小为8的倍数)
// 总大小: 32字节
std::cout << "Size of AlignmentExample: " << sizeof(AlignmentExample) << " bytes" << std::endl; // 输出: 32

// 优化内存对齐:调整成员变量顺序
class OptimizedAlignment {
private:
double d; // 8字节,对齐值8
int i; // 4字节,对齐值4
short s; // 2字节,对齐值2
char c; // 1字节,对齐值1
};

// 内存布局分析:
// - d: 偏移0 (8字节)
// - i: 偏移8 (4字节)
// - s: 偏移12 (2字节)
// - c: 偏移14 (1字节)
// - 填充1字节 (使整个类的大小为8的倍数)
// 总大小: 16字节
std::cout << "Size of OptimizedAlignment: " << sizeof(OptimizedAlignment) << " bytes" << std::endl; // 输出: 16

虚函数表的内存布局

虚函数表(vtable)是实现运行时多态的关键机制,它存储了类的虚函数地址。

虚函数表的特点

  1. 每个包含虚函数的类都有一个虚函数表
  2. 虚函数表存储在只读数据段中
  3. 每个对象都包含一个指向虚函数表的虚指针(vptr)
  4. 虚指针通常存储在对象的起始位置
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
27
28
29
30
31
32
33
34
// 虚函数表示例
class Base {
public:
virtual void func1() { std::cout << "Base::func1()" << std::endl; }
virtual void func2() { std::cout << "Base::func2()" << std::endl; }
void func3() { std::cout << "Base::func3()" << std::endl; } // 非虚函数
};

class Derived : public Base {
public:
void func1() override { std::cout << "Derived::func1()" << std::endl; } // 重写
virtual void func4() { std::cout << "Derived::func4()" << std::endl; } // 新增
};

// 虚函数表布局:
// Base的虚函数表: [&Base::func1, &Base::func2]
// Derived的虚函数表: [&Derived::func1, &Base::func2, &Derived::func4]

// 测试虚函数表
void testVTable() {
Base b;
Derived d;

// 通过基类指针调用虚函数
Base* ptr1 = &b;
Base* ptr2 = &d;

ptr1->func1(); // 调用 Base::func1()
ptr2->func1(); // 调用 Derived::func1()

// 虚指针大小
std::cout << "Size of Base: " << sizeof(Base) << " bytes" << std::endl;
std::cout << "Size of Derived: " << sizeof(Derived) << " bytes" << std::endl;
}

继承中的内存布局

单继承

在单继承中,派生类的内存布局包含基类的成员变量和自己的成员变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base {
private:
int baseData; // 4字节
public:
virtual void baseFunc() { }
};

class Derived : public Base {
private:
int derivedData; // 4字节
public:
void baseFunc() override { }
virtual void derivedFunc() { }
};

// 内存布局:
// Base对象: [vptr][baseData]
// Derived对象: [vptr][baseData][derivedData]

std::cout << "Size of Base: " << sizeof(Base) << " bytes" << std::endl; // 输出: 8 (32位) 或 16 (64位)
std::cout << "Size of Derived: " << sizeof(Derived) << " bytes" << std::endl; // 输出: 12 (32位) 或 24 (64位)

多继承

在多继承中,派生类的内存布局包含所有基类的成员变量,按照继承顺序排列。

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
27
28
29
30
31
class Base1 {
private:
int base1Data; // 4字节
public:
virtual void func1() { }
};

class Base2 {
private:
int base2Data; // 4字节
public:
virtual void func2() { }
};

class Derived : public Base1, public Base2 {
private:
int derivedData; // 4字节
public:
void func1() override { }
void func2() override { }
virtual void func3() { }
};

// 内存布局:
// Derived对象: [vptr1][base1Data][vptr2][base2Data][derivedData]
// 其中vptr1指向Derived的虚函数表(包含func1和func3)
// vptr2指向Derived的第二个虚函数表(包含func2)

std::cout << "Size of Base1: " << sizeof(Base1) << " bytes" << std::endl; // 输出: 8 (32位) 或 16 (64位)
std::cout << "Size of Base2: " << sizeof(Base2) << " bytes" << std::endl; // 输出: 8 (32位) 或 16 (64位)
std::cout << "Size of Derived: " << sizeof(Derived) << " bytes" << std::endl; // 输出: 20 (32位) 或 40 (64位)

虚拟继承

虚拟继承用于解决菱形继承问题,确保派生类只包含一个共享的基类子对象。

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
27
28
29
30
31
class VirtualBase {
private:
int baseData; // 4字节
public:
virtual void baseFunc() { }
};

class Derived1 : public virtual VirtualBase {
private:
int derived1Data; // 4字节
};

class Derived2 : public virtual VirtualBase {
private:
int derived2Data; // 4字节
};

class FinalDerived : public Derived1, public Derived2 {
private:
int finalData; // 4字节
};

// 内存布局:
// FinalDerived对象: [vptr1][derived1Data][vptr2][derived2Data][vptr3][baseData][finalData]
// 其中vptr1和vptr2指向Derived1和Derived2的虚函数表
// vptr3指向VirtualBase的虚函数表

std::cout << "Size of VirtualBase: " << sizeof(VirtualBase) << " bytes" << std::endl; // 输出: 8 (32位) 或 16 (64位)
std::cout << "Size of Derived1: " << sizeof(Derived1) << " bytes" << std::endl; // 输出: 12 (32位) 或 24 (64位)
std::cout << "Size of Derived2: " << sizeof(Derived2) << " bytes" << std::endl; // 输出: 12 (32位) 或 24 (64位)
std::cout << "Size of FinalDerived: " << sizeof(FinalDerived) << " bytes" << std::endl; // 输出: 24 (32位) 或 48 (64位)

静态成员的存储

静态成员存储在静态存储区中,不属于对象的内存空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ClassWithStatic {
private:
static int staticData; // 静态成员,存储在静态存储区
int nonStaticData; // 非静态成员,存储在对象的内存空间
public:
static void staticFunc() { } // 静态成员函数,存储在代码段
void nonStaticFunc() { } // 非静态成员函数,存储在代码段
};

// 初始化静态成员
int ClassWithStatic::staticData = 0;

// 静态成员不影响类的大小
std::cout << "Size of ClassWithStatic: " << sizeof(ClassWithStatic) << " bytes" << std::endl; // 输出: 4

// 所有对象共享同一个静态成员
ClassWithStatic obj1, obj2;
// obj1.staticData 和 obj2.staticData 指向同一个内存位置

类的内存优化

1. 成员变量顺序优化

核心原则:按照成员变量的大小从大到小排列,可以减少内存对齐带来的填充。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 优化前
class Unoptimized {
private:
char c; // 1字节
double d; // 8字节
int i; // 4字节
short s; // 2字节
};

// 优化后
class Optimized {
private:
double d; // 8字节
int i; // 4字节
short s; // 2字节
char c; // 1字节
};

std::cout << "Size of Unoptimized: " << sizeof(Unoptimized) << " bytes" << std::endl; // 输出: 24
std::cout << "Size of Optimized: " << sizeof(Optimized) << " bytes" << std::endl; // 输出: 16

2. 使用位域

核心原则:对于布尔类型或小范围整数,可以使用位域来减少内存占用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 未使用位域
class WithoutBitFields {
private:
bool flag1; // 1字节
bool flag2; // 1字节
bool flag3; // 1字节
bool flag4; // 1字节
int value; // 4字节
};

// 使用位域
class WithBitFields {
private:
unsigned int flag1 : 1; // 1位
unsigned int flag2 : 1; // 1位
unsigned int flag3 : 1; // 1位
unsigned int flag4 : 1; // 1位
unsigned int value : 28; // 28位
};

std::cout << "Size of WithoutBitFields: " << sizeof(WithoutBitFields) << " bytes" << std::endl; // 输出: 8
std::cout << "Size of WithBitFields: " << sizeof(WithBitFields) << " bytes" << std::endl; // 输出: 4

3. 虚函数优化

核心原则:只在需要多态的地方使用虚函数,避免不必要的虚指针开销。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 不需要多态的类
class NoVirtual {
private:
int data;
public:
void doSomething() { } // 非虚函数
};

// 需要多态的类
class WithVirtual {
private:
int data;
public:
virtual void doSomething() { } // 虚函数
};

std::cout << "Size of NoVirtual: " << sizeof(NoVirtual) << " bytes" << std::endl; // 输出: 4
std::cout << "Size of WithVirtual: " << sizeof(WithVirtual) << " bytes" << std::endl; // 输出: 8 (32位) 或 16 (64位)

4. 继承优化

核心原则:合理设计继承层次,避免过深的继承层次和不必要的多重继承。

1
2
3
4
5
6
7
8
9
10
// 过深的继承层次(不推荐)
class Base { /* ... */ };
class Derived1 : public Base { /* ... */ };
class Derived2 : public Derived1 { /* ... */ };
class Derived3 : public Derived2 { /* ... */ };
class Derived4 : public Derived3 { /* ... */ };

// 合理的继承层次(推荐)
class Base { /* ... */ };
class Derived : public Base { /* ... */ };

5. 使用空基类优化(EBO)

核心原则:C++标准允许空基类在作为基类时不占用内存空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 空基类
class EmptyBase { };

// 直接包含空类对象
class WithoutEBO {
private:
EmptyBase base; // 占用1字节
int data; // 4字节
};

// 继承空基类(空基类优化)
class WithEBO : private EmptyBase {
private:
int data; // 4字节
};

std::cout << "Size of WithoutEBO: " << sizeof(WithoutEBO) << " bytes" << std::endl; // 输出: 8
std::cout << "Size of WithEBO: " << sizeof(WithEBO) << " bytes" << std::endl; // 输出: 4

6. 内存池和对象池

核心原则:对于频繁创建和销毁的对象,使用内存池或对象池可以减少内存分配和释放的开销。

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 简单的对象池实现
template <typename T, size_t Size>
class ObjectPool {
private:
std::array<T, Size> objects;
std::bitset<Size> inUse;
public:
T* acquire() {
for (size_t i = 0; i < Size; ++i) {
if (!inUse.test(i)) {
inUse.set(i);
return &objects[i];
}
}
return nullptr;
}

void release(T* obj) {
if (obj >= &objects[0] && obj <= &objects[Size-1]) {
size_t index = obj - &objects[0];
inUse.reset(index);
}
}
};

// 使用对象池
void useObjectPool() {
ObjectPool<MyClass, 10> pool;

// 从池中获取对象
MyClass* obj1 = pool.acquire();
MyClass* obj2 = pool.acquire();

// 使用对象
// ...

// 释放对象回池
pool.release(obj1);
pool.release(obj2);
}

类内存布局的实际应用

1. 序列化和反序列化

核心需求:将对象的内存布局转换为字节流进行存储或传输。

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
27
28
29
30
// 简单的序列化实现
class Serializable {
private:
int x;
double y;
public:
Serializable(int x, double y) : x(x), y(y) {}

// 序列化到字节流
std::vector<char> serialize() const {
std::vector<char> data(sizeof(*this));
std::memcpy(data.data(), this, sizeof(*this));
return data;
}

// 从字节流反序列化
static Serializable deserialize(const std::vector<char>& data) {
Serializable obj(0, 0.0);
std::memcpy(&obj, data.data(), sizeof(obj));
return obj;
}
};

// 使用示例
void testSerialization() {
Serializable obj1(42, 3.14);
std::vector<char> data = obj1.serialize();
Serializable obj2 = Serializable::deserialize(data);
std::cout << "x: " << obj2.x << ", y: " << obj2.y << std::endl;
}

2. 内存调试和分析

核心需求:分析类的内存布局,查找内存泄漏和优化内存使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 内存布局分析工具
void analyzeMemoryLayout() {
// 使用sizeof操作符
std::cout << "Size of int: " << sizeof(int) << " bytes" << std::endl;
std::cout << "Size of double: " << sizeof(double) << " bytes" << std::endl;

// 分析类的成员变量偏移
class TestClass {
public:
char c;
int i;
double d;
};

TestClass obj;
std::cout << "Offset of c: " << reinterpret_cast<char*>(&obj.c) - reinterpret_cast<char*>(&obj) << " bytes" << std::endl;
std::cout << "Offset of i: " << reinterpret_cast<char*>(&obj.i) - reinterpret_cast<char*>(&obj) << " bytes" << std::endl;
std::cout << "Offset of d: " << reinterpret_cast<char*>(&obj.d) - reinterpret_cast<char*>(&obj) << " bytes" << std::endl;
}

3. 自定义内存分配器

核心需求:为特定类型的对象实现自定义内存分配器,提高内存分配效率。

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
27
28
29
30
31
32
33
34
35
// 简单的自定义分配器
template <typename T>
class CustomAllocator {
public:
using value_type = T;

CustomAllocator() = default;

template <typename U>
CustomAllocator(const CustomAllocator<U>&) noexcept {}

T* allocate(std::size_t n) {
if (n > std::numeric_limits<std::size_t>::max() / sizeof(T)) {
throw std::bad_alloc();
}
return static_cast<T*>(std::malloc(n * sizeof(T)));
}

void deallocate(T* p, std::size_t) noexcept {
std::free(p);
}
};

// 使用自定义分配器
void useCustomAllocator() {
std::vector<int, CustomAllocator<int>> vec;
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);

for (int x : vec) {
std::cout << x << " ";
}
std::cout << std::endl;
}

类内存布局的陷阱

1. 内存对齐依赖于编译器

陷阱:不同编译器的内存对齐规则可能不同,导致相同的类在不同编译器下大小不同。

解决方案

  • 使用#pragma pack__attribute__((packed))来控制内存对齐
  • 避免依赖特定的内存布局
1
2
3
4
5
6
7
8
9
10
11
// 使用#pragma pack控制内存对齐
#pragma pack(push, 1) // 按1字节对齐
class Packed {
private:
char c;
int i;
double d;
};
#pragma pack(pop) // 恢复默认对齐

std::cout << "Size of Packed: " << sizeof(Packed) << " bytes" << std::endl; // 输出: 13

2. 虚函数表的实现依赖于编译器

陷阱:虚函数表的具体实现细节依赖于编译器,不同编译器的实现可能不同。

解决方案

  • 避免直接操作虚函数表
  • 只通过标准的多态机制使用虚函数

3. 多重继承中的指针调整

陷阱:在多重继承中,将派生类指针转换为不同基类指针时,指针值可能会发生变化。

解决方案

  • 使用dynamic_cast进行安全的类型转换
  • 避免依赖指针的具体值
1
2
3
4
5
6
7
8
9
10
11
class Base1 { /* ... */ };
class Base2 { /* ... */ };
class Derived : public Base1, public Base2 { /* ... */ };

Derived d;
Base1* b1 = &d; // 指向Derived对象的起始位置
Base2* b2 = &d; // 指向Derived对象中Base2子对象的位置

std::cout << "Address of d: " << &d << std::endl;
std::cout << "Address of b1: " << b1 << std::endl;
std::cout << "Address of b2: " << b2 << std::endl;

4. 虚拟继承的复杂性

陷阱:虚拟继承的内存布局比较复杂,可能会增加内存开销和访问时间。

解决方案

  • 只在必要时使用虚拟继承
  • 考虑使用组合而非继承来避免菱形继承问题

总结

类的内存布局是C++对象模型的重要组成部分,理解类的内存布局对于优化代码性能、理解多态机制和调试内存问题都非常重要。

关键要点

  • 类的大小由成员变量、内存对齐、虚指针和继承决定
  • 内存对齐可以提高访问效率,但会增加内存开销
  • 虚函数表是实现运行时多态的关键机制
  • 合理的成员变量顺序可以减少内存占用
  • 空基类优化、位域和内存池等技术可以优化内存使用
  • 避免依赖特定的内存布局,因为它依赖于编译器实现

通过深入理解类的内存布局,开发者可以编写更加高效、可靠的C++代码,充分发挥C++的性能优势。