文章目录
直接管理内存
C++语言定义了两个运算符来分配和释放内存。运算符 new 分配内存,delete 释放 new 分配的内存。相对于智能指针,使用 new 和 delete 管理内存非常容易出错。
使用 new 动态分配和初始化对象
在自由存储区分配的内存是无名的,因此 new 无法为其分配的对象命名,而是返回一个指向该对象的指针:
int *pi = new int; // pi 指向一个动态分配的、未初始化的无名对象
此 new 表达式在自由存储区构造一个 int 型对象,并返回指向该对象的指针。
默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化:
string *ps = new string; // 初始化为空 string
int *pi = new int; // pi 指向一个未初始化的 int
我们可以使用直接初始化方式来初始化一个动态分配的内存 (圆括号与花括号)。
int *pi = new int(1024); // pi 指向的对象的值为 1024
string *ps = new string(10, '9'); // *ps 为 "9999999999"
vector<int> *pv = new vector<int> {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
也可以对动态分配的对象进行值初始化,只需在类型名之后跟一对空括号即可:
string *ps1 = new string; // 默认初始化为空 string
string *ps2 = new string(); // 值初始化为空 string
int *pi1 = new int; // 默认初始化,*pi1 的值是未定义的
int *pi2 = new int(); // *pi2 为 0
当然,对于类类型,都是通过默认构造函数来初始化的。
如果我们在括号中提供单一初始化器,便可以使用 auto 让编译器推断要分配的类型:
auto p1 = new auto(obj); // p 指向一个与 obj 类型相同的对象,该对象用 obj 进行初始化
auto p2 = new auto {a, b, c}; // 错误,括号中只能有单个初始化器
动态分配的 const 对象
用 new 分配 const 对象是合法的:
// 分配并初始化一个 const int
const int *pci =new const int(1024);
// 分配并默认初始化一个 const 的空 string
const string *ps = new const string;
类似其他任何 const 对象,一个动态分配的 const 对象必须进行初始化。对于一个定义了默认构造函数的类类型,其 const 动态对象可以隐式初始化,而其他类型必须显式初始化。由于分配的对象是 const 的,new 返回的指针是一个指向 const 的指针 (指针本身是可以改变的)。
内存耗尽
目前为止一个程序仍有可能用光所有可用内存。当 new 失败时,在默认情况下,它会抛出一个 bad_alloc 异常。我们可以改变使用 new 的方式来阻止它抛出异常:
int *p1 = new int; // 如果分配失败,new 抛出 std::bad_alloc 异常
int *p2 = new (nothrow) int; // 如果分配失败,new 返回一个空指针
上面第二种形式的 new 为定位 new。定位 new 允许我们向 new 传递额外的参数 (这里就是 nothrow 对象)。如果这种形式的 new 不能分配内存,它会返回一个空指针。bad_alloc 和 nothrow 定义在头文件 new 中。
释放动态内存
为了防止内存耗尽,在动态内存使用完毕后,必须将其归还给系统。我们通过 delete 表达式来将动态内存归还给系统。delete 表达式接受一个指针,指向我们想要释放的对象:
delete p; // p 必须指向一个动态分配的对象或是一个空指针
delete 表达式执行两个动作:销毁给定指针指向的对象;释放对应的内存。
指针值和 delete
我们传递给 delete 的指针必须指向动态分配的内存,或者是一个空指针。释放一块并非 new 分配的内存,或者将相同的指针值释放多次,其行为都是未定义的。
int i, *pi1 = &i, *pi2 = nullptr;
double *pd = new double(33), pd2 = pd;
delete i; // 错误,i 不是一个指针
delete pi1; // 未定义:pi1 指向一个局部变量
delete pd; // 正确
delete pd2; // 错误,pd2 指向的内存已经被释放掉了
delete pi2; // 正确,可以释放一个空指针
需要注意:通常情况下,编译器不能分辨一个指针指向的是静态还是动态分配的对象。类似的,编译器也不能分辨一个指针所指向的内存是否已经被释放。这些 delete 表达式,大多数编译器都会编译通过,尽管它们是错误的。
const 动态对象也是可以销毁的:
const int *pci = new const int(1024);
delete pci;
动态对象的生存期直到被释放时为止
我们知道,shared_ptr 管理的内存在最后一个 shared_ptr 销毁时会被自动释放。但对于通过内置指针来管理的动态内存,就不是这样。对于一个由内置指针管理的动态对象,直到被显式释放之前它都是存在的。
所以,返回指向动态内存的指针的函数,再其调用者使用完之后必须记得释放内存。
这里是一个例子:
假设有函数 factory:
Foo* factory(T arg) {
// 视情况处理 arg
return new Foo(arg);
}
一个使用 factory 的 use_factory 函数:
void use_factory(T arg) {
Foo *p = factory(arg);
// 使用 p 但是不 delete
}
当 use_factory 函数结束后,p 离开了它的作用域,但是 p 所指向的动态内存并没有被释放。所以我们应该这样更改 use_factory:
void use_factory(T arg) {
Foo *p = factory(arg);
// 使用 p
delete p;
}
由于这里只有一个指针指向开辟的动态内存,所以在使用完 p 之后直接 delete 即可。
当其他代码也需要使用 use_factory 使用的对象时,我们应该让此函数返回一个指向它分配的内存的指针:
Foo* use_factory(T arg) {
Foo *p = factory(arg);
// 使用 p
return p;
}
**小心,动态内存的管理非常容易出错 **
使用 new 和 delete 管理动态内存存在三个常见的问题:
- 忘记 delete 内存。忘记释放动态内存会导致“内存泄露”。这种内存永远不会归还给自由存储区。而且查找内存泄露错误非常困难,通常在应用程序运行很长时间后,内存耗尽时,才能检测到这种错误。
- 使用已经释放掉的对象。通过在释放内存后将指针置空(建议),有时可以检测除这种错误。
- 同一块内存释放两次。当有两个指针指向相同的动态分配对象时,可能发生这种错误。重复 delete 可能破坏自由存储区。
建议:使用智能指针。
delete 之后重置指针值……
当我们 delete 一个指针后,指针就变为无效了。虽然指针已经无效,但很多机器上指针仍然保存着(已经释放了的)动态地址内存。在 delete 之后,指针就变成了空悬指针。
未初始化指针的所有缺点空悬指针都有。我们一般在指针关联的内存被释放掉后,将其赋值为 nullptr。
……这只是提供了有限的保护
我们知道,动态内存可以有多个指针指向相同的内存。在 delete 之后重置指针的方法只对这个指针有效,对其他任何仍指向(已释放的)内存的指针是没有作用的。如:
int *p(new int(42));
auto q = p;
delete p; // p 和 q 均无效
p = nullptr; // p 不再绑定到任何对象,但是 q 仍然指向之前的内存(已释放)