C++ 深入智能指针

admin2024-09-01  9

智能指针的三个常用函数:

  1. get() 获取智能指针托管的指针地址
  2. release() 取消智能指针对动态内存的托管
  3. reset() 重置智能指针托管的内存地址,如果地址不一致,原来的会被析构掉

auto_ptr

auto_ptr 的实现原理其实就是RAII,在构造的时候获取资源,在析构的时候释放资源,并进行相关指针操作的重载,使用起来就像普通的指针。

std::auto_ptr<ClassA> pa(new ClassA);

但是由于其构造函数声明为explicit的,因此不能通过隐式转换来构造,只能显式调用构造函数。

特点
1.auto_ptr 不能共享所有权,即不要让两个auto_ptr指向同一个对象。
2.auto_ptr 不能指向数组,因为auto_ptr在析构的时候只是调用delete,而数组应该要调用delete[]。
3.auto_ptr 只是一种简单的智能指针,如有特殊需求,需要使用其他智能指针,比如 share_ptr。
4.auto_ptr 不能作为容器对象,STL容器中的元素经常要支持拷贝,赋值等操作,在这过程中 auto_ptr 会传递所有权,那么 source 与 sink 元素之间就不等价了。

C++11 后auto_ptr 已经被“抛弃”,已使用 unique_ptr 替代!C++11后不建议使用auto_ptr(原因):
1). 复制或者赋值都会改变资源的所有权;
2). 在STL容器中使用auto_ptr存在着重大风险,因为容器内的元素必须支持可复制和可赋值;
3). 不支持对象数组的内存管理;

unique_ptr

特点:独享它指向的对象。也就是说,同时只有一个unique_ptr指向同一个对象,当这个unique_ptr被销毁时,指向的对象也随即被销毁。、

unique_ptr 和 auto_ptr 用法几乎一样,除了一些特殊特性:

1.基于排他所有权模式:两个指针不能指向同一个资源
2.无法进行左值 unique_ptr 复制构造,也无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值
3.保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象;
4.在容器中保存指针是安全的;

典型用途:
1.作为一个类的成员变量,这个变量只在本类使用,不会被赋值给其他类,也不会作为参数传递给某个函数;
2.在一个函数作为局部变量,使用完就不用再管,函数结束,自动释放托管资源;

原理
1.构造时传入托管对象的指针,析构时delete对象;
2.禁用赋值函数;

unique_ptr 手写

#include <utility>
#include<iostream>


/****
 * 智能指针unique_ptr的简单实现
 * 
 * 特点:独享它指向的对象。也就是说,同时只有一个unique_ptr指向同一个对象,当这个unique_ptr被销毁时,指向的对象也随即被销毁
 * 
 * 典型用途:
 * 1. 在一个函数定义一个A* ptr = new A(), 结束还需要用delete,而用unique_ptr,就不需要自己调用delete
 * 2. 作为一个类的变量,这个变量只在本类使用,不会被其他类调用,也不会作为参数传递给某个函数
 * */
template<typename T>
class unique_ptr
{
private:
	T * ptr_resource = nullptr;

public:
    //explicit构造函数是用来防止隐式转换, 即不允许写成unique_ptr<T> tempPtr = T;
    //std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝所以可以提高利用效率,改善性能.
    //move之后,raw_resource内部的资源将不能再被raw_resource使用
	explicit unique_ptr(T* raw_resource) noexcept : ptr_resource(std::move(raw_resource)) {}
	unique_ptr(std::nullptr_t) : ptr_resource(nullptr) {}

	unique_ptr() noexcept : ptr_resource(nullptr) {}

