动态分配内存
Dynamic memory allocation
C++ 的动态空间分配由关键字 new 和 delete 实现,下面是它们的基本用法。
- new
- new int;
分配一个 int 类型的空间。 - new Stash;
分配一个 Stash 类对象的空间,并调用构造函数。 - new int[10];
分配 10 个 int 类型的空间。
- new int;
- delete
- delete p;
销毁 p ,调用析构函数并回收内存(p 必须是被 new 分配的空间) - delete [ ] p;
销毁以 p 为起始地址的连续空间。
- delete p;
例子:Dynamic Arrays
使用 new 申请一个动态的数组。
int * psome = new int [10];
- 这个 new 操作符申请了 10 个 int 大小的空间,并把起始地址交给了 psome 指针。
delete [] psome;
- 通过使用加方括号的 delete 命令告诉编译器,指针 psome 所指的地方有连续的多个对象,这时 delete 会调用所有对象的析构函数并回收所有内存。
delete psome;
- 如果使用这个语句来释放内存,只会调用 psome 所指的对象的析构函数,并回收所有内存。
Details
下面细说在 new 和 delete 的时候,编译器的一些工作。
new 与一个类似于注册表的东西
如图,在申请内存的时候,除了把程序员给定的指针指向申请内存的地址,编译器还会用一张表(我们暂时把它叫做注册表吧,翁恺老师并没有说它的具体名字是什么,也可能它根本就没有名字)记录下申请的这篇内存的起始位置和大小(单位:字节)。
譬如 p 指向了一个 int,编译器在注册表里记录有一块 4 字节的空间,这块空间的开头地址是 A,并把 A 交给 p。
譬如 a 指向了 10 个 int,编译器在注册表里记录有一块 40 字节的空间,这块空间的开头地址是 A,并把 A 交给 a。
对于某个类的对象 Student,上述规则依然适用,这里不再赘述。
delete 时要查表
假设现在要 delete p,编译器会在注册表里找 p 指向的位置为开头的一块内存,并由此得到它指向的内存多大,最后执行删除操作。
但如果执行了 p++ 的话,再 delete [ ] p,这时编译器在表里就找不到以 p 所指向的位置为开头的一块内存,这时就会导致程序报错停止运行。
delete 某个类的对象
假设要 delete q;(上图),q 的类型是一个对象的指针,于是编译器会去调用 q 所指对象的析构函数,然后回收空间。
假设要 delete r;(注意我们申请了一块大小为 10 个对象的内存并把这块内存开头的地址交给了 r)。
因为我们这个操作没带方括号,编译器不知道这里有多个对象,编译器就只会调用 r 所指对象(开头地址所对应的对象)的析构函数,然后回收所有内存。
如果操作带了方括号,那么编译器对 r 指向的这片内存中所有的对象执行析构。
#include <iostream>
using namespace std;
class A
{
private:
int i;
public:
A()
{
i = 0;
cout << "Object Consturcted" << endl;
}
~A()
{
cout << "Object Destoryed, i = " << i <<endl;
}
void f()
{
cout << "Hello" <<endl;
}
void set(int i)
{
this -> i = i;
}
};
int main()
{
A * p = new A[10];
for(int i = 0 ; i < 10 ; i ++)
p[i].set(i);
// delete p;
delete [] p;
return 0;
}
这段代码可以说明上面所讨论的内容。
- 执行 delete p 的时候,只输出了一次 Object Destoryed
- 执行 delete [ ] p 的时候,输出了十次 Object Destoryed
此外,这是程序的输出,可以看到先调用了地址靠后的对象的析构而不是开头地址对应的对象的析构。
这进一步说明 new 所申请的空间是开在系统栈上的。
总结补充一些规则
- 不要用 delete 去释放不是 new 所申请的空间(注册表里找不到,会导致崩溃)。
- 不要 delete 一个对象两次。
- 用带方括号(不带方括号)的 new,也要用带方括号(不带方括号)的 delete 。
- 允许 delete nullptr。
- new 了之后要 delete。如果程序一时半会不会结束,new 了之后不 delete,就会导致申请的内存越堆越多,最终导致内存泄漏,系统崩溃。
关于允许 delete nullptr
假设有这样一个类:
class A
{
int i;
int *p;
A()
{
p = nullptr;
i = 0;
}
~A()
{
delete p;
}
void seti(int i)
{
this -> i = i;
}
void setp(int i)
{
p = new int;
*p = i;
}
}
显然我们不能保证 setp() 函数在程序中一定会被调用。也就是说,p 可能没有被 new 出来(此时根据构造函数,p 应该是空指针),但是析构时需要 delete p,于是我们做出特殊约定,允许 delete nullptr 。