C++新特性之智能指针

目录

一、初识智能指针

1.1  什么是智能指针

1.2  智能指针历史历程

1.3  为什么需要智能指针

1.3.1   内存泄漏

1.3.2   防止内存泄漏

1.3.3   异常的重新捕获

二.   智能指针的原理与使用

2.1  智能指针的原理

2.2  智能指针的使用

2.3   智能指针的拷贝问题

三.   智能指针的众多版本

3.1  auto_ptr

3.2  unique_ptr

3.2.1 unique_ptr的应用场景

3.2.2  unique_ptr的简单实现  

3.3.3  unique_ptr转移控制权

3.3.4 std::shared_ptr 和 std::unique_ptr共有方法

3.3 shared_ptr

3.3.1 shared_ptr的应用场景

3.3.2 shared_ptr的简单实现

3.3.2.1 智能指针基类的简单实现

3.3.2.2 shared_ptr的简单实现

3.3.2.3 make_xxx的简单实现

3.3.3 shared_ptr的循环引用问题

3.3.4  shared_ptr的线程安全问题

3.3.4 shared_ptr的内置方法

3.4 weak_ptr

3.4.1 weak_ptr应用概述

3.4.2 weak_ptr的应用场景

3.4.2.1 用于实现缓存

3.4.2.2 避免循环引用问题

3.4.2.3 用于实现单例模式

3.4.3weak_ptr的简单实现

3.4.4 weak_ptr的内置方法

        四.自定义删除器

4.1 自定义删除器的使用场景

4.2 自定义删除器的使用

4.3 有状态删除器和无状态删除器


一、初识智能指针

1.1  什么是智能指针

智能指针不是指针,是一个管理指针的类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏和空悬指针等等问题。

动态分配的资源,交给一个类对象去管理,当类对象声明周期结束时,自动调用析构函数释放资源。

1.2  智能指针历史历程

  • C++ 98 中产生了第一个智能指针auto_ptr。
  • C++boost给出了更加实用的scoped_ptr(防止拷贝) 和 shared_ptr(引进引用计数) 和 weak_ptr。
  • C++ 11 引入了unquie_ptr 和 shared_ptr 和 weak_ptr .需要注意的是,unique_ptr对应的是boost中的scoped_ptr。并且这些智能指针的实现是参照boost中的实现的。

1.3  为什么需要智能指针

1.3.1   内存泄漏

我们在讲为什么之前先来了解一下什么是内存泄漏。

内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。

内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对 该段内存的控制,因而造成了内存的浪费。

内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现 内存泄漏会导致响应越来越慢,最终卡死。

1.3.2   防止内存泄漏

我们来看看这一个代码:

void fxx()
{
	int* p1 = new int[10];
	int* p2 = new int[20];
    int* p3 = new int[30];
 
	//...
 
	delete[] p1;
	delete[] p2;
	delete[] p3;
}
 

如果指针p2或者p3开辟空间new错误,这里就会导致后面的delete不会被执行,这就导致了指针p1的内存泄漏。这里我们可以用异常来解决,但是很难看:

void fxx()
{
	int* p1 = new int[10];
	int* p2, *p3;
	try
	{
		p2 = new int[20];
		try {
			p3 = new int[30];
		}
		catch (...)
		{
			delete[] p1;
			delete[] p2;
			throw;
		}
	}
	catch (...)
	{
		delete[] p1;
		throw;
	}
 
	//...
 
	delete[] p1;
	delete[] p2;
	delete[] p3;
}

1.3.3   异常的重新捕获

在异常的重新抛出与捕获中,可以用智能指针解决。我们来看看

double Division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
	{
		throw "Division by zero condition!";
	}
	return (double)a / (double)b;
}
 
void fyy() noexcept
{
	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;
}
 
void func()
{
	//这里可以看到如果发生除0错误抛出异常,下面的array数组就没有得到释放
	//所以这里捕获异常但是不处理异常,异常还是交给外面处理,这里捕获了再抛出去
	//就能delete array了
	int* array = new int[10];
	try
	{
		fyy();
	}
	catch (...)
	{
		//捕获异常不是为了处理异常
		//是为了释放内存,然后异常再重新抛出
		cout << "delete[]" << array << endl;
		delete[] array;
		throw;//捕到什么抛什么
	}
	cout << "delete[]" << array << endl;
	delete[] array;
}

但是当有很多个变量要new和delete呢?就跟上面一样,会导致代码的繁琐嵌套,所以我们要用智能指针来解决。

二.   智能指针的原理与使用

2.1  智能指针的原理

智能指针的基本原理是利用RAII。

RAII:RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在 对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做 法有两大好处:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。
template<class T>
class Smartptr
{
public:
	//RAII
	Smartptr(T* ptr)
		:_ptr(ptr)
	{}
	~Smartptr()
	{
		cout << "delete:" << _ptr << endl;
		delete _ptr;
	}
 
	//像指针一样
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};
 
int main()
{
    Smartptr<int> sp1(new int(1));
	Smartptr<int> sp2(new int(2));
	*sp1 += 10;
	Smartptr<pair<string, int>> sp3(new pair<string, int>);
	sp3->first = "apple";
	sp3->second = 1;
	return 0;
}

2.2  智能指针的使用

