C++动态内存与智能指针:new 与 delete 直接管理动态内存

直接管理内存

​ 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 管理动态内存存在三个常见的问题:

  1. 忘记 delete 内存。忘记释放动态内存会导致“内存泄露”。这种内存永远不会归还给自由存储区。而且查找内存泄露错误非常困难,通常在应用程序运行很长时间后,内存耗尽时,才能检测到这种错误。
  2. 使用已经释放掉的对象。通过在释放内存后将指针置空(建议),有时可以检测除这种错误。
  3. 同一块内存释放两次。当有两个指针指向相同的动态分配对象时,可能发生这种错误。重复 delete 可能破坏自由存储区。

建议:使用智能指针。

delete 之后重置指针值……

​ 当我们 delete 一个指针后,指针就变为无效了。虽然指针已经无效,但很多机器上指针仍然保存着(已经释放了的)动态地址内存。在 delete 之后,指针就变成了空悬指针

​ 未初始化指针的所有缺点空悬指针都有。我们一般在指针关联的内存被释放掉后,将其赋值为 nullptr。

……这只是提供了有限的保护

​ 我们知道,动态内存可以有多个指针指向相同的内存。在 delete 之后重置指针的方法只对这个指针有效,对其他任何仍指向(已释放的)内存的指针是没有作用的。如:

int *p(new int(42));
auto q = p;
delete p;				// p 和 q 均无效
p = nullptr;		   // p 不再绑定到任何对象,但是 q 仍然指向之前的内存(已释放)
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值