C++ 高效使用智能指针的8个建议

发布时间 2023-12-16 13:43:47作者: 七昂的技术之旅

C++ 高效使用智能指针的8个建议

前言:智能指针是C++11提供的新特性,它基于RAII实现,可以自动管理内存资源,避免内存泄漏的发生,但是智能指针也并不是万能的,如果不正确使用智能指针,也会导致内存泄漏的发生,因此,我们需要了解如何高效使用智能指针避免一些可能的陷阱。本文总结了8个关于智能指针的建议,希望对大家有所帮助。

1. 优先使用std::unique_ptr, 再考虑std::shared_ptr

  • shared_ptr的大小是unique_ptr的两倍,因为shared_ptr需要维护一个引用计数。
  • shared_ptr由于占据更多内存,且需要通过原子操作维护引用计数,因此效率是比较慢的。在不开启编译器优化的时候,是比new操作慢10倍,此时不应该使用make_shared、shared_ptr。开启优化后,也大概慢2-3倍。 [2]
  • unique_ptr、make_unique、带少许偏差的make_shared几乎和new、delete具有一样的性能。
  • unique_ptr自动管理内存资源,而几乎没有额外开销。因此效率和new、delete几乎一样。

2. 尽量使用std::make_uniquestd::make_shared

尽量使用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分配的内存也不会释放,从而导致内存泄漏。

3. 使std::shared_ptr管理的对象或资源线程安全

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

4. 注意std::shared_ptr的循环引用问题

  • 什么是循环引用问题 ?

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

在观察者模式中使用shared_ptr可能会出现循环引用,在下面的程序中,Observer对象和Subject对象相互引用,导致它们的引用计数都不为0,从而导致内存泄漏。

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,这样就不会导致内存无法正确释放了。

5. 避免使用裸指针创建智能指针

不要用同一个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指向的内存。

6. 用enable_shared_from_this在类内部中获得一个指向当前对象的shared_ptr

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

class A {
public:
    std::shared_ptr<A> get_shared_ptr() {
        return std::shared_ptr<A>(this); // error
    }
};

为了解决这个问题,C++11提供了std::enable_shared_from_this模板类,它可以在类内部获得一个指向当前对象的shared_ptr。

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

class A : public std::enable_shared_from_this<A> {
public:
    std::shared_ptr<A> get_shared_ptr() {
        return shared_from_this();
    }
};

6. 避免使用std::shared_ptrget()方法

std::shared_ptrget()方法返回一个裸指针,这个裸指针指向std::shared_ptr管理的对象。如果通过delete释放了这个裸指针指向的内存,当std::shared_ptr销毁时,其管理的对象会被再次释放。

7. 使用unique_ptrrelease()方法后,不要忘记手动释放资源

std::unique_ptr调用release()方法后,会释放对资源的所有权,但是不会释放资源本身。因此当std::unique_ptr调用release()方法后,需要手动调用delete释放资源。

8. 使用std::weak_ptrstd::shared_ptr对象前,检查是否失效。

std::weak_ptr是一种弱引用,它不会增加引用计数,因此不会影响资源的释放。std::weak_ptrlock()方法可以返回一个std::shared_ptr对象,但是在使用std::shared_ptr对象前,需要检查std::shared_ptr对象是否失效。

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

参考

  1. Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14. Scott Meyers. O'Reilly Media. 2014.

  2. memory-and-performance-overhead-of-smart-pointer


你好,我是七昂,计算机科学爱好者,致力于分享C/C++、操作系统、软件架构等计算机基础知识。如果你有任何问题或者建议,欢迎随时与我交流。如果这篇内容对您有帮助,请点赞关注,之后将会持续分享更多技术干货。希望我们能一起探索程序员修炼之道。感谢你的阅读!