template<class T>
class Smartptr
{
public:
	//RAII
	Smartptr(T* ptr)
		:_ptr(ptr)
	{}
	~Smartptr()
	{
		cout << "delete:" << _ptr << endl;
		delete _ptr;
	}
 
	//像指针一样
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};
 
int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
	{
		throw invalid_argument("除0错误");
	}
	return a / b;
}
void func()
{
	Smartptr<int> sp1(new int(1));
	Smartptr<int> sp2(new int(2));
	*sp1 += 10;
	cout << div() << endl;
}
int main()
{
	try
	{
		func();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

通过SmartPtr对象,无论程序是正常执行结束,还是因为某些中途原因进行返回,或者抛出异常等开始所面临的困境,只要SmartPtr对象的生命周期结束就会自动调用对应的析构函数,不会造成内存泄漏,完成资源释放。

2.3   智能指针的拷贝问题

如果我们用一个智能指针拷贝构造一个智能指针,或者用一个智能指针赋值给另一个智能指针。这样的操作都会导致程序崩溃。

void test()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(sp1);//拷贝构造
	SmartPtr<int> sp3(new int);
	SmartPtr<int> sp4 = sp3;//赋值
}

因为对于我们的智能指针来说,将sp1拷贝给sp2操作是浅拷贝,是将两个指针的指向统一到一块空间。当sp1和sp2释放时,会导致这块空间释放两次。同样的道理,将sp3赋值给sp4的时候,也只是单纯的将指针的指向指到同一块空间,这样在析构的时候也会导致析构两次。

所以对于如何解决这个问题,智能指针分为了很多版本。

三.   智能指针的众多版本

C++中存在4种智能指针:auto_ptr,unquie_ptr,shared_ptr,weak_ptr,他们各有优缺点,以及对应的实用场景。

3.1  auto_ptr

auto_ptr :管理权转移,被拷贝对象把资源管理权转移给拷贝对象,导致被拷贝对象悬空。

auto_ptr是C++98的,通过管理权转移的方式解决智能指针拷贝问题,保证了一个资源只有一个对象对其进行管理,这时候一个资源就不会被多个释放:

int main()
{
	yjy::auto_ptr<int> ap1(new int(1));
	yjy::auto_ptr<int> ap2(ap1);
 
	*ap2 += 10;
 
	//ap1悬空
	//*ap1 += 10;
 
	return 0;
}

 auto_ptr的模拟实现为:

namespace yjy
{
	template<class T>
	class auto_ptr
	{
	public:
		//RAII
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}
 
		//ap2(ap1)
		auto_ptr(auto_ptr<T>& ap)
		{
			_ptr = ap._ptr;
			ap._ptr = nullptr;
		}
 
		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			// 检测是否为自己给自己赋值
 
			if (this != &ap)
			{
				// 释放当前对象中资源
 
				if (_ptr)
					delete _ptr;
				// 转移ap中资源到当前对象中
 
				_ptr = ap._ptr;
				ap._ptr = NULL;
			}
			return *this;
		}
 
 
		~auto_ptr()
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}
 
		//像指针一样
		T& operator*()
		{
			return *_ptr;
		}
 
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
}

构造对象获取资源,析构对象释放资源。对*和->运算符进行重载,使其像指针一样。拷贝构造函数,用传入的对象的资源来构造当前对象,并将传入对象管理资源指针悬空。

3.2  unique_ptr

3.2.1 unique_ptr的应用场景

        因为unique_ptr直接用防止拷贝的方式解决智能指针的拷贝问题,简单而又粗暴。防止智能指针对象拷贝,保证资源不会被多次释放。std::unique_ptr<T>std::shared_ptr<T>具有更小的内存,而且不需要维护引用计数,因此它的性能更好。当我们需要一个独占的指针时,应该优先使用std::unique_ptr<T>

3.2.2  unique_ptr的简单实现  

  • unique_ptr只能移动不能复制, 因此是唯一的所有权. 所有不需要分享的指针都应该用这个来代替
  • unique_ptr还有一个默认模板参数是deleter决定析构时的动作. 默认的default_delete仅仅是调用了delete操作, 可以自定义deleter来决定析构时的操作
  • 我们应该像传递raw指针一样传递智能指针, 不用去考虑引用, 右值引用之类的操作
  • 编写的时候注意尽量不要抛出异常, 用noexcept优化速度
template<typename T>
class UniquePtr {
  using pointer = T*;

private:
  pointer data;

private:
  // 清内存并置空
  void _destroy(UniquePtr& target) noexcept {
      if (target.data != nullptr) {
          delete target.data;
          target.data = nullptr;
      }
  }

public:
  // 用于外部获取指针原始类型
  typedef T ElementType;

  // 默认构造函数, 赋值为nullptr
  UniquePtr() noexcept :data(nullptr) {}

  // 显式构造函数, 为了防止隐式类型转换
  explicit UniquePtr(const pointer& data) noexcept : data(data) {}

  // 析构函数
  ~UniquePtr() noexcept { _destroy(*this); }

  // 移动构造和移动赋值都存在, 用swap实现, 移动后记得清空对方
  UniquePtr(UniquePtr&& moving) noexcept : data(nullptr) {
      swap(moving);
      _destroy(moving);
  }
  // 因为有了nullptr_t构造, 因此可以进行nullptr赋值
  UniquePtr& operator=(UniquePtr&& moving) noexcept {
      if (this != &moving){
          swap(moving);
          _destroy(moving);
      }
      return *this;
  };

