C++智能指针 shared_ptr 和 unique_ptr

1 前言

  • 传统C/C++编程中,使用new或者malloc动态申请内存后,必须手动调用delete或者free去释放,否则就会造成内存泄漏。在C++ 11语法中,提供了智能指针来管理内存,开发者不必再关心内存的释放问题,智能指针可以自动去释放管理的内存空间。
  • C++ 11语法中,主要提供了两种智能指针,shared_ptr 和 unique_ptr。shared_ptr允许多个指针指向同一对象,unique_ptr 则"独占"所指向的对象。

2 shared_ptr

  • 共享的智能指针,允许多个指针指向同一个对象。
  • shared_ptr大小一般为裸指针大小的两倍。它内部包含一个指向到资源的裸指针,还包含一个指向该资源引用计数的裸指针。

2.1 shared_ptr的初始化

  • 初始化一个基本类型
    •   // 动态申请一个int类型,值初始化为 10086
        std::shared_ptr<int> ptr1(new int(10086));
      
  • 通过make_shared
    •   // 使用make_shared动态申请内存(推荐此写法)
        std::shared_ptr<int> ptr3 = std::make_shared<int>();
      
  • 初始化一个类对象
    •   // 动态申请一个Base类对象
        std::shared_ptr<Base> ptr4 = std::make_shared<Base>();
      

2.2 关于内存对象的释放问题

  • 使用new申请的对象,必须手动调用delete去释放资源,而使用shared_ptr申请的对象,离开其作用域时会自动释放资源。通过一个示例演示下。
  • 代码
    •   #include <iostream>
        #include <memory>
        
        class Base {
        public:
        	Base() {
        		std::cout << "Base" << std::endl;
        	}
        	void print() {
        		std::cout << "I am Base" << std::endl;
        	}
        	~Base() {
        		std::cout << "~Base" << std::endl;
        	}
        };
        
        int main() {
        	{
        
        		Base *pBase = new Base;
        		pBase->print();
        	}
        
        	{
        		std::shared_ptr<Base> ptr4 = std::make_shared<Base>();
        		ptr4->print();
        	}
        
        	system("pause");
        	return 0;
        }
      
  • 执行结果
    •   Base
        I am Base
        Base
        I am Base
        ~Base
        请按任意键继续. . .
      

2.3 shared_ptr的引用计数

  • 共享智能指针允许智能指针可以同时管理同一块有效的内存。当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个shared_ptr指向相同的对象。
  • 这个记录值可以称为引用计数,当拷贝一个shared_ptr时,引用计数就会加一,当shared_ptr离开作用域时,引用计数就会减一。
  • 当一个shared_ptr的引用计数变为0时,就会自动释放自己所管理的对象。
  • 可以通过调用 use_count()接口来获取当前智能指针的引用计数。通过实例演示下
  • 代码
    •     #include <iostream>
          #include <memory>
          
          int main() {
          	{
          		std::shared_ptr<int> ptr1(new int(10086));
          		std::cout << "ptr1.use_count(): " << ptr1.use_count() << std::endl;
          
          		std::shared_ptr<int> ptr2 = ptr1;
          		std::cout << "ptr1.use_count(): " << ptr1.use_count() << std::endl;
          
          		std::shared_ptr<int> ptr3(ptr1);
          		std::cout << "ptr1.use_count(): " << ptr1.use_count() << std::endl;
          	}
          
          	system("pause");
          	return 0;
          }
      
  • 执行结果
    •   ptr1.use_count(): 1
        ptr1.use_count(): 2
        ptr1.use_count(): 3
        请按任意键继续. . .
      
  • 也可以调用reset方法手动释放。调用reset后,share_ptr对应的引用计数就会减1,当减到0时就会释放资源。

