1 std::shared_ptr 的概述
std::shared_ptr 是 C++11 标准库中引入的一种智能指针,用于表示共享所有权的智能指针。它允许多个 shared_ptr 实例共享同一个对象的所有权,并在最后一个引用该对象的 shared_ptr 被销毁或被重置时自动删除该对象。这种特性使得 std::shared_ptr 在需要在多个所有者之间共享对象时非常有用。
1.1 std::shared_ptr 的主要特性
(1)引用计数: std::shared_ptr 内部使用一个引用计数器来跟踪有多少个 shared_ptr 实例指向同一个对象。每次创建一个新的 shared_ptr 指向同一个对象时,引用计数就会增加;每次销毁或重置一个 shared_ptr 时,引用计数就会减少。
(2)自动删除: 当引用计数减少到 0 时,std::shared_ptr 会自动删除它所指向的对象,并释放相关资源。这避免了手动管理内存时可能出现的内存泄漏问题。
(3)自定义删除器: 与 std::unique_ptr 类似,std::shared_ptr 也支持自定义删除器。你可以提供一个可调用对象作为删除器,用于在引用计数为 0 时执行特定的清理逻辑。
(4)线程安全: std::shared_ptr 的引用计数操作是线程安全的,可以在多线程环境中安全地使用。
1.2 std::shared_ptr 的原理
以下是 std::shared_ptr 的详细原理:
(1)引用计数:
每个 std::shared_ptr 实例都关联一个引用计数,用于跟踪当前有多少个 shared_ptr 实例指向同一个对象。这个引用计数通常存储在一个控制块(control block)中,该控制块与所管理的对象一起在堆上分配。控制块可能还包含其他信息,比如自定义的删除器。
(2)对象的创建与引用:
当创建一个新的 std::shared_ptr 并让它指向一个对象时,会发生以下事情:
如果该对象是新分配的(例如,通过 std::make_shared 或 new 操作符),则会在堆上分配一个控制块,并将对象的指针和初始引用计数(通常为 1)存储在控制块中。
shared_ptr 内部保存指向这个控制块的指针,而不是直接保存对象的指针。通过控制块,shared_ptr 可以访问对象的指针和引用计数。
当另一个 shared_ptr 被构造为指向同一个对象时(例如,通过拷贝或赋值操作),引用计数会增加。这是因为新的 shared_ptr 实例也会保存指向同一个控制块的指针。
(3)对象的销毁与引用计数减少:
当 std::shared_ptr 被销毁(例如,离开作用域)或被重置为指向另一个对象时,会发生以下事情:
- shared_ptr 所指向的控制块中的引用计数会减少。
- 如果引用计数减少到 0,这意味着没有任何 shared_ptr 再指向该对象,此时控制块会调用相应的删除器(默认为 delete 操作符)来销毁对象,并释放对象和控制块所占用的内存。
(4)线程安全:
std::shared_ptr 的引用计数操作是线程安全的,这意味着在多线程环境中,多个线程可以同时修改同一个 shared_ptr 的引用计数,而不会导致数据竞争或未定义行为。这是通过使用原子操作或其他同步机制来实现的。
(5)自定义删除器:
除了默认的 delete 操作符外,std::shared_ptr 还允许使用自定义的删除器。自定义删除器是一个可调用对象(如函数、函数对象或 lambda 表达式),它会在引用计数减少到 0 时被调用,用于执行特定的清理逻辑。这使得 std::shared_ptr 能够更灵活地管理资源,例如关闭文件句柄、释放网络连接等。
通过引用计数和自动内存管理机制,std::shared_ptr 简化了动态分配对象的生命周期管理,减少了内存泄漏的风险,并提高了代码的可读性和可维护性。
1.3 注意事项
(1)避免循环引用: 在使用 std::shared_ptr 时,要特别注意避免循环引用,即两个或多个 shared_ptr 相互引用,形成一个无法被打破的引用环。这会导致引用计数永远无法减少到 0,从而造成内存泄漏。为了避免循环引用,可以使用 std::weak_ptr 来打破引用环。
(2)与原始指针的交互: 虽然 std::shared_ptr 提供了自动内存管理的便利,但在某些情况下,你可能仍然需要与原始指针进行交互。在这种情况下,要特别小心,确保不会意外地删除由 shared_ptr 管理的对象,或者创建额外的引用计数问题。
(2)性能考虑: 由于 std::shared_ptr 需要维护引用计数,相比裸指针或 std::unique_ptr,它可能会有一些额外的性能开销。因此,在不需要共享所有权的场景中,使用 std::unique_ptr 可能是更好的选择。
2 std::shared_ptr 的创建与初始化
2.1 使用 std::make_shared 创建
std::make_shared 是创建 std::shared_ptr 的推荐方式,因为它在单个内存分配中同时分配控制块和对象本身,从而提高了性能。
// 创建一个指向 int 的 shared_ptr,并初始化为 12
std::shared_ptr<int> ptr1 = std::make_shared<int>(12);
// 创建一个指向自定义类型的 shared_ptr,并调用构造函数
struct MyStruct {
MyStruct(int x, double y) : value1(x), value2(y) {}
int value1;
double value2;
};
std::shared_ptr<MyStruct> ptr2 = std::make_shared<MyStruct>(12, 3.14);
2.2 使用构造函数创建
虽然可以使用 shared_ptr 的构造函数直接创建智能指针,但这种方式不如 std::make_shared 高效,因为它需要两次内存分配:一次用于对象,一次用于控制块。
// 使用原始指针创建 shared_ptr(不推荐,可能导致内存泄漏)
int* raw_ptr = new int(12);
std::shared_ptr<int> ptr1(raw_ptr); // 注意:raw_ptr 不应再被 delete
// 使用空指针初始化 shared_ptr
std::shared_ptr<int> ptr2; // 默认构造的 shared_ptr 不指向任何对象
// 复制另一个 shared_ptr 来初始化新的 shared_ptr(引用计数会增加)
std::shared_ptr<int> ptr3 = ptr1; // ptr3 和 ptr1 现在共享同一个对象的所有权
2.3 使用 std::allocate_shared 创建
对于需要自定义内存分配的场景,可以使用 std::allocate_shared。它接受一个分配器作为参数,并使用该分配器来分配对象和控制块。
std::allocator<int> allocator;
std::shared_ptr<int> ptr = std::allocate_shared<int>(allocator, 12);
2.4 自定义删除器
std::shared_ptr 还允许在构造时提供一个自定义的删除器。删除器是一个可调用对象,当最后一个 shared_ptr 不再指向对象时,它会被调用。
// 自定义删除器函数
void customDelete(int* ptr) {
// 执行一些清理逻辑...
delete ptr;
}
// 使用自定义删除器创建 shared_ptr
std::shared_ptr<int> ptr1(new int(12), customDelete);
// 使用 lambda 表达式作为删除器
std::shared_ptr<int> ptr2(new int(12), [](int* ptr) {
// 执行清理逻辑...
delete ptr;
});
注意:当使用原始指针和自定义删除器创建 std::shared_ptr 时,必须确保原始指针只被一个 shared_ptr 管理,以避免内存泄漏。如果原始指针被多个 shared_ptr 管理,则当其中一个 shared_ptr 被销毁时,对象可能会被意外删除。因此,通常推荐使用 std::make_shared 来创建 std::shared_ptr。
2.5 初始化列表
在类的初始化列表中,也可以初始化 std::shared_ptr 成员变量。
class MyClass {
public:
MyClass() : mySharedPtr(std::make_shared<int>(10)) {}
private:
std::shared_ptr<int> mySharedPtr;
};
3 std::shared_ptr 的自定义删除器
std::shared_ptr 的自定义删除器是一种功能强大的特性,它允许程序员为 shared_ptr 指定一个特定的函数或函数对象,该函数或对象会在最后一个 shared_ptr 失去对对象的所有权时(即引用计数减少到 0 时)被调用,以执行特定的清理逻辑。
自定义删除器通常用于处理那些不是通过 new 操作符分配的对象,或者那些需要特殊处理来释放的对象。例如,当你管理一些不是简单内存块的资源,如文件句柄、网络连接或者自定义内存管理器分配的内存时,你可能需要使用自定义删除器。
3.1 函数指针作为删除器
可以将一个普通的函数指针作为删除器传递给 shared_ptr 的构造函数。这个函数应该接受一个指向对象的指针,并执行适当的清理操作。
struct MyType { };
void customDelete(MyType* ptr) {
// 清理逻辑,比如关闭文件句柄、释放网络连接等
delete ptr; // 假设 MyType 是通过 new 分配的
}
std::shared_ptr<MyType> ptr(new MyType, customDelete);
3.2 函数对象(仿函数)作为删除器
也可以使用函数对象(也称为仿函数)作为删除器。函数对象是一个重载了 operator() 的类,它可以像普通函数一样被调用。
struct MyType { };
struct CustomDeleter {
void operator()(MyType* ptr) const {
// 清理逻辑
delete ptr;
}
};
std::shared_ptr<MyType> ptr(new MyType, CustomDeleter());
3.3 Lambda 表达式作为删除器
C++11 引入了 lambda 表达式,它们也可以作为 shared_ptr 的删除器。Lambda 表达式提供了一种简洁的方式来定义匿名函数对象。
struct MyType { };
auto deleter = [](MyType* ptr) {
// 清理逻辑
delete ptr;
};
std::shared_ptr<MyType> ptr1(new MyType, deleter);
// 也可以直接使用
std::shared_ptr<MyType> ptr2(new MyType, [](MyType* ptr) {
// 清理逻辑
delete ptr;
});
3.4 注意事项
(1)确保正确删除: 自定义删除器必须确保正确地释放资源。如果资源是通过 new 分配的,那么删除器应该使用 delete 来释放它。如果资源是通过其他方式(如自定义内存管理器)分配的,那么删除器应该使用相应的方法来释放它。
(2)避免重复删除: 如果原始指针被多个 shared_ptr 管理,并且每个 shared_ptr 都使用了自定义删除器,那么可能会导致资源被重复释放。因此,必须确保每个原始指针只被一个 shared_ptr 管理,或者确保删除器能够安全地处理重复调用。
(3)使用 std::make_shared: 通常推荐使用 std::make_shared 来创建 shared_ptr,因为它在一个内存分配中同时分配对象和控制块,从而提高了性能。当使用自定义删除器时,你可能需要直接使用 shared_ptr 的构造函数,但请确保正确管理内存。
(4)类型安全: 自定义删除器应该与 shared_ptr 所指向的对象类型相匹配。如果删除器期望一个不同类型的指针,那么可能会导致未定义行为。
4 std::shared_ptr 与数组
std::shared_ptr 是 C++11 引入的一种智能指针,用于共享对象的所有权。虽然 std::shared_ptr 主要用于单个对象的内存管理,但它也可以用于管理动态数组。不过,在使用 std::shared_ptr 管理数组时,需要特别注意一些细节,以确保正确和安全的内存管理。
4.1 使用 std::shared_ptr 的一个特化版本(推荐)
当使用 std::shared_ptr 管理数组时,需要使用特殊的构造函数,这些版本接受一个额外的参数来表示数组的大小。此外,当 std::shared_ptr 销毁其管理的数组时,它会调用 delete[] 而不是 delete。
#include <memory>
#include <iostream>
struct MyStruct {
~MyStruct() {
std::cout << "MyStruct destroyed\n";
}
};
int main()
{
// 使用 std::shared_ptr 管理数组
std::shared_ptr<MyStruct[]> arr_ptr(new MyStruct[5]);
// 当 arr_ptr 离开作用域时,MyStruct 数组会被自动使用 delete[] 删除
return 0;
}
上面代码的输出为:
MyStruct destroyed
MyStruct destroyed
MyStruct destroyed
MyStruct destroyed
MyStruct destroyed
上面的这个例子创建了一个 std::shared_ptr<MyStruct[]> 来管理一个 MyStruct 类型的数组。注意类型中的方括号 [],它告诉 std::shared_ptr 正在管理一个数组,而不是单个对象。
4.2 自定义删除器与数组
就像普通的 std::shared_ptr 一样,也可以为数组版本的 std::shared_ptr 提供自定义删除器。这在某些情况下可能很有用,比如需要使用自定义的内存管理策略时。
void customArrayDelete(MyStruct* ptr) {
// 自定义的数组删除逻辑
delete[] ptr;
}
std::shared_ptr<MyStruct[]> arr_ptr(new MyStruct[5], customArrayDelete);
在这个例子中,传递了一个自定义的删除器 customArrayDelete 给 std::shared_ptr。当 arr_ptr 被销毁时,它会调用 customArrayDelete 来释放数组。
4.3 注意事项
- 不要混用 new 和 delete[] 或 new[] 和 delete:当使用 std::shared_ptr 管理数组时,确保你使用 new[] 来分配数组,这样 std::shared_ptr 就可以在销毁时正确地使用 delete[] 来释放内存。如果错误地使用了 new(而不是 new[]),那么当 std::shared_ptr 尝试使用 delete[] 来释放内存时,会导致未定义行为。
- 不要使用 std::make_shared 管理数组:虽然 std::make_shared 通常是创建 std::shared_ptr 的推荐方式,但它并不直接支持数组。尝试使用 std::make_shared 来创建管理数组的 std::shared_ptr 会导致编译错误,因为 std::make_shared 的模板参数推导不会正确处理数组类型。
5 std::shared_ptr 的循环引用问题
std::shared_ptr 的循环引用问题是在使用智能指针时经常遇到的一个难题。循环引用指的是两个或多个 std::shared_ptr 对象相互引用,形成一个闭环,导致它们的引用计数永远无法降至零,从而无法自动释放所管理的内存资源。
(1)循环引用的产生
循环引用通常发生在对象间存在相互依赖关系时。例如,考虑两个类 A 和 B,每个类都持有一个指向对方类型的 std::shared_ptr。
class B;
class A {
public:
std::shared_ptr<B> b_ptr;
// ... 其他成员 ...
};
class B {
public:
std::shared_ptr<A> a_ptr;
// ... 其他成员 ...
};
void create_cycle() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;
// 此时,a 和 b 形成了循环引用
}
在 create_cycle 函数中,创建了两个 std::shared_ptr 对象 a 和 b,并将它们相互引用。这意味着 a 的引用计数因为 b->a_ptr 的存在而增加,同时 b 的引用计数因为 a->b_ptr 的存在而增加。当 create_cycle 函数结束时,a 和 b 离开作用域,但它们的引用计数仍然不为零,因此它们所管理的内存不会被释放。
(2)循环引用的影响
循环引用会导致内存泄漏,因为 std::shared_ptr 无法检测到循环引用并自动释放内存。即使所有外部对 a 和 b 的引用都已消失,它们所指向的对象仍然不会被销毁,因为它们的引用计数永远不会降到零。
(3)解决循环引用
有几种方法可以解决 std::shared_ptr 的循环引用问题:
使用弱指针 (std::weak_ptr):
弱指针不控制所指向对象的生命周期,它们只是观察 std::shared_ptr 的对象。将循环引用中的一个 std::shared_ptr 替换为 std::weak_ptr 可以打破循环。当只有弱指针指向一个对象时,该对象可以被安全地删除。
在下面的章节会详细讲解该方法。
手动管理内存:
在某些情况下,可能需要回退到使用原始指针和手动管理内存。这通常不是首选方法,因为它容易引入内存泄漏和野指针等问题,但它可以作为一种解决方案,特别是在其他方法不适用或过于复杂时。
使用其他智能指针:
根据具体情况,可以考虑使用其他类型的智能指针,如 std::unique_ptr 或 std::observer_ptr(C++20 中引入),这些智能指针具有不同的所有权语义,可能更适合某些情况。
解决循环引用问题的关键是确保没有路径使得对象的引用计数永远无法降至零。通过仔细设计类的关系和内存管理策略,可以有效地避免循环引用导致的内存泄漏问题。
6 std::weak_ptr 的引入与使用
std::weak_ptr 是 C++11 引入的一种智能指针,主要用于解决 std::shared_ptr 之间的循环引用问题,以避免内存泄漏。与 std::shared_ptr 不同,std::weak_ptr 不控制所指向对象的生命周期,它只是对对象的一个弱引用,不会增加对象的引用计数。
(1)引入
在 C++ 中,智能指针被设计用来自动管理动态分配的内存,以防止内存泄漏。std::shared_ptr 允许多个智能指针共享同一个对象的所有权,当最后一个 std::shared_ptr 被销毁或重置时,它所指向的对象才会被删除。然而,当两个或多个 std::shared_ptr 对象相互引用,形成一个闭环时,它们的引用计数将永远无法降至零,导致内存无法被释放,这就是循环引用问题。
为了解决这个问题,C++11 引入了 std::weak_ptr。它允许程序员创建对 std::shared_ptr 所管理对象的弱引用,但不会增加对象的引用计数。当最后一个 std::shared_ptr 销毁时,即使还有 std::weak_ptr 引用该对象,对象也会被删除。同时,std::weak_ptr 提供了检查其引用的 std::shared_ptr 是否仍然有效的机制,从而避免了悬空指针的问题。
(2)使用
使用 std::weak_ptr 的主要场景是在需要观察 std::shared_ptr 所管理的对象,但又不希望控制其生命周期时。例如,在解决循环引用问题时,可以将其中一个 std::shared_ptr 替换为 std::weak_ptr。
下面是一个使用 std::weak_ptr 解决循环引用的示例:
#include <memory>
#include <iostream>
class B;
class A {
public:
~A() {
std::cout << "A destroyed\n";
}
public:
std::weak_ptr<B> b_ptr; // 使用 weak_ptr 替代 shared_ptr 以解决循环引用问题
};
class B {
public:
~B() {
std::cout << "B destroyed\n";
}
public:
std::shared_ptr<A> a_ptr;
};
int main()
{
{
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;
}
// 此时,a 和 b 之间存在循环引用,但由于 a 使用的是 weak_ptr,
// 因此当离开作用域时,b 会首先被销毁,随后 a 也会被销毁,避免了内存泄漏。
// 输出 "B destroyed" 和 "A destroyed"
return 0;
}
在这个例子中,A 类使用 std::weak_ptr 来引用 B 类的对象,而 B 类使用 std::shared_ptr 来引用 A 类的对象。这样,当 create_objects 函数结束时,即使 a 和 b 之间存在循环引用,由于 a 使用的是弱引用,因此当 b 的 std::shared_ptr 被销毁时,B 的对象会被删除,随后 A 的对象也会因为没有任何 std::shared_ptr 引用它而被删除,从而避免了内存泄漏。
需要注意的是,std::weak_ptr 没有重载 * 和 -> 等运算符,因此不能单独用来访问对象。如果需要访问 std::weak_ptr 所引用的对象,需要首先将其转换为 std::shared_ptr,这可以通过调用 std::weak_ptr 的 lock 方法来实现。如果转换成功(即 std::weak_ptr 仍然有效),lock 方法将返回一个指向对象的 std::shared_ptr;否则,将返回一个空的 std::shared_ptr。