  // 拷贝构造和拷贝赋值都被禁止, 采用const&来写就是万能引用
  UniquePtr(const UniquePtr&) noexcept = delete;
  UniquePtr& operator=(const UniquePtr&) noexcept = delete;
  // 仅允许使用nullptr进行拷贝赋值, 因为这相当于reset
  UniquePtr& operator=(std::nullptr_t) noexcept {
      reset();
      return *this;
  }

  // 显式bool转换函数
  explicit operator bool() const noexcept { return data != nullptr; }

  // 只能指针都需要模仿原生指针, 注意不要对nullptr操作
  T& operator*() const noexcept { assert(data != nullptr); return *data; }
  pointer operator->() const noexcept { assert(data != nullptr); return data; }
  pointer get() const noexcept { return data; }

  // 用于交换指针的成员函数, 非常非常常用
  void swap(UniquePtr& other) noexcept {
      std::swap(data, other.data);
  }
  void swap(UniquePtr&& other) noexcept {
      std::swap(data, other.data);
  }

  // 将内部指针置为外部值并删去当前值, 注意防止自我赋值
  void reset(pointer p = nullptr) noexcept {
      swap(UniquePtr(p));
  }
};

3.3.3  unique_ptr转移控制权

         虽然unique_ptr直接用防止拷贝的方式解决智能指针的拷贝问题,但是可以使用std::move() 将一个unique_ptr转移给另一个unique_ptr。转移后,原来的unique_ptr将不再拥有对内存的控制权,将变为空指针。

std::unique_ptr<int> p1 = std::make_unique<int>(0);
std::unique_ptr<int> p2 = std::move(p1); 
// now, p1 is nullptr

3.3.4 std::shared_ptr 和 std::unique_ptr共有方法

方法用途
p.get()返回p中保存的指针,不会影响p的引用计数。
p.reset()释放p指向的对象,将p置为空。
p.reset(q)释放p指向的对象,令p指向q。
p.reset(new T)释放p指向的对象,令p指向一个新的对象。
p.swap(q)交换p和q中的指针。
swap(p, q)交换p和q中的指针。
p.operator*()解引用p。
p.operator->()成员访问运算符,等价于(*p).member。
p.operator bool()检查p是否为空指针。

3.3 shared_ptr

3.3.1 shared_ptr的应用场景

  1. 资源管理: shared_ptr能够帮助我们自动管理动态内存的分配和释放,避免内存泄漏和野指针的问题。在一个资源需要在多个地方共享使用,且难以确定释放时机的情况下,使用shared_ptr能够很好地解决这类问题。

  2. 循环引用: 当对象之间存在循环引用关系时,如果使用裸指针很容易导致内存泄漏。而使用shared_ptr可以通过引用计数的方式很好地解决循环引用的问题,在最后一个引用退出作用域后能够自动释放内存。

  3. 异常安全: 在面对代码中存在异常抛出的情况时,使用shared_ptr能够帮助我们简化资源管理的复杂性,确保在发生异常时能够正确释放资源,防止资源泄漏。

  4. STL容器中的使用: shared_ptr可被安全地存储在STL容器中,如std::vectorstd::map等,使得在容器中传递和管理对象更加便捷。

  5. 在多线程环境下进行资源管理:std::shared_ptr可以被多个指针共享同一个对象,避免资源被多次释放的问题,适用于多线程环境下的资源管理

  6. 作为函数参数传递和返回值返回:std::shared_ptr可以作为函数参数传递和返回值返回,可以确保资源在函数调用结束后正确释放,避免资源泄漏。

总的来说,shared_ptr适用于需要在多个地方共享资源、解决循环引用问题、简化异常处理和与STL容器结合使用的场景中,能够提高代码的安全性和可维护性。

在实际编码中使用shared_ptr可以大大简化内存管理和加强资源的安全性,提高代码的可读性和可靠性。

3.3.2 shared_ptr的简单实现

3.3.2.1 智能指针基类的简单实现
  • 由于shared_ptr和weak_ptr都有一个堆储存的计数器来维护计数进行内存回收, 为了编写的方便将其写为一个基类来继承
  • 由于shared_ptr和weak_ptr的计数器是共享的, 有可能被多线程竞争修改, 因此需要有额外的mutex来保护, 所有堆counter的修改都需要经过mutex原子保护
class PtrBase {
public:
  // stl实现的智能指针还会在Counter中放入注册好的deleter
  struct Counter {
      int uses = 0;
      int weaks = 0;
  };
  using p_counter = Counter*;
  using p_mutex = std::mutex*;
  // 堆计数器用来在多个ptr间共享
  p_counter counter;
  // 堆内存mutex保证线程安全, 计数器为nullptr时才回收
  p_mutex mutex;

protected:
  // 这里用到了委托构造的技巧. 需要new计数器和互斥量
  // 注意由于用到new所以可能产生异常. 让异常逃离构造函数很麻烦, 因此用nothrow然后自己处理
  PtrBase() noexcept : PtrBase(
      new (std::nothrow) Counter(),
      new (std::nothrow) std::mutex())
  {}
  PtrBase(std::nullptr_t) noexcept : PtrBase() {}
  PtrBase(p_counter counter, p_mutex mutex) noexcept :
      counter(counter),
      mutex(mutex)
  {}