2.4 shared_ptr删除器介绍

  • shared_ptr默认不支持数组的管理,因为它的默认删除器本质上就是调用delete,因此无法释放数组。
  • 比如这段代码 std::shared_ptr<int> ptr2(new int[5]{0});,申请了一个整型数组,但shared_ptr释放对象只会调用delete而不是 delete []释放,因此只会释放数组中的第一块空间,后面的内存就不会释放了。
  • 为了有直观的感受,我们申请一块类对象数组来看下效果。
  • 代码
    •   #include <iostream>
        #include <memory>
        
        class Base {
        public:
        	Base() {
        		std::cout << "Base" << std::endl;
        	}
        	void print() {
        		std::cout << "I am Base" << std::endl;
        	}
        	~Base() {
        		std::cout << "~Base" << std::endl;
        	}
        };
        
        int main() {
        	{
        		std::shared_ptr<Base> ptr(new Base[3]);
        		ptr->print();
        	}
        
        	system("pause");
        	return 0;
        }
      
  • 打印结果。
    •   Base
        Base
        Base
        I am Base
        ~Base
      
  • 可以看到,只释放了一块对象。需要说明的是,上面这段代码会产生未定义行为而导致程序崩溃。
  • 使用删除器就可以解决这个问题,修改代码如下
    •   #include <iostream>
        #include <memory>
        
        class Base {
        public:
        	Base() {
        		std::cout << "Base" << std::endl;
        	}
        	void print() {
        		std::cout << "I am Base" << std::endl;
        	}
        	~Base() {
        		std::cout << "~Base" << std::endl;
        	}
        };
        
        
        void deletePtr(Base* p)
        {
        	delete[] p;
        }
        
        
        int main() {
        	{
        		// 写法, 手动调用自定义删除器
        		std::shared_ptr<Base> ptr(new Base[2], deletePtr);
        		ptr->print();
        	}
        
        	{
        		// 写法2, 使用lamaba表达式
        		std::shared_ptr<Base> ptr(new Base[2], [](Base* p) {delete[] p; });
        	}
        
        	{
        		// 写法3, 使用c++提供的删除器,要释放数组时就传入数组类型
        		std::shared_ptr<Base> ptr(new Base[2], std::default_delete<Base[]>());
        	}
        
        	system("pause");
        	return 0;
        }
      
      
  • 打印结果
    •   Base
        Base
        I am Base
        ~Base
        ~Base
        Base
        Base
        ~Base
        ~Base
        Base
        Base
        ~Base
        ~Base
        请按任意键继续. . .
      
  • 需要注意的是,C++17中,shared_ptr已经支持管理数组,可以直接传入数组类型 std::shared_ptr<int[]> ptr2(new int[5]{0});,不必再指定自定义删除器。
  • 使用删除器不仅可以管理数组,也可以管理文件开关,数据库的连接与断开等。
  • 下面通过一个实例看下通过删除器如何管理文件。
    •   #include <iostream>
        #include <memory>
        #include <fstream>
        
        class CFileDelete {
        public:
        	CFileDelete(const std::string strFile):mStrFile(strFile){
        
        	}
        
        	void operator()(std::ofstream* fp) {
        		fp->close();
        	}
        private:
        	std::string mStrFile;
        };
        
        
        int main() {
        	{
        		std::shared_ptr<std::ofstream> fp(new std::ofstream("test.exe"), CFileDelete("test.txt"));
        	}
        
        	system("pause");
        	return 0;
        }
      

2.5 获取原始指针

  • 使用shared_ptr管理数组时,不允许通过下标访问数组。这个时候就可以通过get来获取shared_ptr的原始指针来访问数组中的数据。具体示例如下
    •   #include <iostream>
        #include <memory>
        
        int main() {
        	{
        		int num = 5;
        		std::shared_ptr<int> ptr(new int[num] {0});
        
        		// 通过get获取原始指针来访问数组对象
        		int *p = ptr.get();
        		for (int i = 0; i < num; i++) {
        			p[i] = i;
        			std::cout << p[i] << " ";
        		}
        		std::cout << std::endl;
        	
        	}
        
        	system("pause");
        	return 0;
        }
      
  • 打印结果
    •   0 1 2 3 4
        请按任意键继续. . .
      

2.6 避免通过同一个裸指针创建多个share_ptr对象

  • 先看以下一段代码
    •   #include <iostream>
        #include <memory>
        
        int main() {
        
        	{
        		int* pData = new int;
        
        		std::shared_ptr<int> ptr1(pData);
        		std::shared_ptr<int> ptr2(pData);
        
        		std::cout << "ptr1.use_count: " << ptr1.use_count() << std::endl;
        		std::cout << "ptr2.use_count: " << ptr2.use_count() << std::endl;
        	}
        
        	system("pause");
        	return 0;
        }
      
  • 这段代码会导致程序崩溃,因为ptr1和ptr2是没有关联的,引用计数都为1,当离开作用域时都会调用delete去释放内存,这就造成同一块内存delete了两次,会导致程序崩溃。实际使用时要避免这种写法。

2.7 shared_ptr的线程安全

  • 一个shared_ptr对象可以被多个线程同时读
  • 两个shared_ptr对象可以被两个线程同时读写,即使它们管理的是同一个对象
  • 多个线程读写同一个shared_ptr对象,那么需要加锁

3 unique_ptr

  • unique_ptr是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,即多个unique_ptr不能指向同一块内存区域。

