欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 汽车 > 新车 > c++智能指针

c++智能指针

2025/2/6 22:47:57 来源:https://blog.csdn.net/weixin_53425006/article/details/145418024  浏览:    关键词:c++智能指针

RAII

RAII是一种编程技术,要求资源的生命周期与持有该资源的对象的生命周期严格绑定。其核心思想是:

  • 资源获取:在对象构造时获取资源。
  • 资源释放:在对象析构时自动释放资源。

RAII的优点在于确保资源在对象生命周期结束时自动释放,避免资源泄漏。智能指针的设计正是基于RAII理念。

RAII的实际应用

RAII不仅限于内存管理,还可应用于文件句柄、锁等资源管理。例如:

class FileHandle {
public:FileHandle(const char* filename) {file = fopen(filename, "r");if (!file) throw std::runtime_error("Failed to open file");}~FileHandle() {if (file) fclose(file);}// 提供对文件的操作接口 
private:FILE* file;
};

智能指针简介历史

智能指针是一种特殊的类模板,用于管理动态内存资源。它通过封装原始指针,并在其生命周期内自动管理内存释放,从而避免了手动内存管理的复杂性和潜在错误(如内存泄漏、双重释放等)。智能指针的设计基于RAII理念,确保资源在对象生命周期结束时自动释放。

在智能指针发展的过程中,C++98的auto_ptr:C++98标准引入了auto_ptr,这是智能指针的早期实现。auto_ptr的设计目的是在对象被销毁时自动删除它所指向的内存。然而,auto_ptr存在一些问题,如所有权转移语义可能导致意外的行为,特别是在进行复制操作时。由于这些缺陷,auto_ptr在C++11中被弃用。

由于c++更新太慢,大佬们随后在Boost库中,设计了Scoped_PtrShared_PtrWeak_Ptr这三种智能指针。这些智能指针提供了更强大和灵活的功能,以更好地管理内存。

  • Scoped_Ptr:类似于auto_ptr,但具有更严格的语义,确保同一时间只有一个Scoped_Ptr可以拥有某个对象。

  • Shared_Ptr:实现了引用计数功能,允许多个Shared_Ptr共享同一个对象。当最后一个Shared_Ptr被销毁时,它会自动删除所指向的对象。

  • Weak_Ptr:与Shared_Ptr一起使用,用于解决循环引用问题。它不会对对象的引用计数产生影响,但可以安全地检查对象是否仍然存在。

C++11及以后的智能指针:C++11标准对智能指针进行了改进,并引入了std::unique_ptrstd::shared_ptrstd::weak_ptr这三种标准库智能指针。这些智能指针提供了更好的性能、更安全的语义和更多的功能。

  • std::unique_ptr:类似于Scoped_Ptr,但提供了更多的功能和更好的性能。它确保同一时间只有一个unique_ptr可以拥有某个对象。
  • std::shared_ptr:与Boost库中的Shared_Ptr类似,实现了引用计数功能,允许多个shared_ptr共享同一个对象。
  • std::weak_ptr:与Boost库中的Weak_Ptr类似,用于解决循环引用问题。

智能指针

所谓智能指针(smart pointer),就是智能的管理指针所指向的动态资源的释放。它其实是一个对象,当中存储指向动态分配(堆)对象的指针。智能指针可以很好地帮助我们,每次 new 出来的内存自动去 delete。不仅仅是这样,异常通常也会导致程序过早退出,进而让delete未得到执行。这时候智能指针就显得格外有用了,因为它们能够确保正确的销毁动态分配的对象。它们也可以用于跟踪被多用户共享的动态分配对象。

事实上,智能指针能够做的还有很多事情,例如处理线程安全(加同步锁),提供写时复制,确保协议,并且提供远程交互服务等一系列类似于“前后需要呼应”起来的操作(如new/delete、malloc/free、fopen/fclose、线程安全的lock/unlock等)。其实智能指针只是怕你忘了写上delete、free、fclose…,而专门设置出来的一个对象。所以它是有很强的实用性。

现在我们来看看智能指针的使用。首先一个智能指针至少要符合两点:

  1. 拥有RAII的机制来管理指针。对于编译器来说,智能指针实际上是一个栈对象,并非指针类型,在栈对象生命期即将结束时,智能指针通过析构函数释放它管理的堆内存。
  2. 有指针的基本功能。所有智能指针都重载了“*”和“->”操作符,让它可以直接返回对象的引用以及能用->去操作其指向的对象。若要访问智能指针原来的方法则使用“·”操作符。
  3. 其次就是它还需要考虑深浅拷贝问题

