温故而知新,本文浅聊和回顾下C++内存布局的知识。
一、c++内存布局
C++的内存布局主要包括以下几个部分:
- 代码段:存储程序的机器代码。
- .数据段:存储全局变量和静态变量。数据段又分为初始化数据段(存储初始化的全局变量和静态变量)和未初始化数据段(存储未初始化的全局变量和静态变量)。
- 堆:用于动态内存分配。当你使用new或malloc函数时,内存会从堆中分配。
- 栈:用于存储局部变量和函数调用的信息(例如返回地址和参数)。当你调用一个函数时,一个新的栈帧会被压入栈,当函数返回时,这个栈帧会被弹出。
- 常量段:存储常量字符串和其他常量。
代码示例
#include <iostream>
int global_var = 0; // 初始化的全局变量,存储在初始化数据段
int uninit_global_var; // 未初始化的全局变量,存储在未初始化数据段
void foo() {
int local_var = 0; // 局部变量,存储在栈
static int static_local_var = 0; // 静态局部变量,存储在初始化数据段
int* dynamic_var = new int(0); // 动态分配的内存,地址在堆,dynamic_var指针变量的生命周期是foo函数栈
std::cout << "local_var: " << &local_var << std::endl;
std::cout << "static_local_var: " << &static_local_var << std::endl;
std::cout << "dynamic_var: " << dynamic_var << std::endl;
delete dynamic_var; // 释放动态分配的内存
}
int main() {
std::cout << "global_var: " << &global_var << std::endl;
std::cout << "uninit_global_var: " << &uninit_global_var << std::endl;
foo();
return 0;
}
二、C++ 类的内存布局
C++类的内存布局主要取决于类的数据成员和继承关系。以下是一些基本的规则:
- 数据成员变量:类的数据成员按照它们在类定义中的顺序存储在内存中。每个数据成员的偏移量是它的类型对齐要求的倍数。
// x64
#pragma pack(push,4) //指定4字节对齐
class TmpClass{}; // 空类sizeof,大小为1
class NoVirtual
{
public:
int m_i; // 4字节
double m_d; // 8字节
shared_ptr<int> m_ptr; // 8字节 ==》64bit system ;4字节 ==》 32bit system
};
#pragma pack(pop)
- 成员函数:在C++中,成员函数并不直接存储在每个对象中。相反,所有对象共享同一个成员函数的副本。成员函数的代码存储在代码段,而不是每个对象的内存空间。因此,成员函数不影响类的
sizeof
大小。
当你调用一个对象的成员函数时,编译器会自动将对象的地址作为隐藏参数传递给成员函数。这个隐藏参数通常被称为this指针。通过this指针,成员函数可以访问调用它的对象的数据成员。
class NoVirtual
{
void dc(){} // 成员函数,内存在代码段
public:
int m_i; // 4字节
double m_d; // 8字节
shared_ptr<int> m_ptr; // 8字节 ==》64bit system ;4字节 ==》 32bit system
};
-
静态成员变量:静态成员变量不属于类的任何一个对象,它们在所有对象之间共享。静态成员变量存储在全局数据段,而不是对象的内存空间。
-
静态成员函数:静态成员函数也不属于类的任何一个对象。它们没有this指针,因此不能访问类的非静态成员。静态成员函数的地址存储在代码段。
-
继承:如果一个类继承自一个或多个基类,那么基类的数据成员会先于派生类的数据成员存储在内存中。如果有多个基类,那么基类的数据成员按照它们在类定义中的顺序存储。
class Iface
{
public:
Iface(){MYTRACE();}
virtual ~Iface(){MYTRACE();}
virtual void Ifun() = 0;
};
// 继承
class MemLayout : public Iface
{
public:
MemLayout(){ MYTRACE(); }
~MemLayout(){ MYTRACE(); }
virtual void Ifun() override { MYTRACE(); }
virtual void dc0(){ MYTRACE(); }
virtual void dc1(){ MYTRACE(); }
private:
int m_num = 0;
static std::string m_desc;
};
std::string MemLayout::m_desc = "hello";
- 虚函数:如果一个类有虚函数(
virtual
关键字修饰),那么编译器会为这个类生成一个虚函数表(vtable
: 函数指针数组),并在每个对象中添加一个指向虚函数表的指针(vptr
)。虚函数表中存储了虚函数的地址。如果一个类继承自一个有虚函数的基类,那么它会继承基类的虚函数表。
class VirtualClass
{
virtual void dc(){}
public:
int m_i; // 4字节
double m_d; // 8字节
shared_ptr<int> m_ptr; // 8字节 ==》64bit system ;4字节 ==》 32bit system
};
- 虚继承:如果一个类使用虚继承,那么编译器会为这个类生成一个虚基类表(
vbtable
),并在每个对象中添加一个指向虚基类表的指针。虚基类表中存储了虚基类的偏移量。*直白点说,虚继承的派生类的实例化对象,会包含多张虚函数表的(下面有图为证)~,具体有几个vptr
,我们会在《C++内存布局(二)》中详细研究下,尤其是多重继承和钻石继承的场景下。
class NoVirtual
{
void dc(){}
public:
int m_i; // 4字节
double m_d; // 8字节
shared_ptr<int> m_ptr; // 8字节 ==》64bit system ;4字节 ==》 32bit system
};
class Iface
{
public:
Iface(){MYTRACE();}
virtual ~Iface(){MYTRACE();}
virtual void Ifun() = 0;
};
// 虚继承
class MemLayout : virtual public Iface
{
public:
MemLayout(){ MYTRACE(); }
~MemLayout(){ MYTRACE(); }
virtual void Ifun() override { MYTRACE(); }
virtual void dc0(){ MYTRACE(); }
virtual void dc1(){ MYTRACE(); }
private:
int m_num = 0;
static std::string m_desc;
};
std::string MemLayout::m_desc = "hello";