smartPtr心得

2024/08/01 cplusplus 共 4833 字,约 14 分钟

1. 不使用smart_ptr可能产生的问题

1.1 内存泄漏

#include <iostream>
#include <cstdlib>

int main() {
    int* ptr = (int*)malloc(sizeof(int)); // 分配内存
    if (ptr == NULL) {
        std::cerr << "Memory allocation failed!" << std::endl;
        return 1;
    }
    // ... 使用 ptr 访问分配的内存 ...
    // 错误: 没有对 ptr 进行释放
    return 0;
}

上述代码使用malloc分配内存,但没有使用free来释放,造成内存泄漏

1.2 double free

#include <iostream>
#include <cstdlib>

int main() {
    int* ptr = (int*)malloc(sizeof(int)); // 分配内存
    free(ptr); // 第一次释放
    free(ptr); // 第二次释放,导致double free错误
    return 0;
}

以上代码一个malloc对应了两个free,造成了double free的错误

1.3 野指针

void main()
{
    char* p = (char *) malloc(10);
    strcpy(p, abc);
    free(p);  //p所指的内存被释放,但是p所指的地址仍然不变
    strcpy(p, def); // 错误
}

2. smart_ptr 小解

2.1 unique_ptr

独享指针,不能拷贝构造和赋值,但可以使用move转移所有权;离开作用域就自动释放独占了指针

// Disable copy from lvalue.
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;

释放所有权 sp.release(), 返回raw pointer

unique_ptr<int> p1 = make_unique<int>(1);
int* a = p1.release();
std::cout<<*a<<std::endl;
delete a; // you need to delete it manually

重置所有权 sp.reset(), 释放所有权,指向空指针

unique_ptr<int> p1 = make_unique<int>(1);
p1.reset();
std::cout<<p1.get()<<std::endl; // 0

std::move() 可以将一个unique_ptr转移给另一个unique_ptr或者shared_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

2.2 shared_ptr

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

2.2.1 底层原理

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大小的两倍。

2.2.2 什么时候用shared_ptr

通常用于一些资源创建昂贵比较耗时的场景, 比如涉及到文件读写、网络连接、数据库连接等。当需要共享资源的所有权时,例如,一个资源需要被多个对象共享,但是不知道哪个对象会最后释放它,这时候就可以使用std::shared_ptr<T>

2.3 weak_ptr

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

2.3.1 读取引用对象

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

2.3.2 weak_ptr指向对象是否存在

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

2.3.3 weak_ptr可以std::shared_ptr的构造参数

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.3.4 使用场景

2.3.4.1 缓存

使用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; // use std::shared_ptr to construct std::weak_ptr
    }
    return objPtr;
}

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

2.3.4.2 解决循环引用

class IObserver {
public:
    virtual void update(const string& msg) = 0;
};

class Subject {
public:
    void attach(const std::shared_ptr<IObserver>& observer) {
        observers_.emplace_back(observer);
    }
    void detach(const std::shared_ptr<IObserver>& observer) {
        observers_.erase(std::remove(observers_.begin(), observers_.end(), observer), observers_.end());
    }
    void notify(const string& msg) {
        for (auto& observer : observers_) {
            observer->update(msg);
        }
    }
private:
    std::vector<std::shared_ptr<IObserver>> observers_;
};

class ConcreteObserver : public IObserver {
public:
    ConcreteObserver(const std::shared_ptr<Subject>& subject) : subject_(subject) {}
    void update(const string& msg) override {
        std::cout << "ConcreteObserver " << msg<< std::endl;
    }
private:
    std::shared_ptr<Subject> subject_;
};

int main() {
    std::shared_ptr<Subject> subject = std::make_shared<Subject>();
    std::shared_ptr<IObserver> observer = std::make_shared<ConcreteObserver>(subject);
    subject->attach(observer);
    subject->notify("update");
    return 0;
}

以上的代码会造成循环引用;

解决方法:将Observer类中的subject_成员变量改为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;

std::weak_ptr实现单例模式的优点:

  1. 避免循环应用:避免了内存泄漏。
  2. 访问控制:可以访问对象,但是不会延长对象的生命周期。
  3. 可以在单例对象不被使用时,自动释放对象。

文档信息

Search

    Table of Contents