智能指针 (Smart Pointer)
1. 介绍
当多个指针指向同一个对象的时候,为了确保“指针的寿命”和“其所指向的对象的寿命”一致,是一件比较复杂的事情。
智能指针的出现就是为了解决这种场的,智能指针内部会维护一个对指针指向对象的引用计数,在对象析构的时候,会去对该对象的引用计数减减,当应用计数为0的时候,就会去释放对象。
但是尽管智能指针是很方便,但是也要抱有敬畏心,若误用可能会出现资源使用无法被释放的大问题。
自 C++11 起, C++ 标准库提供两大类型的智能指针:
std::shared_ptr
实现共享式拥有
的概念,多个智能指针可以指向相同的对象,该对象和其相关资源会在“指向该对象的最后一个引用被销毁”时被释放。为了满足复杂情况,标准库还提供了 std::weak_ptr、std::bad_weak_ptr 和 enable_share_from_this 等辅助类。std::unique_ptr
实现独占式拥有或严格拥有
的概念,保证同一个事件内只有一个智能指针可以指向该对象。你可以移交拥有权。
2. std::shared_ptr
通常我们都会需要“在相同时间的多处地点处理或使用对象”的能力,在程序的多个地方引用同一个对象 。这就需要我们在多个地方使用完该对象,在“指向该对象的最后一个引用被销毁”时来删除该对象本身,执行该对象的析构函数,来释放内存或归还资源等。 std::shared_ptr 提供了这样共享式拥有
的语义。也就是说多个 shared_ptr 可以共享(或说拥有)同一对象,对象的最末一个拥有着有责任销毁对象,并清理与该对象相关的资源。
如果对象以 new 产生,默认情况下清理工作就是由 delete 完成。但是你也可以 (并且往往必须)定义其他清理办法。举个例子,如果你的对象是以 new[] 分配的 array,你必须定义自己的 delete[] 加以清理。
2.1 使用 std::shared_ptr
你可以像使用任何其他指针一样地使用 shared_ptr。你可以赋值、拷贝、比较它们,也可以使用操作符 * 和 -> 访问其所指向的对象的成员或方法。见下面这个例子:
#include <memory>
#include <string>
int main() {
// 创建智能指针指向的 std::string 对象
// 方式1
std::shared_ptr<std::string> piStr1(new std::string("str1"));
// 方式2 --- 更推荐,这种方式比较快,也比较安全
auto piStr2 = std::make_shared<std::string>("str2");
// 方式3
std::shared_ptr<std::string> piStr3{ new std::string("str3") };
// 方式4
std::shared_ptr<std::string> piStr4;
//piStr4 = new std::string("str4"); // ERROR 不允许直接赋值
piStr4.reset(new std::string("str4"));
// 错误方式,由于其构造函数是 explicit 所以这里是不可以直接赋值的,直接赋值操作会进行隐式转换
// std::shared_ptr<std::string> piStr3 = new std::string("str3"); // ERROE
// 使用 -> 或 * 直接访问 std::string 对象的方法
std::cout << "piStr1 is \" " << piStr1->c_str() << "\"" << std::endl;
std::cout << "piStr2 is \" " << (*piStr2).c_str() << "\"" << std::endl;
std::cout << "piStr2 after ->replace(0, 1, \"S\") ---->" << piStr2->replace(0, 1, "S") << std::endl;
// 使用 vector 存储 string 对象,use_count() 方法获取“当前拥有者”数量
std::vector<std::shared_ptr<std::string>> vecStr;
vecStr.push_back(piStr1);
std::cout << "piStr1.use_count()--> " << piStr1.use_count() << endl; // count is 2
vecStr.push_back(piStr1);
std::cout << "piStr1.use_count()--> " << piStr1.use_count() << endl; // count is 3
// string 对象销毁时机
// 1. 程序终点处,当 string 的最后一个拥有者被销毁,shared_ptr 会对其所指向的对象调用 delete
// 2. 将 piStr1 = nullptr; 也会触发对 piStr1 拥有的 string 对象引用计数减减
// 3. 调用 shared_ptr<>.reset() ,也会触发对 string 对象引用计数减减
// 4. 将 piStr1 = std::shared_ptr<int>(new int); 也会触发对 piStr1 拥有的 string 对象引用计数减减,然后在将 piStr1 指向新的对象
return 0;
}
2.2 自定义Deleter,处理 Array
我们可以声明属于自己的 deleter, 例如让它在“删除被指向对象”之前先打印一条信息:
std::shared_ptr<std::string> piStr5(
new std::string("str5"),
[] (std::string* p) {
std::cout << "delete " << p->c_str() << std::endl;
delete p;
});
应用场景,对付 Array。请注意,shared_ptr 提供的 default Deleter 调用的是 delete,而不是 delete[]。这就意味着只有当 shared_ptr 拥有 “由 new 建立起来的单一对象”,default Deleter 才合适,当我们为 Array 创建一个 shared_ptr 的时候,却会出错。自定义 Deleter 是为了解决这种情况的,代码如下:
std::shared_ptr<int> pArry(
new int[10],
[](int* p) {
delete[] p;
}
);
2.3 注意事项
- 自定义 Deleter 函数是不允许抛出异常的
- shared_ptr 只提供 operator* 和 operator ->,指针运算(pointer++)和 operator [] 都未提供。因此,如果想访问内存,你必须使用 get() 获取被 shared_ptr 包括的内部指针,然后对指针进行运算操作,如:
pArry.get()[1]; pArry.get() ++; - shared_ptr 是非线程安全的,所以当在多个线程中以 shared_ptr 指向同一个对象的时候,必须使用诸如 mutex 或 lock技术,防止出现由于资源竞争导致的问题
2.4 误用 std::shared_ptr
(1)错误的赋值,导致相同资源被多次释放,记得 shared_ptr 之间赋值应该赋值 shared_ptr 对象
int main() {
// 错误的案例
auto p = new int;
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 都会在丢失p的拥有权的时候释放相应的资源
// 由于传给智能指针 sp1 和 sp2 都是 p 的地址,所以这相当于是两个对象在管理同一个 p 的生命周期
// 意味着 sp1 和 sp2 析构的时候,一共会释放两次 p 从而导致程序崩溃
// 正确的做法
std::shared_ptr<int> sp3(new int);
std::shared_ptr<int> sp4 = sp3;
return 0;
}
2.5 shared_from_this
在某些情况下,需要在类成员函数中,将类对象(this)转换成 shared_ptr 传递给成员方法中使用,使用的规则如下:
(1)类需要继承 std::enable_shared_from_this
(2)在成员函数中,使用 shared_from_this() 便可以建立起一个源自 this 的正确 shared_ptr
(3)注意,不可以在构造函数中调用 shared_from_this(), 由于 shared_ptr 本身是在基类中,也就是enable_shared_from_this<>内部的一个private成员,在当前对象构造结束之前是无法建立 shared_pointer的循环引用。所以在构造函数中,这样会做导致程序运行期错误。
class CPerson : public std::enable_shared_from_this<CPerson> { // ①
public:
CPerson(
const std::string name,
std::shared_ptr<CPerson> mother = nullptr,
std::shared_ptr<CPerson> father = nullptr
) : m_strName(name), m_piFather(father), m_piMother(mother) {}
~CPerson() {
std::cout << "delete " << m_strName << std::endl;
}
void SetParentsAndTheirKids(
std::shared_ptr<CPerson> m = nullptr,
std::shared_ptr<CPerson> f = nullptr
) {
m_piMother = m;
m_piFather = f;
if (m != nullptr) {
m->m_vecKids.push_back(shared_from_this()); // ②
}
if (f != nullptr) {
f->m_vecKids.push_back(shared_from_this());
}
}
std::string m_strName;
std::shared_ptr<CPerson> m_piMother;
std::shared_ptr<CPerson> m_piFather;
std::vector<std::weak_ptr<CPerson>> m_vecKids;
};
2.6 shared_ptr 的各项操作
点击查看详情
3. std::weak_ptr
使用 shared_ptr 主要是为了避免操心指向的资源,然而在某些场景下,无法使用 shared_ptr 或者说是无法满足:
- 环向指向:两个对象使用 shared_ptr 互相指向对方,而一旦不存在其他的引用指向它们时,你想释放它们和其相应资源。这种情况下 shared_ptr 不会释放数据,因为互相指向导致每个对象的 use_count() 仍是1。此时你或许会想使用寻常的指针,但这么做又得自行管理“相应资源的释放”。
- “明确想共享但不拥有”某对象的情况下,即共享的对象的生命周期明确是最长的, shared_ptr 绝不会提前释放对象。若使用寻常的指针,可能不会注意到它们指向的对象已经不再有效,导致“访问已被释放的数据”的风险
于是标准库提供了 weak_ptr,允许“共享但不拥有”某对像,它会建立起一个 shared_ptr,一旦最后一个拥有该对象的 shared_ptr 失去拥有权,任何 weak_ptr 都会自动成空。你不可以直接使用 operator * 和 -> 访问 weak_ptr 执向的对象,而是必须另外建立起一个 shared_ptr 去访问。 weak_ptr 只提供小量操作,只能够用来创建、复制、赋值 weak_ptr 以及转换为一个 shared_ptr 或检查自己是否指向某个对象。
3.1 shared_ptr 环向指向 导致资源无法释放
//////////////////////////////////////////////////////////////////////////
#include <memory>
#include <string>
class CPerson {
public:
CPerson(
const std::string name,
std::shared_ptr<CPerson> mother = nullptr,
std::shared_ptr<CPerson> father = nullptr
) : m_strName(name), m_piFather(father), m_piMother(mother) {}
~CPerson() {
std::cout << "delete " << m_strName << std::endl;
}
std::string m_strName;
std::shared_ptr<CPerson> m_piMother;
std::shared_ptr<CPerson> m_piFather;
std::vector<std::shared_ptr<CPerson>> m_vecKids;
};
//////////////////////////////////////////////////////////////////////////
std::shared_ptr<CPerson> initFamily(const std::string& name) {
auto mom = std::make_shared<CPerson>(name + "'s Mom");
auto dad = std::make_shared<CPerson>(name + "'s Dad");
auto kid = std::make_shared<CPerson>(name, mom, dad);
mom->m_vecKids.push_back(kid);
dad->m_vecKids.push_back(kid);
return kid;
}
//////////////////////////////////////////////////////////////////////////
int main() {
auto Sam = initFamily("Sam");
std::cout << "Sam's family Info: " << std::endl;
std::cout << "- Sam is shared " << Sam.use_count() << " times" << endl;
std::cout << "- Sam's mom is shared " << Sam->m_piMother.use_count() << " times" << endl;
std::cout << "- Sam's dad is shared " << Sam->m_piFather.use_count() << " times" << endl;
// 输出:
// Sam's family Info:
// - Sam is shared 3 times
// - Sam's mom is shared 1 times
// - Sam's dad is shared 1 times
// 手动释放 Sam 对 CPerson 的引用
Sam = nullptr;
// 此时:
// Sam's family Info:
// - Sam is shared 2 times
// - Sam's mom is shared 1 times
// - Sam's dad is shared 1 times
// 由于 mom 和 dad 的 vector 还持有 kid 对象的引用 kid 对象无法释放
// kid 对象持有 mom 和 dad 的引用,mom 和 dad 无法释放,产生 环向指向 的现象
return 0;
}
要解这个,就要用到 weak_ptr。其实不论是 kid 还是 dad、mom,只要任何一方在 CPerson 中定义为 weak_ptr 都可以解这个节。但是在这个场景下,由于在kid中还可能会使用 dad 或 mom,所以 dad 和 mom 还是得使用 shared_ptr ,vector 中的 kid 使用 weak_ptr 。
3.2 std::weak_ptr 使用
int main() {
try {
auto piStr = std::make_shared <std::string>("str");
// 创建一个 weak_ptr 直接赋值 std::shared_ptr
std::weak_ptr<std::string> piWeakStr = piStr;
// 访问对象
{
auto piStrShare = piWeakStr.lock();
if (piStrShare != nullptr) { // piStrShare 可能为nullptr
std::cout << "piStrShare -> " << piStrShare->c_str() << std::endl;
std::cout << "piStrShare shared " << piStrShare.use_count() << " times" << std::endl; // shared 2 times
}
}
// 手动释放 piStr 智能指针的引用
piStr = nullptr;
// 三种方式检查 weak_ptr是否还可以使用
// 方式一 -- 推荐,expired() 在 weak_ptr 不在共享对象时返回true,等同于检查 use_count() 是否为0,但速度较快
std::cout << "piWeakStr.expired() ->" << std::boolalpha << piWeakStr.expired() << std::endl; // true
// 方式二 -- 判断 use_count() 是否为0,这个效率较差
std::cout << "piWeakStr.use_count() -> " << piWeakStr.use_count() << std::endl; // piWeakStr.use_count() -> 0
// 方式三 -- 使用 lock()方法,判断返回的 shared_ptr 是否等于 nullptr,若是也认为不在共享对象
auto piStrShare = piWeakStr.lock();
if (piStrShare == nullptr) { // piStrShare 可能为nullptr
std::cout << "piStrShare is a nullptr" << std::endl;
}
// 方式四 -- 使用相应的 shared_ptr 构造函数明确将 weak_ptr 转换为一个 shared_ptr,如果对象已经不存在,该构造
// 函数会抛出 bad_weak_ptr 的异常 (e.what() 输出 bad_weak_ptr)
std::shared_ptr<std::string> sharedConvert(piWeakStr);
}
catch (const std::exception& e) {
std::cout << "exception: " << e.what() << std::endl;
}
return 0;
}
3.3 weak_ptr 的各项操作
点击查看详情
4. std::unique_ptr
unique_ptr 是 C++ 标准库自 C++11 起开始提供的类型。它是一种在异常发生的时候可以帮助避免资源泄露的智能指针。一般而言,这个智能指针实现了独占式拥有
概念,意味着它可确保一个对象和其他相应的资源同一时间只能被一个 unique_ptr 拥有。一旦拥有着被销毁或变成nullptr,或开始拥有另一个对象,先前拥有的那个对象就会被销毁,其任何相应的资源也会被释放。
unique_ptr 继承自 auto_ptr,后者由 C++98 引入但已不再被认可。
4.1 std::unique_ptr 使用
int main() {
// 创建智能指针指向的 std::string 对象
// 方式1
std::unique_ptr<std::string> piStr(new std::string("str"));
// 方式2 -- 更推荐
auto piStr1 = std::make_unique<std::string>("str1");
// 方式3
std::unique_ptr<std::string> piStr3{ new std::string("str3") };
// 方式4
std::unique_ptr<std::string> piStr4;
//piStr4 = new std::string("str4"); // ERROR 不允许直接赋值
piStr4.reset(new std::string("str4"));
// 错误方式,由于其构造函数是 explicit 所以这里是不可以直接赋值的,直接赋值操作会进行隐式转换
// std::unique_ptr<std::string> piStr3 = new std::string("str3"); // ERROE
// 使用 -> 或 * 直接访问 std::string 对象的方法
(*piStr).replace(0, 1, "S");
piStr->append("_test");
std::cout << piStr->c_str() << std::endl;
// 赋值
// std::unique_ptr<std::string> piStr5(piStr); // ERROR 不允许
std::unique_ptr<std::string> piStr5(std::move(piStr)); // okk
// 与 shared_ptr 不同的是,可以调用 release 方法,放弃对对象的拥有权
std::string* strNew = piStr.release();
std::cout << "after piStr.release() ---> piStr is ---> " << piStr << std::endl; // piStr is 0
std::unique_ptr<std::string> newStr(strNew);
std::cout << "newStr is ---> " << newStr->c_str() << std::endl; // newStr is ---> Str_test
// string 对象销毁时机
// 1. 程序终点处,当 string 的最后一个拥有者被销毁,unique_ptr 会对其所指向的对象调用 delete
// 2. 将 piStr1 = nullptr; 也会触发对 piStr1 拥有的 string 对象引用计数减减
// 3. 调用 unique_ptr<>.reset() ,也会触发对 string 对象引用计数减减
// 4. 将 piStr1 = std::unique_ptr<int>(new int); 也会触发对 piStr1 拥有的 string 对象引用计数减减,然后在将 piStr1 指向新的对象
return 0;
}
4.2 std::unique_ptr 处理 Array
int main() {
// 使用智能指针,创建对象数组
// 如果使用 shared_ptr 需要自己定义 deleter 才能处理 array
std::shared_ptr<std::string> sp1(new std::string[10], [](std::string* p) { delete[] p; });
// unique_ptr 提供了一个偏特化版本用来处理array
// 在 sp2 对象销毁时候,会使用 delete[] 来释放其所拥有的 string 对象
std::unique_ptr<std::string[]> sp2(new std::string[10]);
// sp2 不支持使用 operator * 和 -> ,改而提供 operator [],用于访问其所指向的 array 中某一个对象
// 一如既往,确保 [] 索引合法是程序员的责任,不合法的索引会导致不确定的行为
// std::cout << *sp2 << std::endl; // ERROR 编译错误
sp2[0].append("str2");
std::cout << "sp2[0] is " << sp2[0].c_str() << std::endl;
return 0;
}
4.3 自定义Deleter
unique_ptr的 Deleter 定义方式与 shared_ptr略不相同,必须知名 deleter 的类型T2作为 unique_ptr<T1, T2> 实参,改类型可以是类、函数、函数指针或者是函数对象。如果是函数对象,其 function call 操作() 应该接受一个“指向对象”的指针。
// 类
class ADeleter
{
public:
void operator () (ClassA* p) {
delete p;
}
};
std::unique_ptr<ClassA, ADeleter> up(new ClassA());
// 如果定义的 Deleter 是个函数或 lambda,你必须声明 Deleter 的类型为 void(*)(T*) 或 std::function<void(T*)>,在或者就使用 decltype
std::unique_ptr<int[], void(*)(int* p)> up1(new int[10], [](int* p) {delete[] p; });
#include <functional>
std::unique_ptr<int[], std::function<void(int*)>> up2(new int[10], [](int* p) {delete[] p; });
4.4 注意事项
- unique_ptr 只提供 operator* 和 operator -> 或 Array 提供 operator [],指针运算(pointer++)未提供。因此,如果想访问内存,你必须使用 get() 获取被 unique_ptr 包括的内部指针,然后对指针进行运算操作,如:piStr.get() ++;