自动管理资源的语言(Java,python)通常内置垃圾回收机制,能够自动识别不再使用的对象并释放它们占用的资源。垃圾回收器负责清理未被引用的对象,所以使用这类语言的程序员不需要手动管理每个对象的生命周期,减少了出错的可能性。虽然方便,但这意味资源的具体回收时间点是不确定的,这可能会影响到性能敏感的应用。
C++更倾向于拥有对资源生命周期的精细控制,这样可以更好地优化性能和资源利用。通过显式地定义构造函数、析构函数以及使用RAII技术,C++程序可以在特定的时间点准确地释放资源。但是这种级别的控制也增加了代码的复杂度,并且需要程序员更加小心以防止资源泄露等问题。
为了解决上述两种观点之间的矛盾,C++11引入了std::shared_ptr智能指针。std::shared_ptr结合了自动化管理和可预测销毁的优点:
(1)共享所有权:多个std::shared_ptr可以指向同一个对象,每个std::shared_ptr都持有该对象的所有权。
(2)自动销毁:当最后一个指向某个对象的std::shared_ptr被销毁或重新分配给另一个对象时,该对象就会被自动销毁。
std::shared_ptr的基本机制
引用计数:std::shared_ptr通过维护一个引用计数来跟踪有多少个std::shared_ptr实例指向同一个资源。每当一个新的std::shared_ptr被创建或复制时,该计数递增;当一个std::shared_ptr被销毁或重置为指向其他对象时,该计数递减。一旦引用计数降至0,没有活跃的std::shared_ptr再指向该资源,则会自动调用资源的析构函数并释放内存。
性能考量
大小增加
与普通指针相比,std::shared_ptr通常需要额外的空间来存储引用计数的信息,因此其占用的内存通常是原始指针的两倍左右。std::shared_ptr内部不仅包含了一个指向实际资源的指针,还包含了一个指向引用计数结构的指针。
动态内存分配
引用计数本身是存储在独立于所管理对象的内存块中的。这意味着即使是很小的对象也可能导致一次额外的内存分配操作,以存放这个计数器。这增加内存使用的复杂性和可能的碎片化问题。不过,使用std::make_shared可以优化这一点,因为它允许在一个单独的内存分配中同时创建std::shared_ptr和它所管理的对象,从而减少内存分配次数。
线程安全的引用计数更新
在多线程程序中,多个线程可能同时尝试修改同一个std::shared_ptr对象的引用计数。例如,一个线程可能正在销毁一个std::shared_ptr实例(这会导致引用计数递减),而另一个线程可能正在创建一个新的std::shared_ptr实例指向同一个资源(这会导致引用计数递增)。如果这些操作不是原子性的,那么就可能发生数据竞争,即两个或多个线程同时读取和写入相同的内存位置,导致不确定的行为。为了保证引用计数的一致性和正确性,每次递增或递减操作都必须是原子的,这意味着在任何给定时间点,只有一个线程能够执行这些操作。
shared_ptr移动构造/复制
如果使用移动构造函数或移动赋值运算符从另一个std::shared_ptr中移动资源(而不是复制),则不会增加引用计数。相反,源std::shared_ptr将被设置为nullptr,而新的std::shared_ptr将接管对资源的所有权。移动构造函数和移动赋值运算符使得std::shared_ptr能够更高效地转移所有权,因为它们避免了引用计数的增减操作。这允许std::shared_ptr在某些情况下比使用拷贝构造函数或拷贝赋值运算符更快,尤其是在频繁进行所有权转移的情景下。
shared_ptr 自定义删除器
std::shared_ptr支持自定义删除器,允许用户指定如何释放所管理的对象。与std::unique_ptr不同的是,对于std::shared_ptr来说,删除器类型不是智能指针类型的一部分。这意味着即使两个std::shared_ptr实例拥有不同的删除器,它们仍然可以共享相同的类型,并且可以相互赋值或者一起存储在容器中。
代码展示使用lambda表达式作为删除器:
auto loggingDel = [](Widget *pw) { makeLogEntry(pw); delete pw; };
std::shared_ptr<Widget> spw(new Widget, loggingDel);auto customDeleter1 = [](Widget *pw){…};//自定义删除器,
auto customDeleter2 = [](Widget *pw){…};//每种类型不同
std::shared_ptr<Widget> pw1(new Widget,customDeleter1);
std::shared_ptr<Widget> pw2(new Widget,customDeleter2);
std::vector<std::shared_ptr<Widget>> vpw{ pw1, pw2 };
shared_ptr 控制块
另一个不同于std::unique_ptr
的地方是,指定自定义删除器不会改变std::shared_ptr
对象的大小。不管删除器是什么,一个std::shared_ptr
对象都是两个指针大小。自定义删除器可以是函数对象,函数对象可以包含任意多的数据。std::shared_ptr
怎么能引用一个任意大的删除器而不使用更多的内存?它不能。它必须使用更多的内存。然而那部分内存不是std::shared_ptr
对象的一部分。那部分在堆上面,std::shared_ptr
创建者利用std::shared_ptr
对自定义分配器的支持能力,那部分内存随便在哪都行。前面提到了std::shared_ptr
对象包含了所指对象的引用计数的指针有点误导人。因为引用计数是另一个更大的数据结构的一部分,那个数据结构通常叫做控制块(control block)。每个std::shared_ptr
管理的对象都有个相应的控制块。控制块除了包含引用计数值外还有一个自定义删除器的拷贝,当然前提是存在自定义删除器。如果用户还指定了自定义分配器,控制块也会包含一个分配器的拷贝。
控制块的创建规则
- 使用std::make_shared:总是创建一个新的控制块。这是因为std::make_shared不仅分配内存给对象本身,还为控制块分配内存,并且初始化引用计数。
- 从独占指针(如std::unique_ptr)构造:创建新的控制块。因为独占指针没有共享的控制块,所以转换成std::shared_ptr时需要创建一个。
- 从原始指针构造:同样会创建新的控制块。如果有一个已经存在的对象,并希望创建指向它的std::shared_ptr,则应该确保只有一个控制块被创建。如果尝试从同一个原始指针创建多个std::shared_ptr,将会导致每个std::shared_ptr都有自己的控制块,从而导致对象被多次销毁。
auto pw = new Widget; //创建原始指针
...
std::shared_ptr<Widget> spw1(pw, loggingDel); // 为*pw创建第一个控制块
...
std::shared_ptr<Widget> spw2(pw, loggingDel); // 为*pw创建第二个控制块
这段代码的问题在于它创建了两个独立的std::shared_ptr,每个都拥有自己的控制块。这将导致当两个std::shared_ptr都离开作用域或被重置时,它们都会尝试删除同一个对象,造成双重释放。
正确的方法
使用std::make_shared:
auto spw1 = std::make_shared<Widget>(); // 使用std::make_shared创建
如果必须使用原始指针和自定义删除器,应直接使用new表达式的结果,而不是存储后的指针变量:
std::shared_ptr<Widget> spw1(new Widget, loggingDel); // 直接使用new结果
当你需要另一个std::shared_ptr来共享所有权时,通过拷贝现有的std::shared_ptr来避免创建新的控制块:
std::shared_ptr<Widget> spw2(spw1); //使用spw1的控制块.
在C++中,this指针是一个指向当前对象的指针。如果你尝试将this指针直接传递给std::shared_ptr的构造函数,可能会导致多重所有权问题,尤其是在Widget对象已经被std::shared_ptr管理的情况下。为了避免这种情况,可以使用std::weak_ptr或者确保通过std::shared_ptr来引用对象。
class Widget {
public:void process() {// ...processedWidgets.emplace_back(this); // 错误地使用this指针}
};std::vector<std::shared_ptr<Widget>> processedWidgets;
问题:this是一个原始指针,而processedWidgets中的元素是std::shared_ptr<Widget>。如果Widget对象已经被std::shared_ptr管理,那么将this传递给emplace_back会创建一个新的std::shared_ptr,这可能导致多重控制块问题(即多个std::shared_ptr管理同一个对象)。
(1)使用std::weak_ptr
#include <memory>
#include <vector>
class Widget {
public:void process(){processedWidgets.emplace_back(std::weak_ptr<Widget>(std::static_pointer_cast<Widget>(self)));
}private:std::weak_ptr<Widget> self; // 保存一个弱引用
};
std::vector<std::weak_ptr<Widget>> processedWidgets;
self是一个std::weak_ptr<Widget>,它不会增加引用计数。std::weak_ptr用于记录Widget对象,但不会影响对象的生命周期。std::static_pointer_cast用于将self转换为std::shared_ptr<Widget>,以便将其存储在processedWidgets中。
(2)通过std::shared_ptr引用对象
class Widget {
public:void process(std::shared_ptr<Widget> self) {//...processedWidgets.push_back(self);}
};
std::vector<std::shared_ptr<Widget>> processedWidgets;
int main() {auto widget = std::make_shared<Widget>();widget->process(widget); // 传递self
}
process方法接受一个std::shared_ptr<Widget>参数self。self是对Widget对象的std::shared_ptr引用,这样可以确保processedWidgets中的std::shared_ptr不会引起多重控制块问题。在调用process方法时,传递widget这个std::shared_ptr。
使用enable_shared_from_this
直接使用 this 指针创建新的 std::shared_ptr 可能会导致多重控制块问题,因为每个新创建的 std::shared_ptr 都会尝试创建自己的控制块。通过继承 std::enable_shared_from_this,类可以调用 shared_from_this() 成员函数来获取当前对象的 std::shared_ptr,而不会创建新的控制块。在类的成员函数中,可以通过调用 shared_from_this() 来获取指向当前对象的 std::shared_ptr。shared_from_this() 会查找当前对象已有的控制块,并返回一个新的 std::shared_ptr,该指针与现有控制块关联。调用 shared_from_this() 之前,必须已经有一个 std::shared_ptr 管理着当前对象。如果没有这样的 std::shared_ptr 存在,调用 shared_from_this() 将抛出异常。
私有构造函数和工厂函数:
为了确保对象总是通过 std::shared_ptr 创建,通常将类的构造函数设为私有,并提供一个静态工厂函数来创建并返回 std::shared_ptr。这样可以防止客户端直接创建对象,从而保证对象总是被 std::shared_ptr 管理。
#include <memory>
#include <vector>
class Widget : public std::enable_shared_from_this<Widget> {
public:// 工厂函数,用于创建并返回一个 std::shared_ptr<Widget>template<typename... Ts>static std::shared_ptr<Widget> create(Ts&&... params) {auto widget = std::shared_ptr<Widget>(new Widget(std::forward<Ts>(params)...));return widget;}void process() {// 处理 Widget// 使用 shared_from_this() 获取指向当前对象的 std::shared_ptrprocessedWidgets.emplace_back(shared_from_this());}
private:// 私有构造函数Widget(/* 构造参数 */) {// 初始化}
};
// 存储已处理的 Widget 对象
std::vector<std::shared_ptr<Widget>>processedWidgets;
int main() {// 通过工厂函数创建 Widget 对象auto widget = Widget::create(/* 参数 */);// 调用 process 方法widget->process();// 现在 processedWidgets 中包含了一个指向 widget 的 std::shared_ptrreturn 0;
使用建议
(1)默认配置:使用默认删除器和默认分配器,并通过 std::make_shared 创建 std::shared_ptr 时,控制块的大小和开销都较小。
(2)独占资源:如果不需要共享所有权,使用 std::unique_ptr 更合适,因为它具有接近原始指针的性能,并且可以从 std::unique_ptr 转换为 std::shared_ptr,反之则不行。
(3)std::shared_ptr 通过引用计数自动管理对象的生命周期,提供了强大的共享所有权功能。尽管有控制块、虚函数和原子操作带来的开销,但在大多数情况下,这些开销是可以接受的。对于不需要共享所有权的情况,应优先考虑使用 std::unique_ptr。
(4)使用 std::shared_ptr 管理数组时,应使用 std::shared_ptr<T[]>,而不是尝试用 std::shared_ptr<T> 加自定义删除器来管理数组。