从 Java 引用说起
在 Java 中,所有对象都在堆上分配,变量只是对象的引用(reference):
// Java:所有对象都在堆上,变量是引用
Person p = new Person("Alice");
Person q = p; // q 是同一个对象的另一个引用
q.setName("Bob"); // p.getName() 也变成 "Bob"
在 C++ 中,这个问题分裂成了三种不同的机制:值语义(value semantics)、引用(reference)和指针(pointer)。理解这三者的区别,是 Java 开发者学好 C++ 的第一道门槛。
值语义:C++ 的默认行为
#include <string>
#include <iostream>
struct Person {
std::string name;
int age;
};
int main() {
Person p{"Alice", 30}; // p 就是对象本身,在栈上
Person q = p; // q 是 p 的独立副本(值拷贝)!
q.name = "Bob"; // p.name 仍然是 "Alice"!
std::cout << p.name; // 输出 "Alice"
std::cout << q.name; // 输出 "Bob"
}
这是 Java 开发者最需要适应的转变:在 C++ 里,等号就是拷贝,不是创建引用。
值语义是 C++ 的基石之一。int、double、std::string、std::vector 都是值语义——它们可以独立复制,每个副本拥有自己的生命周期。
💡 好的编码习惯:默认使用值语义。只有当确定需要共享或避免拷贝开销时,才使用引用或指针。
引用:C++ 的"别名"
C++ 的引用(reference)是变量的别名,必须在创建时绑定,之后不能改绑:
#include <iostream>
int main() {
int a = 10;
int& ref = a; // ref 是 a 的别名
ref = 20; // a 也变成 20
int b = 30;
ref = b; // 不是让 ref 指向 b!而是把 b 的值赋给 a(a 变成 30)
std::cout << a; // 输出 30
}
引用最常见的用途是函数传参和返回值:
#include <vector>
#include <iostream>
// 引用传参(避免拷贝代价)
void print(const std::vector<int>& data) {
for (int v : data) {
std::cout << v << " ";
}
}
// 引用返回(允许修改容器元素)
int& get_element(std::vector<int>& vec, size_t index) {
return vec[index]; // 返回引用,可被修改
}
int main() {
std::vector<int> vec = {1, 2, 3, 4};
get_element(vec, 2) = 100; // vec[2] 变为 100
print(vec); // 输出: 1 2 3 100
}
⚠️ 坏味道:返回局部变量的引用
int& bad() { int local = 42; return local; // ❌ 局部变量在函数返回时销毁,返回的是悬空引用! } // bad() 的返回值是未定义行为(undefined behavior)正确做法:返回值(按值返回),或确保返回的引用指向函数外部分配的对象。
引用 vs 指针:快速对比
| 特性 | 指针 | 引用 |
|---|---|---|
| 语法 | * 声明和访问 |
& 声明,直接使用 |
| 可空 | 可以 nullptr |
必须绑定到合法对象 |
| 可重新绑定 | 可以指向不同对象 | 一生只绑定一个对象 |
| 可做算术运算 | 支持 | 不支持 |
| 占用内存 | 通常占用一个指针大小的内存 | 不额外占用(只是别名) |
指针:C++ 的终极武器
指针存储的是内存地址。Java 参考 ≈ 自动管理的指针。
#include <iostream>
int main() {
int a = 10;
int* ptr = &a; // ptr 存储 a 的地址
*ptr = 20; // 解引用:修改 ptr 指向的值
std::cout << a; // 输出 20
int* null_ptr = nullptr; // 空指针
if (null_ptr) { // 检查指针是否为空
*null_ptr = 42;
}
}
const 指针的四重境界
int value = 10;
int* p1 = &value; // 普通指针:值和指向都可改
const int* p2 = &value; // 指向 const int:不能改值,可改指向
int* const p3 = &value; // const 指针:可改值,不能改指向
const int* const p4 = &value; // 两者皆 const:既不能改值,也不能改指向
记忆口诀:const 在 * 左边表示指向的值不能改,const 在 * 右边表示指针本身不能改。
指针算术
#include <iostream>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int* ptr = arr; // 指向 arr[0]
ptr++; // 指向 arr[1]
std::cout << *ptr; // 输出 20
std::cout << *(ptr + 2); // 输出 40(从 arr[1] 后移 2 个 int)
}
⚠️ 坏味道:裸指针管理所有权
void leaky() { int* p = new int(42); // 使用 p ... // ❌ 忘记 delete p,内存泄漏! }正确做法:使用智能指针或栈对象。需要动态生命周期时,优先
std::unique_ptr。
栈 vs 堆
| 特性 | 栈(Stack) | 堆(Heap) |
|---|---|---|
| 分配方式 | 自动(函数调用压栈) | 手动(new/malloc) |
| 释放方式 | 自动(函数返回弹栈) | 手动(delete/free) |
| 速度 | 快(仅移动栈指针) | 较慢(需要查找空闲块) |
| 大小 | 有限(通常 1-8 MB) | 大(受限于系统内存) |
| 生命周期 | 函数作用域内 | 程序员控制 |
struct Person {
std::string name;
int age;
};
void example() {
Person p{"Alice", 30}; // 栈上分配,自动销毁
Person* hp = new Person{"Bob", 25}; // 堆上分配
delete hp; // 必须手动释放!
}
C++ 黄金法则:优先栈对象,用 RAII
// ❌ Java 思维:什么都 new
Person* p = new Person("Alice");
delete p;
// ✅ C++ 思维:栈对象优先
Person p{"Alice"}; // 栈上
std::vector<int> v{1, 2, 3}; // 栈上
// ✅ 必须用堆时,使用智能指针
auto v2 = std::make_unique<std::vector<int>>(10);
auto v3 = std::make_shared<Person>("Bob");
RAII:C++ 的灵魂
RAII(Resource Acquisition Is Initialization)是 C++ 最核心的设计思想:将资源的生命周期与对象的生命周期绑定。 构造时获取资源,析构时释放资源。
基本原理
#include <cstdio>
#include <stdexcept>
#include <iostream>
class FileHandle {
FILE* file;
public:
FileHandle(const char* filename, const char* mode = "r") {
file = fopen(filename, mode);
if (!file) {
throw std::runtime_error("打开文件失败");
}
std::cout << "文件已打开: " << filename << std::endl;
}
~FileHandle() {
if (file) {
fclose(file);
std::cout << "文件已自动关闭" << std::endl;
}
}
// 禁止拷贝(RAII 类通常不可复制)
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// 读取一行
std::string readLine() {
char buf[256];
if (fgets(buf, sizeof(buf), file)) {
return buf;
}
return "";
}
};
void process_file() {
FileHandle fh("data.txt"); // 构造时打开
// 使用文件...
std::string line = fh.readLine();
} // 离开作用域时 fh 析构,文件自动关闭——即使有异常也会析构!
RAII 的三大优势:
- 异常安全:即使抛出异常,栈回溯也会调用析构函数
- 确定性释放:离开作用域立刻释放,没有 GC 延迟
- 代码简洁:不需要 try-finally
RAII 真实场景 1:互斥锁
#include <mutex>
#include <iostream>
std::mutex g_mutex;
int shared_counter = 0;
void safe_increment() {
std::lock_guard<std::mutex> lock(g_mutex); // 构造时 lock
++shared_counter; // 安全操作共享数据
if (shared_counter == 3) {
throw std::runtime_error("模拟异常");
}
} // lock 析构时自动 unlock,即使是异常路径也保证解锁!
int main() {
try {
safe_increment();
} catch (...) {
// g_mutex 已经被 unlock(lock_guard 析构保证)
std::cout << "异常被捕获,互斥锁已安全释放" << std::endl;
}
}
RAII 真实场景 2:数据库连接
#include <iostream>
#include <stdexcept>
// 模拟数据库 API
struct DBConnection {
bool connected = false;
void connect() {
std::cout << "数据库连接已建立" << std::endl;
connected = true;
}
void disconnect() {
if (connected) {
std::cout << "数据库连接已关闭" << std::endl;
connected = false;
}
}
void query(const std::string& sql) {
if (!connected) throw std::runtime_error("未连接");
std::cout << "执行查询: " << sql << std::endl;
}
};
class DatabaseGuard {
DBConnection& conn_;
public:
DatabaseGuard(DBConnection& conn) : conn_(conn) {
conn_.connect();
}
~DatabaseGuard() {
conn_.disconnect();
}
DatabaseGuard(const DatabaseGuard&) = delete;
DatabaseGuard& operator=(const DatabaseGuard&) = delete;
};
int main() {
DBConnection conn;
{
DatabaseGuard guard(conn); // 连接数据库
conn.query("SELECT * FROM users");
// 即使这里抛出异常,guard 析构也会断开连接
} // guard 析构 → conn.disconnect() 被调用
}
智能指针:RAII 的内存管理
C++11 引入了三种智能指针,它们都是 RAII 思想的体现——将堆内存的生命周期与智能指针对象的生命周期绑定。
std::unique_ptr(独占所有权)
unique_ptr 是"独享所有权"的智能指针。它不能被拷贝,只能被移动(转移所有权)。
#include <memory>
#include <iostream>
struct Person {
std::string name;
Person(const std::string& n) : name(n) {
std::cout << "Person: " << name << " 构造" << std::endl;
}
~Person() {
std::cout << "Person: " << name << " 析构" << std::endl;
}
};
int main() {
auto ptr = std::make_unique<Person>("Alice"); // C++14
// auto ptr2 = ptr; // ❌ 编译错误!不能复制 unique_ptr
auto ptr3 = std::move(ptr); // ✅ 转移所有权,ptr 变为空
// 在容器中使用
std::vector<std::unique_ptr<Person>> people;
people.push_back(std::make_unique<Person>("Bob"));
// 自定义删除器
auto deleter = [](FILE* f) {
if (f) {
fclose(f);
std::cout << "文件已关闭(自定义删除器)" << std::endl;
}
};
std::unique_ptr<FILE, decltype(deleter)> file_ptr(
fopen("test.txt", "w"), deleter
);
}
💡 make_unique 的实现原理
// 简化版 make_unique 实现 template<typename T, typename... Args> std::unique_ptr<T> my_make_unique(Args&&... args) { return std::unique_ptr<T>( new T(std::forward<Args>(args)...) // 完美转发参数 ); }
make_unique的优势:
- 异常安全:避免
new T(args)和unique_ptr构造之间抛出异常导致的泄漏- 代码简洁:不用写
new关键字- 避免重复类型:类型只写一次
std::shared_ptr(共享所有权)
shared_ptr 使用引用计数来管理共享所有权。当最后一个 shared_ptr 销毁时,资源被释放。
#include <memory>
#include <iostream>
struct Person {
std::string name;
Person(const std::string& n) : name(n) {}
~Person() { std::cout << name << " 析构" << std::endl; }
};
int main() {
auto ptr1 = std::make_shared<Person>("Alice");
std::cout << "引用计数: " << ptr1.use_count() << std::endl; // 1
{
auto ptr2 = ptr1; // 引用计数 +1
std::cout << "引用计数: " << ptr1.use_count() << std::endl; // 2
} // ptr2 析构,引用计数 -1
std::cout << "引用计数: " << ptr1.use_count() << std::endl; // 1
} // ptr1 析构,引用计数为 0,Person 析构
💡 引用计数机制详解
shared_ptr内部包含两个指针:
- 指向管理对象的指针
- 指向控制块(control block)的指针
控制块包含:
- 引用计数(shared count):记录共享所有权的
shared_ptr数量- 弱引用计数(weak count):记录
weak_ptr数量(用于控制块的存活)- 删除器(deleter)和分配器(allocator)
// 示意图(伪代码) template<typename T> class shared_ptr_inner { T* ptr_; ControlBlock* cb_; }; struct ControlBlock { long shared_count_; // 引用计数 long weak_count_; // 弱引用计数 void (*deleter_)(void*); };为什么推荐
make_shared?
- 一次分配:
make_shared将对象和控制块分配在同一块内存中,比分开new+ 控制块少一次内存分配- 异常安全:同
make_unique- 性能更好:缓存局部性更优
但
make_shared的缺点:当仍有weak_ptr存在时,即使shared_count已为 0,控制块无法释放,导致对象内存无法释放。对内存敏感的场景,可考虑new + shared_ptr分开分配。
std::weak_ptr(弱引用——打破循环引用)
weak_ptr 是 shared_ptr 的"观察者",它不增加引用计数。主要用于打破循环引用。
⚠️ 坏味道:循环引用导致内存泄漏
#include <memory> #include <iostream> struct Node { std::shared_ptr<Node> next; std::shared_ptr<Node> prev; // ❌ shared_ptr 导致循环引用 ~Node() { std::cout << "Node 析构" << std::endl; } }; void circular_reference_demo() { auto a = std::make_shared<Node>(); auto b = std::make_shared<Node>(); a->next = b; b->prev = a; // a 和 b 互相持有 shared_ptr,引用计数永远不为 0 // 函数结束:a 和 b 都不会析构,内存泄漏! }
解决方案:用 weak_ptr 打破循环
#include <memory>
#include <iostream>
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // ✅ weak_ptr 不增加引用计数
~Node() { std::cout << "Node 析构" << std::endl; }
void print_prev() {
if (auto sp = prev.lock()) { // lock() 返回 shared_ptr,若已释放返回空
std::cout << "前驱节点存在" << std::endl;
} else {
std::cout << "前驱节点已释放" << std::endl;
}
}
};
int main() {
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->prev = a; // ✅ weak_ptr,不增加引用计数
std::cout << "a use_count: " << a.use_count() << std::endl; // 1(只有 next)
std::cout << "b use_count: " << b.use_count() << std::endl; // 1(只有 main 持有)
a->next->print_prev(); // "前驱节点存在"
a.reset(); // 释放 a
b->print_prev(); // "前驱节点已释放"
} // b 正常析构,无内存泄漏
💡 智能指针选择原则
优先栈对象 → 需要堆内存时用 unique_ptr → 确实需要共享所有权时用 shared_ptr → 需要打破循环引用时用 weak_ptr这是一个金字塔形选择原则:从上层开始考虑,只有当前层不能满足需求时才降级到下一层。
移动语义:C++11 最重要的革命
Java 程序员完全没有接触过这个概念——移动语义。它是 C++11 引入的最重要的性能特性。
问题:不必要的拷贝
#include <vector>
#include <string>
#include <iostream>
// 返回大量数据时,传统 C++98 会拷贝
std::vector<int> create_large_vector() {
std::vector<int> v(1000000, 42);
return v; // C++98 会拷贝整个数组(一!百!万!个!整!数!)
}
C++11 之前,上面的代码会拷贝整个数组。C++11 之后,编译器会移动而非拷贝。
左值与右值
int a = 10; // a 是左值(有名字、可取地址、表达式结束后依然存在)
int b = a + 1; // (a + 1) 是右值(临时对象、表达式结束后不再存在)
根据 changkun 的《现代 C++ 教程》,C++11 将右值进一步划分为:
| 类别 | 英文 | 说明 | 示例 |
|---|---|---|---|
| 左值 | lvalue | 表达式结束后依然存在的持久对象 | 变量名、函数返回的左值引用 |
| 纯右值 | prvalue | 纯粹的字面量或临时对象 | 10, true, 1+2, Lambda 表达式 |
| 将亡值 | xvalue | 资源即将被转移的右值 | std::move() 的返回值 |
理解左值和右值的关键区别:左值可以取地址,右值不能。右值引用(
&&)可以绑定到右值并"窃取"其资源。
右值引用
#include <iostream>
#include <string>
#include <vector>
void process(int& x) {
std::cout << "左值引用: " << x << std::endl;
}
void process(int&& x) {
std::cout << "右值引用: " << x << std::endl;
}
int main() {
int a = 42;
process(a); // 调用左值版本
process(100); // 调用右值版本
process(std::move(a)); // std::move 将 a 转换为右值,调用右值版本
}
std::move:将左值"伪装"成右值
#include <vector>
#include <string>
#include <iostream>
int main() {
std::vector<int> v1(1000000, 42);
std::cout << "v1 size: " << v1.size() << std::endl; // 1000000
std::vector<int> v2 = std::move(v1); // 移动:v1 的资源被"偷"给 v2
std::cout << "v1 size after move: " << v1.size() << std::endl; // 0(v1 被置空)
std::cout << "v2 size: " << v2.size() << std::endl; // 1000000
}
移动语义的本质:把"拷贝整个数组"变成"拷贝指针 + 置空源对象"——O(1) 操作代替 O(n) 操作。
💡 std::move 实现原理
// 简化版 std::move 实现 template<typename T> typename std::remove_reference<T>::type&& move(T&& t) noexcept { return static_cast<typename std::remove_reference<T>::type&&>(t); }
std::move实际上什么也不移动——它只是一个强制类型转换,将参数转换为右值引用。移动操作发生在接收右值的移动构造函数或移动赋值运算符中。
移动构造函数(以 std::string 为例)
#include <iostream>
#include <string>
class MyString {
char* data_;
size_t size_;
public:
// 构造函数
MyString(const char* str) : size_(std::strlen(str)), data_(new char[size_ + 1]) {
std::copy(str, str + size_ + 1, data_);
std::cout << "构造" << std::endl;
}
// 拷贝构造函数
MyString(const MyString& other) : size_(other.size_), data_(new char[size_ + 1]) {
std::copy(other.data_, other.data_ + size_ + 1, data_);
std::cout << "拷贝构造 (深拷贝)" << std::endl;
}
// 移动构造函数
MyString(MyString&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr; // 置空源对象!关键步骤
other.size_ = 0;
std::cout << "移动构造 (窃取资源)" << std::endl;
}
// 移动赋值运算符
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] data_; // 释放已有资源
data_ = other.data_; // 窃取指针
size_ = other.size_;
other.data_ = nullptr; // 置空源
other.size_ = 0;
}
return *this;
}
~MyString() {
delete[] data_;
}
void print() const {
if (data_) std::cout << data_ << std::endl;
else std::cout << "(空)" << std::endl;
}
};
int main() {
MyString s1("Hello");
MyString s2 = s1; // 拷贝构造(深拷贝)
MyString s3 = std::move(s1); // 移动构造(窃取资源,s1 变空)
s2.print(); // Hello
s3.print(); // Hello
s1.print(); // (空)
}
⚠️ 坏味道:不必要的拷贝
void process(const std::vector<int>& v) { std::vector<int> copy = v; // ❌ 不必要地拷贝了整个 vector // ... 使用 copy }如果确实需要一个副本以便修改,可以按值传参让调用者决定是拷贝还是移动:
void process(std::vector<int> v) { // ✅ 调用者传左值则拷贝,传右值则移动 // ... 使用 v }
完美转发:std::forward
完美转发解决的问题:在函数模板中,将参数以原始的值类别(左值/右值)转发给另一个函数。
#include <iostream>
#include <memory>
#include <utility>
struct Person {
std::string name;
int age;
Person(const std::string& n, int a)
: name(n), age(a) {
std::cout << "Person 构造 (拷贝 name)" << std::endl;
}
Person(std::string&& n, int a)
: name(std::move(n)), age(a) {
std::cout << "Person 构造 (移动 name)" << std::endl;
}
};
// 转发引用(forwarding reference):T&& 在模板中具有特殊语义
template<typename T, typename... Args>
std::unique_ptr<T> my_make_unique(Args&&... args) {
// std::forward 保持参数的值类别:
// 如果原始参数是左值 → Args&& 解析为 Args& → forward 返回左值引用
// 如果原始参数是右值 → Args&& 解析为 Args&& → forward 返回右值引用
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
int main() {
std::string name = "Alice";
// name 是左值 → 调用 Person(const std::string&, int)
auto p1 = my_make_unique<Person>(name, 30);
// std::move(name) 是右值 → 调用 Person(std::string&&, int)
auto p2 = my_make_unique<Person>(std::move(name), 25);
}
💡 转发引用(Forwarding Reference)与右值引用的区别
template<typename T> void foo(T&& arg); // T&& 是转发引用(在模板中) void bar(int&& arg); // int&& 是右值引用(非模板)转发引用可以根据实参自动推导为左值引用或右值引用:
- 传左值时:
T推导为int&,int& &&折叠为int&- 传右值时:
T推导为int,int&&就是右值引用这正是完美转发的基础。
编码习惯与坏味道总结
✅ 良好编码习惯
| 习惯 | 说明 |
|---|---|
| 栈对象优先 | 除非需要动态生命周期,默认用栈 |
传参用 const & |
避免不必要的拷贝(只读场景) |
| 使用智能指针层级 | unique_ptr → shared_ptr → weak_ptr |
使用 make_shared/make_unique |
异常安全,代码简洁 |
| RAII 封装资源 | 互斥锁、文件句柄、数据库连接都用 RAII 包装 |
| 移动代替拷贝 | 对于大对象,用 std::move 转移所有权 |
| 返回局部对象用值 | 编译器会做 RVO(返回值优化)或自动移动 |
nullptr 而不是 NULL |
类型安全,避免重载歧义 |
| Rule of Five / Rule of Zero | 要么自定义所有五个特殊成员函数,要么都不自定义 |
❌ 需要避免的坏味道
| 坏味道 | 问题 | 解决方案 |
|---|---|---|
| 裸指针管理所有权 | 忘记 delete,异常安全差 |
用 unique_ptr |
忘记 delete |
内存泄漏 | 智能指针 / RAII |
shared_ptr 循环引用 |
内存泄漏 | 用 weak_ptr 打破循环 |
| 返回局部变量的引用 | 悬空引用(UB) | 返回值或传给输出参数 |
| 不必要的拷贝 | 性能差 | 传 const &,或用 std::move |
手动 new/delete |
代码啰嗦,易错 | make_unique/make_shared |
| 在容器中存裸指针 | 生命周期管理困难 | 存 unique_ptr |
完整示例:综合应用
下面是一个综合应用 RAII、智能指针和移动语义的例子:
#include <iostream>
#include <memory>
#include <mutex>
#include <vector>
#include <string>
#include <fstream>
// 1. RAII 封装互斥锁
class SafeCounter {
mutable std::mutex mtx_;
int count_ = 0;
public:
int increment() {
std::lock_guard<std::mutex> lock(mtx_);
return ++count_;
}
};
// 2. RAII 封装文件
class LogFile {
std::ofstream file_;
public:
LogFile(const std::string& path) {
file_.open(path, std::ios::app);
if (!file_.is_open()) {
throw std::runtime_error("无法打开日志文件");
}
}
~LogFile() {
if (file_.is_open()) {
file_.close();
}
}
void write(const std::string& msg) {
std::lock_guard<std::mutex> lock(mtx_);
file_ << msg << std::endl;
}
LogFile(const LogFile&) = delete;
LogFile& operator=(const LogFile&) = delete;
private:
std::mutex mtx_;
};
// 3. 智能指针 + 移动语义
struct BigData {
std::vector<double> data;
BigData(size_t n) : data(n, 3.14) {
std::cout << "BigData 构造, size=" << n << std::endl;
}
~BigData() {
std::cout << "BigData 析构" << std::endl;
}
};
using BigDataPtr = std::unique_ptr<BigData>;
BigDataPtr create_big_data(size_t n) {
return std::make_unique<BigData>(n);
}
int main() {
// RAII 互斥锁
SafeCounter counter;
std::cout << "计数: " << counter.increment() << std::endl;
// RAII 文件
try {
LogFile log("app.log");
log.write("应用启动");
log.write("处理数据...");
} catch (const std::exception& e) {
std::cerr << "错误: " << e.what() << std::endl;
}
// unique_ptr 移动语义
auto data = create_big_data(1000);
auto data2 = std::move(data); // 转移所有权
// data 现在为空
// vector 中的 unique_ptr
std::vector<BigDataPtr> dataset;
dataset.push_back(create_big_data(100));
dataset.push_back(create_big_data(200));
std::cout << "数据处理完成" << std::endl;
// 所有资源自动释放
}
原理解析:从 changkun 现代 C++ 教程看右值引用
引用 changkun 的《现代 C++ 教程》中对右值引用的总结:
“右值引用是 C++11 引入的与 Lambda 表达式齐名的重要特性之一。它的引入解决了 C++ 中大量的历史遗留问题,消除了诸如
std::vector、std::string之类的额外开销,也才使得函数对象容器std::function成为了可能。”
右值引用解决的核心问题:
- 临时对象的效率问题:C++ 长期以来被诟病临时对象拷贝开销大,右值引用允许"窃取"临时对象的资源
- 完美转发:使得编写能够保持参数值类别的通用转发函数成为可能
- 移动语义:使得只可移动(move-only)的类型成为可能,如
std::unique_ptr、std::thread、std::future
核心概念一句话总结:
左值持久,右值短暂;左值引用绑定左值,右值引用绑定右值;
move把左值变右值(允许窃取),forward保持原始值类别(完美转发)。
下一篇我们将学习 C++ 的面向对象编程,看看和 Java 的类有什么异同。