C++ 智能指针:示例、原理与适用场景详解
智能指针的设计初衷是为了解决原生指针带来的内存管理问题。要理解它的作用,得先看看原生指针有哪些'痛点'。
原生指针的'痛'
原生指针是 C++ 里的'双刃剑',底层灵活但也容易出错。在引入智能指针之前,裸指针常引发以下典型问题:
- 内存泄漏:C++ 没有垃圾回收机制,全靠程序员手动
delete。一旦忘记或逻辑跳转跳过释放操作,堆内存就会泄露。 - 野指针:包括悬空指针、未初始化指针、指向已销毁栈内存的指针等。它们指向无效地址,访问会导致未定义行为。
- 双重释放:对同一指针重复调用
delete,属于未定义行为,大概率导致崩溃。 - 指向超出作用域的栈内存:函数返回局部变量的地址,函数结束后栈帧弹出,指针即失效。
- 容器里存裸指针但未清理:
std::vector<int*>存储裸指针时,容器析构不会自动删除内部元素,需手动遍历清理。 - 指针重定向导致原对象丢失:指针重新赋值后,原对象失去引用且无法找回。
- 异常导致释放被跳过:抛出异常时若未捕获并释放资源,会造成泄漏。
代码示例
#include <iostream>
using namespace std;
// 指向超出作用域的栈内存
int* getNum() {
int a = 10; // a 在栈上,函数结束后销毁
return &a; // 返回 a 的地址
}
int main() {
int* p = getNum();
cout << *p << endl; // 访问野指针,结果不可控
return 0;
}
// 双重释放
int* p = new int(10);
delete p;
delete p; // 未定义行为
智能指针的核心思想
上述问题的根源在于:指针只是一个地址值,它没有其指向对象的'所有权'。这导致了指针和对象在生命周期上不一致。
智能指针通过封装原生指针,利用控制块记录引用计数,确保在最后一个实例销毁时同步销毁目标对象。它重载了运算符,模拟指针的使用习惯,让对象像指针一样使用。
建议在实际场景中结合业务背景选择智能指针类型,不要过度纠结语义推导。从熟悉场景(如树形结构)反向体会会更清晰。
C++ 标准库提供了三种主要智能指针:shared_ptr、unique_ptr、weak_ptr。auto_ptr 已在 C++98 后被弃用。
shared_ptr:共享所有权
shared_ptr 允许多个实例指向同一对象,当最后一个实例销毁时自动释放资源。它是使用范围最广的智能指针,类似于 Java 的引用类型,但实现机制完全不同。
简单示例
int main() {
// 注意:p1 是直接初始化的,创建在栈上
shared_ptr<int> p1(new int(10));
// 复制一份,引用计数 +1
shared_ptr<int> p2 = p1;
cout << "p1 use_count : " << p1.use_count() << endl; // 输出:2
cout << "p2 use_count : " << p2.use_count() << endl; // 输出:2
p2 = shared_ptr<int>(new int(6)); // p2 指向新对象,旧对象计数 -1
cout << "p1 use_count : " << p1.use_count() << endl; // 输出:1
cout << "p2 use_count : " << p2.use_count() << endl; // 输出:1
return 0;
}
关键点:智能指针对象本身应创建在栈上,利用栈退出自动析构的特性省去了显式 delete。
实现原理
核心是引用计数。复制指针时计数 +1,销毁或重定向时计数 -1。计数存放在堆上的控制块中,所有指向同一对象的智能指针共享该控制块。
图 1. shared_ptr 内部实现结构
适用场景
当多个组件都需要访问同一份数据,且谁都不是唯一所有者时(如 Cache、Config、Session),适合使用 shared_ptr。
UI 组件案例:在 UI 系统中,父组件拥有子组件,但如果布局系统、事件系统也需要持有组件指针,则必须使用 shared_ptr,否则组件可能在其他系统使用前就被销毁。
class Widget {
public:
// 使用 shared_ptr 是因为事件、布局等其他系统也需要使用组件
std::vector<std::shared_ptr<Widget>> children;
};
unique_ptr:独占所有权
unique_ptr 同一时刻只允许有一个实例,但支持移动语义(move),可以将所有权移交。
简单示例
#include <iostream>
#include <memory>
class File {
public:
File(const std::string &name) { std::cout << "Open file: " << name << std::endl; }
~File() { std::cout << "Close file" << std::endl; }
void write() { std::cout << "Writing..." << std::endl; }
};
void processFile(std::unique_ptr<File> file) {
file->write();
}
int main() {
// 创建文件对象和它的独占指针
std::unique_ptr<File> f = std::make_unique<File>("data.txt");
// unique_ptr 不允许拷贝
// std::unique_ptr<File> f2 = f; // ❌ 编译错误
processFile(std::move(f)); // 转移所有权
if (!f) std::cout << "f no longer owns the file\n";
return 0;
}
程序输出显示文件在 processFile 返回时自动关闭,因为所有权已移交。
适用场景
默认情况下优先使用 unique_ptr。如果明确知道对象属于且只属于一个对象,或者工厂模式返回对象所有权给调用者,都应首选 unique_ptr。
weak_ptr:弱引用观察
weak_ptr 不拥有对象所有权,仅观察生命周期。它通常配合 shared_ptr 使用,解决循环引用问题。
简单示例
int main() {
auto p1 = make_shared<int>(10);
weak_ptr<int> p2(p1); // 创建弱引用
cout << "p1 use_count is: " << p1.use_count() << endl; // 输出:1
// weak_ptr 不影响 shared_ptr 的引用计数
return 0;
}
正确用法:使用 lock() 方法获取有效的 shared_ptr 后再操作,避免对象意外销毁。
if (auto ptr = w_ptr.lock()) {
// lock() 返回 shared_ptr,为空则表示目标对象已销毁
// do something...
} else {
cout << "A 对象已销毁" << endl;
}
循环依赖问题
weak_ptr 的主要用途是解除 shared_ptr 的循环引用导致的内存泄漏。
问题分析
如果 A 持有 B 的 shared_ptr,B 又持有 A 的 shared_ptr,双方引用计数永远无法归零,导致内存泄漏。
class B; class A;
class A {
public:
int v;
shared_ptr<B> b;
A(int val) : v(val) { cout << "A 构造函数" << endl; }
~A() { cout << "A 析构函数" << endl; }
};
class B {
public:
int v;
shared_ptr<A> a;
B(int val) : v(val) { cout << "B 构造函数" << endl; }
~B() { cout << "B 析构函数" << endl; }
};
int main() {
shared_ptr<A> ptrA = make_shared<A>(10);
shared_ptr<B> ptrB = make_shared<B>(20);
ptrA->b = ptrB; // A 持有 B
ptrB->a = ptrA; // B 持有 A
return 0; // 离开 main 后,ptrA, ptrB 销毁,但 A, B 对象因互相持有而无法释放
}
打破循环
将其中一方的强引用改为弱引用即可。
class B {
public:
int v;
weak_ptr<A> a; // 关键修改:使用 weak_ptr
B(int val) : v(val) { cout << "B 构造函数" << endl; }
~B() { cout << "B 析构函数" << endl; }
};
int main() {
shared_ptr<A> ptrA = make_shared<A>(10);
shared_ptr<B> ptrB = make_shared<B>(20);
ptrA->b = ptrB; // A 仍持有 B 的强引用
ptrB->a = ptrA; // B 持有 A 的弱引用
return 0; // 正常释放,无泄漏
}
传值还是引用?
讨论智能指针的参数传递方式:
- shared_ptr + 值传递:默认方式,会复制智能指针实例,增加引用计数。适用于需要共享所有权的场景。
- shared_ptr + 引用传递:表达'借用'姿态,不改变生命周期。需注意多线程环境下对象可能被别处释放的风险。
- unique_ptr + 引用传递:由于
unique_ptr不可复制,这是唯一在别处使用的方式,语义为只借用对象,不拥有。
在实际开发中,如果没有特殊需求,默认使用 unique_ptr,遇到明确需要共享所有权时再改用 shared_ptr。


