智能指针
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 = q | p是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_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
(3) 内置方法
std::shared_ptr
和 std::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_ptr
和std::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; }