什么是智能指针?
智能指针是 C++ 标准库基于 RAII 思想设计的 “类裸指针封装器” ,它用栈对象的生命周期绑定堆内存的生命周期,自动化完成堆内存的分配、释放、所有权转移等操作,从根源上避免手动管理内存的三大致命问题:内存泄漏、重复释放、异常安全缺失。
回顾
class shape_wrapper {
public:
explicit shape_wrapper(
shape* ptr = nullptr)
: ptr_(ptr) {}
~shape_wrapper()
{
delete ptr_;
}
shape* get() const { return ptr_; }
private:
shape* ptr_;
};
这个类可以完成智能指针的最基本的功能:对超出作用域的对象进行释放。但它缺了点东西:
- 这个类只适用于 shape 类
- 该类对象的行为不够像指针
- 拷贝该类对象会引发程序行为异常
模板化和易用性
要让这个类能够包装任意类型的指针,需要把它变成一个类模板。这实际上相当容易:
template <typename T>
class smart_ptr {
public:
explicit smart_ptr(T* ptr = nullptr)
: ptr_(ptr) {}
~smart_ptr()
{
delete ptr_;
}
T* get() const { return ptr_; }
private:
T* ptr_;
};
目前这个 smart_ptr 的行为还是和指针有点差异的:
- 它不能用 * 运算符解引用
- 它不能用 -> 运算符指向对象成员
- 它不能像指针一样用在布尔表达式里
不过,这些问题也相当容易解决,加几个成员函数就可以:
template <typename T>
class smart_ptr {
public:
…
T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }
operator bool() const { return ptr_; }
}
拷贝构造和赋值
这里先介绍一下C++ 中一个经典的异常安全、代码复用的赋值运算符实现惯用法:拷贝并交换(Copy and Swap) 。赋值操作并不是天然分为 “拷贝” 和 “交换”,而是人们为了解决手动实现赋值运算符时的三大痛点(自赋值安全、异常安全、代码复用),主动设计出了这种 “分两步走” 的最佳实践。
-
第一步:拷贝(利用拷贝构造函数)
不直接修改当前对象,而是先利用拷贝构造函数生成一个
other的临时副本。- 这一步把 “可能抛异常的操作”(分配内存、拷贝数据)都放在了临时对象里,如果失败,当前对象完全不受影响,临时对象也会自动清理。
- 完美复用了拷贝构造函数的代码,不用重复写。
-
第二步:交换(Swap)
把当前对象的资源(指针、大小等)和刚才生成的临时副本的资源进行浅交换(只交换成员变量的值,不做深拷贝)。
- 交换操作是不抛异常的(只是交换几个指针和整数),绝对安全。
- 交换后,临时对象持有了当前对象原来的旧资源,当临时对象离开作用域析构时,会自动释放旧资源。
- 天然解决了自赋值问题(即使是自赋值,拷贝自己再交换自己,也不会出错)。
代码实现实例如下:
class StringBuffer {
public:
// ... 之前的构造、拷贝构造、析构函数不变 ...
// 【关键1】添加 swap 成员函数:不抛异常,仅交换成员
void swap(StringBuffer& other) noexcept {
using std::swap; // ADL(参数依赖查找),如果有自定义 swap 会优先调用
swap(data_, other.data_); // 只交换指针(浅交换,极快)
swap(size_, other.size_); // 只交换大小
}
// 【关键2】赋值运算符:拷贝并交换
// 注意:参数是按值传递(pass by value),这一步自动触发了“第一步:拷贝构造”
StringBuffer& operator=(StringBuffer other) {
swap(other); // 第二步:和临时拷贝交换
return *this;
} // 函数结束,other 临时对象析构,自动释放旧资源
};
// 【可选】提供非成员 swap,方便 STL 算法调用
void swap(StringBuffer& a, StringBuffer& b) noexcept {
a.swap(b);
}
以a = b为例,下面是流程中发生的操作:
-
调用
operator=(StringBuffer other):- 因为参数是按值传递,编译器会自动调用
StringBuffer的拷贝构造函数,用b拷贝构造出临时对象other。 - 这一步完成了 “深拷贝”,如果
new失败抛异常,a还没被碰过,完全安全。
- 因为参数是按值传递,编译器会自动调用
-
执行
swap(other):- 把
a的data_、size_和临时对象other的对应成员交换。 - 现在
a持有了b的数据(通过交换拿到了other的深拷贝结果),other持有了a原来的旧数据。 - 这一步只是交换指针,不抛异常,绝对安全。
- 把
-
函数结束,
other析构:- 临时对象
other离开作用域,析构函数被调用,delete[]了它持有的旧数据(也就是a原来的旧资源)。 - 旧资源被自动、安全地释放了
- 临时对象
拷贝构造和赋值,暂且简称为拷贝,这是个比较复杂的问题了。关键还不是实现问题,而是如何定义其行为。假设有下面的代码:
smart_ptr<shape> ptr1{create_shape(shape_type::circle)};
smart_ptr<shape> ptr2{ptr1};
对于第二行,究竟应当让编译时发生错误,还是可以有一个更合理的行为?
这是一个非常经典的智能指针所有权设计问题,答案取决于你希望这个 smart_ptr 实现的是 「独占所有权」 还是 「共享所有权」— 而现代 C++ 的最佳实践告诉我们:默认应当让编译时发生错误(禁止拷贝) ,如果确实需要共享所有权,再通过专门的「共享型智能指针」(如 std::shared_ptr)来实现。
拷贝模拟移动
template <typename T>
class smart_ptr {
…
smart_ptr(smart_ptr& other)
{
ptr_ = other.release();
}
smart_ptr& operator=(smart_ptr& rhs)
{
smart_ptr(rhs).swap(*this);
return *this;
}
…
T* release()
{
T* ptr = ptr_;
ptr_ = nullptr;
return ptr;
}
void swap(smart_ptr& rhs)
{
using std::swap;
swap(ptr_, rhs.ptr_);
}
…
};
这段代码是 C++11 之前,人们为了在没有右值引用和移动语义的情况下,模拟 “转移所有权” 行为而设计的一种 “另类智能指针” 实现。它的核心特点是:用 “拷贝构造 / 赋值” 的语法,实现 “移动语义” 的效果。
release() 成员函数:资源转移的核心工具
T* release() {
T* ptr = ptr_;
ptr_ = nullptr;
return ptr;
}
这和 std::unique_ptr::release() 完全一致:
- 保存当前的原始指针
ptr_; - 将自身的
ptr_置为nullptr(放弃所有权); - 返回原来的原始指针(供新的所有者接管)。
它的作用是安全地剥离资源,保证源对象析构时不会重复释放。
swap() 成员函数:资源交换的工具
void swap(smart_ptr& rhs) {
using std::swap;
swap(ptr_, rhs.ptr_);
}
这就是上面 “拷贝并交换” 惯用法的配套工具:
- 用 ADL(参数依赖查找)调用
std::swap; - 仅交换两个
smart_ptr的内部原始指针ptr_,不做深拷贝,成本极低。
【核心】拷贝构造函数:用 “拷贝” 语法实现 “转移”
smart_ptr(smart_ptr& other) { // 注意:参数是 non-const 左值引用!
ptr_ = other.release();
}
这是这段代码最 “另类” 的地方 ——传统拷贝构造函数的参数是 const smart_ptr&(常量左值引用),保证不修改源对象;但这里的参数是 smart_ptr&(非常量左值引用),可以修改源对象。
- 接受一个非常量的源对象
other; - 调用
other.release(),把other的资源 “偷” 过来(other.ptr_被置空); - 将偷来的指针赋值给当前对象的
ptr_。
当你写 smart_ptr<shape> ptr2(ptr1); 时:你可能以为是 “拷贝 ptr1 给 ptr2,两者都指向资源”;但实际效果是:ptr2 接管了资源,ptr1 变成了 nullptr!这完全不符合传统 “拷贝” 的语义,是 C++11 之前没有移动语义时的 “无奈之举”。
【核心】拷贝赋值运算符:用 “拷贝并交换” 实现转移
smart_ptr& operator=(smart_ptr& rhs) { // 参数也是 non-const 左值引用
smart_ptr(rhs).swap(*this); // 先构造临时对象(转移 rhs 的资源),再交换
return *this;
}
这是 “拷贝并交换” 惯用法的变体,但结合了上面的 “转移型拷贝构造”:
-
构造临时对象
smart_ptr(rhs):调用上面的 “转移型拷贝构造”,把rhs的资源 “偷” 给临时对象;此时rhs.ptr_被置空,临时对象持有原来的资源。 -
调用
swap(*this):把临时对象的资源和当前对象*this的资源交换;此时*this拿到了原来rhs的资源,临时对象持有*this原来的旧资源。 -
临时对象析构:函数结束,临时对象离开作用域,析构函数释放它持有的旧资源(即
*this原来的资源)。 -
返回
*this:当前对象已经成功接管了rhs的资源,rhs保持空状态。
在 C++11 引入右值引用(&&)和移动语义之前,人们无法明确区分 “拷贝” 和 “转移”。为了避免浅拷贝导致的重复释放,同时实现资源的高效转移,就设计了这种 “用非常量左值引用的拷贝构造来模拟转移” 的手法。你可能听说过的 std::auto_ptr(C++98 引入,C++17 已移除),就是这种设计的典型代表 —— 这段代码本质上就是一个简化版的 auto_ptr。
移动指针
template <typename T>
class smart_ptr {
…
smart_ptr(smart_ptr&& other)
{
ptr_ = other.release();
}
smart_ptr& operator=(smart_ptr rhs)
{
rhs.swap(*this);
return *this;
}
…
};
这段代码是现代 C++(C++11+)智能指针的标准、简洁且安全的实现思路,完美结合了移动语义和拷贝并交换(Copy and Swap)惯用法,可以看作是 std::unique_ptr 核心逻辑的简化版。它解决了之前旧版本(如 std::auto_ptr)的 “语义混淆” 问题,同时保持了代码的简洁性、安全性和高效性。
【核心 1】移动构造函数:明确转移所有权
smart_ptr(smart_ptr&& other) { // 参数是右值引用 &&
ptr_ = other.release();
}
参数 smart_ptr&& other:右值引用 && 明确表示这个构造函数只接受临时对象、即将销毁的对象(如 std::move 转换后的左值) 。语义上清晰告诉用户:“我们要转移 other 的资源,而不是拷贝”。
ptr_ = other.release():调用 other.release() 让源对象 other 放弃资源所有权(other.ptr_ 被置空),并返回原始指针。将返回的指针赋值给当前对象的 ptr_,完成资源转移。比直接 ptr_ = other.ptr_; other.ptr_ = nullptr; 更简洁,且复用了 release() 的安全逻辑。
【核心 2】赋值运算符:一个函数同时支持拷贝和移动
smart_ptr& operator=(smart_ptr rhs) { // 参数是按值传递!
rhs.swap(*this);
return *this;
}
这是 “拷贝并交换” 惯用法的现代升级版 ——按值传递的参数让它能自动根据实参类型选择 “拷贝构造” 或 “移动构造”,从而用一个函数同时实现 “拷贝赋值” 和 “移动赋值”。
参数 smart_ptr rhs(按值传递) :这是最巧妙的地方,编译器会根据实参的类型自动选择构造方式。
如果实参是左值(比如 ptr1 = ptr2,ptr2 是有名字的对象):用 ptr2 调用拷贝构造函数生成临时对象 rhs(深拷贝资源,前提是你实现了拷贝构造,或者像 unique_ptr 一样禁止拷贝)。
如果实参是右值(比如 ptr1 = create_circle() 或 ptr1 = std::move(ptr2)):用实参调用移动构造函数生成临时对象 rhs(零成本转移资源)。
子类指针向基类指针的转换
一个 circle* 是可以隐式转换成 shape* 的,但上面的 smart_ptr<circle> 却无法自动转换成 smart_ptr<shape>。这个行为显然还是不够“自然”。不过,只需要额外加一点模板代码,就能实现这一行为。在目前给出的实现里,只需要增加一个构造函数即可——这也算是我们让赋值函数利用构造函数的好处了。
template <typename U>
smart_ptr(smart_ptr<U>&& other)
{
ptr_ = other.release();
}
这样,我们自然而然利用了指针的转换特性:现在 smart_ptr<circle> 可以移动给 smart_ptr<shape>,但不能移动给 smart_ptr<triangle>。不正确的转换会在代码编译时直接报错。
引用计数
unique_ptr 算是一种较为安全的智能指针了。但是,一个对象只能被单个 unique_ptr 所拥有,这显然不能满足所有使用场合的需求。一种常见的情况是,多个智能指针同时拥有一个对象;当它们全部都失效时,这个对象也同时会被删除。这也就是 shared_ptr 了。
unique_ptr 和 shared_ptr 的主要区别如下图所示:
多个不同的 shared_ptr 不仅可以共享一个对象,在共享同一对象时也需要同时共享同一个计数。当最后一个指向对象(和共享计数)的 shared_ptr 析构时,它需要删除对象和共享计数。
指针类型转换
C++ 里的不同的类型强制转换:
- static_cast
- reinterpret_cast
- const_cast
- dynamic_cast
它们是 C++ 为了替代危险的 C 风格强转 (Type)value 设计的专用转换工具,各司其职、安全可控、编译器能做严格检查。
1. const_cast:只修改 “只读权限”,不改类型
唯一用途:添加 / 移除 const / volatile 限定符不能改变变量的类型,不能做指针类型转换。
适用场景:
- 函数需要非 const 参数,但你只有 const 对象
- 底层接口要求非 const 指针
2. static_cast:日常开发 90% 用它(最安全、最常用)
编译期静态转换,做合法、有意义的类型转换,编译器会严格检查。
适用场景:
- 基本数据类型转换
- 继承体系中【向上转型】(安全)
void*转具体类型指针- 显式调用构造函数的类型转换
3. reinterpret_cast:暴力二进制重解释(极度危险)
直接把内存二进制重新解释,相当于告诉编译器:“别管类型对不对,直接把这块内存当成另一种类型看!”
适用场景
- 底层开发:驱动、硬件操作、网络协议、指针转整数、整数转指针
- 正常业务代码永远不要用!
4. dynamic_cast:多态继承的【安全向下转型】
唯一在运行时检查类型的转换,专门用于:基类指针 / 引用 → 派生类指针 / 引用(向下转型)