现在就可以来模拟标准库中的智能指针实现一种简单的智能指针auto_ptr

auto_ptr

namespace jt
{template<class T>class auto_ptr{public:auto_ptr(T* ptr = nullptr):_ptr(ptr){}auto_ptr(auto_ptr<T>& ap):_ptr(ap._ptr){ap._ptr = nullptr; }auto_ptr& operator=(auto_ptr<T>& ap){if(*this != ap){delete _ptr;_ptr = ap._ptr;ap._ptr = nullptr;}return *this;}~auto_ptr(){if(_ptr) delete _ptr;}T* operator->(){return _ptr;}T& operator*(){return *_ptr;}private:T* _ptr;};
}int main()
{auto_ptr<int> p (new int);auto_ptr<int> p2(p);* p = 2; //error
}

auto_ptr缺陷

可以看出auto_ptr的缺陷是在于其所有权转移语义。当一个auto_ptr对象被赋值给另一个auto_ptr对象时,它会自动将其所拥有的对象的所有权转移给后者,同时使前者失效。这种设计初衷是为了简化内存管理,但在实际使用中却带来了很多问题。

首先,这种所有权转移语义使得auto_ptr对象在赋值操作后变得不可预测。由于赋值后原对象失效,任何对原对象的后续操作都将导致未定义行为。这使得代码更加难以理解和维护,也增加了出错的可能性。

其次,所有权转移语义使得auto_ptr无法与标准库容器(如vectorlist等)一起使用。因为容器在复制或重新分配内存时需要对元素进行复制或移动操作,而auto_ptr的所有权转移语义会导致在复制或移动过程中原对象失效,从而引发错误。

不支持引用计数和共享所有权,即一个对象只能由一个auto_ptr拥有。

不支持数组类型只支持指向单个对象的指针,而不支持指向数组的指针。这是因为auto_ptr在析构时会调用delete操作符来释放内存,而对于数组则需要使用delete[]操作符。

总结缺陷:

  1. 所有权转移:赋值操作会转移所有权,导致原对象失效。
  2. 不支持容器:无法作为标准容器的元素使用。
  3. 不支持数组:不能管理动态数组。
  4. 缺乏异常安全性:构造或赋值过程中抛出异常可能导致内存泄漏。

unique_ptr(scoped_ptr)

auto_ptr存在严重的缺陷,所以auto_ptr通常是被严禁使用的。但是从C++98到C++11出来期间,C++标准库却迟迟没有给出改良版的智能指针,但是在此期间有一群大牛在叫一个boost的社区讨论造一个第三方库,好去解决C++中的一些诟病同时对c++进行一些完善。这里的scoped_ptr就是其中的一个杰作(后来在C++11出来之后,c++11参考boost版本将scoped_ptr改名为unique_ptr)。

它的思想也是特别的直接,它做的就是暴力的防止用户调用拷贝构造和赋值运算符“=”进行夺权管理。它直接将拷贝构造和“=”运算符重载函数给封装起来,以此防止用户来调用或恶意的去实现它。

namespace jt {template<class T, class D = std::default_delete<T>>class unique_ptr {public:explicit unique_ptr(T* ptr = nullptr) : _ptr(ptr) {}~unique_ptr() { if (_ptr) D{}(_ptr); }unique_ptr(unique_ptr&& up) : _ptr(up.release())  {}unique_ptr& operator=(unique_ptr&& up) {if (this != &up) {reset(up.release()); }return *this;}T* release() { T* tmp = _ptr; _ptr = nullptr; return tmp; }void reset(T* ptr = nullptr) {if (_ptr) D{}(_ptr);_ptr = ptr;}T* operator->() { return _ptr; }T& operator*() { return *_ptr; }private:T* _ptr;};
}
特点
  • 独占所有权:确保同一时间只有一个unique_ptr拥有资源。
  • 高效:无引用计数开销。
  • 支持移动语义:允许通过右值引用转移所有权。
  • 自定义删除器:支持非标准的内存释放逻辑。

shared_ptr

特点
  • 共享所有权:多个shared_ptr实例可以共享同一资源。
  • 引用计数管理:通过引用计数控制资源生命周期。
  • 线程安全:使用原子操作确保引用计数线程安全。
  • 支持弱引用:配合weak_ptr解决循环引用问题。
