【C++11新特性】智能指针相关,shared_ptr、unique_ptr、weak_ptr

智能指针的引入

在传统 C++ 中,总是需要使用delete手动释放资源,很有可能就忘记了去释放资源而导致泄露。

通常的做法是对于一个对象而言,我们在构造函数的时候申请空间,而在析构函数(在离开作用域时调用)的时候释放空间,也就是我们常说的 RAII 资源获取即初始化技术
——欧长坤《现代 C++ 教程》

C++11 引入了智能指针的概念,使用引用计数的思想,让程序员不再需要关心手动释放内存。

  • 这些智能指针包括std::shared_ptr/std::unique_ptr/std::weak_ptr
  • 使用它们需要包含头文件 <memory>

智能指针和普通指针的区别

智能指针和普通指针的区别在于,智能指针实际上是对普通指针加了一层封装机制,它负责自动释放所指的对象,这样的一层封装机制使得智能指针可以方便的管理一个对象的生命周期。

引用计数的思想

  • 引用计数是为了防止内存泄露而产生的,基本思想是对于动态分配的对象,每当增加一次对同一个对象的引用,那么引用对象的引用计数就会增加一,每删除一次引用,引用计数就会减一,当一个对象的引用计数减为零时,就自动删除指向的堆内存
  • 引用计数不是垃圾回收,引用计数能够尽快收回不再被使用的对象,同时在回收的过程中也不会造成长时间的等待,更能够清晰明确的表明资源的生命周期。

std::unique_ptr

unique_ptr 是一种独占所有权的智能指针,即同一时间只能有一个 unique_ptr 指向某个对象。当 unique_ptr 被销毁时(例如超出作用域或被重置),它所指向的对象也会被自动删除。这种特性使得 unique_ptr 非常适用于管理在堆上动态分配的单个对象。

  • 它禁止其他智能指针与其共享同一个对象,从而保证代码的安全
// 独占所有权
std::unique_ptr<int> pointer = std::make_unique<int>(10); 
std::unique_ptr<int> pointer2 = pointer; // 非法
  • C++11中没有提供std::make_unique方法,C++14中才引入,不过可以自行实现
// make_unique 自行实现
template<typename T, typename ...Args>
std::unique_ptr<T> make_unique( Args&& ...args ) {
	return std::unique_ptr<T>( new T( std::forward<Args>(args)... ) );
}
  • 既然是独占,就是不可复制。但是可以利用 std::move 将其转移给其他的 unique_ptr

更多 unique_ptr 相关可以参考这篇文章 用C++从0到1实现一下自己的unique_ptr

std::shared_ptr

std::shared_ptr 是一种共享式智能指针,允许多个shared_ptr共享对同一个对象的所有权,使用引用计数机制记录有多少个 shared_ptr 共同指向一个对象。

  • 当最后一个指向该对象的shared_ptr超出作用域或被销毁时,引用计数变为零,对象将自动删除,从而避免显式的调用 delete
void func() {
    std::shared_ptr<int> sptr1(new int(42));
    {
        std::shared_ptr<int> sptr2 = sptr1; // sptr1 和 sptr2 指向同一个整数
    } // sptr2 超出作用域,但由于 sptr1 仍然存在,所以不释放内存
} // sptr1 超出作用域,引用计数为0,自动释放内存
  • std::make_shared 用来消除显式的使用 newstd::make_shared 会创建传入参数中的对象并分配内存,返回这个对象类型的 std::shared_ptr 指针。
void foo(std::shared_ptr<int> i) {
	(*i)++;
}
int main() {
	// auto pointer = new int(10); // illegal, no direct assignment
	auto pointer = std::make_shared<int>(10);
	foo(pointer);
	std::cout << *pointer << std::endl; // 11
	return 0;
}
  • std::shared_ptr 可以通过 get()方法来获取原始指针,通过 reset() 来减少一个引用计数,并通过 use_count() 来查看一个对象的引用计数。

std::weak_ptr

std::weak_ptr是一种弱引用智能指针,它不对所指向的对象的内存进行管理,不会引起引用计数增加。weak_ptr可以从shared_ptr或另一个weak_ptr创建。weak_ptr通常与shared_ptr一起使用,用于避免循环引用(导致内存泄漏)的问题

  • std::weak_ptr 没有* 运算符和 -> 运算符,所以不能够对资源进行操作,但可以用于检查std::shared_ptr 是否存在;
  • expired() 方法能在资源未被释放时,会返回 false,否则返回 true
  • 它也可以用于获取指向原始对象的 std::shared_ptr 指针,其 lock() 方法在原始对象未被释放时,返回一个指向原始对象的 std::shared_ptr 指针,进而访问原始对象的资源,否则返回nullptr