  void increase_shared_count() noexcept {
      if (counter != nullptr) {
          mutex->lock();
          ++(counter->uses);
          mutex->unlock();
      }
  }

  void increase_weak_count() noexcept {
      if (counter != nullptr) {
          mutex->lock();
          ++(counter->weaks);
          mutex->unlock();
      }
  }

  // 只要share计数为0就返回给指针本身以回收目标对象的内存
  bool reduce_shared_count() noexcept {
      bool is_zero = true;
      if (counter != nullptr) {
          mutex->lock();
          --(counter->uses);
          if (counter->uses > 0) {
              is_zero = false;
          }
          mutex->unlock();
      }
      return is_zero;
  }

  // 只有当两种引用都为0时才可以回收计数器本身的内存, 记得所有对堆内存的修改都加锁
  void reduce_weak_count() noexcept {
      if (counter != nullptr) {
          mutex->lock();
          if (counter->weaks > 0) {
              --(counter->weaks);
          }
          if (counter->uses == 0 && counter->weaks == 0) {
              delete counter;
              counter = nullptr;
          }
          mutex->unlock();
      }
  }

  void check_mutex() noexcept {
      if (counter == nullptr) {
          delete mutex;
          mutex = nullptr;
      }
  }

  // new失败的时候做的补救措施
  void revert() noexcept {
      if (mutex != nullptr) {
          reduce_shared_count();
          reduce_weak_count();
          delete mutex;
          mutex = nullptr;
      }
      if (counter != nullptr) {
          delete counter;
          counter = nullptr;
      }
  }

  void swap(PtrBase& other) noexcept {
      std::swap(counter, other.counter);
      std::swap(mutex, other.mutex);
  }
};
3.3.2.2 shared_ptr的简单实现
  1. shared_ptr需要一个间接层处理引用计数的问题, 因此带来了额外的开销, unique_ptr则完全没有额外的空间开销
  2. 对于性能不敏感的情况, 最好不要使用原始指针
  3. 建议不要对某个对象进行两次以上的shared, 我们的脑子处理不了太多的共享, 用weak代替
  4. stl中通过让自己的类继承enable_shared_from_this类, 我们可以生成指向自身this的shared_ptr
  5. 这个问题是由于非侵入式访问的标准库设计哲学, shared_ptr的计数器和对象本身是分离的, 如果在类中对this构造一个shared_ptr, 那么产生的是第二个计数器, 和初始化两次shared_ptr的效果是一样的, 并不是拷贝. 因此在类中这个构造函数结束后, 这个对象(自己)就会被调用析构, 然后一切都boom了
  6. enable_shared_from_this则通过weak_ptr安全地生成了一个自己的shared_ptr, 防止了析构问题. 这种现象常出现在多线程的回调中, 其实不是很常见
  7. stl实现的make_shared还支持了优化, 让目标对象和间接层连续储存从而减少了new和delete的开销
  8. stl的unique_ptr可以被赋值给shared_ptr
  9. shared_ptr并非完美, 例如用同一个原生指针构造两个智能指针的话, 目标内存会被重复析构而报错, 因此最好避免这种直接的指针操作
template<typename T>
class SharedPtr : public PtrBase {
  using pointer = T*;
  // 需要和WeakPtr形成friend方便两者之间的转型
  friend class WeakPtr<T>;

private:
  // 原生指针
  pointer data;

private:
  // 先减少计数, 如果为0则释放资源
  void _destroy(SharedPtr& target) noexcept {
      if (data != nullptr) {
          if (target.reduce_shared_count()) {
              delete target.data;
              target.data = nullptr;
          }
          target.check_mutex();
      }
  }

  // 给WeakPtr用的构造
  SharedPtr(const WeakPtr<T>& wptr) noexcept : data(wptr.data), PtrBase(wptr.counter, wptr.mutex) {
      increase_shared_count();
  }

public:
  // 用于外部获取指针原始类型
  typedef T ElementType;
  
  // 默认构造函数, 全部赋为nullptr
  SharedPtr() noexcept : data(nullptr), PtrBase() {};

  // 记得显式以防止隐式类型转换
  explicit SharedPtr(const pointer& data) noexcept :
      data(data), PtrBase() {
      // nullptr代表空间申请失败
      if (counter == nullptr || mutex == nullptr) {
          this->data = nullptr;
          revert();
      }
      if (data != nullptr) {
          increase_shared_count();
      }
  }

  ~SharedPtr() noexcept {
      _destroy(*this);
  }

  // 拷贝构造, shared_ptr拷贝后会将计数器+1
  SharedPtr(const SharedPtr& copy) noexcept : data(copy.data), PtrBase(copy.counter, copy.mutex) {
      if (data != nullptr) {
          increase_shared_count();
      }
  }
  // 拷贝赋值, 采用copy-swap写法, 由于右值引用的存在, 折叠式写法会造成二义性
  // 旧的内存会由于tmp的析构而释放, 新的内存的申请也在tmp的拷贝构造中完成了
  SharedPtr& operator=(const SharedPtr& copy) noexcept {
      SharedPtr tmp(copy);
      swap(tmp);
      return *this;
  }
  // 用nullptr赋值时相当于清空
  SharedPtr& operator=(std::nullptr_t) noexcept {
      _destroy(*this);
      return *this;
  }

