C++动态内存与智能指针:shared_ptr和new结合使用、智能指针和异常

shared_ptr 和 new 结合使用

​ 我们知道,如果不初始化智能指针,它就会被初始化为一个空指针。其实还可以用 new 返回的指针来初始化智能指针:

shared_ptr<int> p2(new int(42));	// p2 指向一个值为 42 的 int

接受指针参数的智能指针构造函数是 explicit,所以我们只能使用直接初始化来初始化一个智能指针:

shared_ptr<int> p1 = new int(1024);		// 错误,explicit 的构造函数不能执行隐式转换。
shared_ptr<int> p2(new int(1024));		// 正确,使用了直接初始化

同理,一个返回 shared_ptr 的函数也不能在返回语句中隐式转换一个普通指针:

shared_ptr<int> clone(int p) {
    return new int(p);		//错误:普通指针不能执行隐式转换为 shared_ptr<int>
  //return shared_ptr<int>(new int(p));		// ok
}

​ 默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用 delete 来释放它所关联的资源。

​ 我们可以将智能指针绑定到一个指向其他类型资源的指针上,但是为了这样做,必须提供自己的操作来替代 delete。这里有 定义和改变 shared_ptr 的其他方法:

shared_ptr<T> p(q);		// p 管理内置指针 q 所指向的对象;q 必须指向 new 分配的内存且能够转换为 T*

shared_ptr<T> p(u);		// p 从 unique_ptr u 那里接管了对象的所有权; 将 u 置为空

shared_ptr<T> p(q, d);	// p 接管内置指针 q 所指向的对象的所有权;q 必须指向 new 分配的内存且能够转
						// 换为 T*。p 将使用可调用对象(lambda,函数,函数指针等) d 来代替 delete

shared_ptr<T> p(p2,d);	// p 是 shared_ptr p2 的拷贝,用可调用对象 d 来代替 delete

p.reset();				// 若 p 是唯一指向其对象的 shared_ptr,reset 会释放此对象。若传递了可选的
p.reset(q);				// 参数内置指针 q,会令 p 指向 q,否则将 p 置空。若还传递了参数d,将会调用
p.reset(q,d);			// d 而不是 delete 来释放 q
不要混合使用普通指针和智能指针……

​ shared_ptr 可以协调对象的析构,但这仅限于自身的拷贝 (也是 shared_ptr) 之间。所以我们推荐使用 make_shared 而不是 new。这样,我们就能在分配对象的同时就将 shared_ptr 与之绑定,从而避免了无意中将同一块内存绑定到多个独立的 shared_ptr 上。

​ 假设有以下 process 函数:

void process(shared_ptr<int> ptr) {
    // 使用 ptr
}
  1. 当我们调用此函数传入的实参是 shared_ptr 时:
   shared_ptr<int> p(new int(42));
   process(p);
   int i = *p;		// 正确,引用计数值为 1

分析:起始时,我们的引用计数为 1;调用 process 函数,引用计数递增;函数调用结束,引用计数递减;那么最后的引用计数为 1,所以 int i = *p 这是合法的。

  1. 当我们调用此函数传入的实参是临时 shared_ptr 时:
   int *x(new int(1024));	// x 是普通指针,不是智能指针
   process(x);		// 错误,这里意图执行隐式转换
   process(shared_ptr<int>(x));	// 合法
   int j = *x;		// x 是空悬指针

分析:调用 process(shared_ptr<int>(x)) 时,引用计数是 1;process 调用结束后,引用计数递减,此时值为 0,故原对象也将被销毁;最后 int j = *x 这一句话不合法,因为此时 x 所指的对象已经被销毁了。

当将一个shared_ptr 绑定到一个普通指针时,我们就将内存的管理责任交给了这个 shared_ptr,所以就不应该再用内置指针来访问 shared_ptr 所指向的内存了。因为使用内置指针访问一个智能指针所赋值的对象是很危险的,我们不知道对象何时会被销毁。

……也不要使用 get 初始化另一个智能指针或为智能指针赋值

​ 智能指针中有一个 get 函数,它返回一个内置指针,指向智能指针管理的对象。此函数的设计是为了向不能使用智能指针的代码传递一个内置指针。同时,我们不能 delete 此指针,也不能用此指针初始化或者赋值给另一个智能指针上。这是错误的:

