One more thing
1 std::unique_ptr
和 std::make_unique
std::unique_ptr
和 std::make_unique
是 C++ 现代内存管理的核心组件,分别用于实现独占所有权和提供安全的对象创建机制。
1.1 std::unique_ptr
:独占所有权的智能指针
-
独占所有权
- 同一时间仅允许一个
std::unique_ptr
拥有动态分配的对象,确保资源不被多个指针管理 - 禁止复制:拷贝构造函数和赋值运算符被删除,避免所有权冲突
- 支持移动语义:通过
std::move
转移所有权,转移后原指针变为nullptr
- 同一时间仅允许一个
-
自动资源释放
- 离开作用域时自动调用删除器销毁对象(默认使用
delete
或delete[]
),杜绝内存泄漏。
- 离开作用域时自动调用删除器销毁对象(默认使用
-
轻量高效
- 与原始指针大小相同(通常 8 字节),无额外内存或性能开销。
-
自定义删除器
- 支持自定义删除逻辑(如关闭文件句柄)
auto fileDeleter = [](FILE* f) {if (f) fclose(f);}
std::unique_ptr<FILE, decltype(fileDeleter)> filePtr(fopen("data.txt", "r"), fileDeleter);
1.2 std::make_unique
:安全创建对象的工厂函数
-
异常安全性
-
封装
new
和构造函数调用,避免因构造函数异常导致内存泄漏。 -
对比直接
new
:
// 危险:若 computePriority() 抛出异常,new 的对象可能泄漏 process(std::unique_ptr<Widget>(new Widget), computerPriority()); // 安全:make_unique 保证原子性 process(std::make_unique<Widget>(), computerPriority());
-
-
代码简洁性
- 自动推导类型,减少冗余代码:
auto ptr = std::make_unique<int>(42); // 替代 std::unique_ptr<int> ptr(new int(42));
-
性能优化(针对
std::make_shared
)std::make_shared
将对象和控制块合并分配(单次内存分配),但make_unique
本身无此优化(仅分配对象)
1.3 使用场景
场景 | std::unique_ptr |
std::make_unique |
---|---|---|
对象创建 | 需要显式调用 new | 更推荐,安全且简洁 |
自定义删除器 | 支持 | 不支持 |
动态数组 | std::unique_ptr<int[]> |
C++14 起支持数组 |
多态与工厂模式 | 基类指针管理派生类 | 创建时直接指定类型 |
与容器结合 | 需 push_back(std::move(ptr)) |
直接创建并移动 |
1.4 使用示例
- 管理动态数组
auto arr = std::make_unique<int[]>(5); // 创建含 5 个整数的数组
for (int i = 0; i < 5; i++) arr[i] = i * 10; // 直接索引访问
- 多态对象管理
class Base {/*...*/};
class Derived: public Base {/*...*/};
std::unique_ptr<Base> obj = std::make_unique<Derived>(); // 基类指针指向派生类
- 资源所有权转移
auto ptr1 = std::make_unique<Resource>();
auto ptr2 = std::move(ptr1); // ptr1 变为 nullptr,ptr2 接管资源
说明:
-
优先使用
std::make_unique
,默认选择以保障异常安全和代码简洁性,除非需要自定义删除器或花括号初始化 -
避免
get()
滥用,仅在必须传递原始指针的 API 中使用ptr.get()
,且确保不涉及所有权转移。 -
禁用裸
new
和delete
, 现代 C++ 中,std::make_unique
和std::unique_ptr
应替代手动内存管理。 -
与标准容器配合, 使用
std::vector<std::unique_ptr<T>>
时,通过emplace_back(std::make_unique<T>(...))
避免拷贝问题。 -
处理大型对象, 若对象极大(
>1MB
),考虑分离分配控制块和对象(直接new + unique_ptr
)以减少堆碎片 - C++11:引入
std::unique_ptr
,但需手动new
初始化。 - C++14:新增
std::make_unique
,成为对象创建标准方式。 - C++17:支持类模板参数推导(
std::unique_ptr ptr(new Resource);
)
2 std::move
std::move
是 C++11 引入的核心工具,用于触发移动语义以提升性能。它通过类型转换实现资源的高效转移,但需谨慎使用以避免未定义行为。
2.1 核心功能与工作原理
- 类型转换(非真实移动)
std::move
仅执行静态类型转换,将左值强制转为右值引用(T&&
),告知编译器该对象可被资源窃取。其底层实现为:
template<typename T>
typename std::remove_reference<T>::type&& move(T&& arg) noexcept {
return static_cast<typename std::remove_reference<T>::type&&>(arg);
}
* 保留 `const` 属性:若原对象为 const,则返回 `const T&&`。
* 对右值无影响:若参数已是右值(如临时对象),`std::move` 无实际作用
- 触发移动语义
转换后的右值引用可调用移动构造函数/赋值运算符,实现资源转移而非深拷贝
std::string s1 = "Hello";
std::string s2 = std::move(s1); // 调用移动赋值,s1 的资源被转移至 s2
* 资源窃取:如指针、文件句柄等动态资源被直接转移,源对象置为空状态(如 s1 变为空字符串)
2.2 使用场景
- 实现移动语义的类,在自定义类的 移动构造/赋值函数中,需显式使用
std::move
转移成员资源
class ResourceHolder {
std::vector<int> data;
public:
// 移动构造函数
ResourceHolder(ResourceHolder&& other) noexcept:
data(std::move(other.data)) {} // 转移 vector 资源
};
-
容器操作优化
- 添加元素:向容器插入不再需要的对象时,用
std::move
避免拷贝
std::vectore<std::string> vec; std::string str = "test"; vec.push_back(std::move(str)); // 移动而非拷贝,str 被掏空
- 容器间转移:大型容器资源可通过移动高效转移
std::vector<int> v12 = {1, 2, 3}; auto v2 = std::move(v1); // v1 变为空,v2 接管资源
- 添加元素:向容器插入不再需要的对象时,用
-
转移独占资源
管理独占资源的对象(如 std::unique_ptr
)必须通过 std::move
转移所有权
auto ptr1 = std::unique_ptr<int>(43);
auto ptr2 = std::move(ptr1); // ptr1 变为 nullptr,ptr2 接管资源
- 完美转发(结合
std::forward
)
在模板中保持参数的原始值类别(左值/右值),实现高效转发
template<typename T>
void relay(T&& arg) {
process(std::forward<T>(arg)); // 保留 arg 的值类别
}
2.3 注意事项
-
被移动对象的状态
- 有效但未定义:被
std::move
后的对象仍可析构或重新赋值,但内容不可依赖(如std::string
可能为空)。 - 安全操作:仅可执行无状态依赖的操作(如
clear()
或重新赋值)
std::string s = "abc"; auto s2 = std::move(s); s.clear(); // 安全:清空 s s = "reused"; // 安全:重新赋值
- 有效但未定义:被
-
误用风险
- 无移动语义的类:若类未实现移动构造/赋值,
std::move
会退化为拷贝操作,反而增加开销。 - 基本类型无效:对 int 等基本类型使用
std::move
无意义且降低可读性。 - 干扰编译器优化:函数返回局部对象时,显式
return std::move(obj)
可能禁用返回值优化(RVO),导致额外移动。
- 无移动语义的类:若类未实现移动构造/赋值,
-
noexcept
的必要性
移动构造函数/赋值运算符应标记 noexcept
,否则标准库容器(如 std::vecto
r)可能退化为拷贝操作
class MyType {
public:
MyType(MyType&& other) noexcept {...} // 确保容器扩容时使用移动
};
- 显式移动基类与成员
派生类移动操作需手动移动基类和成员,避免隐式拷贝
class Derived: public Base {
std::vector<int> data;
public:
Derived(Derived&& other) noexcept:
Base(std::move(other)), // 移动基类
data(std::move(other.data)) {} // 移动成员
};
- 与
std::exchange
结合
在移动赋值中安全重置源对象
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
delete[] data;
data = std::exchange(other.data, nullptr); // 转移资源并置空 other
}
return *this;
}
-
识别无效使用场景
- 局部变量返回:依赖编译器自动优化(RVO/NRVO),而非显式
std::move
。 - 链式调用:
obj.method(std::move(member))
可能导致成员失效,需评估后续操作安全性
- 局部变量返回:依赖编译器自动优化(RVO/NRVO),而非显式
std::move
的核心价值在于通过类型转换触发移动语义,实现资源零拷贝转移。但是:
- 仅转换类型,不保证性能:若类无移动语义,则退化为拷贝
- 移动后对象状态未定义:避免依赖其值,必要时重新初始化
- 优先编译器优化:返回局部对象时避免手动
std::move
noexcept
保障效率:确保容器操作优先选择移动而非拷贝
3 std::forward
std::forward
是 C++11 引入的核心工具,用于实现完美转发(Perfect Forwarding),即在函数模板中将参数以原始值类别(左值/右值)和类型属性(const、volatile)无损地传递给其他函数。
3.1 核心原理:保留值类别的转发
- 引用折叠规则(Reference Collapsing)
std::forward
依赖引用折叠规则处理模板推导中的引用组合:
* T& & → T&(左值引用)
* T& && → T&(左值引用)
* T&& & → T&(左值引用)
* T&& && → T&&(右值引用)
template<typename T>
void wrapper(T&& arg) { // 万能引用(Universal Reference)
target(std::forward<T>(arg)); // 折叠决定转发类型
}
* 若 arg 为左值,T 推导为 T&,折叠后返回左值引用。
* 若 arg 为右值,T 推导为 T,返回右值引用
-
与
std::move
的本质区别std::forward
,条件性转换(保留原始值类别),依赖模板推导,需通过 T 确定类型,用于完美转发std::move
,无条件转为右值,不依赖模板推导,用于显式启用移动语义
3.2 实现机制:静态转换与类型萃取
- 简化实现代码
标准库中的 std::forward
核心逻辑如下
// 左值转发版本
template<typename T>
T&& forward(typename std::remove_reference<T>::type& arg) noexcept {
return static_cast<T&&>(arg); // 引用折叠发生在此
}
// 右值转发版本(防止错误转发左值)
template<typename T>
T&& forward(typename std::remove_reference<T>::type&& arg) noexcept {
static_assert(!std::is_lvalue_reference<T>::value, "Bad forward call");
return static_cast<T&&>(arg);
}
* `std::remove_reference<T>::type`:去除 T 的引用属性,确保参数类型匹配。
* `static_cast<T&&>`:根据 T 的推导结果折叠为正确引用类型
- 值类别传递示例
#include <iostream>
void process(int&) { std::cout << "Lvalue\n"; }
void process(int&&) { std::cout << "Rvalue\n"; }
template<typename T>
void relay(T&& arg) {
process(std::forward<T>(arg)); // 正确传递值类别
}
int main() {
int x = 10;
relay(x); // 输出 "Lvalue"(左值转发)
relay(20); // 输出 "Rvalue"(右值转发)
relay(std::move(x)); // 输出 "Rvalue"(右值转发)
return 0;
}
3.3 应用场景
- 工厂模式与对象构造
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
class Widget {
public:
Widget(int id, const std::string& name);
};
auto w = make_unique<Widget>(42, "MyWidget"); // 避免临时对象拷贝
直接转发参数给构造函数,避免额外拷贝/移动
- 通用包装器与回调
template<typename F, typename... Args>
auto async_call(F&& f, Args&&... args) {
return std::async(
std::launch::async,
std::forward<F>(f),
std::forward<Args>(args)...
);
}
保留函数对象和参数的值类别,确保线程任务高效执行
- STL 容器的
emplace
方法
std::vector<std::string> vec;
vec.emplace_back("Hello"); // 直接构造元素,避免临时字符串拷贝
emplace_back
内部使用 std::forward
将参数完美转发给元素的构造函数。
3.4 注意事项
- 必须搭配万能引用使用
仅在与 T&&
结合的模板函数中使用(万能引用), 在非模板或固定类型函数中使用无意义。
void foo(int&& x) {
std::forward<int>(x); // 错误!T 非模板参数
}
- 避免
const
导致的移动失效
若参数被声明为 const
,即使使用 std::forward
也无法触发移动语义:
void func(const std::string s) {
auto tmp = std::forward<const std::string>(s); // 调用拷贝构造函数
}
移动构造函数不接受 const
右值.
- 警惕悬空引用
确保被转发对象的生命周期足够长
template<typename T>
auto bad_forward(T&& arg) {
return std::thread(worker, std::forward<T>(arg)); // 若 arg 是局部变量,线程可能访问已销毁对象
}
通过 std::shared_ptr
管理资源或显式延长生命周期
- 与返回值优化(RVO)的冲突
返回局部对象时,避免显式使用 std::forward
template<typename T>
T create() {
T obj;
return std::forward<T>(obj); // ❌ 可能阻止 RVO
// return obj; // ✅ 依赖编译器优化
}
4 std::exchange
std::exchange
是 C++14 引入的实用工具函数,核心功能是原子性地替换对象的值并返回其旧值,结合移动语义和完美转发实现高效资源管理。
4.1 核心原理:移动语义与完美转发的结合
实现机制
template <class T, class U = T>
T exchange(T& obj, U&& new_value) {
T old_value = std::move(pbj); // 1. 移动构造保存旧值
obj = std::forward<U>(new_value); // 2. 完美转发赋值新值
return old_value; // 3. 返回旧值(可能触发移动构造)
}
- 移动构造旧值:通过
std::move
将obj
的当前值转移到临时变量,避免深拷贝 - 完美转发新值:
std::forward
保持new_val
的值类别(左值/右值),确保高效赋值。 - 异常安全:若移动构造和赋值均为
noexcept
,则整个操作为noexcept
原子性误解
尽管函数名暗示原子性,但 std::exchange
不保证多线程安全(标准未要求原子性),实际依赖编译器实现。若需线程安全,需额外同步机制。
4.2 应用场景
- 资源所有权转移(移动语义)
在移动构造/赋值中安全转移资源并重置源对象
class ResourceHolder {
int* data;
public:
// 移动构造函数
ResourceHolder(ResourceHolder&& other) noexcept
: data(std::exchange(other.data, nullptr)) {} // 转移资源并置空源对象
// 移动赋值运算符
ResourceHolder& operator=(ResourceHolder&& other) noexcept {
if (this != &other) {
delete data;
data = std::exchange(other.data, nullptr); // 接管资源并置空源对象
}
return *this;
}
};
避免手动暂存旧值和重置源对象,代码简洁且安全.
- 状态机管理
原子性更新状态并记录旧状态
enum class State{
Idle,
Running,
Error
};
// 更新状态并获取旧状态
State old = std::exchange(current, State::Running);
if (old == State::Error) handle_error_recovery();
用于状态机、日志系统、事务回滚等场景。
- 循环与算法优化
简化值更新逻辑,避免临时变量
// 斐波那契数列生成
for (int a=0, b=1; a<100; a = std::exchange(b, a+b)) {
std::cout << a << " ";
}
// 输出:0, 1, 1, 2, 3, 5...
- 格式化输出分隔符
动态更新分隔符并复用旧值
std::vector<int> vec{1, 2, 3};
const char* delim = "";
for (int val : vec) {
std::cout << std::exchange(delim, ", ") << val;
}
// 输出:1, 2, 3
避免多次检查分隔符状态。
4.3 注意事项
- T 必须满足可移动构造(MoveConstructible)
- U 类型需能赋值给 T(隐式转换或移动赋值)
- 被移动后的对象处于有效但未定义状态(如
std::string
可能为空),需显式重置或析构 - 避免自赋值问题
MyClass& operator=(MyClass&& other) {
// 自赋值安全:exchange 先返回旧值再赋值,即使 this == &other 也能正确处理
// 无需显式检查 this != &other,因 exchange 操作顺序保证安全
data = std::exchange(other.data, nullptr);
return *this;
}
-
多线程场景
若需原子性,结合
std::atomic
使用std::atomic<int> counter(0); int old = =std::exchange(counter, 42); // 非原子! // 正确做法, 使用 atomic::exchange int old = counter.exchange(42);
std::exchange
的核心价值在于将取值-赋值操作原子化,通过移动语义实现零额外开销的资源转移,尤其适用于:- 资源管理:安全转移所有权(如智能指针、句柄)。
- 状态机:原子性状态变更与回溯。
- 算法优化:简化循环更新逻辑
- 慎用:
- 基本数据类型(直接赋值更高效)。
- 非移动构造的类型(退化为拷贝,性能差)。
- 多线程环境(需额外同步)。
5 左值和右值
左值(lvalue)和右值(rvalue)是C++中表达式的核心分类标准,直接影响内存管理、资源优化和现代特性(如移动语义)的实现。
5.1 左值与右值的本质区别
- 左值
有明确身份、持久存在的对象,可寻址、有变量名,生命周期超出当前表达式。
特点: * 可出现在赋值左侧(非常量左值)或右侧。 * 可被 & 取地址。 * 通常是变量、解引用结果、数组元素等
int a = 10; // a 是左值
int* p = &a; // 对 a 取地址合法
*p = 20; // 解引用*p是左值
- 右值
临时、短暂的值,无持久身份,不可寻址,生命周期限于当前表达式。
特点:
* 仅能出现在赋值右侧。
* 不可被 & 取地址。
* 包括字面量(42
)、表达式结果(x+y
)、函数返回的临时对象
int b = 10 + 5; // 10+5 是右值
string s = "temp"; // "temp" 是右值
- 区分方法
- 取地址测试:能用 & 取地址 → 左值;否则 → 右值
- 赋值测试:能否合理放在 = 左侧 → 左值;否则 → 右值(如 42 = a 非法)
5.2 C++11的扩展:值类别细化
C++11将表达式值类别细化为5种,核心为三类:
- 纯右值(prvalue)
- 传统右值:字面量、表达式结果、非引用函数返回值。
- 示例:
10
,std::string("hello")
- 将亡值(xvalue)
- 生命周期即将结束但可被资源接管的对象,通过
std::move
或返回右值引用的函数生成。 - 示例:
std::move(a)
,函数返回T&&
。
- 生命周期即将结束但可被资源接管的对象,通过
- 广义左值(glvalue)
- 包含传统左值和将亡值,有身份(可寻址)
5.3 左值引用 vs 右值引用
- 左值引用(
T&
)
绑定规则:仅能绑定左值(除 const T&
可绑右值)
int a = 10;
int& ref1 = a; // ✅ 绑定左值
// int& ref2 = 20; // ❌ 错误
const int& ref3 = 20; // ✅ const左值引用可绑右值
- 右值引用(
T&&
)
绑定规则:仅能绑定右值(纯右值或将亡值)
int&& rref1 = 10; // ✅ 绑定字面量
int b = 5;
// int&& rref2 = b; // ❌ 错误:b是左值
int&& rref3 = std::move(b); // ✅ 将左值转为将亡值
具名的右值引用变量本身是左值(如 rref1 可被 & 取地址)。
用于实现移动语义和完美转发。
5.4 移动语义:右值引用的核心价值
- 解决拷贝性能问题
- 传统拷贝缺陷:深拷贝大型对象(如
std::vector
)成本高,尤其临时对象拷贝浪费资源。 - 移动语义原理:通过右值引用窃取临时对象的资源(如堆内存),避免深拷贝
- 传统拷贝缺陷:深拷贝大型对象(如
// 移动构造函数
Vector(Vector&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // 置空源对象,避免双重释放
}
- 移动 vs 拷贝性能对比
Vector createVector() {
Vector v(1000); // 局部对象
return v; // 触发移动构造(非拷贝)
}
Vector v2 = createVector(); // 高效接管v的资源
若未实现移动构造,退化为拷贝构造,性能下降。
std::move
的作用
将左值显式转换为右值引用,标记为可移动
string s1 = "Hello";
string s2 = std::move(s1); // 调用移动赋值,s1被掏空
5.5 注意事项
- 避免返回局部对象的右值引用
string&& badExample() {
string s = "tmp";
return std::move(s); // ❌ s销毁后引用悬空
}
- 移动后对象状态管理
移动后源对象应置为有效但未定义状态(如指针置 nullptr
),并避免依赖其值
- 标记移动操作为
noexcept
确保标准库容器扩容时优先选择移动而非拷贝