	//析构时, 释放托管的对象资源
	~unique_ptr() noexcept
	{
		delete ptr_resource;
	}
	// Disables the copy/ctor and copy assignment operator. We cannot have two copies exist or it'll bypass the RAII concept.
    //重要,禁止两种拷贝的赋值方式
    //使用"=delete"修饰,表示函数被定义为deleted,也就意味着这个成员函数不能再被调用,否则就会出错。
	unique_ptr(const unique_ptr<T>&) noexcept = delete;
	unique_ptr& operator = (const unique_ptr&) noexcept = delete;
	
public:
    //&& 是右值引用,见https://zhuanlan.zhihu.com/p/107445960
	// 允许移动语义。虽然无法复制unique_ptr,但可以安全地移动。
    //例子:unique_ptr<Test> tPtr3(std::move(tPtr1));
	unique_ptr(unique_ptr&& move) noexcept
	{
        std::cout << "construct for unique_ptr&&" << std::endl;
		move.swap(*this);
	}
	// ptr = std::move(resource)
	unique_ptr& operator=(unique_ptr&& move) noexcept
	{
        std::cout << "operator= for unique_ptr&&" << std::endl;

		move.swap(*this);
		return *this;
	}

	explicit operator bool() const noexcept
	{
		return this->ptr_resource;
	}
	// releases the ownership of the resource. The user is now responsible for memory clean-up.
	T* release() noexcept
	{
		return std::exchange(ptr_resource, nullptr);
	}
	// returns a pointer to the resource
	T* get() const noexcept
	{
		return ptr_resource;
	}
	// swaps the resources
	void swap(unique_ptr<T>& resource_ptr) noexcept
	{
		std::swap(ptr_resource, resource_ptr.ptr_resource);
	}
	// reset就删除老的,指向新的
	void reset(T* resource_ptr) noexcept(false)
	{
		// ensure a invalid resource is not passed or program will be terminated
		if (resource_ptr == nullptr)
			throw std::invalid_argument("An invalid pointer was passed, resources will not be swapped");

		delete ptr_resource;

		ptr_resource = nullptr;

		std::swap(ptr_resource, resource_ptr);
	}
public:
	// overloaded operators
	T * operator->() const noexcept
	{
		return this->ptr_resource;
	}
	T& operator*() const noexcept
	{
		return *this->ptr_resource;
	}
	// 额外说明noexcept
    //noexcept C++11关键字, 告诉编译器,函数中不会发生异常,有利于编译器对程序做更多的优化
    //C++中的异常处理是在运行时而不是编译时检测的。为了实现运行时检测,编译器创建额外的代码,然而这会妨碍程序优化
};

#include <utility>

template <typename T>
class unique_ptr {
public:
  explicit unique_ptr(T* ptr = nullptr)
    : ptr_(ptr) {}
  ~unique_ptr()
  {
    delete ptr_;
  }
  unique_ptr(unique_ptr&& other)
  {
    ptr_ = other.release();
  }
  // 子类指针向基类指针的转换
  template <typename U>
  unique_ptr(unique_ptr<U>&& other)
  {
    ptr_ = other.release();
  }
  unique_ptr& operator=(unique_ptr rhs)
  {
    rhs.swap(*this);
    return *this;
  }
  T* release()
  {
    T* ptr = ptr_;
    ptr_ = nullptr;
    return ptr;
  }
  void swap(unique_ptr& rhs)
  {
    using std::swap;
    swap(ptr_, rhs.ptr_);
  }
  T* get() const { return ptr_; }
  T& operator*() const { return *ptr_; }
  T* operator->() const { return ptr_; }
  operator bool() const { return ptr_; }
private:
  T* ptr_;
};

shared_ptr

共享对其所指堆内存空间的所有权,通过引用计数技术来追踪指向动态分配内存对象的所有者数量,当最后⼀个指涉到该对象的 shared_ptr 不再指向他时,shared_ptr会⾃动析构所指对象,其所指向的动态分配内存也会随之释放,有效地避免了内存泄漏问题。