  // 移动构造函数, 由于是构造所以可以直接夺取指针内容
  // 析构的时候由于目标是nullptr所以自然结束掉
  SharedPtr(SharedPtr&& moving) noexcept : data(nullptr), PtrBase() {
      swap(moving);
      _destroy(moving);
  }
  // 移动赋值函数
  SharedPtr& operator=(SharedPtr&& moving) noexcept {
      if (this != &moving) {
          swap(moving);
          _destroy(moving);
      }
      return *this;
  }

  // 老三样
  pointer operator->() const noexcept { assert(data != nullptr); return data; }
  T& operator*() const noexcept { assert(data != nullptr); return *data; }
  pointer get() const noexcept { return data; }

  // 用于交换指针的成员函数
  void swap(SharedPtr& other) noexcept {
      std::swap(data, other.data);
      PtrBase::swap(other);
  }

  void swap(SharedPtr&& other) noexcept {
      std::swap(data, other.data);
      PtrBase::swap(other);
  }

  // 通过与新构造的对象交换来简化代码
  void reset(pointer p = nullptr) noexcept {
      swap(SharedPtr(p));
  }

  // 返回当前计数器次数
  int use_count() const noexcept { assert(counter != nullptr); return counter->uses; }

  explicit operator bool() const noexcept { return data != nullptr; }
};
3.3.2.3 make_xxx的简单实现

主要就是使用完美转发和变长参数来无损包装new操作, 从而让new不用暴露在用户面前

template<typename T, typename... Args>
inline UniquePtr<T> MakeUnique(Args&&... args) {
  return UniquePtr<T>(new T(std::forward<Args>(args)...));
}

template<typename T, typename... Args>
inline SharedPtr<T> MakeShared(Args&&... args) {
  return SharedPtr<T>(new T(std::forward<Args>(args)...));
}

3.3.3 shared_ptr的循环引用问题

我们先来看一段代码

struct ListNode
{
	int _val;
	yjy::shared_ptr<ListNode> _next;
	yjy::shared_ptr<ListNode> _prev;
 
	ListNode(int val = 0)
		:_val(val)
	{}
 
	~ListNode()
	{
		cout << "ListNode" << endl;
	}
};
 
int main()
{
	yjy::shared_ptr<ListNode> n1(new ListNode(10));
	yjy::shared_ptr<ListNode> n2(new ListNode(20));
 
	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;
 
	n1->_next = n2;
	n2->_next = n1;
 
	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;
	return 0;
}

我们可以看到定义了两个对象,对象里面的prev和next对应指向另一个对象,这时候我们的shared_ptr就会存在缺陷。

在我们出作用域销毁的时候,会发生下面的情况:

n2对象销毁时-》_prev指针释放-》n1对象销毁-》_next指针释放-》n2对象销毁

运行结果:

可以看见销毁时是出错了的。

可以看到这个销毁的过程是一个互相影响的过程,是一个死循环。这样的结构就是我们的循环引用。该怎么办呢?

这里就要用到我们的weak_ptr:

//不支持RAII,不参与资源管理
template<class T>
class weak_ptr
{
public:
	//RAII
	weak_ptr()
		:_ptr(nullptr)
	{}
 
	weak_ptr(const shared_ptr<T>& wp)
	{
		_ptr = wp.get();
	}
 
	weak_ptr<T>& operator=(const shared_ptr<T>& wp)
	{
		_ptr = wp.get();
		return *this;
	}
 
	// 像指针一样
	T& operator*()
	{
		return *_ptr;
	}
 
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

 这里的weak_ptr就不涉及RAII,不参与资源管理,从根源上杜绝了这个问题

struct ListNode
{
	int _val;
 
	yjy::weak_ptr<ListNode> _next;
	yjy::weak_ptr<ListNode> _prev;
	ListNode(int val = 0)
		:_val(val)
	{}
 
	~ListNode()
	{
		cout << "ListNode" << endl;
	}
};
 
int main()
{
	yjy::shared_ptr<ListNode> n1(new ListNode(10));
	yjy::shared_ptr<ListNode> n2(new ListNode(20));
 
	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;
 
	n1->_next = n2;
	n2->_next = n1;
 
	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;
	return 0;
}

3.3.4  shared_ptr的线程安全问题

        如果多个线程同时拷贝同一个 shared_ptr 对象,不会有问题,因为 shared_ptr 的引用计数是线程安全的。但是如果多个线程同时修改同一个 shared_ptr 对象,不是线程安全的。因此,如果多个线程同时访问同一个 shared_ptr 对象,并且有写操作,需要使用互斥量来保护。