// weak_ptr 创建与对象访问
std::shared_ptr<int> sptr(new int(42));
std::weak_ptr<int> wptr = sptr;

if (auto locked = wptr.lock()) { // 提升为 shared_ptr
    // 使用 locked 访问对象
} else {
    // 对象已被释放
}

循环引用问题及解决

循环引用问题:当两个对象同时使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄露

以下代码中,pa, pb 都不会被销毁,这是因为struct A,B 内部的指针ptrb、ptra同时又引用了 pa, pb,这使得 pa, pb 的引用计数均变为了 2,当离开作用域时,pa, pb 智能指针被析构,但只能造成这块区域的引用计数减一,这就导致 pa, pb 对象指向的内存区域引用计数不为零,而外部已经没有办法找到这块区域了,也就造成了内存泄露。
循环引用
常用的解决方法是使用 weak_ptr,它不会增加引用计数,但可以在需要时获取 shared_ptr 实例。通过将循环引用中的某个 shared_ptr 替换为 weak_ptr,可以避免循环引用,从而解决内存泄漏问题。

比如将struct B 内改为weak_ptr,则当pb->ptra = pa时,指针pa的引用计数不会加1,此时pa, pb 的引用计数分别为1,2,当离开作用域时,pa、pb的引用计数都减1,pa的引用计数变为0,所以析构,因此也不再有指针指向pb,pb的引用计数变为0,所以也可以析构。
将struct B 内改为weak_ptr

//智能指针测试:循环引用
#include <iostream>
#include <memory>
using namespace std;
struct A;
struct B;

struct A{
	shared_ptr<B> ptrb;
//	weak_ptr<B> ptrb;
	~A() {cout << "A has destoried!" << endl;}
};
struct B{
	shared_ptr<A> ptra;
//	weak_ptr<A> ptra;
	~B() {cout << "B has destoried!" << endl;}
};

int main()
{
	shared_ptr<A> pa(new A);
	shared_ptr<B> pb(new B);
	cout << "pa: "<< pa.use_count() << endl;
	cout << "pb: "<< pb.use_count() << endl;
	
	pa->ptrb = pb;
	cout << "pa: "<< pa.use_count() << endl;
	cout << "pb: "<< pb.use_count() << endl;
	
	pb->ptra = pa;
	cout << "pa: "<< pa.use_count() << endl;
	cout << "pb: "<< pb.use_count() << endl;
}
/* 两个都是shared_ptr
pa: 1
pb: 1
pa: 1
pb: 2
pa: 2
pb: 2
*/

/* struct B 内改为weak_ptr
pa: 1
pb: 1
pa: 1
pb: 2
pa: 1
pb: 2
A has destoried!
B has destoried!
*/

/* struct A 内改为weak_ptr
pa: 1
pb: 1
pa: 1
pb: 1
pa: 2
pb: 1
B has destoried!
A has destoried!
*/

/* 两个都改为weak_ptr
pa: 1
pb: 1
pa: 1
pb: 1
pa: 1
pb: 1
B has destoried!
A has destoried!
*/

线程安全性

  • std::unique_ptr通常是线程安全的,因为在任何给定时间只有一个线程可以访问资源。但是,如果在多个线程之间转移std::unique_ptr的所有权,需要确保在转移所有权之前正确地同步访问。
  • 如果尝试在多个线程之间共享同一个std::unique_ptr,这通常是不安全的,因为没有内置的线程安全机制来确保同时访问资源的安全性。
  • 由于引用计数的增减操作需要原子性,因此std::shared_ptr的引用计数操作通常是线程安全的。
  • 多个线程同时读同一个shared_ptr对象是线程安全的,但是如果是多个线程对同一个shared_ptr对象进行读和写,则需要加锁。
  • 多线程读写shared_ptr所指向的同一个对象,不管是相同的shared_ptr对象,还是不同的shared_ptr对象,都需要加锁保护。

参考资料

内容参考自:
用C++从0到1实现一下自己的unique_ptr
CSView计算机招聘知识分享
现代 C++ 教程:高速上手 C++ 11/14/17/20

文中图来自:
现代 C++ 教程:高速上手 C++ 11/14/17/20 第5章5.4节

  • 13
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值