3.1 unique_ptr的初始化

  • 初始化一个基本类型
    •   // 动态申请一个int类型,值初始化为 10086
        std::unique_ptr<int> ptr1(new int(10086));
      
  • 初始化一个类对象
    •   // 动态申请一个Base类对象
        std::unique_ptr<Base> ptr1(new Base);
      
  • unique_ptr不允许进行赋值操作
    •   std::unique_ptr<int> ptr1(new int);
        std::unique_ptr<int> ptr2 = ptr1; // 不允许
      

3.2 unique_ptr管理数组

  • 使用unique_ptr来管理数组时,可以直接使用下标来访问或者修改元素值。
  • 代码
    •   #include <iostream>
        #include <memory>
        
        int main() {
        	{
        		int num = 5;
        		std::unique_ptr<int[]> ptr(new int[num]{0});
        
        		// 修改数组元素
        		for (int i = 0; i < num; i++) {
        			ptr[i]  = i;
        		}
        
        
        		// 访问数组元素
        		for (int i = 0; i < num; i++) {
        			std::cout << ptr[i] << " ";
        		}
        		std::cout << std::endl;
        	}
        
        
        	system("pause");
        	return 0;
        }
      
  • 打印结果
    •   0 1 2 3 4
        请按任意键继续. . .
      
  • 使用unique_ptr管理数组时,内存释放问题。
  • 上面介绍过,使用shared_ptr来管理数组,默认调用delete释放对象,因此必须指定删除器。而使用unique_ptr管理数组时,会调用delete[],这样就不必手动指定删除器。具体看以下示例
  • 代码
    •   #include <iostream>
        #include <memory>
        
        class Base {
        public:
        	Base() {
        		std::cout << "Base" << std::endl;
        	}
        	void print() {
        		std::cout << "I am Base" << std::endl;
        	}
        	~Base() {
        		std::cout << "~Base" << std::endl;
        	}
        };
        
        int main() {
        	{
        		int num = 3;
        		std::unique_ptr<Base[]> ptr(new Base[num]);
        		for (int i = 0; i < num; i++) {
        			ptr[i].print();
        		}
        	}
        
        	system("pause");
        	return 0;
        }
      
  • 打印结果。可以看到,没有手动指定删除器,数组对象也都全部释放了。
    •   Base
        Base
        Base
        I am Base
        I am Base
        I am Base
        ~Base
        ~Base
        ~Base
        请按任意键继续. . .
      

3.3 unique_ptr删除器介绍

  • 上面介绍过shared_ptr的删除器,本质上就是调用delete,而unique_ptr的删除器,本质上是调用 delete [], 这也是它为什么可以直接管理数组的原因。

  • unique_ptr的删除器与shared_ptr稍有不同。shared_ptr的删除器是它的构造函数的一个参数,而unique_ptr的删除器是它的模板参数,是类型的一部分。

    •   // shared_ptr删除器
        template< class Y, class Deleter> shared_ptr( Y* ptr, Deleter d );
        
        // unique_ptr删除器
        template<class T, class Deleter = std::default_delete<T>> class unique_ptr;
      
  • 具体写法区别如下

    •   // 自定义shared_ptr删除器写法
        std::shared_ptr<std::ofstream> fps(new std::ofstream("test.exe"), CFileDelete("test.txt"));
      
        // 自定义unique_ptr删除器写法
        std::unique_ptr<std::ofstream, CFileDelete> fpu(new std::ofstream("test.exe"), CFileDelete("test.txt"));
      

3.4 典型应用:实现工厂模式

  • 代码
    •   #include <iostream>
        #include <memory>
        #include <string>
        
        class CBase {
        public:
        	CBase() {
        		std::cout << "CBase " << std::endl;
        	}
        	virtual ~CBase() {
        		std::cout << "~CBase " << std::endl;
        	}
        };
        
        class CSubA : public CBase {
        public:
        	CSubA() {
        		std::cout << "CSubA " << std::endl;
        	}
        	virtual ~CSubA() {
        		std::cout << "~CSubA " << std::endl;
        	}
        };
        
        class CSubB : public CBase {
        public:
        	CSubB() {
        		std::cout << "CSubB " << std::endl;
        	}
        	virtual ~CSubB() {
        		std::cout << "~CSubB " << std::endl;
        	}
        };
        
        // 实现工厂方法
        std::unique_ptr<CBase> CBaseMakeU(std::string name) {
        	std::unique_ptr<CBase> pBase;
        	if (name.compare("CSubA") == 0) {
        		pBase.reset(new CSubA);
        	}
        	else if (name.compare("CSubB") == 0) {
        		pBase.reset(new CSubB);
        	}
        	else {
        		pBase.reset(new CBase);
        	}
        
        	return pBase;
        }
        
        int main() {
        	{
        		std::unique_ptr<CBase> base = CBaseMakeU("CSubA");
        	}
        
        	system("pause");
        	return 0;
        }
      
  • 执行结果
    •   CBase
        CSubA
        ~CSubA
        ~CBase
        请按任意键继续. . .
      

