1 为什么要使用智能指针
在传统的C++中,动态内存的管理完全由程序员手动管理(new 和 delete)。这会带来一个巨大的风险:内存泄露。
手动管理的痛点
忘记
delete: 尤其是在函数提前返回或抛出异常时,很容易漏掉delete。重复
delete: 对同一个指针delete多次会导致未定义行为(通常是程序崩溃)。难以跟踪所有权: 很难清晰地知道哪个函数或对象负责释放一块内存。
解决方案:RAII (Resource Acquisition Is Initialization)
智能指针是 RAII 思想的典型应用:
将资源(内存)的生命周期与对象的生命周期绑定。
在构造函数中获取资源(初始化)。
在析构函数中释放资源。
这样,只要智能指针对象离开其作用域(无论是正常离开还是因为异常),它的析构函数就会被自动调用,从而确保资源被安全释放。
2 C++ 11 中的四种智能指针
C++11 在 <memory> 头文件中提供了四种智能指针:
std::unique_ptr(C++11)std::shared_ptr(C++11)std::weak_ptr(C++11)std::auto_ptr(C++17 中移除,已废弃,不做讨论)
2.1 独占指针 std::unique_ptr
核心思想:
独占所有权。一个 unique_ptr 在任何时候都唯一地拥有其指向的对象。它不能被拷贝,只能被移动(std::move)。当 unique_ptr 被销毁时,它指向的对象也会被销毁。
轻量高效: 开销很小,通常与裸指针相同。
禁止拷贝: 拷贝构造函数和拷贝赋值运算符被标记为
= delete。支持移动: 可以通过
std::move转移所有权。
创建方式
#include <memory>
// 方式一:推荐使用 std::make_unique (C++14)
std::unique_ptr<int> u1 = std::make_unique<int>(42);
auto u2 = std::make_unique<std::string>("Hello");
// 方式二:直接构造(不推荐,可能涉及不必要的内存分配)
std::unique_ptr<int> u3(new int(100));代码示例
{
auto ptr = std::make_unique<MyClass>(); // 创建
ptr->doSomething(); // 使用 -> 操作符
(*ptr).doSomething(); // 使用 * 操作符解引用
// std::unique_ptr<int> ptr2 = ptr; // 错误!不能拷贝
std::unique_ptr<int> ptr2 = std::move(ptr); // 正确!转移所有权
// 现在 ptr 变为 nullptr,ptr2 拥有资源
} // 作用域结束,ptr2 被销毁,它管理的 MyClass 对象自动被 delete2.2 共享指针 std::shared_ptr
核心思想
共享所有权。多个 shared_ptr 可以指向同一个对象,并通过一个引用计数器来协同管理对象的生命周期。每当一个新的 shared_ptr 被创建来指向该对象时,计数器加1;每当一个 shared_ptr 被销毁或重置时,计数器减1。当计数器变为0时,对象被自动删除。
支持拷贝和赋值。
有额外开销: 需要维护一个控制块(包含引用计数、弱计数、删除器等),内存和性能开销比
unique_ptr大。不是线程安全的: 引用计数的增减是原子操作(线程安全的),但指向的对象本身不是线程安全的。
创建方式
// 方式一:推荐使用 std::make_shared (更高效,单次分配内存)
auto s1 = std::make_shared<MyClass>();
std::shared_ptr<int> s2 = std::make_shared<int>(42);
// 方式二:直接构造
std::shared_ptr<MyClass> s3(new MyClass);
// 方式三:通过另一个 shared_ptr 拷贝
auto s4 = s1;代码示例
void func(std::shared_ptr<MyClass> sp) {
// 引用计数+1
sp->doSomething();
} // 函数结束,sp 析构,引用计数-1
auto mainSp = std::make_shared<MyClass>(); // 引用计数 = 1
func(mainSp); // 传入时引用计数+1(=2),函数返回后-1(=1)
std::cout << mainSp.use_count() << std::endl; // 输出 1
{
auto anotherSp = mainSp; // 引用计数+1(=2)
} // anotherSp 析构,引用计数-1(=1)
} // mainSp 析构,引用计数-1(=0),对象被删除2.3 弱共享指针 std::weak_ptr
核心思想:
弱引用。weak_ptr 是为了解决 shared_ptr 的循环引用问题而设计的。它指向一个由 shared_ptr 管理的对象,但不增加其引用计数。它不能直接访问对象,需要先转换为 shared_ptr。
不拥有对象,不控制生命周期。
用于打破
shared_ptr的循环引用。必须从
shared_ptr或另一个weak_ptr创建。
循环引用问题
两个或多个对象通过 shared_ptr 互相持有,导致引用计数永远无法降为0。
class B;
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyed" << std::endl; }
};
class B {
public:
std::shared_ptr<A> a_ptr; // 这里造成了循环引用
~B() { std::cout << "B destroyed" << std::endl; }
};
void cycle() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b; // A 引用 B,B 的引用计数=2
b->a_ptr = a; // B 引用 A,A 的引用计数=2
} // 函数结束,a 和 b 析构,但 A 和 B 的引用计数都只减为1,无法释放!内存泄漏!使用 weak_ptr 解决
分析对象关系,将其中一方持有另一方的 shared_ptr 改为 weak_ptr。
class B;
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyed" << std::endl; }
};
class B {
public:
std::weak_ptr<A> a_ptr; // 将 shared_ptr 改为 weak_ptr
~B() { std::cout << "B destroyed" << std::endl; }
};
void no_cycle() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a; // weak_ptr 不会增加 A 的引用计数,A 的计数仍为1
} // 函数结束,a 析构,A 计数减为0,A 被销毁。然后 b 析构,B 计数减为0,B 被销毁。如何使用 weak_ptr 访问对象
auto shared_p = std::make_shared<int>(10);
std::weak_ptr<int> weak_p = shared_p;
// 方法一:使用 lock(),返回一个 shared_ptr,如果对象存在则有效
if (auto temp_sp = weak_p.lock()) {
std::cout << *temp_sp << std::endl; // 对象存在,可以访问
} else {
std::cout << "Object has been destroyed" << std::endl;
}
// 方法二:使用 expired(),检查对象是否已被销毁(不推荐,因为检查和使用非原子操作)
// if (!weak_p.expired()) { ... } // 可能刚检查完对象就被另一个线程释放了由 lock() 返回的 shared_ptr 会暂时增加引用计数,从而锁定资源。当你不再需要访问该资源时,应该让这个临时的 shared_ptr 尽快离开作用域以减少引用计数,避免不必要的资源占用。
3 扩展
make_shared 和直接 new 的区别
性能:
make_shared通常只进行一次内存分配,同时分配对象和控制块。而shared_ptr<T>(new T)会进行两次分配(一次给对象,一次给控制块),效率更低。异常安全:
make_shared是异常安全的。例如func(std::shared_ptr<T>(new T), other_func()),如果other_func()抛出异常,new T分配的内存可能会泄漏。而func(std::make_shared<T>(), other_func())则不会。
智能指针的大小开销
unique_ptr: 通常与裸指针大小相同。shared_ptr: 通常是裸指针的两倍大小(一个指向对象,一个指向控制块)。weak_ptr: 通常与shared_ptr大小相同。
评论区