  • 引用计数更新,线程安全

这里我们讨论对shared_ptr进行拷贝的情况,由于此操作读写的是引用计数,而引用计数的更新是原子操作,因此这种情况是线程安全的。下面这个例子,两个线程同时对同一个shared_ptr进行拷贝,引用计数的值总是20001。

std::shared_ptr<int> p = std::make_shared<int>(0);
constexpr int N = 10000;
std::vector<std::shared_ptr<int>> sp_arr1(N);
std::vector<std::shared_ptr<int>> sp_arr2(N);

void increment_count(std::vector<std::shared_ptr<int>>& sp_arr) {
    for (int i = 0; i < N; i++) {
        sp_arr[i] = p;
    }
}

std::thread t1(increment_count, std::ref(sp_arr1));
std::thread t2(increment_count, std::ref(sp_arr2));
t1.join();
t2.join();
std::cout<< p.use_count() << std::endl; // always 20001
  • 同时修改内存区域,线程不安全

下面这个例子,两个线程同时对同一个shared_ptr指向内存的值进行自增操作,最终的结果不是我们期望的20000。因此同时修改shared_ptr指向的内存区域不是线程安全的。


std::shared_ptr<int> p = std::make_shared<int>(0);
void modify_memory() {
    for (int i = 0; i < 10000; i++) {
        (*p)++;
    }
}

std::thread t1(modify_memory);
std::thread t2(modify_memory);
t1.join();
t2.join();
std::cout << "Final value of p: " << *p << std::endl; // possible result: 16171, not 20000

  • 直接修改shared_ptr对象本身的指向,线程不安全。下面这个程序示例,两个线程同时修改同一个shared_ptr对象的指向,程序发生了异常终止。

std::shared_ptr<int> sp = std::make_shared<int>(1);
auto modify_sp_self = [&sp]() {
    for (int i = 0; i < 1000000; ++i) {
        sp = std::make_shared<int>(i);
    }
};

std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
    threads.emplace_back(modify_sp_self);
}
for (auto& t : threads) {
    t.join();
}

报错为:

pure virtual method called
terminate called without an active exception

用gdb查看函数调用栈,发现是在调用std::shared_ptr<int>::~shared_ptr()时出错,

(gdb) bt
#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50
#1  0x00007ffff7bc7859 in __GI_abort () at abort.c:79
#2  0x00007ffff7e73911 in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#3  0x00007ffff7e7f38c in ?? () from /lib/x86_64-linux-gnu/libstdc++.so.6
#4  0x00007ffff7e7f3f7 in std::terminate() () from /lib/x86_64-linux-gnu/libstdc++.so.6
#5  0x00007ffff7e80155 in __cxa_pure_virtual () from /lib/x86_64-linux-gnu/libstdc++.so.6
#6  0x00005555555576c2 in std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_release() ()
#7  0x00005555555572fd in std::__shared_count<(__gnu_cxx::_Lock_policy)2>::~__shared_count() ()
#8  0x0000555555557136 in std::__shared_ptr<int, (__gnu_cxx::_Lock_policy)2>::~__shared_ptr() ()
#9  0x000055555555781c in std::__shared_ptr<int, (__gnu_cxx::_Lock_policy)2>::operator=(std::__shared_ptr<int, (__gnu_cxx::_Lock_policy)2>&&) ()
#10 0x00005555555573d0 in std::shared_ptr<int>::operator=(std::shared_ptr<int>&&) ()
#11 0x000055555555639f in main::{lambda()#1}::operator()() const ()
... 

其原因为:在并发修改的情况下,对正在析构的对象再次调用析构函数,导致了此异常。

对程序加锁后,程序可正常运行:

std::shared_ptr<int> sp = std::make_shared<int>(1);
std::mutex m;
auto modify = [&sp]() {
    // make the program thread safe
    std::lock_guard<std::mutex> lock(m);
    for (int i = 0; i < 1000000; ++i) {
        sp = std::make_shared<int>(i);
    }
};

std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
    threads.emplace_back(modify);
}
for (auto& t : threads) {
    t.join();
}
std::cout << *sp << std::endl;  // running as expected, result: 999999

3.3.4 shared_ptr的内置方法

方法用途
make_shared<T>(args)返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化此对象。
shared_ptr<T>p(q)p是q的拷贝,此操作递增q中的计数器。q中的指针必须能转换为T*。
shared_ptr<T>p = qp是q的拷贝,此操作递增q中的计数器。q中的指针必须能转换为T*。
p.unique()如果p.use_count()为1,返回true,否则返回false。
p.use_count()返回与p共享对象的智能指针数量。

示例:


std::shared_ptr<int> sp1 = std::make_shared<int>(42);
std::cout<<sp1.unique()<<std::endl; // 1
std::shared_ptr<int> sp2 = sp1;
std::shared_ptr<int> sp3(sp1);
std::shared_ptr<int> sp4(new int(44)); // Not recommended
std::cout<<sp1.use_count()<<std::endl; // 3
sp1.reset();
std::cout<<sp1.use_count()<<std::endl; // 0
std::cout<<sp2.use_count()<<std::endl; // 2

3.4 weak_ptr

3.4.1 weak_ptr应用概述

        weak_ptr是一种弱引用,指向shared_ptr所管理的对象,而不影响所指对象的生命周期,也就是将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。不论是否有weak_ptr指向,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。

  weak_ptr对它所指向的shared_ptr所管理的对象没有所有权,不能对它解引用,因此若要读取引用对象,必须要转换成shared_ptrC++中提供了lock函数来实现该功能。如果对象存在,lock()函数返回一个指向共享对象的shared_ptr,否则返回一个空shared_ptr

        weak_ptr提供了一个成员函数expired()来判断所指对象是否已经被释放。如果所指对象已经被释放,expired()返回true,否则返回false。

        std::weak_ptr可以作为std::shared_ptr的构造函数参数,但如果std::weak_ptr指向的对象已经被释放,那么std::shared_ptr的构造函数会抛出std::bad_weak_ptr异常。

