当前位置:首页 » 《资源分享》 » 正文

【C++】智能指针详解

22 人参与  2024年12月27日 16:01  分类 : 《资源分享》  评论

点击全文阅读


?个人主页?:孤寂大仙V
?收录专栏?:C++从小白到高手
?往期回顾?:C++异常
?流水不争,争的是滔滔不


一、智能指针简介二、为什么要用智能指针三、 RAII和智能指针auto_ptrunique_ptrshared_ptrshared_ptr的循环引用与weak_ptr 四、删除器

一、智能指针简介

智能指针是C++标准库中的一个重要概念,主要用于管理动态分配内存的对象。与传统指针不同,智能指针能够自动管理内存的分配和释放,从而减少内存泄漏和其他内存相关错误的风险。C++中主要有三种智能指针:std::unique_ptrstd::shared_ptrstd::weak_ptr

二、为什么要用智能指针

像前面抛异常的捕获try、catch就非常容易造成内存泄漏。

#include <iostream>#include <stdexcept>void causeMemoryLeak() {    int* ptr = new int(42); // 动态分配内存    // 模拟抛出异常    throw std::runtime_error("Something went wrong!");    // 此处如果异常未抛出,应该释放内存    delete ptr;}int main() {    try {        causeMemoryLeak();    } catch (const std::exception& e) {        std::cout << "Caught exception: " << e.what() << std::endl;        // 这里 ptr 的内存没有被释放,造成内存泄漏    }    return 0;}

前面的C++异常中聊过,当throw执行时,throw后面的语句将不再被执行。所以当这里抛出runtime_error异常后,导致delete per被跳过。造成了内存泄漏。虽然我们可以通过对代码进行优化,以防止出现这种情况。但是如果用智能指针来对资源进行管理就会非常的方便。
C++98提供了std::auto_ptr
C++11提供了std::unique_ptrstd::shared_ptrstd::weak_ptr

三、 RAII和智能指针

RAII是Resource Acquisition Is Initialization(资源获取即初始化)的缩写,他是⼀种管理资源的类的设计思想,本质是⼀种利用对象生命周期来管理获取到的动态资源,避免资源泄漏,这里的资源可以是内存、文件指针、网络连接、互斥锁等等。RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问,资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。

#include <iostream>#include <memory>class Resource {public:    Resource() { std::cout << "Resource acquired." << std::endl; }    ~Resource() { std::cout << "Resource released." << std::endl; }};void useResource() {    std::unique_ptr<Resource> resPtr(new Resource()); // RAII    // 可以在这里使用 resPtr        // 当函数结束,resPtr 超出作用域时,Resource 的析构函数会被调用}int main() {    try {        useResource();    } catch (const std::exception& e) {        std::cout << "Exception caught: " << e.what() << std::endl;    }    return 0; // 此时 Resource 的内存已经被释放,无需手动 delete}

auto_ptr

auto_ptr是C++98时设计出来的智能指针,他的特点是拷贝时把被拷贝对象的资源的管理权转移给拷贝对象,这是⼀个非常糟糕的设计,因为他会到被拷贝对象悬空,访问报错的问题,C++11设计出新的智能指针后,强烈建议不要使用auto_ptr。其他C++11出来之前很多公司也是明令禁止使用这个智能指针的。

int main(){auto_ptr<int> p1(new int(1));auto_ptr<int> p2(p1);return 0;}

在这里插入图片描述
在这里插入图片描述

p2拷贝p1,p1的资源管理权给了p2。p1悬空,此时p1是空指针。如果这时访问p1程序就挂了。所以auto_ptr是一个非常糟糕的设计。
在这里插入图片描述

auto_ptr的简单模拟实现

