• 【重学C++】02 脱离指针陷阱:深入浅出 C++ 智能指针


    文章首发

    【重学C++】02 脱离指针陷阱:深入浅出 C++ 智能指针

    前言

    大家好,今天是【重学C++】系列的第二讲,我们来聊聊C++的智能指针。

    为什么需要智能指针

    在上一讲《01 C++如何进行内存资源管理》中,提到了对于堆上的内存资源,需要我们手动分配和释放。管理这些资源是个技术活,一不小心,就会导致内存泄漏。

    我们再给两段代码,切身体验下原生指针管理内存的噩梦。

    void foo(int n) {
        int* ptr = new int(42);
        ...
        if (n > 5) {
    	    return;
        }
        ...
        delete ptr;
    }
    
    
    void other_fn(int* ptr) {
    	...
    };
    void bar() {
        int* ptr = new int(42);
        other_fn(ptr);
        // ptr == ?
    }
    
    

    foo函数中,如果入参n > 5, 则会导致指针ptr的内存未被正确释放,从而导致内存泄漏。

    bar函数中,我们将指针ptr传递给了另外一个函数other_fn,我们无法确定other_fn有没有释放ptr内存,如果被释放了,那ptr将成为一个悬空指针,bar在后续还继续访问它,会引发未定义行为,可能导致程序崩溃。

    上面由于原生指针使用不当导致的内存泄漏、悬空指针问题都可以通过智能指针来轻松避免。

    C++智能指针是一种用于管理动态分配内存的指针类。基于RAII设计理念,通过封装原生指针实现的。可以在资源(原生指针对应的对象)生命周期结束时自动释放内存。

    C++标准库中,提供了两种最常见的智能指针类型,分别是std::unique_ptrstd::shared_ptr
    接下来我们分别详细展开介绍。

    吃独食的unique_ptr

    std::unique_ptr 是 C++11 引入的智能指针,用于管理动态分配的内存。每个 std::unique_ptr 实例都拥有对其所包含对象的唯一所有权,并在其生命周期结束时自动释放对象。

    创建unique_ptr对象

    我们可以std::unique_ptr的构造函数或std::make_unique函数(C++14支持)来创建一个unique_ptr对象,在超出作用域时,会自动释放所管理的对象内存。示例代码如下:

    #include 
    #include 
    class MyClass {
    public:
        MyClass() {
            std::cout << "MyClass constructed" << std::endl;
        }
    
        ~MyClass() {
            std::cout << "MyClass destroyed" << std::endl;
        }
    };
    int main() {
    	std::unique_ptr ptr1(new MyClass);
    	// C++14开始支持std::make_unique
        std::unique_ptr<int> ptr2 = std::make_unique<int>(10);
        return 0;
    }
    

    代码输出:

    MyClass constructed
    MyClass destroyed
    

    访问所管理的对象

    我们可以像使用原生指针的方式一样,访问unique_ptr所指向的对象。也可以通过get函数获取到原生指针。

    MyClass* naked_ptr = ptr1.get();
    std::cout << *ptr2 << std::endl; // 输出 10
    
    

    释放/重置所管理的对象

    使用reset函数可以释放unique_ptr所管理的对象,并将其指针重置为nullptr或指定的新指针。reset`大概实现原理如下

    template<class T> 
    void unique_ptr::reset(pointer ptr = pointer()) noexcept { 
    	// 释放指针指向的对象
    	delete ptr_; 
    	// 重置指针
    	ptr_ = ptr;
    }
    

    该函数主要完成两件事:

    1. 释放 std::unique_ptr 所管理的对象,以避免内存泄漏。
    2. std::unique_ptr 重置为nullptr或管理另一个对象。

    code show time:

    #include 
    #include 
    
    class MyClass {
    public:
        MyClass() {
            std::cout << "MyClass constructed" << std::endl;
        }
    
        ~MyClass() {
            std::cout << "MyClass destroyed" << std::endl;
        }
    };
    
    int main() {
        // 创建一个 std::unique_ptr 对象,指向一个 MyClass 对象
        std::unique_ptr ptr(new MyClass);
    
        // 调用 reset,将 std::unique_ptr 重置为管理另一个 MyClass 对象
        ptr.reset(new MyClass);
        return;
    }
    
    

    移动所有权

    一个对象资源只能同时被一个unique_ptr管理。当尝试把一个unique_ptr直接赋值给另外一个unique_ptr会编译报错。

    #include 
    int main() {
        std::unique_ptr<int> p1 = std::make_unique<int>(42);
        std::unique_ptr<int> p2 = p1; // 编译报错
        return 0;
    }
    

    为了把一个 std::unique_ptr 对象的所有权移动到另一个对象中,我们必须配合std::move移动函数。

    #include 
    #include 
    int main() {
        std::unique_ptr<int> p1 = std::make_unique<int>(42);
        std::unique_ptr<int> p2 = std::move(p1); // ok
        std::cout << *p2 << std::endl; // 42
        std::cout << (p1.get() == nullptr) << std::endl; // true
        return 0;
    }
    

    这个例子中, 我们把p1通过std::move将其管理对象的所有权转移给了p2, 此时p2接管了对象,而p1不再拥有管理对象的所有权,即无法再操作到该对象了。

    乐于分享的shared_ptr

    shared_ptr是C++11提供的另外一种常见的智能指针,与unique_ptr独占对象方式不同,shared_ptr是一种共享式智能指针,允许多个shared_ptr指针共同拥有同一个对象,采用引用计数的方式来管理对象的生命周期。当所有的 shared_ptr 对象都销毁时,才会自动释放所管理的对象。

    创建shared_ptr对象

    同样的,C++也提供了std::shared_ptr构造函数和std::make_shared函数来创建std::shared_ptr对象。

    #include 
    int main() {
    	std::shared_ptr<int> p1(new int(10));
    	std::shared_ptr<int> p2 = std::make_shared<int>(20);
    	return;
    }
    

    多个shared_ptr共享一个对象

    可以通过赋值操作实现多个shared_ptr共享一个资源对象,例如

    std::shared_ptr<int>p3 = p2;
    

    shared_ptr采用引用计数的方式管理资源对象的生命周期,通过分配一个额外内存当计数器。

    当一个新的shared_ptr被创建时,它对应的计数器被初始化为1。每当赋值给另外一个shared_ptr共享同一个对象时,计数器值会加1。当某个shared_ptr被销毁时,计数值会减1,当计数值变为0时,说明没有任何shared_ptr引用这个对象,会将对象进行回收。
    image.png

    C++提供了use_count函数来获取std::shared_ptr所管理对象的引用计数,例如

    std::cout << "p1 use count: " << p1.use_count() << std::endl;
    

    释放/重置所管理的对象

    可以使用reset函数来释放/重置shared_ptr所管理的对象。大概实现原理如下(不考虑并发场景)

    void reset(T* ptr = nullptr) {
    	if (ref_count != nullptr) { 
    		(*ref_count)--;
    		if (*ref_count == 0) { 
    			delete data; 
    			delete ref_count; 
    		} 
    	} 
    	data = ptr; 
    	ref_count = (data == nullptr) ? nullptr : new size_t(1); 
    }
    
    

    data指针来存储管理的资源,指针ref_count 来存储计数器的值。

    在 reset 方法中,需要减少计数器的值,如果计数器减少后为 0,则需要释放管理的资源,如果减少后不为0,则不会释放之前的资源对象。

    如果reset指定了新的资源指针,则需要重新设置 data 和 ref_count,并将计数器初始化为 1。否则,将计数器指针置为nullptr

    shared_ptr使用注意事项

    避免循环引用

    由于 shared_ptr 具有共享同一个资源对象的能力,因此容易出现循环引用的情况。例如:

    struct Node { 
    	std::shared_ptr next; 
    };
    
    int main() {
    	std::shared_ptr node1(new Node);
    	std::shared_ptr node2(new Node); 
    	node1->next = node2; 
    	node2->next = node1;
    }
    

    在上述代码中,node1node2 互相引用,在析构时会发现计数器的值不为0,不会释放所管理的对象,产生内存泄漏。

    为了避免循环引用,可以将其中一个指针改为 weak_ptr 类型。weak_ptr也是一种智能指针,通常配合shared_ptr一起使用。

    weak_ptr是一种弱引用,不对所指向的对象进行计数引用,也就是说,不增加所指对象的引用计数。当所有的shared_ptr都析构了,不再指向该资源时,该资源会被销毁,同时对应的所有weak_ptr都会变成nullptr,这时我们就可以利用expired()方法来判断这个weak_ptr是否已经失效。

    我们可以通过weak_ptrlock()方法来获得一个指向共享对象的shared_ptr。如果weak_ptr已经失效,lock()方法将返回一个空的shared_ptr

    下面是weak_ptr的基本使用示例:

    #include 
    #include 
    
    int main() {
        std::shared_ptr<int> sp = std::make_shared<int>(42);
        // 创建shared_ptr对应的weak_ptr指针
        std::weak_ptr<int> wp(sp);
    
    	// 通过lock创建一个对应的shared_ptr
        if (auto p = wp.lock()) {
            std::cout << "shared_ptr value: " << *p << std::endl;
            std::cout << "shared_ptr use_count: " << p.use_count() << std::endl;
        } else {
            std::cout << "wp is expired" << std::endl;
        }
    
    	// 释放shared_ptr指向的资源,此时weak_ptr失效
        sp.reset();
        std::cout << "wp is expired: " <<  wp.expired() << std::endl;
        return 0;
    }
    

    代码输出如下

    shared_ptr value: 42
    shared_ptr use_count: 2
    wp is expired: 1
    

    回到shared_ptr的循环引用问题,利用weak_ptr不会增加shared_ptr的引用计数的特点,我们将Node.next的类型改为weak_ptr, 避免node1和node2互相循环引用。修改后代码如下

    ```cpp
    struct Node { 
    	std::weak_ptr next; 
    };
    
    int main() {
    	std::shared_ptr node1(new Node);
    	std::shared_ptr node2(new Node); 
    	node1->next = std::weak_ptr(node2); 
    	node2->next = std::weak_ptr(node1); ;
    }
    

    避免裸指针与shared_ptr混用

    先看看以下代码

    int* q = new int(9);
    {
    	std::shared_ptr<int> p(new int(10));
    	...
    	q = p.get();
    }
    std::cout << *q << std::endl;
    

    get函数返回 std::shared_ptr 所持有的指针,但是不会增加引用计数。所以在shared_ptr析构时,将该指针指向的对象给释放掉了,导致指针q变成一个悬空指针。

    避免一个原始指针初始化多个shared_ptr

    int* p = new int(10);
    std::shared_ptr<int> ptr1(p);
    // error: 两个shared_ptr指向同一个资源,会导致重复释放
    std::shared_ptr<int> ptr2(p);
    

    总结

    避免手动管理内存带来的繁琐和容易出错的问题。我们今天介绍了三种智能指针:unique_ptrshared_ptrweak_ptr
    每种智能指针都有各自的使用场景。unique_ptr用于管理独占式所有权的对象,它不能拷贝但可以移动,是最轻量级和最快的智能指针。shared_ptr用于管理多个对象共享所有权的情况,它可以拷贝和移动。weak_ptr则是用来解决shared_ptr循环引用的问题。

    下一节,我们将自己动手,从零实现一个C++智能指针。敬请期待

  • 相关阅读:
    1-10嵌入式Linux系统开发与应用|嵌入式Linux|第三章 Linux编程环境
    Jaeger系统实现对Harbor的链路追踪
    Kotlin 委托
    JVM技术文档--JVM优化思路以及问题定位--JVM可调整参数汇总
    【后端】韩顺平Java学习笔记(入门篇)
    【主题广范|见刊快】2024年城市工程与建筑规划国际会议(ICURAP 2024)
    10.第十部分 Scrapy框架
    前端项目使用钉钉字体
    [NOIP2001 提高组] 一元三次方程求解
    https转到http的nginx配置参考
  • 原文地址:https://www.cnblogs.com/huiwancode/p/17417709.html