3.4.2 weak_ptr的应用场景

3.4.2.1 用于实现缓存

        weak_ptr可以用来缓存对象,当对象被销毁时,weak_ptr也会自动失效,不会造成野指针。

        假设我们有一个Widget类,我们需要从文件中加载Widget对象,但是Widget对象的加载是比较耗时的。

std::shared_ptr<Widget> loadWidgetFromFile(int id); 
// a factory function which returns a shared_ptr, which is expensive to call
// may perform file or database I/O

        因此,我们希望Widget对象可以缓存起来,当下次需要Widget对象时,可以直接从缓存中获取,而不需要重新加载。这个时候,我们就可以使用std::weak_ptr来缓存Widget对象,实现快速访问。如以下代码所示:


std::shared_ptr<Widget> fastLoadWidget(int id) {
    static std::unordered_map<int, std::weak_ptr<Widget>> cache;
    auto objPtr = cache[id].lock(); 
    if (!objPtr) {
        objPtr = loadWidgetFromFile(id);
        cache[id] = objPtr; // use std::shared_ptr to construct std::weak_ptr
    }
    return objPtr;
}

        当对应id的Widget对象已经被缓存时,cache[id].lock()会返回一个指向Widget对象的std::shared_ptr,否则cache[id].lock()会返回一个空的std::shared_ptr,此时,我们就需要重新加载Widget对象,并将其缓存起来,这一步会由std::shared_ptr构造std::weak_ptr

        为什么不直接存储std::shared_ptr呢?因为这样会导致缓存中的对象永远不会被销毁,因为std::shared_ptr的引用计数永远不会为0。而std::weak_ptr不会增加对象的引用计数,因此,当缓存中的对象没有被其他地方引用时,std::weak_ptr会自动失效,从而导致缓存中的对象被销毁。