namespace jt {template<class T>class shared_ptr {public:explicit shared_ptr(T* p = nullptr) : _ptr(p), _refCount(new size_t(1)) {}~shared_ptr() { release(); }shared_ptr(const shared_ptr& other) : _ptr(other._ptr), _refCount(other._refCount) { incrementRefCount(); }shared_ptr& operator=(const shared_ptr& other) {if (this != &other) {release();_ptr = other._ptr;_refCount = other._refCount;incrementRefCount();}return *this;}void release() {if (--*_refCount == 0) {delete _ptr;delete _refCount;}}T* operator->() { return _ptr; }T& operator*() { return *_ptr; }private:void incrementRefCount() { ++*_refCount; }T* _ptr;size_t* _refCount;};
}

shared_ptr添加了引用计数,所以在它的内部就不会出现调用多次析构函数导致的程序崩溃,shared_ptrt同时也不像scoped_ptr那样没有拷贝构造和赋值运算符重载,难以被利用。它每次调用析构时,只需要将引用计数减1即可,当指向该资源的引用计数 _pRefCount变成0的时候,才真正的去释放资源。和前面一样,在boost中也有一个shared_array版本的智能指针用来管理new[]的对象数组。(C++11没有这个版本)。

shared_ptr有一个缺陷,就是不能解决循环引用的问题。我们先来看看是什么是循环引用。

循环引用

在C++中,循环引用通常指的是两个或多个对象相互持有对方的指针或引用,从而形成一个循环的依赖关系。然而,C++的原生指针并不直接支持循环引用检测,因为这通常是垃圾收集器(如Java或C#中的)所关注的问题。但在C++中,当使用智能指针(如std::shared_ptr)时,循环引用可能会导致资源无法正确释放,因为std::shared_ptr会追踪引用计数,只有当引用计数为0时才会删除所指向的对象。

struct ListNode
{int _data;shared_ptr<ListNode> _prev;shared_ptr<ListNode> _next;
}
int main()
{shared_ptr<ListNode> node1 (new ListNode);shared_ptr<ListNode> node2 (new ListNode);node1 -> _next = node2;node2 -> _prev = node1;
}
  1. node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
  2. node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
  3. node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
  4. 也就是说_next析构了,node2就释放了。也就是说_prev析构了,node1就释放了。
  5. 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放

为了解决循环引用这个问题,引入了weak_ptr。

weak_ptr

weak_ptr是为了配合shared_ptr而引入的一种智能指针,它更像是shared_ptr的一个助手而不是智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况.具体一点讲就是,首先weak_ptr 是专门为shared_ptr 而准备的。

它是 boost::shared_ptr 的观察者对象,作为观察者,那也就意味着weak_ptr它只对shared_ptr 进行引用却不会去改变其引用计数,当被观察的shared_ptr 失效后,相应的weak_ptr 也相应失效,间接地解决了循环引用的问题,当然week_ptr指针的功能不是只有这一个。但是现在我们只要知道它可以解决循环引用就好。

namespace jt {template<class T>class weak_ptr {public:weak_ptr(const shared_ptr<T>& sp) : _ptr(sp.get()),  _refCount(sp.use_count())  {}~weak_ptr() {}bool is_expired() const { return *_refCount == 0; }shared_ptr<T> lock() {if (is_expired()) return shared_ptr<T>();return shared_ptr<T>(_ptr, _refCount);}private:T* _ptr;size_t* _refCount;};
}
特点
  • 弱引用:不增加引用计数,避免循环引用。
  • 检查有效性:通过is_expired()检查资源是否有效。
  • 锁定资源:通过lock()方法获取强引用。

shared_ptr是线程安全的吗?

引用计数的线程安全性

C++ 标准规定,对单个 std::shared_ptr 的引用计数操作的修改是线程安全的(例如,std::shared_ptr 的复制、赋值、析构等操作)。这意味着,如果你有两个线程,每个线程都持有一个指向同一对象的 shared_ptr,并且这两个线程都试图销毁或重新分配它们的 shared_ptr,那么引用计数会被正确地更新,不会出现数据损坏。

然而,这并不意味着你可以安全地在多线程环境中随意使用 std::shared_ptr。例如,如果你有两个线程,每个线程都试图通过 std::shared_ptr 访问它所指向的对象的方法或成员,并且这些方法或成员不是线程安全的,那么你的程序可能会出现问题。

对象的线程安全性

std::shared_ptr 仅仅是一个智能指针,它负责管理对象的生命周期。但是,它并不负责确保对象本身的线程安全性。如果你通过 std::shared_ptr 访问的对象不是线程安全的,那么你的程序可能会出现问题。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com