C++智能指针详解


智能指针

C++ 使用内存的时候很容易出现野指针、悬空指针、内存泄露、重复释放的问题。所以C++11引入了智能指针来管理内存。

智能指针是模板类,在栈上创建智能指针对象,把普通指针交给智能指针对象,智能指针过期时,调用析构函数释放普通指针的内存。


1. std::shared_ptr

(1) 概念

std::shared_ptr<T>是一个类模板,它能记录有多少个对象共享它管理的内存对象。多个std::shared_ptr<T>可以共享同一个对象。当最后一个std::shared_ptr<T>对象被销毁时,它会自动释放它所指向的内存。一个shared_ptr<T>指针可以通过make_shared<T>函数来创建,也可以通过拷贝或赋值另一个shared_ptr来创建。

如图所示,sp1和sp2指向同一个对象,内存对象的引用计数为2。当sp1被销毁时,引用计数减为1,sp2仍然指向该对象。当sp2被销毁时,引用计数减为0,内存对象被销毁。

在这里插入图片描述

(2) 底层原理

element_type*  _M_ptr;         			// Contained pointer
__shared_count<_Lp>  _M_refcount;    	// Reference counter

在这里插入图片描述

std::shared_ptr在内部只有两个指针成员,一个指针是所管理的数据地址;另一个指针是控制块地址,包括引用计数、weak_ptr计数、删除器(Deleter)、分配器(Allocator)。因为不同shared_ptr指针需要共享相同的内存对象,因此引用计数的存储是在堆上的。而unique_ptr只有一个指针成员,指向所管理的数据的地址。因此一个shared_ptr对象的大小是raw_pointer(裸指针)大小的两倍。

(3) 代码实现

其中成员m_refCount指向引用计数内存块,实际上atomic类型,因为要考虑多线程情况,此次只展示原理

// 手写智能指针
template<typename T>
class shared_ptr {
public:
    // 有参构造
    shared_ptr(T* ptr = nullptr) :m_ptr(ptr), m_refCount(new int(1)) {}

    // 拷贝构造
    shared_ptr(const shared_ptr& other) :m_ptr(other.m_ptr), m_refCount(other.m_refCount) {
        (*m_refCount)++;
    }

    // 析构
    ~shared_ptr() {
        // decrease the reference count
        (*m_refCount)--;
        // if the reference count is zero, delete the pointer
        if (*m_refCount == 0) {
            delete m_ptr;
            delete m_refCount;
        }
    }

    // 重载=运算符
    shared_ptr& operator=(const shared_ptr& other) {
        // check self-assignment
        if (this != &other) {
            // decrease the reference count for the old pointer
            (*m_refCount)--;
            // if the reference count is zero, delete the pointer
            if (*m_refCount == 0) {
                delete m_ptr;
                delete m_refCount;
            }
            // copy the data and reference pointer and increase the reference count
            m_ptr = other.m_ptr;
            m_refCount = other.m_refCount;
            // increase the reference count
            (*m_refCount)++;
        }
        return *this;
    }

private:
    T* m_ptr;            // 管理的数据地址
    int* m_refCount;     // 引用计数地址
};

(4) 内置方法

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

(5) 应用场景

  • 通常用于一些资源创建昂贵比较耗时的场景,比如涉及到文件读写、网络连接、数据库连接等。

  • 需要共享资源的所有权时,例如,一个资源需要被多个对象共享,但是不知道哪个对象会最后释放它,这时候就可以使用std::shared_ptr<T>

2. std::unique_ptr

(1) 概念

智能指针unique_ptr,它拥有对象的独有权,只能指向一个对象,即两个 unique_ptr 不能指向一个对象,不能进行复制操作只能进行移动操作。当它指向其他对象时,之前所指向的对象会被摧毁。其次,当 unique_ptr 超出作用域时,指向的对象也会被自动摧毁,帮助程序员实现了自动释放的功能。

由于unique_ptr源码中构造函数前面使用了explicit关键字修饰,导致unique_ptr不能用于转换函数。