3.4.2.2 避免循环引用问题

        循环引用是指两个或多个对象之间通过shared_ptr相互引用,形成了一个环,导致它们的引用计数都不为0,从而导致内存泄漏。

        在观察者模式中使用shared_ptr可能会出现循环引用,在下面的程序中,Observer对象和Subject对象相互引用,导致它们的引用计数都不为0,从而导致内存泄漏。

    class IObserver {
    public:
        virtual void update(const string& msg) = 0;
    };
    
    class Subject {
    public:
        void attach(const std::shared_ptr<IObserver>& observer) {
            observers_.emplace_back(observer);
        }
        void detach(const std::shared_ptr<IObserver>& observer) {
            observers_.erase(std::remove(observers_.begin(), observers_.end(), observer), observers_.end());
        }
        void notify(const string& msg) {
            for (auto& observer : observers_) {
                observer->update(msg);
            }
        }
    private:
        std::vector<std::shared_ptr<IObserver>> observers_;
    };
    
    class ConcreteObserver : public IObserver {
    public:
        ConcreteObserver(const std::shared_ptr<Subject>& subject) : subject_(subject) {}
        void update(const string& msg) override {
            std::cout << "ConcreteObserver " << msg<< std::endl;
        }
    private:
        std::shared_ptr<Subject> subject_;
    };
    
    int main() {
        std::shared_ptr<Subject> subject = std::make_shared<Subject>();
        std::shared_ptr<IObserver> observer = std::make_shared<ConcreteObserver>(subject);
        subject->attach(observer);
        subject->notify("update");
        return 0;
    }
    
    • 避免循环引用的方法

    将Observer类中的subject_成员变量改为weak_ptr,这样就打破循环引用,不会导致内存无法正确释放了。

    3.4.2.3 用于实现单例模式

            单例模式是指一个类只能有一个实例,且该类能自行创建这个实例的一种模式。单例模式的实现方式有很多种,其中一种就是使用std::weak_ptr

    class Singleton {
    public:
        static std::shared_ptr<Singleton> getInstance() {
            std::shared_ptr<Singleton> instance = m_instance.lock();
            if (!instance) {
                instance.reset(new Singleton());
                m_instance = instance;
            }
            return instance;
        }
    private:
        Singleton() {}
        static std::weak_ptr<Singleton> m_instance;
    };
    
    std::weak_ptr<Singleton> Singleton::m_instance;
    

    std::weak_ptr实现单例模式的优点:

    • 避免循环应用:避免了内存泄漏。
    • 访问控制:可以访问对象,但是不会延长对象的生命周期。
    • 可以在单例对象不被使用时,自动释放对象。

    3.4.3weak_ptr的简单实现

    • weak_ptr的实现与shared_ptr类似, 只是修改的是weak计数
    • 不允许直接从原始指针构造, 必须绑定在shared_ptr上
    • 当share计数为0时, weak_ptr失效
    • weak_ptr不能用来直接操作目标, 只有当指针有效的时候, 通过lock()函数构造一个shared_ptr才能进行操作, 无效的时候lock返回nullptr
    class WeakPtr : public PtrBase {
      using pointer = T*;
      friend class SharedPtr<T>;
    
    private:
      pointer data;
    
    private:
      void _destroy(WeakPtr& target) noexcept {
          if (data != nullptr) {
              target.reduce_weak_count();
              target.check_mutex();
          }
      }
    
    public:
      // 用于外部获取指针原始类型
      typedef T ElementType;
      WeakPtr() noexcept : data(nullptr), PtrBase() {}
      ~WeakPtr() noexcept { _destroy(*this); }
      WeakPtr(const SharedPtr<T>& sptr) noexcept :data(sptr.data), PtrBase(sptr.counter, sptr.mutex) {
          if (data != nullptr) {
              increase_weak_count();
          }
      }
      WeakPtr& operator=(const SharedPtr<T>& copy) noexcept {
          WeakPtr<T> tmp(copy);
          swap(tmp);
          return *this;
      }
      WeakPtr(const WeakPtr& copy) noexcept :data(copy.data), PtrBase(copy.counter, copy.mutex) {
          if (data != nullptr) {
              increase_weak_count();
          }
      }
      WeakPtr& operator=(const WeakPtr& copy) noexcept {
          WeakPtr tmp(copy);
          swap(tmp);
          if (data != nullptr) {
              increase_weak_count();
          }
          return *this;
      }
      WeakPtr& operator=(std::nullptr_t) noexcept {
          reset();
          return *this;
      }
    
      WeakPtr(WeakPtr&& moving) noexcept : data(nullptr), PtrBase() {
          swap(moving);
          _destroy(moving);
      }
      WeakPtr& operator=(WeakPtr&& moving) noexcept {
          if (this != &moving) {
              swap(moving);
              _destroy(moving);
          }
          return *this;
      }
    
      SharedPtr<T> lock() noexcept {
          if (expired()) {
              return SharedPtr<T>(nullptr);;
          }
          else {
              return SharedPtr<T>(*this);
          }
      }
    
      void reset() noexcept {
          swap(WeakPtr());
      }
    
      void swap(WeakPtr& other) noexcept {
          std::swap(data, other.data);
          PtrBase::swap(other);
      }
      
      void swap(WeakPtr&& other) noexcept {
          std::swap(data, other.data);
          PtrBase::swap(other);
      }
    
      int use_count() const noexcept { assert(counter != nullptr);  return counter->uses; }
      bool expired() const noexcept { return counter->uses == 0; }
    };

    3.4.4 weak_ptr的内置方法

    方法用途
    use_count()返回与之共享对象的shared_ptr的数量
    expired()检查所指对象是否已经被释放
    lock()返回一个指向共享对象的shared_ptr,若对象不存在则返回空shared_ptr
    owner_before()提供所有者基于的弱指针的排序
    reset()释放所指对象
    swap()交换两个weak_ptr对象

            四.自定义删除器

    4.1 自定义删除器的使用场景

    自定义删除器的作用是在智能指针释放所管理的对象时,执行一些特殊的操作,比如:

    • 内存释放时打印一些日志。

    • 管理除内存以外的其它资源,例如文件句柄、数据库连接等。

    • 与自定义分配器(Allocator)配合使用,将资源释放给自定义分配器。

    4.2 自定义删除器的使用

    自定义删除器可以是一个函数,也可以是一个类的对象, 也可以是一个lambda表达式。

    如果是一个函数,它的形式如下:

    void free_memory(int* p) {
        std::cout << "delete memory" << std::endl;
        delete p;
    }
    

    如果是一个类的对象,它的形式如下:

    class FreeMemory {
    public:
        void operator()(int* p) {
            std::cout << "delete memory" << std::endl;
            delete p;
        }
    };

    如果是一个lambda表达式,它的形式如下:

    auto free_memory_lambda = [](int* p) {
        std::cout << "delete memory" << std::endl;
        delete p;
    }
    • shared_ptr自定义删除器的使用:

      对于shared_ptr, 不管删除器什么类型,是否有状态都不会增加shared_ptr的大小, 均为两个字长。因为删除器是存储在控制块中,而控制块的大小为两个字长。

    std::shared_ptr<int> sp1(new int(0), free_memory); // size: 8
    std::shared_ptr<int> sp2(new int(0), FreeMemory()); // size: 8
    std::shared_ptr<int> sp3(new int(0), free_memory_lambda); // size: 8
    
    • unique_ptr自定义删除器的使用:

      • unique_ptr的删除器类型是一个模板参数,因此需要指定删除器类型。
      • 如果删除器是函数指针类型,std::unique_ptr大小从1个字长增长到2个字长,因为需要存储函数指针。
      • 如果删除器是无状态删除器(stateless function),比如不进行捕获的lambda表达式,std::unique_ptr大小不变,因为无状态删除器不需要存储任何成员变量。
    std::unique_ptr<int, FreeMemory> up1(new int(0)); // size: 4
    std::unique_ptr<int, void(*)(int*)> up2(new int(0), free_memory);  // size: 8
    std::unique_ptr<int, decltype(free_memory)*> up3(new int(0), free_memory); // size: 4
    

    4.3 有状态删除器和无状态删除器

    什么是有状态删除器?什么是无状态删除器?有状态删除器是指删除器类中包含有成员变量,无状态删除器是指删除器类中不包含有成员变量。

    如果std::unique_ptr的函数对象删除器是具有扩展状态的,其大小可能会非常大。如果大得无法接受,可能需要设计一个无状态删除器。

    下面是一个有状态删除器的例子:

    class DeleteObject {
    public:
        DeleteObject(int n) : n_(n) {}
        void operator()(int* p) {
            std::cout << "delete memory " << n_ << std::endl;
            delete p;
        }
    private:
        int n_;
    };
    

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值