C++智能指针的功能和使用细节
本节我们来深入、全面地探讨现代C++中智能指针的功能、用法、细节和陷阱。
智能指针是C++11及以后版本中最重要的特性之一,它旨在通过自动化资源管理来彻底解决裸指针带来的内存泄漏、悬空指针等问题。
核心思想
所有智能指针都建立在 RAII(Resource Acquisition Is Initialization) 这一核心思想之上:
- 获取资源即初始化: 在构造函数中获取资源(如内存、文件句柄、锁)。
- 释放资源即析构: 在析构函数中自动释放资源。
这意味着资源的生命周期与对象的生命周期完全绑定。只要智能指针对象离开其作用域,它的析构函数就会被调用,从而自动释放其管理的资源。这种机制确保了异常安全。
三大智能指针详解
C++标准库主要提供了三种智能指针,位于 <memory> 头文件中。
1. std::unique_ptr(独占所有权指针)
核心思想:"独一无二的所有权"。同一时间只有一个 unique_ptr 可以拥有和管理一个对象。当该 unique_ptr 被销毁时,它所管理的对象也会被自动销毁。
特性:
不可拷贝: 拷贝构造函数和拷贝赋值运算符被禁用(= delete)。
可移动: 支持移动构造函数和移动赋值运算符。所有权可以从一个 unique_ptr 转移给另一个。
创建与用法:
#include <memory>
#include <iostream>
class MyClass {
public:
MyClass() { std::cout << "MyClass constructed\n"; }
~MyClass() { std::cout << "MyClass destroyed\n"; }
void doSomething() { std::cout << "Doing something!\n"; }
};
int main() {
// 1. 推荐使用 std::make_unique (C++14)
auto ptr1 = std::make_unique<MyClass>();
// 2. 传统构造(不推荐,因为可能因为异常导致内存泄漏)
// std::unique_ptr<MyClass> ptr2(new MyClass());
ptr1->doSomething(); // 使用 -> 操作符
(*ptr1).doSomething(); // 使用 * 解引用
// --- 所有权转移 ---
std::cout << "--- Transferring Ownership ---\n";
auto ptr2 = std::move(ptr1); // ptr1 的所有权转移给 ptr2, ptr1 现在为 nullptr
if (ptr1) { // 重载了 bool() 操作符,可用于判断是否为空
std::cout << "ptr1 still owns the object\n";
} else {
std::cout << "ptr1 is now empty\n"; // 这行会被执行
}
ptr2->doSomething();
// 函数结束时,ptr2 离开作用域,MyClass 对象被自动销毁。
// ptr1 离开作用域,但它已经是 nullptr,什么都不做。
return 0;
}
输出:
MyClass constructed
Doing something!
Doing something!
--- Transferring Ownership ---
ptr1 is now empty
Doing something!
MyClass destroyed
适用场景:
1.默认选择。当你不需要共享所有权时,优先使用 unique_ptr。它开销极小(和裸指针几乎一样),并且语义明确。
2.替代以前需要 new/delete 的大多数场景。
3.作为类的成员变量,表示独占 Composition 关系。
2. std::shared_ptr(共享所有权指针)
核心思想:"共享所有权"。多个 shared_ptr 可以共同拥有和管理同一个对象。系统内部通过引用计数来跟踪有多少个 shared_ptr 指向该对象。当最后一个指向该对象的 shared_ptr 被销毁或重置时,对象才会被销毁。
特性:
可拷贝,可移动: 拷贝一个 shared_ptr 会增加引用计数。
有开销: 需要维护一个控制块(包含引用计数、弱计数、删除器等),大小通常是裸指针的两倍。
创建与用法:
#include <memory>
#include <iostream>
class MyClass {
public:
MyClass(int val) : value(val) { std::cout << "MyClass " << value << " constructed\n"; }
~MyClass() { std::cout << "MyClass " << value << " destroyed\n"; }
int value;
};
void use_shared_ptr(std::shared_ptr<MyClass> sp) {
std::cout << "In function: use count = " << sp.use_count() << "\n"; // 引用计数为 2
}
int main() {
// 1. 推荐使用 std::make_shared(更高效,单次分配内存)
auto sp1 = std::make_shared<MyClass>(42);
std::cout << "After creation: use count = " << sp1.use_count() << "\n"; // 输出 1
{
auto sp2 = sp1; // 拷贝构造,引用计数 +1 => 2
std::cout << "After copy: use count = " << sp1.use_count() << "\n"; // 输出 2
sp2->value = 100;
use_shared_ptr(sp1); // 传参时再次拷贝,函数内部计数为 3
} // sp2 离开作用域被销毁,引用计数 -1 => 1
std::cout << "After block: use count = " << sp1.use_count() << "\n"; // 输出 1
// 2. 传统构造(不推荐)
// std::shared_ptr<MyClass> sp3(new MyClass(7));
// 函数结束,sp1 离开作用域,引用计数 -1 => 0,对象被销毁。
return 0;
}
输出:
MyClass 42 constructed
After creation: use count = 1
After copy: use count = 2
In function: use count = 3
After block: use count = 1
MyClass 100 destroyed
适用场景:
1.需要多个对象共同管理同一个资源的生命周期,且无法确定哪个对象最后使用它。
2.放入容器中管理一组动态分配的对象。
3.实现类似观察者模式(Observer Pattern)的注册回调机制。
3. std::weak_ptr(弱引用指针)
核心思想:"不拥有所有权的观察者"。它指向一个由 shared_ptr 管理的对象,但不增加其引用计数。它用于解决 shared_ptr 的循环引用问题。
特性:
不控制生命周期: 不会阻止所指向对象的销毁。
必须从 shared_ptr 或另一个 weak_ptr 创建。
不能直接访问资源,必须通过 lock() 方法转换为一个 shared_ptr 才能使用。
创建与用法(解决循环引用):
#include <memory>
#include <iostream>
class B; // 前向声明
class A {
public:
std::shared_ptr<B> b_ptr;
~A() { std::cout << "A destroyed\n"; }
};
class B {
public:
// 关键点:这里使用 weak_ptr 而不是 shared_ptr 来打破循环引用
std::weak_ptr<A> a_weak_ptr;
~B() { std::cout << "B destroyed\n"; }
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b; // A 强引用 B (B的引用计数为2)
b->a_weak_ptr = a; // B 弱引用 A (A的引用计数仍为1)
// 函数结束,a 和 b 离开作用域
// a 的引用计数减为 0,A 被销毁。
// A 销毁导致其成员 b_ptr 被销毁,B 的引用计数从 2 减为 1。
// 然后 b 的引用计数减为 0,B 被销毁。
// 如果没有使用 weak_ptr,而是用了 shared_ptr,则循环引用导致两者都无法销毁(内存泄漏)。
return 0;
}
输出:
A destroyed
B destroyed
weak_ptr 的另一个常见用法:临时访问
void use_if_alive(std::weak_ptr<MyClass> weak_obj) {
// 使用 lock() 尝试获取一个 shared_ptr
if (auto shared_obj = weak_obj.lock()) {
// 对象还存在,可以安全使用 shared_obj
std::cout << "Value: " << shared_obj->value << "\n";
} else {
// 对象已被销毁
std::cout << "Object is no longer alive.\n";
}
}
适用场景:
1.打破 shared_ptr 的循环引用。这是其最主要的作用。
2.实现缓存:缓存持有对象的弱引用。当需要时,如果对象还在缓存中(未被销毁),则提升使用;如果已被销毁,则重新加载。
3.观察者列表:观察者不应该影响被观察者的生命周期。
细节与实践
1.优先使用 make_unique 和 make_shared:
更安全: 避免了显式使用 new,防止因异常导致的内存泄漏。
// 危险:如果 process() 抛出异常,new 出来的内存会泄漏
process(std::shared_ptr<MyClass>(new MyClass), some_function());
// 安全:make_shared 和参数评估是原子的
process(std::make_shared<MyClass>(), some_function());
更高效: make_shared 只需一次内存分配(将对象本身和控制块分配在一起),而 new + shared_ptr 构造需要两次。
2.不要混用裸指针和智能指针:
MyClass* raw_ptr = new MyClass();
std::shared_ptr<MyClass> sp1(raw_ptr);
// std::shared_ptr<MyClass> sp2(raw_ptr); // 灾难!两个独立的 sp 都会试图 delete raw_ptr
一旦将裸指针交给智能指针,就不要再自己用这个裸指针,更不要用它创建另一个智能指针。
3.不要使用 get() 获取的裸指针来创建新的智能指针:
get() 方法返回管理的裸指针,但它不转让所有权。
auto sp = std::make_shared<MyClass>();
MyClass* p = sp.get();
{
std::shared_ptr<MyClass> sp2(p); // 错误!p 的生命周期由 sp 管理,sp2 不知道
} // 此处 sp2 析构,会 delete p!
// 现在 sp 变成了悬空指针,后续使用或析构时会导致未定义行为(通常是崩溃)
避免循环引用:这是 shared_ptr 的致命弱点,务必使用 weak_ptr 来破解。
4.性能考量:
unique_ptr 几乎无开销,可默认使用。
shared_ptr 有引用计数的原子操作开销和控制块的内存开销,不要滥用。
5.管理数组:
std::unique_ptr 支持数组:std::unique_ptr<MyClass[]> arr(new MyClass[10]);,它会调用 delete[]。
std::shared_ptr 不直接支持数组(C++17前)。你需要提供自定义删除器:std::shared_ptr<MyClass> arr(new MyClass[10], std::default_delete<MyClass[]>());。
更好的选择:对于动态数组,几乎总是应该优先使用 std::vector,它内部已经用智能指针很好地管理了内存。

更多推荐


所有评论(0)