namespace hbx{template<class T>class auto_ptr{public://构造auto_ptr(T* ptr):_ptr(ptr){}//拷贝构造auto_ptr(auto_ptr<T>& ap):_ptr(ap._ptr){ap._ptr=nullptr;//指针置为空,管理权转换}//赋值重载auto_ptr<T>& operator=(const auto_ptr<T>& ap){if (this != &ap){if (_ptr){delete _ptr;//释放被赋值对象的资源,这个对象是已经存在的被new出来的。}_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;};}

unique_ptr

unique_ptr是C++11设计出来的智能指针,他的名字翻译出来是唯一指针,他的特点的不支持拷贝只支持移动。如果不需要拷贝的场景就非常建议使用他。不允许左值赋值操作,可以通过move使左值转化为右值。移动后,移动的对象的指针也悬空,要谨慎使用。

int main(){unique_ptr<Date> u1(new Date(1, 1, 1));unique_ptr<Date> u2(move(u1));//不支持拷贝,支持移动。但是也要谨慎使用u1照样悬空。return 0;}

跟auto_ptr主要区别是,用unique_ptr那么程序员是知道移动对象是悬空的情况下使用。
在这里插入图片描述
unique_ptr的简单模拟实现

namespace hbx{template<class T>class unique_ptr{public://构造unique_ptr(T* ptr):_ptr(ptr){}//析构~unique_ptr(){if (_ptr){delete _ptr;}}//拷贝构造unique_ptr(const unique_ptr<T>& up) = delete;//复制重载unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;//移动构造unique_ptr(unique_ptr<T>&& up):_ptr(up._ptr){up._ptr = nullptr;}//移动赋值unique_ptr<T>& operator=(unique_ptr<T>&& up){if (_ptr){delete _ptr;}_ptr = up.ptr;up.ptr = nullptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};}int main(){hbx::unique_ptr<Date> u1(new Date(1, 1, 1));hbx::unique_ptr<Date> u2(move(u1));return 0;}

shared_ptr

shared_ptr是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是支持拷贝,也支持移动。如果需要拷贝的场景就需要使用他了。底层是用引用计数的方式实现的。

shared_ptr当复制或则拷贝的时候,引用计数+1,当智能指针析构的时候引用计数-1。如果引用计数为0,那么这块内存就没有资源了就释放它。
在这里插入图片描述
在这里插入图片描述

int main(){shared_ptr<Date> sp1(new Date(1, 1, 1));shared_ptr<Date> sp2(sp1);//use_count():引用计数cout << sp2.use_count() << endl;shared_ptr<Date> sp3(new Date(2, 2, 2));cout << sp3.use_count() << endl;}

shared_ptr的简单模拟实现

namespace hbx{template<class T>class shared_ptr{public://构造shared_ptr(T* ptr):_ptr(ptr),_pcount(new int(1)){}//析构~shared_ptr(){if (--(*_pcount) == 0){delete _ptr;delete _pcount;}}//拷贝构造shared_ptr(const shared_ptr<T>& ps):_ptr(ps._ptr), _pcount(ps._pcount){++(*_pcount);}//赋值重载shared_ptr<T>& operator=(const shared_ptr<T>& ps){if (--(*_pcount) == 0){delete _ptr;delete _pcount;}_ptr = ps._ptr;_pcount = ps._pcount;++(*_pcount);}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;int* _pcount;};}

shared_ptr 除了支持用指向资源的指针构造,还支持 make_shared 用初始化资源对象的值直接构造。

#include <iostream>#include <memory>class MyClass {public:    MyClass(int num) : data(num) {        std::cout << "MyClass constructor called" << std::endl;    }    void printData() const {        std::cout << "Data: " << data << std::endl;    }private:    int data;};int main() {    // 使用make_shared直接初始化资源对象的值来构造shared_ptr    std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>(10);    ptr->printData();    return 0;}

shared_ptr 和 unique_ptr 都支持了operator bool的类型转换,如果智能指针对象是⼀个空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否为空。

#include <iostream>#include <memory>class MyClass {public:    MyClass(int num) : data(num) {}    void printData() const     {        std::cout << "Data: " << data << std::endl;    }private:    int data;};int main() {    // shared_ptr使用示例    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(20);    if (ptr1)     {        ptr1->printData();    }     else     {        std::cout << "ptr1 is empty" << std::endl;    }    std::shared_ptr<MyClass> ptr2;    if (ptr2)     {        ptr2->printData();    }     else     {        std::cout << "ptr2 is empty" << std::endl;    }    // unique_ptr使用示例    std::unique_ptr<MyClass> uptr1 = std::make_unique<MyClass>(30);    if (uptr1)    {        uptr1->printData();    }     else     {        std::cout << "uptr1 is empty" << std::endl;    }    std::unique_ptr<MyClass> uptr2;    if (uptr2)     {        uptr2->printData();    }     else     {        std::cout << "uptr2 is empty" << std::endl;    }    return 0;}

shared_ptr 和 unique_ptr 都得构造函数都使用explicit 修饰,防止普通指针隐式类型转换成智能指针对象。

#include <iostream>#include <memory>class MyClass {public:    MyClass(int num) : data(num) {}    void printData() const {        std::cout << "Data: " << data << std::endl;    }private:    int data;};int main() {    // shared_ptr使用示例    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(20);    if (ptr1) {        ptr1->printData();    } else {        std::cout << "ptr1 is empty" << std::endl;    }    std::shared_ptr<MyClass> ptr2;    if (ptr2) {        ptr2->printData();    } else {        std::cout << "ptr2 is empty" << std::endl;    }    // unique_ptr使用示例    std::unique_ptr<MyClass> uptr1 = std::make_unique<MyClass>(30);    if (uptr1) {        uptr1->printData();    } else {        std::cout << "uptr1 is empty" << std::endl;    }    std::unique_ptr<MyClass> uptr2;    if (uptr2) {        uptr2->printData();    } else {        std::cout << "uptr2 is empty" << std::endl;    }    return 0;}

shared_ptr的循环引用与weak_ptr

weak_ptr是C++11设计出来的智能指针,他的名字翻译出来是弱指针,他完全不同于上面的智能指针,他不支持RAII,也就意味着不能用它直接管理资源,weak_ptr的产生本质是要解决shared_ptr的⼀个循环引用导致内存泄漏的问题

hared_ptr大多数情况下管理资源非常合适,支持RAII,也支持拷贝。但是在循环引用的场景下会导致资源没得到释放内存泄漏,所以我们要认识循环引用的场景和资源没释放的原因,并且学会使用weak_ptr解决这种问题。

右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放了。_next什么时候析构呢,_next是左边节点的的成员,左边节点释放,_next就析构了。左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释放了。_prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了。
• 至此逻辑上成功形成回旋镖似的循环引用,谁都不会释放就形成了循环引用,导致内存泄漏
• 把ListNode结构体中的_next和_prev改成weak_ptr,weak_ptr绑定到shared_ptr时不会增加它的引用计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引用,解决了这里的问题
在这里插入图片描述
struct ListNode{int _data;std::shared_ptr<ListNode> _next;std::shared_ptr<ListNode> _prev;// 这里改成weak_ptr,当n1->_next = n2;绑定shared_ptr时// 不增加n2的引用计数,不参与资源释放的管理,就不会形成循环引用了/*std::weak_ptr<ListNode> _next;std::weak_ptr<ListNode> _prev;*/~ListNode(){cout << "~ListNode()" << endl;}};int main(){// 循环引用 -- 内存泄露std::shared_ptr<ListNode> n1(new ListNode);std::shared_ptr<ListNode> n2(new ListNode);cout << n1.use_count() << endl;cout << n2.use_count() << endl;n1->_next = n2;n2->_prev = n1;cout << n1.use_count() << endl;cout << n2.use_count() << endl;// weak_ptr不支持管理资源,不支持RAII// weak_ptr是专⻔绑定shared_ptr,不增加他的引用计数,作为⼀些场景的辅助管理//std::weak_ptr<ListNode> wp(new ListNode);return 0;}

weak_ptr不支持RAII,也不支持访问资源,所以我们看文档发现weak_ptr构造时不支持绑定到资源,只支绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引用计数,那么就可以解决上述的循环引用问题。

weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果它绑定的shared_ptr已经释放了资源,那么他去访问资源就是很危险的weak_ptr支持expired检查指向的资源是否过期use_count也可获取shared_ptr的引用计数,weak_ptr想访问资源时,可以调用lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。


当weak_ptr的expired函数返回值为 0(在 C++ 中,0 通常代表false)时,这意味着weak_ptr所关联shared_ptr仍然有效,即对应的对象尚未被销毁。打印出来引用计数是2,weak_pt不增加计数但是不代表不指向计数。
在这里插入图片描述
weak_ptr所关联的share_ptr过期了
在这里插入图片描述
调用lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。

在这里插入图片描述

四、删除器

智能指针析构时默认是进行delete释放资源,这也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。智能指针支持在构造时给⼀个删除器,所谓删除器本质就是⼀个可调用对象,这个可调用对象中实现你想要的释放资源的方式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调用删除器去释放资源。
因为new[]经常使用,所以为了简洁⼀点,unique_ptr和shared_ptr都特化了⼀份[]的版本,管理new []的资源。

unique_ptr<Date[]> up1(newDate[5]);shared_ptr<Date[]> sp1(new Date[5]); 

定制删除器还有lambda版本和仿函数版本

在shared_ptr下建议使用lambda版本

class Fclose{public:    void operator()(FILE* ptr)    {        cout << "fclose:" << ptr << endl;        fclose(ptr);    }};template<class T>void DeleteArrayFunc(T* ptr){    delete[] ptr;}int main(){std::shared_ptr<Date> sp1(new Date);std::shared_ptr<Date[]> sp2(new Date[10]);//lambda版本 bit::shared_ptr<Date> sp3(new Date[10], [](Date* ptr) {delete[] ptr; }); //仿函数版本 std::shared_ptr<Date> sp4(new Date[5], DeleteArrayFunc<Date>);//文件操作//lambda版本shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {fclose(ptr);});//仿函数版本std::shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose()); }

在unique_ptr下建议使用仿函数版本

class Fclose{public:    void operator()(FILE* ptr)    {        cout << "fclose:" << ptr << endl;        fclose(ptr);    }};std::unique_ptr<Date> up1(new Date);std::unique_ptr<Date[]> up2(new Date[10]);//lambda版本auto fcloseFunc = [](FILE* ptr) {fclose(ptr); };std::unique_ptr<FILE, decltype(fcloseFunc)> up4(fopen("Test.cpp", "r"), fcloseFunc);//仿函数版本std::unique_ptr<FILE, Fclose> up3(fopen("Test.cpp", "r"));

点击全文阅读


本文链接:http://m.zhangshiyu.com/post/207801.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1