shared_ptr<int> p(new int(42));
int *q = p.get();		// 使用 p 要注意上面几点。
{
    shared_ptr<int> r(q);	// 未定义:两个独立的 shared_ptr 指向相同的内存
}
int foo = *p;		// 上述代码块结束之后,p 指向的内存已经被释放了。

​ 我们会发现,当执行完代码中的语句块之后,p 指向的内存已经被释放了。而且,当 p 销毁时,这块内存还会被 delete 一次

其他 shared_ptr 操作

​ shared_ptr 还定义了一些其他操作。如,我们可以用 reset 来将一个新的指针赋给一个 shared_ptr:

shared_ptr<int> p = make_shared<int>(52);
p = new int(1024);		// 错误,不能将一个指针赋给 shared_ptr
p.reset(new int(1024));	// 正确,p 指向一个新的对象。

与赋值类似,reset 会更新引用计数。reset 通常与 unique 一起使用,来控制多个 shared_ptr 的共享对象。

智能指针和异常

​ 使用异常处理的程序需要确保在异常发生后资源能被正确地释放。一个简单的确保资源被释放的方法是使用智能指针。

​ 如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内层不再需要时将其释放:

void f() {
    shared_ptr<int> sp(new int(1024));
    // 这段代码抛出一个异常,但未在 f 中捕获
}	// 函数结束时 shared_ptr 自动释放内存

智能指针方面:我们知道,当函数退出时(正常结束或者异常结束),其局部对象都会被销毁。所以上述代码无论哪种情况下退出,sp 都会销毁,然后检查引用计数。在此例中,sp 指向的内存也会被释放掉。

直接管理内存方面:但是,假设我们是直接管理内存,且在 new 之后,在对应的 delete 之前发生了异常,则内存是不会释放掉的。

智能指针和哑类

​ C++ 中,很多类都定义了析构函数,但不是所有类都有定义。对于那些分配了资源,而又没有定义析构函数来释放这些资源的类,可能会遇到与使用动态内存相同的错误——忘记释放资源或者在资源分配和释放之前发生了异常,程序也会发生资源泄露。

​ 例如,我们正在使用 C++ 的网络库,代码是这样的:

struct destination;					// 表示我们正在连接什么
struct connection;					// 使用连接所需的信息
connection connect(destination*);	// 打开连接
void disconnect(connection);		// 关闭给定连接
void f(destination &d,/*其他参数*/) {
    // 获得一个连接,使用完后记得关闭它
    connection c = connect(&d);
    // 使用连接
    // 如果我们在 f 退出前忘记调用 disconnect,就无法关闭 c 了
}

如果 connection 有析构函数,可以在 f 结束时由析构函数自动关闭连接。但是若 connection 没有析构函数,这问题与使用 shared_ptr 内存泄露等价。使用 shared_ptr 来保证 connection 被正确关闭,是一种有效的方法。

使用我们自己的释放操作

​ 默认情况下,shared_ptr 假的它们指向的是动态内存。因此,当一个 shared_ptr 被销毁时,它默认对它管理的指针进行 delete 操作。

​ 这里为了用 shared_ptr 来管理一个 connection,我们必须首先定义一个删除器函数来替代 delete。这个函数能够完成对 shared_ptr 中保存的指针进行释放操作。

void end_connection(connection *p) { disconnect(p); }

当我们创建一个 shared_ptr 时,可以传递一个指向删除函数的参数:

void f(destination &d,/*其他参数*/){
    connection c = connect(&d);
    shared_ptr<connection> p(&c, end_connection);
    // 使用连接
    // 当 f 退出时(即使是由于异常而退出),connection 会被正确关闭
}

​ 当 p 被销毁时,它不会对自己保存的指针指向 delete,而是调用 end_connection。无论是否正常结束,p 都会被销毁,从而关闭连接。

注意:智能指针的陷阱

​ 为了正确使用智能指针,我们必须坚持一些基本规范:

  • 不使用相同内置指针初始化 (或 reset) 多个智能指针
  • 不 delete get() 返回的指针
  • 如果使用了 get() 返回的指针,注意当最后一个对应的智能指针销毁后,之前 get() 返回的指针就无效了
  • 如果使用智能指针管理的资源不是 new 分配的内存,记住传递给它一个删除器
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值