前言:智能指针是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_unique
和std::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_ptr
的get()
方法
std::shared_ptr
的get()
方法返回一个裸指针,这个裸指针指向std::shared_ptr
管理的对象。如果通过delete释放了这个裸指针指向的内存,当std::shared_ptr
销毁时,其管理的对象会被再次释放。
7. 使用unique_ptr
的release()
方法后,不要忘记手动释放资源
std::unique_ptr
调用release()
方法后,会释放对资源的所有权,但是不会释放资源本身。因此当std::unique_ptr
调用release()
方法后,需要手动调用delete释放资源。
8. 使用std::weak_ptr
的std::shared_ptr
对象前,检查是否失效。
std::weak_ptr
是一种弱引用,它不会增加引用计数,因此不会影响资源的释放。std::weak_ptr
的lock()
方法可以返回一个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
异常。
参考
-
Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14. Scott Meyers. O’Reilly Media. 2014.
你好,我是七昂,致力于分享C/C++、操作系统、软件架构等计算机基础知识。如果你有任何问题或者建议,欢迎随时与我交流。如果这篇内容对您有帮助,请点赞关注,之后将会持续分享更多技术干货。希望我们能一起探索程序员修炼之道。感谢你的阅读!