前言
需要先纠正题面的一个前提:boost::scoped_ptr不是 C++ 标准库的组件,它来自 Boost 库的boost/scoped_ptr.hpp。更关键的是,Boost 从 1.54 版起已将其标记为废弃,官方推荐用std::unique_ptr取代。C++11 把unique_ptr纳入标准之后,scoped_ptr的历史使命基本结束:它诞生于没有移动语义的 C++03 时代,用"禁止拷贝"来保证独占所有权;而unique_ptr既能独占,又能通过移动转移所有权,还能携带自定义删除器。
那为什么还要讲它?因为大量老项目、老教材和第三方头文件里仍然出现scoped_ptr,维护代码时必须看懂它的语义;它也是理解 RAII 与独占所有权的一个极简样本——整个类只做一件事:构造时接管指针,析构时delete,中间不允许复制。把它的边界讲清楚,反过来能更准确地理解unique_ptr为什么这样设计。
本文先讲scoped_ptr的所有权模型与接口,再逐项对比unique_ptr,给出迁移写法,最后列出维护老代码时的高频坑点。示例中的 Boost 用法以其官方文档为准,标准库用法以 C++17 为准。
一、所有权模型:独占且不可转移
scoped_ptr的语义可以用一句话概括:一个scoped_ptr对象在它的生命周期内独占一个动态分配的对象,并在析构时释放它。它不可拷贝,也不可移动,因此所有权不会外流,始终绑定在声明它的作用域上——这也是 "scoped"(作用域)这个名字的由来。
// 适用于 C++17(Boost 1.5x,兼容旧代码)
#include <boost/scoped_ptr.hpp>
class Connection {
public:
Connection() = default;
~Connection() = default;
void send(const char* text);
};
void demo() {
boost::scoped_ptr<Connection> conn(new Connection());
conn->send("hello"); // operator-> 转发
if (conn) { // 布尔转换,判断是否持有对象
(*conn).send("world"); // operator* 返回引用
}
conn.reset(new Connection()); // 释放旧对象,接管新对象
} // 作用域结束,delete 当前对象
构造函数接收裸指针并立即接管。若new Connection()抛出异常,scoped_ptr尚未构造,不会发生泄漏;一旦构造完成,后续无论正常返回还是异常退出,析构函数都会执行delete,这就是 RAII 的价值。
reset的行为是"先释放旧对象,再接管新指针"。如果传入的指针恰好就是当前持有的指针,先删除再保存会留下悬垂指针,因此实现里对自赋值设有断言。日常使用中应直接避免这种写法,不要依赖断言之外的行为。
get()返回裸指针,但不转移所有权。拿它去传参给不拥有所有权的函数是合理的;拿它去delete就会造成二次释放。scoped_ptr没有release(),这是它与unique_ptr最本质的区别之一:一旦接管,就无法主动放弃所有权。
二、接口细节与几个边界
scoped_ptr提供的操作很少:
explicit scoped_ptr(T* p = 0) | 接管指针,默认可空 |
void reset(T* p = 0) | 释放旧对象并接管新指针 |
T* get() const | 返回裸指针,不转移所有权 |
T* operator->() const | 成员访问 |
void swap(scoped_ptr&) | 交换两个对象所持指针 |
拷贝构造和拷贝赋值被禁用,所以它不能按值传参、不能按值返回、不能放进需要拷贝的容器。在 C++03 时代这是刻意的限制:没有移动语义,任何"转移"都只能靠拷贝,而拷贝语义对独占所有权来说是危险的。
析构函数是内联的,并且直接对T*调用delete。这带来一个使用约束:在实例化析构函数的位置,T必须是完整类型。如果在一个头文件里用scoped_ptr<Impl>作为成员,而Impl只有前置声明,那么该头文件里隐式生成的析构函数会要求Impl完整,编译可能失败。常见做法是把析构函数声明在头文件、定义在能看到完整类型的.cpp里。这个技巧对unique_ptr同样适用。
scoped_ptr不支持自定义删除器。它固定使用delete,因此不能管理malloc分配的内存、fopen返回的FILE*、数组(数组要用scoped_array),也不能管理由平台 API 创建、需要专用函数释放的句柄。需要这些能力时必须换unique_ptr。
boost::scoped_array<T>是配套的数组版本,析构时调用delete[],并提供下标访问。但它同样已被废弃,现代替代是std::unique_ptr<T[]>或直接使用std::vector<T>。
三、与 std::unique_ptr 的对比
| 特性 | boost::scoped_ptr | std::unique_ptr |
|---|
| 数组支持 | scoped_array | unique_ptr<T[]>特化 |
unique_ptr在保持独占语义的同时补上了移动能力,因此可以按值返回、可以放进std::vector、可以在工厂函数之间传递。它还有release(),允许把所有权交还给裸指针接口(虽然应谨慎使用)。自定义删除器让它能管理文件句柄、套接字、C 库资源。
// 适用于 C++17
#include <memory>
void modern_demo() {
auto conn = std::make_unique<Connection>(); // C++14
conn->send("hello");
std::unique_ptr<Connection> other = std::move(conn);
// 此后 conn 为空,other 持有对象
if (!conn) {
// 被移动后的 unique_ptr 处于有效但未指定的状态,
// 可以判空、重新赋值或销毁,但不要解引用
}
auto buffer = std::make_unique<int[]>(64); // 数组特化
buffer[0] = 1;
} // 逆序析构,buffer 先释放,other 后释放
std::make_unique是 C++14 引入的。它把new与智能指针的构造合并到一个表达式里,避免了某些求值顺序导致的泄漏风险,也让代码更短。C++11 环境下需要写std::unique_ptr<Connection>(new Connection())。
四、迁移策略
把老代码里的scoped_ptr换成unique_ptr,多数情况是机械替换:
- 头文件从
boost/scoped_ptr.hpp改为<memory>。
- 类型从
boost::scoped_ptr<T>改为std::unique_ptr<T>。
scoped_array<T>改为std::unique_ptr<T[]>,或改用std::vector<T>。
- 构造处优先用
std::make_unique<T>(...)(需要 C++14)。
- 检查是否有按值传递或返回的地方——老代码若通过引用传递
scoped_ptr,改成unique_ptr后仍应传引用;若想转移所有权,显式std::move。
如果项目仍停留在 C++03,无法使用unique_ptr,scoped_ptr依然可以用,但要接受它不能移动、不能进容器、不能自定义删除器的限制。新项目没有理由再引入它。
常见坑点
- ❌ 试图拷贝或按值传递
scoped_ptr。✅ 拷贝构造被禁用;需要转移所有权就换std::unique_ptr并std::move。
- ❌ 用
scoped_ptr<T>管理new T[n]分配的数组。✅ 它对单个对象调用delete;数组用scoped_array,现代代码用std::unique_ptr<T[]>或std::vector。
- ❌ 对
get()返回的指针执行delete。✅get()不转移所有权,手动删除会导致析构时二次释放。
- ❌ 用同一个裸指针构造两个智能指针。✅ 两个控制者各自
delete,属于重复释放;一个对象只交给一个所有者。
- ❌ 用
scoped_ptr接管malloc或栈上对象的地址。✅ 它固定调用delete;分配与释放方式必须匹配,特殊资源用带自定义删除器的unique_ptr。
- ❌ 在只看到前置声明的头文件里让
scoped_ptr的析构函数隐式生成。✅ 析构处需要完整类型;把析构函数定义移到能看到完整定义的源文件。
- ❌ 认为
scoped_ptr可以像unique_ptr一样从函数返回。✅ 它不可移动,按值返回无法通过编译;返回unique_ptr或裸指针加明确的所有权约定。
- ❌ 在新项目里继续使用
boost::scoped_ptr。✅ 它自 Boost 1.54 起已废弃,标准库的std::unique_ptr功能更全、无额外依赖。
总结
| 定位 | Boost 的 C++03 时代独占智能指针,非标准 |
| 能力边界 | 无release、无自定义删除器、数组需scoped_array |
| 替代 | std::unique_ptr+std::make_unique |
scoped_ptr是一个把 RAII 压缩到极致的教学样本:它用"禁止复制"换来了所有权的确定性,也因此失去了灵活性。理解它的限制,就理解了unique_ptr为什么要引入移动语义和删除器。今天再写 C++,默认答案应当是std::unique_ptr;只有在维护无法升级标准的老代码时,才需要与scoped_ptr打交道。