核心特性
1.共享所有权:一个std::shared_ptr实例可以被复制或移动到另一个std::shared_ptr实例,复制后两者会共享同一份资源,并且都参与到引用计数的维护中。
2.弱引用std::weak_ptr,它是一种弱引用,不会增加引用计数,用于解决循环引用导致的对象无法释放的问题。
3.自定义删除器std::shared_ptr允许指定自定义的删除器,在资源不再需要时执行特定的清理操作。
4.原子性:在多线程环境下,引用计数的增减操作是原子性的,确保了线程安全。

shared_ptr除了有一个指针,指向所管理数据的地址,还有一个指针指向一个控制块的地址,里面存放了所管理数据的数量(常说的引用计数)、weak_ptr 的数量、删除器、分配器等。

shared_ptr 是一个模板类,所以对于shared_ptrT的并发操作的安全性,也会被纳入讨论范围。因此造成了探讨其线程安全性问题上的复杂性。

手写 shared_ptr

实现一个简单的 shared_ptr类,主要两个成员变量:
1.指向对象的指针:用于保存要管理的对象的地址。
2.引用计数:用于记录当前有多少个shared_ptr共享同一个对象,引用计数需要使用指针来在多个shared_ptr 对象之间共享计数,实际上比这个复杂,一般是一个类对象,内部包含多个引用计数相关的信息。

template<typename T>
class myShared_ptr {
public:
    //use_count初始化的意思是,如果初始化shared_ptr的是一个空指针,那么开始引用计数就是0。如果是一个有效指针,那么开始引用计数就是1
    // 构造函数要设计成explicit的,防止隐式的类类型转换,因为普通指针是不能直接赋值给智能指针的。必须使用直接初始化(用括号)的方式才行。
    explicit myShared_ptr(T* ptr = nullptr):use_count(ptr == nullptr?nullptr:new int (1)),_ptr(ptr){}
    
    // 在拷贝构造函数中,将指向对象的指针和引用计数成员变量复制到新对象,并递增引用计数。
    myShared_ptr(const myShared_ptr& other_ptr):use_count(other_ptr.use_count),_ptr(other_ptr._ptr)
    {
        if(use_count != nullptr)//防止用空指针拷贝构造
        {
            (*use_count) ++;
        }
    }
	// 在拷贝赋值运算符中,处理自我赋值情况并更新引用计数
    myShared_ptr& operator=(const myShared_ptr& other_ptr)
    {
        if(&other_ptr != this)//防止自身赋值,自身赋值的话就不用管,引用计数就不用加一了
        {
            release();//先将自身计数减一
            _ptr = other_ptr._ptr;
            use_count = other_ptr.use_count;
            //(*use_count) ++;//这里要注意不能直接++,防止other_ptr是空指针
            if(use_count)
                ++ (*use_count);
        }
        return *this;
    }

    ~ myShared_ptr()
    {
        release();
    }


    T& operator*()const//重载解引用运算符
    {
        return *_ptr;//返回被管理对象的引用
    }

    T* operator->()const//重载->运算符
    {
        return _ptr;
    }

    T* get()const//和->一样,返回的是指针
    {
        return _ptr;
    }

    int get_use_count() const
    {
        return use_count != nullptr?(*use_count):0;//如果use_count是空指针的话,就说明没有引用计数,说明shared_ptr是空的,就返回0
    }

private:
    int* use_count;//引用计数
    T* _ptr;//指向所管理的对象
	
	// 析构函数:在析构函数中处理引用计数的递减和内存的释放。
    void release()
    {
        if(use_count && -- (*use_count) == 0)
        {
            delete use_count;
            delete _ptr;
        }

    }
};

weak_ptr

std::weak_ptr是C++ 11引入的一种弱引用智能指针,它不拥有所指向对象的所有权,而是对shared_ptr持有的对象提供一种非拥有但可观察的访问方式。
weak_ptr 主要用于打破共享所有权循环引用的问题,防止出现内存泄漏。

