栈Stack,一般调用函数时,在作用域中会栈用来存放参数和返回地址。
在消亡时自动归还内存
堆Heap,系统提供的全局内存空间,一般通过动态分配取得的都是这种全局内存。
结束后要手动释放内存,否则内存泄漏
- 栈对象
在作用域中创建的对象,可以理解成临时变量。
在作用域结束时自动消亡,执行析构。因此也叫自动(auto)对象
- 静态对象&全局对象
相比普通的栈对象,生存期扩大到了整个程序。程序结束后才会消亡
某些情况会导致内存泄漏
全局对象因为在main外面,又只能放在前面,所以可以说是全局
- 堆对象
为指针开辟内存空间,要在指针改变或消亡前释放空间,才能结束。
若没有释放空间,则对象一直存在,内存泄漏。
内存泄漏: 本来划的一块内存因为某些原因不再被控制了。
指针可以改变指向,而且有的指针存放在栈中,当指针被释放后,会失去对内存的控制。
在使用new的时候,实际先调用operator new分配内存,再调用构造函数创建对象。
//以Complex对象为例,当执行Complex* pc = new Complex(1, 2);时 Complex *pc; void* mem = operator new( sizeof(Complex) ); //operator new不是运算符,是函数,实质是调用malloc。但operator new可重载 pc = static_cast(mem); //c++特有的类型转换方式,当然用c的方式也行 pc->Complex::Complex(1,2); //通过指向对象的指针调用构造函数。(此时参数this等于pc
在delete时,先析构,再回收内存。
//以String为例,我们动态分配一个String对象,其内部又是一个动态分配。当delete ps时 String::~String(ps); //内容是delete[]字符串,所以共delete两次 operator delete(ps); //实质是调用free,同operator new
- 拓展:动态内存分配,真正所得的内存块(以VC为例,了解底层)
单个对象:
- 如果new了一个Complex,其中包含 2*4=8 字节。而为了标记会在前后各加4字节的cookie。cookie记录了这块new的内存的主要信息并起引导作用(可以认为指针通过cookie进而指向对象本身)
VC的特点是内存为16字节的倍数。我们以4字节为一个单元(方便看,毕竟 int 就是4字节),则为4单元的倍数。
- 正常模式:此时这块内存为 8 + 2 * 4 = 16,分配完成,并将cookie记录为11h(16进制的16+1,16为占用内存,末位1或0标记此处是否占用。因此占用内存必须是16的倍数,即4单元的倍数)
- 调试模式:Complex前面会有8个单元的调试头,后面会紧跟着加1个调试用单元,共9个。所以要加3个pad单元,cookie记为41h
pad 的意思就是垫子。
咲夜的那个
- 如果是String,其内容只有一个指针,4个字节
- 正常模式:需要加一个pad,分配完成
- 调试模式:2+9+1=12是4的倍数。不用加pad
而String内又会指向另外的内存分配,也要经历这样的过程,但分配的是字符串
new 函数将内存分配进行了封装,相比底层的 malloc ,更加直观
对象数组:
-
Complex* p = new Complex [3];cookie和调试字节不变,只是要加一个int(4字节)用来记录数组容量3,内容变成3块Complex连起来,即6个单元(也要构造3次)
再实际计算需要的pad数和cookie数据。
垫,都可以垫 -
String* p = new String[3];同上
以上解释了数组为何是物理意义上的连续内存
回收内存:原理是读cookie存储的数据,直接整体回收对应大小的内存。单个数据用delete,只析构一次;而数组特别是含指针的如String类要用delete[],将数组中每个对象分别析构。
- 简单的类不需要特别的析构函数,可以当做打包的基本数据,所以简单数组也可以delete
- 而String等类析构时要回收指针指向的具体内存,否则内存泄漏
- delete 的原理是搜索内存首尾的 cookie 并执行一次析构
- 而delete[ ]会逐个检查每个单元,对每个元素分别执行析构
- 小补充:
要在栈(作用域)中创建对象时,编译器会检查析构函数的访问性。如果为private或protected则无法创建(很合理)。
这样可保证只能在堆中建立对象,防止在栈中建立。若有子类,则析构需要重载,作为虚函数,放在protected更合适。
相对应的,要防止在堆中建立对象,可以将标准的new函数放入private中。



