第16章 类的内存
类的内存布局
类的内存布局是指类的成员变量和成员函数在内存中的存储方式。理解类的内存布局对于优化代码性能、理解对象模型和调试问题都非常重要。
基本概念
- 成员变量:存储在对象的内存空间中,每个对象都有自己的成员变量副本
- 成员函数:存储在代码段中,所有对象共享同一个成员函数副本
- 静态成员:存储在静态存储区中,所有对象共享同一个静态成员
- 虚函数表:存储在只读数据段中,包含虚函数的地址
- 虚指针:存储在对象的内存空间中,指向虚函数表
类的大小计算
类的大小由以下因素决定:
- 成员变量的大小:所有非静态成员变量的大小之和
- 内存对齐:为了提高访问效率,编译器会对成员变量进行内存对齐
- 虚指针:如果类包含虚函数,会额外增加一个虚指针的大小
- 继承:派生类会包含基类的成员变量
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 { };
std::cout << "Size of EmptyClass: " << sizeof(EmptyClass) << " bytes" << std::endl;
class SimpleClass { private: char c; int i; double d; };
std::cout << "Size of SimpleClass: " << sizeof(SimpleClass) << " bytes" << std::endl;
class ClassWithVirtual { private: int x; public: virtual void doSomething() { } };
std::cout << "Size of ClassWithVirtual: " << sizeof(ClassWithVirtual) << " bytes" << std::endl;
|
内存对齐
内存对齐是指变量存储的起始地址必须是某个值的倍数,这个值称为对齐值。内存对齐的目的是提高内存访问效率。
默认对齐规则:
基本类型的对齐值通常是其大小
char:1字节short:2字节int、float:4字节double:8字节
结构体/类的对齐值是其成员中最大的对齐值
每个成员的起始地址必须是其对齐值的倍数
整个结构体/类的大小必须是其对齐值的倍数
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; int i; double d; short s; };
std::cout << "Size of AlignmentExample: " << sizeof(AlignmentExample) << " bytes" << std::endl;
class OptimizedAlignment { private: double d; int i; short s; char c; };
std::cout << "Size of OptimizedAlignment: " << sizeof(OptimizedAlignment) << " bytes" << std::endl;
|
虚函数表的内存布局
虚函数表(vtable)是实现运行时多态的关键机制,它存储了类的虚函数地址。
虚函数表的特点:
- 每个包含虚函数的类都有一个虚函数表
- 虚函数表存储在只读数据段中
- 每个对象都包含一个指向虚函数表的虚指针(vptr)
- 虚指针通常存储在对象的起始位置
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; } };
void testVTable() { Base b; Derived d; Base* ptr1 = &b; Base* ptr2 = &d; ptr1->func1(); ptr2->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; public: virtual void baseFunc() { } };
class Derived : public Base { private: int derivedData; public: void baseFunc() override { } virtual void derivedFunc() { } };
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 22 23 24 25 26 27 28 29 30 31
| class Base1 { private: int base1Data; public: virtual void func1() { } };
class Base2 { private: int base2Data; public: virtual void func2() { } };
class Derived : public Base1, public Base2 { private: int derivedData; public: void func1() override { } void func2() override { } virtual void func3() { } };
std::cout << "Size of Base1: " << sizeof(Base1) << " bytes" << std::endl; std::cout << "Size of Base2: " << sizeof(Base2) << " 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 22 23 24 25 26 27 28 29 30 31
| class VirtualBase { private: int baseData; public: virtual void baseFunc() { } };
class Derived1 : public virtual VirtualBase { private: int derived1Data; };
class Derived2 : public virtual VirtualBase { private: int derived2Data; };
class FinalDerived : public Derived1, public Derived2 { private: int finalData; };
std::cout << "Size of VirtualBase: " << sizeof(VirtualBase) << " bytes" << std::endl; std::cout << "Size of Derived1: " << sizeof(Derived1) << " bytes" << std::endl; std::cout << "Size of Derived2: " << sizeof(Derived2) << " bytes" << std::endl; std::cout << "Size of FinalDerived: " << sizeof(FinalDerived) << " bytes" << std::endl;
|
静态成员的存储
静态成员存储在静态存储区中,不属于对象的内存空间。
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;
ClassWithStatic obj1, obj2;
|
类的内存优化
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; double d; int i; short s; };
class Optimized { private: double d; int i; short s; char c; };
std::cout << "Size of Unoptimized: " << sizeof(Unoptimized) << " bytes" << std::endl; std::cout << "Size of Optimized: " << sizeof(Optimized) << " bytes" << std::endl;
|
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; bool flag2; bool flag3; bool flag4; int value; };
class WithBitFields { private: unsigned int flag1 : 1; unsigned int flag2 : 1; unsigned int flag3 : 1; unsigned int flag4 : 1; unsigned int value : 28; };
std::cout << "Size of WithoutBitFields: " << sizeof(WithoutBitFields) << " bytes" << std::endl; std::cout << "Size of WithBitFields: " << sizeof(WithBitFields) << " bytes" << std::endl;
|
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; std::cout << "Size of WithVirtual: " << sizeof(WithVirtual) << " bytes" << std::endl;
|
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; int data; };
class WithEBO : private EmptyBase { private: int data; };
std::cout << "Size of WithoutEBO: " << sizeof(WithoutEBO) << " bytes" << std::endl; std::cout << "Size of WithEBO: " << sizeof(WithEBO) << " bytes" << std::endl;
|
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() { 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(push, 1) class Packed { private: char c; int i; double d; }; #pragma pack(pop)
std::cout << "Size of Packed: " << sizeof(Packed) << " bytes" << std::endl;
|
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; Base2* b2 = &d;
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++的性能优势。