4 weak_ptr

  • weak_ptr是 C++ 标准库中的一个智能指针类型,主要用于解决 shared_ptr引用循环的问题。weak_ptr不增加所指向对象的引用计数,因此不会影响对象的生命周期。它通常与 shared_ptr一起使用,提供了一种观察共享对象而不增加其引用计数的方法。

4.1 weak_ptr主要特点

  • 不增加引用计数:
    • weak_ptr不持有对象的所有权,因此不会增加对象的引用计数。
    • 当最后一个 shared_ptr被销毁时,对象会被删除,即使还有weak_ptr存在。
  • 防止引用循环:
    • 在某些情况下,多个 shared_ptr之间可能存在循环引用,导致对象无法被正确释放。使用 weak_ptr可以打破这种循环引用。
  • 临时访问:
    • weak_ptr提供了 lock()方法,可以临时获取一个 shared_ptr,以便在需要时访问对象。
    • 如果对象已经被删除,lock()会返回一个空的 shared_ptr
  • 检测对象是否已删除:
    • weak_ptr提供了 expired()方法,可以检查对象是否已经被删除。

4.2 weak_ptr 构造

  • 代码
    •   #include <iostream>
        #include <memory>
        
        int main() {
        
        	{
        		std::shared_ptr<int> ptrs = std::make_shared<int>(0);
        		std::cout <<"ptrs.use_count: " << ptrs.use_count() << std::endl;
        
        
        		std::weak_ptr<int> ptrw(ptrs);
        		std::cout << "ptrs.use_count: " << ptrs.use_count() << std::endl;
        		std::cout << "ptrw.use_count: " << ptrw.use_count() << std::endl;
        	}
        
        	system("pause");
        	return 0;
        }
      
  • 打印结果
    •   ptrs.use_count: 1
        ptrs.use_count: 1
        ptrw.use_count: 1
        请按任意键继续. . .
      

4.3 weak_ptr lock方法

  • 一个典型的使用场景如下
    •   #include <iostream>
        #include <memory>
        
        int main() {
        
        	{
        		std::shared_ptr<int> ptrs = std::make_shared<int>(10010);
        		std::weak_ptr<int> ptrw(ptrs);
        		ptrs.reset();
        
        		// lock方法会检查当前指针是否有效,如果有效则返回对应指针,如果无效则返回NULL
        		std::shared_ptr<int> ptrs1 = ptrw.lock();
        		if (ptrs1) {
        			std::cout << "*ptrs1" << *ptrs1 << std::endl;
        		}
        		else {
        			std::cout << "ptrs1 is null" << std::endl;
        		}
        	}
        
        	system("pause");
        	return 0;
        }
      

4.4 通过weak_ptr解决循环引用问题

  • 先看以下一段代码,了解下循环引用的产生场景以及导致的问题
    •   #include <iostream>
        #include <memory>
        
        class B;
        
        class A {
        public:
        	A() {
        		std::cout << "A " << std::endl;
        	}
        	~A() {
        		std::cout << "~A " << std::endl;
        	}
        
        	void setB(std::shared_ptr<B> &pb) {
        		mPb = pb;
        	}
        private:
        	std::shared_ptr<B> mPb;
        };
        
        class B {
        public:
        	B() {
        		std::cout << "B " << std::endl;
        	}
        	~B() {
        		std::cout << "~B " << std::endl;
        	}
        
        	void setA(std::shared_ptr<A> &pa) {
        		mPa = pa;
        	}
        private:
        	std::shared_ptr<A> mPa;
        };
        
        int main() {
        	{
        		std::shared_ptr<A> pA = std::make_shared<A>();
        		std::shared_ptr<B> pB = std::make_shared<B>();
        
        		pA->setB(pB);
        		pB->setA(pA);
        
        		std::cout << "pA use_count: " << pA.use_count() <<std::endl;
        		std::cout << "pB use_count: " << pB.use_count() << std::endl;
        	}
        
        	system("pause");
        	return 0;
        }
      
  • 执行结果
    •   A
        B
        pA use_count: 2
        pB use_count: 2
        请按任意键继续. . .
      
  • 从执行结果可以看到,A和B都没有被释放。这是由于pA和pB离开作用域的时候,引用计数都为1,因此不会调用析构释放资源。
    • 在这里插入图片描述
  • 如何解决这个问题,只需要将类B的成员变量mPa由std::shared_ptr<A>改为std::weak_ptr<A>即可。

5 参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大草原的小灰灰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值