unique_ptr禁用拷贝构造函数和赋值函数。unique_ptr设计的目标就是独享,如果允许unique_ptr对象进行赋值,会出现多个unique_ptr指向同一块内存的情况,当其中一个unique_ptr对象过期的时候,释放内存,造成一块内存释放多次,造成操作野指针。但是它提供了一个移动构造函数,所以可以通过std::move将指针指向的对象交给另一个unique_ptr,转交之后自己就失去了这个指针对象的所有权。

(2) 底层原理

unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;

std::move() 可以将一个unique_ptr转移给另一个unique_ptrshared_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) 内置方法

std::shared_ptrstd::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中的指针。
p.operator*()解引用p。
p.operator->()成员访问运算符,等价于(*p).member。
p.operator bool()检查p是否为空指针。

(4) 应用场景

std::unique_ptr<T>std::shared_ptr<T>具有更小的内存,而且不需要维护引用计数,因此它的性能更好。当我们需要一个独占的指针时,应该优先使用std::unique_ptr<T>

3. std::weak_ptr

(1) 概念

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

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

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

std::shared_ptr<int> sp1(new int(22));
std::shared_ptr<int> sp2 = sp1;
std::weak_ptr<int> wp = sp1; // point to sp1
std::cout<<wp.use_count()<<std::endl; // 2
if(!wp.expired()){
    std::shared_ptr<int> sp3 = wp.lock();
    std::cout<<*sp3<<std::endl; // 22
}

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

std::shared_ptr<int> sp1(new int(22));
std::weak_ptr<int> wp = sp1; // point to sp1
std::shared_ptr<int> sp2(wp);
std::cout<<sp2.use_count()<<std::endl; // 2
sp1.reset();
std::shared_ptr<int> sp3(wp); // throw std::bad_weak_ptr

(2) 内置方法

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

(3) 应用场景

  • 用于实现缓存

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

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

    // 这个函数用来加载Widget对象,但是很耗时,比如是数据库I/O
    std::shared_ptr<Widget> loadWidgetFromFile(int id); 
    

    因此,我们希望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; // 用std::shared_ptr去构造std::weak_ptr
        }
        return objPtr;
    }
    

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

  • 避免循环引用问题

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

    class A {
    public:
        std::shared_ptr<B> ptrB;
    };
    
    class B {
    public:
        std::shared_ptr<A> ptrA;
    };
    
    int main() {
        auto a = std::make_shared<A>();
        auto b = std::make_shared<B>();
        
        a->ptrB = b; // A 拥有 B
        b->ptrA = a; // B 拥有 A
        
        // 当 main 函数结束时,a 和 b 会被销毁
        // 但由于循环引用,A 和 B 实例不会被释放,因为各自的成员变量shared_ptr都指向对方,无法释放
        return 0;
    }
    

    为了避免循环引用,可以使用 std::weak_ptr 来打破循环。

    class A {
    public:
        std::shared_ptr<B> ptrB;
    };
    
    class B {
    public:
        std::weak_ptr<A> ptrA; 	// 使用 weak_ptr
    };
    
    int main() {
        auto a = std::make_shared<A>();
        auto b = std::make_shared<B>();
        
        a->ptrB = b; // A 拥有 B
        b->ptrA = a; // B 拥有 A 但使用 weak_ptr
        
        // 当 main 函数结束时,a 和 b 会被销毁
        // a销毁后,A的引用计数变为0,因为B中是weak_ptr<A>,不计数,所以A被释放
        // A被释放,则A中的ptrB指针释放,此时B的引用计数也变为0,所以B也被释放
        return 0;
    }
    
  • 用于实现单例模式

    单例模式是指一个类只能有一个实例,且该类能自行创建这个实例的一种模式。单例模式的实现方式有很多种,其中一种就是使用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;		// 初始化静态成员
    

