C++栈/堆创建含动态成员类对象的内存分配机制疑问
假设我们按如下方式在栈上创建对象:
class Test{ Test(int i) {i_=i;} Test(std::vector<int> v) {v_=v;} int i_; std::vector<int> v_; }; int main() { Test a; // how much memory is occupied/reserved now for a? . . . return 0; }
核心疑问:
- 尚未确定会调用哪个构造函数的前提下,编译器如何判定对象a所需的内存大小?
- 栈上会为该对象预留多少内存?
- 如果使用new关键字创建对象,对应的内存分配逻辑是怎样的?
上述示例做了极简化处理以明确核心疑问,目的是深入理解栈、堆上对象创建与初始化的底层逻辑。
对象需要的内存大小根本不需要等确定调用哪个构造函数才算得出来。
C++中任意一个类的实例大小,在编译阶段就已经是固定常量了,和运行时调用哪个构造函数、构造函数里给成员赋什么值没有任何关系。构造函数的作用只是在一块已经分配好的、大小固定的内存上执行初始化逻辑,完全不参与对象内存大小的计算。
先明确一点:贴出的示例代码实际上是编译不通过的——代码里只定义了接受int和接受vector的两个构造函数,没有定义无参默认构造函数,写Test a;会直接触发编译错误,根本到不了运行时内存分配的步骤。
如果补上默认构造函数,或者改成Test a(1);/Test a({1,2,3});这类合法写法,栈上为a预留的内存大小是固定的,只和类的非静态成员、内存对齐规则有关:
- 成员
int i_:绝大多数平台下占4字节 - 成员
std::vector<int> v_:vector本身的大小是固定值,和它存了多少个元素完全无关——vector内部只持有三个指针(或等价实现:指向数据块起始的指针、指向有效数据末尾的指针、指向已分配容量末尾的指针),64位系统下每个指针8字节,三个共24字节;32位系统下每个指针4字节,三个共12字节 - 内存对齐填充:编译器会按照类中最大基础成员的对齐要求补空位,64位系统下一般对齐到8字节边界,所以4字节的int后面会补4字节的填充位
加起来64位环境下Test类的实例大小固定为32字节,不管你调用哪个构造函数、给vector里塞多少个元素,栈上给a预留的都是这32字节。
注意:vector存储实际元素的内存是在堆上动态分配的,这部分内存不属于Test对象a本身,既不占a的栈空间,也不会改变a本身的大小。
编译器在编译整个函数的时候,会提前扫描函数内所有局部变量,把所有局部变量需要的栈空间总和、对齐要求全部算完,生成的汇编代码在刚进入函数的时候,就会直接移动栈指针,一次性把整个函数需要的栈空间全部预留出来,根本不会等运行时执行到对应变量定义行的时候再临时分配栈内存。
等运行时真正执行到对象定义语句时,只需要在之前已经预留好的、对应大小的栈内存位置上,调用匹配的构造函数完成初始化即可,全程不需要再做任何内存大小判断、内存申请操作。
函数执行完成准备返回时,会直接按逆序调用所有局部对象的析构函数,再把栈指针移回函数进入时的位置,一次性回收所有预留的栈空间,不需要逐个释放。
new创建堆对象的逻辑和栈上有相似点,也有明显区别:
- 和栈上规则一致,要创建的对象本身的大小在编译期就已经确定,是固定常量,和调用哪个构造函数无关
- 运行时执行new表达式时,第一步会先向堆内存管理器申请一块大小等于对象固定大小的连续内存,如果申请失败默认会抛出
std::bad_alloc异常 - 堆内存申请成功后,才会在这块刚申请到的内存上调用匹配的构造函数,完成对象初始化
- 初始化完成后返回指向这块堆内存的指针
和栈上创建对象的核心差异:
- 堆内存是运行时动态申请的,不是编译期提前预留的,申请时机就是执行到new语句的时刻
- 堆内存不会自动回收,必须手动调用
delete触发对象析构、释放对应内存,否则会出现内存泄漏 - 和栈对象一样,对象本身的内存大小固定,像vector这类成员持有的动态堆内存,依然是独立于new出来的对象本身单独分配的,不算在new申请的对象内存块大小里。
内容的提问来源于stack exchange,提问作者Denis