核心特性
1.不增加引用计数
当创建一个weak_ptr时,它不会增加其所指向的对象的引用计数。这意味着,即使有多个weak_ptr指向同一对象,只要没有对应的shared_ptr存在,该对象仍会在所有shared_ptr释放后被正确销毁。
2.检查有效性
通过调用weak_ptrlock()成员函数,可以获取一个指向同一对象的shared_ptr。如果此时对象已被删除,则返回的shared_ptr为空。因此,在使用weak_ptr之前通常需要先调用lock()来检查对象是否仍然有效。
3.不阻止对象析构
由于weak_ptr仅提供了对目标对象的弱引用,所以在没有活跃的shared_ptr指向该对象时,对象会被正常回收。

应用场景:
1、打破循环引用;
2、缓存与观察者模式:在设计缓存系统或者观察者模式时,某个对象可能需要知道另一个对象的状态变化,但并不希望影响该对象的生命周期。这时,可以使用weak_ptr来跟踪对象,而不增加其引用计数。

weak_ptr 手写

template<typename T>
class _weak_ptr{

friend class _shared_ptr<T>;

public:    
    _weak_ptr():use_count(new int(0)),ptr(nullptr){}

    _weak_ptr(const _weak_ptr& wp):use_count(wp?wp.use_count:new int(0)),ptr(wp.ptr){}

    _weak_ptr(const _shared_ptr<T>& sp):use_count(sp.use_count),ptr(sp._ptr){}

    _weak_ptr& operator=(const _weak_ptr& wp)
    {
        if(&wp != this)
        {
            ptr = wp.ptr;
            use_count = wp.use_count;
        }
        return *this;
    }

    _weak_ptr& operator=(const _shared_ptr<T>& sp)
    {
        ptr = sp._ptr;
        use_count = sp.use_count;
        return *this;
    }
    T* operator->()
    {
        return ptr;
    }
    T& operator*()
    {
        return *ptr;
    }
    int get_use_count()
    {
        //return *use_count;
        return use_count == nullptr?0:(*use_count);
    }

    bool expire()//根据use——count是否为零来判断
    {
        return use_count == nullptr || *(use_count) == 0;
    }

    _shared_ptr<T>& lock()
    {
        if(expire())//如果expire为true,就返回一个空的shared——ptr指针
            return _shared_ptr<T>();
        return _shared_ptr<T>(*this);
    }

    void reset()//将weak_ptr置为空
    {
        ptr = nullptr;
        use_count = nullptr;
    }
private:
    int* use_count;
    T* ptr;
};

智能指针的缺点以及智能指针引发的问题

1.性能开销:智能指针需要进行额外的内存管理和引用计数操作,这可能会导致程序的性能下降。相比于原始指针,智能指针需要更多的计算资源和时间来完成内存管理任务。
2.循环引用:如果智能指针被用于管理对象之间的循环引用,就可能会出现内存泄漏的问题。当两个对象相互引用时,它们的引用计数永远不会达到零,因此它们的内存也永远不会被释放。
3.难以调试:由于智能指针管理的内存是自动分配和释放的,因此在程序运行时,很难确定哪个指针指向哪个内存块,以及哪个指针可能导致内存泄漏或悬挂指针等问题。这使得调试非常困难。
4.不适用于某些场景:智能指针通常适用于单线程环境,但在某些多线程或异步环境中,智能指针的使用可能会导致竞态条件或死锁等问题。此外,智能指针也不适用于需要在不同的进程之间共享内存的场景。

当我们谈论shared_ptr的线程安全性时,我们在谈论什么?
深入理解shared_ptr与weak_ptr之手写
智能指针有什么不足之处?
C++ 11新特性之week_ptr
C++ 智能指针unique_ptr原理与自定义实现

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明原文出处。如若内容造成侵权/违法违规/事实不符,请联系SD编程学习网:675289112@qq.com进行投诉反馈,一经查实,立即删除!