4. 注意

  • 尽量使用std::make_shared<T>而不是shared_ptr<T>(new T)

    std::make_shared<T>是更异常安全的做法。std::make_shared<T>是一个函数模板,它在动态内存中分配一个对象并初始化它,返回指向此对象的std::shared_ptr<T>std::make_shared<T>的好处是它只进行一次内存分配,而std::shared_ptr<T>(new T)则进行两次内存分配,一次是为T分配内存,另一次是为std::shared_ptr<T>的控制块分配内存。因此,std::make_shared<T>是更好的选择。

    std::shared_ptr<int> sp(new int(42)); // exception unsafe
    

    当new int(42)抛出异常时,sp将不会被创建,从而对应new分配的内存也不会释放,从而导致内存泄漏。

  • 智能指针与裸指针的性能对比

    • shared_ptr由于占据更多内存,且需要通过原子操作维护引用计数,因此效率是比较慢的;
    • unique_ptr自动管理内存资源,而几乎没有额外开销。因此效率和new、delete几乎一样。
  • shared_ptr的线程安全问题

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

  • 不要用同一个raw pointer初始化多个shared_ptr

    因为多个shared_ptr由同一个raw pointer创建时会导致生成两个独立的引用计数控制块,从以下程序可见sp1、sp2的引用计数都为1。

    int* p = new int(0);
    std::shared_ptr<int> sp1(p);
    std::shared_ptr<int> sp2(p);
    std::cout<<sp1.use_count()<<std::endl; // 1
    std::cout<<sp2.use_count()<<std::endl; // 1
    

    当sp1、sp2销毁时会产生未定义行为,因为shared_ptr的析构函数会释放它所管理的对象,当sp1析构时,会释放p指向的内存,当sp2析构时,会再次释放p指向的内存。

  • enable_shared_from_this模板类

    如果通过this指针创建shared_ptr时,相当于通过一个裸指针创建shared_ptr,多次创建会导致多个shared_ptr对象管理同一个内存。当shared_ptr对象销毁时,会释放this指向的内存,但是this指针可能还会被使用,导致程序崩溃。

    使用方法: 继承enable_shared_from_this类;通过shared_from_this()方法返回。

    class MyClass : public std::enable_shared_from_this<MyClass> {
    public:
        std::shared_ptr<MyClass> getSharedPtr() {
            return shared_from_this();
        }
    };
    
    int main() {
        std::shared_ptr<MyClass> obj1 = std::make_shared<MyClass>();
        std::shared_ptr<MyClass> obj2 = obj1->getSharedPtr();			// obj2 和 obj1 指向同一个对象
        std::cout << "Use count: " << obj1.use_count() << std::endl;	// 2
        return 0;
    }
    

    经典应用场景:在异步操作中使用 shared_from_this,确保对象在操作完成前不被销毁:

    class MyClass : public std::enable_shared_from_this<MyClass> {
    public:
        void startAsyncOperation() {
            std::shared_ptr<MyClass> self = shared_from_this();
            std::thread([self]() {
                std::this_thread::sleep_for(std::chrono::seconds(2)); // Simulate async operation
                self->asyncOperationComplete();
            }).detach();
        }
    
        void asyncOperationComplete() {
            std::cout << "Async operation completed for MyClass object: " << this << std::endl;
        }
    };
    
    int main() {
        std::shared_ptr<MyClass> obj = std::make_shared<MyClass>();
        obj->startAsyncOperation();
    
        std::this_thread::sleep_for(std::chrono::seconds(3)); // Wait for async operation to complete
        return 0;
    }
    

    startAsyncOperation 启动了一个异步操作,通过捕获 shared_ptr 的副本,确保 MyClass 对象在异步操作完成之前不会被销毁。

  • 智能指针模板中的类型可以是数组

    std::shared_ptrstd::unique_ptr都可以指向数组。在C++17后,std::shared_ptr也提供了operator[]操作符,可以像访问数组一样访问std::shared_ptr指向的数组。 [] 而在C++17之前是不支持的。

    std::shared_ptr<int[]> sp1(new int[10]);
    std::unique_ptr<int[]> up1(new int[10]);
    for (int i = 0; i < 10; i++) {
        sp1[i] = i;
        up1[i] = i;
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值