在C++中,智能指针是一类模板类,用于管理指向动态分配(堆分配)对象的指针。使用智能指针可以帮助防止内存泄漏,自动释放不再使用的对象。在本文中,我们将会对C++11标准库中所包含的三种主要的智能指针:std::unique_ptr
、std::shared_ptr
和std::weak_ptr
展开探讨。
std::unique_ptr
std::unique_ptr
是一种独占所有权的智能指针,意味着任何时候只能有一个unique_ptr
指向一个给定的对象。当unique_ptr
被销毁时,它指向的对象也会被自动销毁。这种指针非常适合用于保证资源在作用域结束时被释放。
cpp#include <memory>
void useUniquePtr() {
std::unique_ptr<int> ptr(new int(10)); // 独占指向一个int值
std::cout << *ptr << std::endl; // 输出10
// 当ptr离开作用域时,自动释放内存
}
unique_ptr
不能被复制,只能被移动,这确保了其独占所有权的语义:
cppstd::unique_ptr<int> ptr1(new int(10));
std::unique_ptr<int> ptr2 = std::move(ptr1); // 现在ptr2拥有资源,ptr1为空
std::make_unique
std::make_unique
是 C++14 引入的一个实用函数,用于创建一个 std::unique_ptr
。相比直接使用 new
来构造 std::unique_ptr
要更加安全和简洁。
代码示例:
c++#include <memory>
#include <iostream>
class Test {
public:
Test() { std::cout << "Test Created\n"; }
~Test() { std::cout << "Test Destroyed\n"; }
};
int main() {
std::unique_ptr<Test> ptr = std::make_unique<Test>();
return 0;
}
new
更加安全?假设你有一个函数,它需要两个 std::unique_ptr
作为参数:
cppvoid process(std::unique_ptr<Test> a, std::unique_ptr<Test> b);
如果你调用这个函数,使用 new
操作符直接创建 unique_ptr
:
cppprocess(std::unique_ptr<Test>(new Test()), std::unique_ptr<Test>(new Test()));
以下情况可能发生:
new Test()
调用成功,分配了内存。new Test()
调用发生在第一个调用之后,这是编译器决定的参数评估顺序。new Test()
在创建对象时抛出异常(比如因为内存不足),那么第一个 new Test()
已经分配的内存将没有机会被 std::unique_ptr
接管,因为 std::unique_ptr
的构造函数还没有被调用。这就导致了第一个 new Test()
的内存泄漏,因为异常发生时,没有任何机制来释放已经成功分配的内存。
使用 std::make_unique
:
而 make_unique
则封装了内存分配和对象构造的过程,在内部创建对象后立即将其封装在 unique_ptr
中,使整个操作成为一个原子操作,避免了上述问题,从而提供了异常安全保障。
cppprocess(std::make_unique<Test>(), std::make_unique<Test>());
在这个例子中:
std::make_unique<Test>()
都是一个独立的表达式,先分配内存并构造对象,然后立即将其封装在 unique_ptr
中。process
之前,任何一个 make_unique
调用失败(例如,由于抛出异常),已经成功创建的 unique_ptr
将会自动释放其管理的资源。这样,即使出现异常,也不会导致内存泄漏。这就是为什么 std::make_unique
被认为提供了更好的异常安全性:它通过确保即使在异常情况下也能正确管理资源,避免了内存泄漏的风险。
因此推荐使用 std::make_unique
来创建 unique_ptr
。
std::shared_ptr
std::shared_ptr
是一种共享所有权的智能指针。多个shared_ptr
可以指向同一个对象,内部使用引用计数来跟踪有多少个指针指向同一个资源。当最后一个这样的指针被销毁时,指向的对象也会被自动销毁。
cppvoid useSharedPtr() {
std::shared_ptr<int> ptr1(new int(20));
{
std::shared_ptr<int> ptr2 = ptr1; // ptr1和ptr2共享所有权
std::cout << *ptr2 << std::endl; // 输出20
} // ptr2离开作用域,但对象不会被删除因为ptr1还在指向它
std::cout << *ptr1 << std::endl; // 输出20
} // 离开ptr1的作用域,对象被自动删除
也可以使用make_shared
创建一个std::shared_ptr
,相比new
来说也会更加安全。同时可以使用use_count
方法返回指向当前对象的 shared_ptr
实例的数量。这是通过引用计数机制实现的,该机制跟踪有多少个 shared_ptr
实例共享同一个对象。
cpp#include <iostream>
#include <memory>
class Test {
public:
Test() { std::cout << "Test created\n"; }
~Test() { std::cout << "Test destroyed\n"; }
};
int main() {
auto ptr1 = std::make_shared<Test>();
std::cout << "Use count after ptr1 creation: " << ptr1.use_count() << std::endl;
{
auto ptr2 = ptr1; // 复制 shared_ptr,增加引用计数
std::cout << "Use count after ptr2 creation: " << ptr1.use_count() << std::endl;
} // ptr2 离开作用域,引用计数减少
std::cout << "Use count after ptr2 goes out of scope: " << ptr1.use_count() << std::endl;
}
std::shared_ptr
的实现原理shared_ptr
使用一个引用计数器来跟踪有多少个 shared_ptr
实例指向同一个对象。当你创建一个 shared_ptr
时,计数器会初始化为1。当新的 shared_ptr
通过拷贝构造函数或赋值运算符复制现有的 shared_ptr
时,这个计数器会增加。当一个 shared_ptr
被销毁(例如,它离开了作用域)时,计数器会减少。当计数器回到零时,指向的对象和计数器本身会被销毁。
为了管理这个计数,shared_ptr
使用一个称为“控制块”的内部结构。这个控制块通常包含:
shared_ptr
的数量)weak_ptr
的数量)控制块是在堆上分配的。这是因为控制块的生命周期需要独立于任何单个 shared_ptr
实例,而且必须在最后一个引用(无论是 shared_ptr
还是 weak_ptr
)被销毁后,控制块也能正确地维护引用计数。
std::weak_ptr
std::weak_ptr
是一种不拥有资源的智能指针,主要用来解决shared_ptr
相互引用导致的循环引用问题。weak_ptr
指向一个由shared_ptr
管理的对象,但不会增加对象的引用计数。这意味着weak_ptr
不会阻止它指向的对象被销毁。
cppstd::shared_ptr<int> ptr1(new int(30));
std::weak_ptr<int> weakPtr = ptr1; // 创建一个weak_ptr指向同一个int
{
if (std::shared_ptr<int> ptr2 = weakPtr.lock()) { // 如果对象还存在,获取一个shared_ptr
std::cout << *ptr2 << std::endl; // 输出30
}
}
ptr1.reset(); // 销毁由ptr1管理的int对象
if (std::shared_ptr<int> ptr2 = weakPtr.lock()) { // 尝试获取shared_ptr失败
std::cout << "Object exists";
} else {
std::cout << "Object has been destroyed"; // 输出这个消息
}
本文作者:Rowlet
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!