C++11关键特性

0.1 C++11

0.1.1 自动类型推导

让编译器在编译期帮你推断类型,而不是你手写类型。真正难点在于:推导规则

核心工具:

  • auto
  • decltype 精确类型
  • decltype(auto) 完美转发
0.1.1.1 auto

基本规则

c++ 复制代码
auto x = 10; // int
auto y = 3.14; // double

根据初始化表达式推导类型

关键规则:

  • 忽略引用
c++ 复制代码
int a = 10;
int& ref = a;

auto x = ref; // int 而不是 int&
  • 忽略顶层 const
c++ 复制代码
const int a = 10;
auto x = a; // int(const 丢了)
  • 想保留需要手动加
c++ 复制代码
const int a = 10;
const auto x = a;   // const int
auto& y = a;        // const int&

需要注意:

c++ 复制代码
int& func();

auto x = func();   // int(引用丢了)
auto& y = func();  // int&

// 给出以下示例
#include <iostream>
using namespace std;
int a = 10;
int& func() {
  return a;
}

int main()
{
  auto x = func();
  auto& y = func();
  int *p = &a;

  cout << p << endl;
  cout << &x << endl;
  cout << &y << endl;

  return 0;
}

// 程序执行结果如下:
// 0x1027d0000
// 0x16d636ab8
// 0x1027d0000
0.1.1.2 decltype 获取精确类型
c++ 复制代码
int a = 10;
decltype(a) x = 20; // int

auto 推导来源是初始化值,decltype 推导来源为表达式,与 auto 不同,decltype 能够保留引用和 const。

关键规则:

c++ 复制代码
int a = 10;

decltype(a) x = a;   // int
decltype((a)) y = a; // int&

括号() 在 C++ 中是运算符,作用是产生一个表达式 。因此编译器不再看声明,而是分析表达式的值类别 。每个表达式都有两个属性:类型 + 值类别,值类别分为左值和右值

  • lvalue 有名字、有地址、可以放在赋值表达式左边的
  • rvalue 临时的、没有名字、只能放在赋值表达式右边的

(a) 这个表达式: a 是一个有名字、有地址的变量,(a) 求值之后仍然指向那块内存,所以是 lvalue。

decltype 对表达式的规则:

  • 表达式是 lvalue → 结果加 & → T&
  • 表达式是 rvalue → 结果不变 → T

类型推导在使用 STL 容器时很好用,通过推导类型避免写一长串 iterator 类类型。

0.1.1.3 decltype(auto)
c++ 复制代码
int& func();

auto a = func();           // int
decltype(auto) b = func(); // int&

decltype(auto) = 不会丢失引用,这在模板中非常重要,比如:

c++ 复制代码
template<typename T>
decltype(auto) wrapper(T&& t) {
    return t;
}

这样可以保证返回值类型完全一致。

0.1.1.4 总结
  • auto 会丢失引用和 const
  • auto& 强制引用
  • decltype 完全精确
  • decltype(auto) 自动+完全精确

推荐使用:

c++ 复制代码
auto it = vec.begin();       // 简化类型
for (auto& x : vec)          // 避免拷贝

需要警惕:

c++ 复制代码
auto x = func();   // 可能偷偷拷贝(性能问题)

模板/泛型才用:

c++ 复制代码
decltype(auto)

0.1.2 智能指针

先说一下智能指针的核心目的:让堆内存的生命周期自动管理,不需要手动 delete。

0.1.2.1 为什么需要智能指针?

裸指针存在的问题:异常、提前返回、多个出口,任何一个地方漏掉 delete 就会导致泄露。

智能指针通过 RAII 解决这个问题:对象离开作用域时,析构函数自动释放内存。

0.1.2.2 unique_ptr 独占所有权

一块内存只有一个主人,主人销毁时,内存跟着销毁。

c++ 复制代码
#include <memory>
std::unique_ptr<int> p = std::make_unique<int>(42);
// 离开作用域自动 delete,不需要手动释放

不能拷贝,只能移动

c++ 复制代码
auto p1 = std::make_unique<int>(42);

auto p2 = p1;            // ❌ 编译错误,不允许拷贝
auto p2 = std::move(p1); // ✅ 转移所有权,p1 变成 nullptr

移动之后 p1 就失效了,整个体系中,始终只有一个指针拥有这块内存。

常用操作:

c++ 复制代码
auto p = std::make_unique<int>(42);

*p            // 解引用,得到 42
p.get()       // 获取裸指针,但不转移所有权
p.release()   // 放弃所有权,返回裸指针,p 变 nullptr(需要自己 delete)
p.reset()     // 释放当前内存,p 变 nullptr
p.reset(new int(99))  // 释放旧的,接管新的

作为函数参数:

c++ 复制代码
// 转移所有权进去,调用方失去控制权
void take_ownership(std::unique_ptr<int> p) { ... }

// 只是借用,不转移
void borrow(int* p) { ... }
void borrow(const std::unique_ptr<int>& p) { ... }

管理数组:

c++ 复制代码
auto arr = std::make_unique<int[]>(10);
arr[0] = 42;  // 支持下标访问
// 离开作用域自动调用 delete[]
0.1.2.3 shared_ptr 共享所有权

基本概念 :多个指针可以共同拥有一块内存,内部用引用计数追踪有多少人拥有它,计数归零时才释放。

c++ 复制代码
auto p1 = std::make_shared<int>(42);  // 引用计数 = 1
auto p2 = p1;                          // 引用计数 = 2
auto p3 = p1;                          // 引用计数 = 3

p1.reset();  // 引用计数 = 2,内存还在
p2.reset();  // 引用计数 = 1,内存还在
p3.reset();  // 引用计数 = 0,内存释放

内部结构
shared_ptr 内部有两块东西:

c++ 复制代码
shared_ptr 对象
├── 指向堆对象的指针    →  [ int: 42 ]
└── 指向控制块的指针   →  [ use_count: 3 | weak_count: 0 | deleter ]

控制块是单独分配的一块内存,所有指向同一对象的 shared_ptr 共享同一个控制块。

make_sharednew 的区别

c++ 复制代码
// 方式一:两次内存分配(对象一次,控制块一次)
std::shared_ptr<int> p1(new int(42));

// 方式二:一次内存分配(对象和控制块在一起)
auto p2 = std::make_shared<int>(42);  // 推荐

make_shared 更高效,且异常安全,优先使用。

引用计数的代价

引用计数的增减是原子操作(线程安全),但这不意味着 shared_ptr 本身是线程安全的:

c++ 复制代码
// 引用计数的修改是安全的
// 但同一个 shared_ptr 对象被多线程同时读写仍然不安全
auto p = std::make_shared<int>(42);

// 多线程同时拷贝 p → 安全(原子操作保护计数)
// 多线程同时对 p 赋值 → 不安全(p 本身不是原子的)
0.1.2.4 weak_ptr 弱引用,不参与计数

为什么需要它
shared_ptr 的循环引用会导致内存永远无法释放:

c++ 复制代码
struct Node {
    std::shared_ptr<Node> next;
    std::shared_ptr<Node> prev;  // 双向链表
};

auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();

a->next = b;  // b 的引用计数 = 2
b->prev = a;  // a 的引用计数 = 2

// a、b 离开作用域,引用计数从 2 降到 1,永远不会到 0
// 内存泄漏!

weak_ptr 打破循环

c++ 复制代码
struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;   // 弱引用,不增加计数
};

auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();

a->next = b;  // b 的引用计数 = 2
b->prev = a;  // a 的引用计数仍然 = 1(weak_ptr 不计数)

// 离开作用域,a 引用计数降为 0,a 释放
// a 释放导致 a->next 释放,b 引用计数降为 0,b 释放

使用 weak_ptr
weak_ptr 不能直接解引用,必须先提升成 shared_ptr,提升时检查对象是否还活着:

c++ 复制代码
auto sp = std::make_shared<int>(42);
std::weak_ptr<int> wp = sp;

// 方式一:lock(),对象已销毁则返回空 shared_ptr
if (auto locked = wp.lock()) {
    std::cout << *locked;  // 安全访问
} else {
    // 对象已经销毁
}

// 方式二:expired(),检查是否已销毁
if (!wp.expired()) {
    // 注意:这里和 lock() 之间可能有竞争,生产中用 lock()
}

三种智能指针对比

unique_ptr shared_ptr weak_ptr
所有权 独占 共享
引用计数 不增加
可拷贝
可移动
开销 几乎零 原子操作+控制块 同shared_ptr
直接解引用 ❌需要lock()
适用场景 单一所有者 共享所有者 打破循环引用/观察者
0.1.2.5 常见陷阱
  • 陷阱1:裸指针构造多个 shared_ptr
c++ 复制代码
int* raw = new int(42);

auto p1 = std::shared_ptr<int>(raw);
auto p2 = std::shared_ptr<int>(raw);  // ❌ 两个独立控制块!

// p1 和 p2 各自认为自己是唯一拥有者
// 任何一个析构时就会 delete raw
// 另一个析构时再次 delete → double free,崩溃

解决:永远用 make_shared,或者只从一个 shared_ptr 拷贝。

  • 陷阱2:在类内部获取自身的 shared_ptr
c++ 复制代码
struct Foo {
    void bad() {
        // this 是裸指针,这里又创建了一个独立的 shared_ptr
        auto p = std::shared_ptr<Foo>(this);  // ❌ 危险
    }
};

正确的做法是继承 enable_shared_from_this

c++ 复制代码
struct Foo : std::enable_shared_from_this<Foo> {
    void good() {
        auto p = shared_from_this();  // ✅ 从已有控制块获取
    }
};

auto foo = std::make_shared<Foo>();
foo->good();  // 安全
  • 陷阱3:unique_ptr move 后继续使用
c++ 复制代码
auto p1 = std::make_unique<int>(42);
auto p2 = std::move(p1);

*p1;  // ❌ p1 已经是 nullptr,未定义行为
  • 陷阱4:shared_ptr 管理 this 导致提前释放
c++ 复制代码
void Foo::register_self() {
    // 如果外部没有 shared_ptr 持有这个对象
    // shared_from_this() 会抛异常
    auto p = shared_from_this();
}

Foo foo;              // 栈上对象
foo.register_self();  // ❌ 崩溃,栈对象没有控制块

0.1.3 lambda 表达式

0.1.3.1 lambda 是什么

lambda 就是一个 可以就地定义的匿名函数 ,本质上编译器会把它翻译成一个类(重载了 operator() 的函数对象)。

c++ 复制代码
// 普通函数
int add(int a, int b) { return a + b; }

// 等价的 lambda
auto add = [](int a, int b) { return a + b; };

add(1, 2);  // 调用方式完全一样,结果 3
0.1.3.2 完整语法结构
c++ 复制代码
[捕获列表] (参数列表) 说明符 -> 返回类型 { 函数体 }
    ①        ②      ③        ④       ⑤

每个部分都可以省略,最简形式:

c++ 复制代码
auto f = [] { return 42; };  // 没有参数,没有捕获,返回类型自动推导
0.1.3.3 最核心概念------捕获列表

捕获列表决定 lambda 能访问外部哪些变量,以及怎么访问。

  • [] 什么都不捕获
c++ 复制代码
int x = 10;
auto f = [] { return x; };  // ❌ 编译错误,x 没有被捕获
  • [x] 值捕获------拷贝一份进来
c++ 复制代码
int x = 10;
auto f = [x] { return x; };

x = 99;    // 修改外部 x
f();       // 返回 10,lambda 内部是捕获时的副本,不受影响

捕获的副本默认是 const 的,不能在 lambda 内部修改:

c++ 复制代码
int x = 10;
auto f = [x] {
    x = 99;  // ❌ 编译错误,值捕获默认是 const
};
  • [&x] 引用捕获------绑定原变量
c++ 复制代码
int x = 10;
auto f = [&x] { return x; };

x = 99;
f();  // 返回 99,引用捕获,x 怎么变 lambda 就看到什么

引用捕获可以在 lambda 内部修改原变量:

c++ 复制代码
int x = 10;
auto f = [&x] { x = 99; };

f();
// x 现在是 99
  • [=]隐式值捕获所有变量
c++ 复制代码
int x = 10, y = 20;
auto f = [=] { return x + y; };  // x、y 都被值捕获
  • [&] 隐式引用捕获所有变量
c++ 复制代码
int x = 10, y = 20;
auto f = [&] { x = 99; y = 88; };  // x、y 都被引用捕获
f();
// x == 99, y == 88
  • 混合捕获
c++ 复制代码
int x = 10, y = 20, z = 30;

// 默认值捕获,但 y 用引用
auto f = [=, &y] { y = 99; return x + y + z; };

// 默认引用捕获,但 x 用值
auto g = [&, x] { y = 99; return x; };

如果需要捕获多个,依次用逗号隔开即可。

  • this 相关
c++ 复制代码
[this]  // 捕获当前对象的指针
[*this] // 捕获当前对象的副本(C++ 17)
0.1.3.4 mutable------让值捕获可以修改

值捕获的副本默认是 const,加 mutable 去掉这个限制:

c++ 复制代码
int x = 10;

auto f = [x]() mutable {
    x = 99;   // ✅ 修改的是副本,不影响外部 x
    return x;
};

f();   // 返回 99
// 外部 x 仍然是 10

注意:修改的是 lambda 内部自己的副本,外部变量不受影响。

0.1.3.5 返回类型
  • 自动推导,大多情况都够用
c++ 复制代码
auto f = [](int a, int b) { return a + b; };  // 推导为 int
  • 多个 return 语句时需要明确指定
c++ 复制代码
// ❌ 两个 return 类型不同,推导失败
auto f = [](bool flag) {
    if (flag) return 1;
    return 1.0;
};

// ✅ 明确指定返回类型
auto f = [](bool flag) -> double {
    if (flag) return 1;
    return 1.0;
};
0.1.3.6 lambda 的类型与存储

lambda 的类型是唯一的匿名类型

每个 lambda 都有自己独一无二的类型,不能直接写出来,只能使用 auto:

c++ 复制代码
auto f = [] { return 42; };
// f 的类型类似于编译器生成的:
// struct __lambda_xyz {
//     int operator()() const { return 42; }
// };

std::function 存储(有开销)

需要统一存储不同 lambda 时用 std::function:

c++ 复制代码
#include <functional>

std::function<int(int, int)> op;

op = [](int a, int b) { return a + b; };
op(1, 2);  // 3

op = [](int a, int b) { return a * b; };
op(3, 4);  // 12

无捕获的 lambda 可以转化为函数指针

c++ 复制代码
auto f = [](int a, int b) { return a + b; };

// 没有捕获,可以退化为函数指针
int (*fp)(int, int) = f;
fp(1, 2);  // 3

// 有捕获的 lambda 不能转函数指针
int x = 10;
auto g = [x](int a) { return a + x; };
int (*gp)(int) = g;  // ❌ 编译错误
0.1.3.7 实际使用场景

场景一:配合 STL 算法

c++ 复制代码
std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};

// 排序
std::sort(v.begin(), v.end(), [](int a, int b) {
    return a > b;  // 降序
});

// 查找
auto it = std::find_if(v.begin(), v.end(), [](int x) {
    return x > 4;
});

// 遍历
std::for_each(v.begin(), v.end(), [](int x) {
    std::cout << x << " ";
});

场景二:回调函数

c++ 复制代码
void do_something(int x, std::function<void(int)> callback) {
    // 做一些事
    callback(x * 2);
}

int result = 0;
do_something(21, [&result](int val) {
    result = val;  // 引用捕获,修改外部变量
});
// result == 42

场景三:延迟执行/任务封装

c++ 复制代码
std::vector<std::function<void()>> tasks;

for (int i = 0; i < 5; i++) {
    tasks.push_back([i] {                    // 值捕获 i
        std::cout << "task " << i << "\n";
    });
}

for (auto& task : tasks) task();
// 输出 task 0 ~ task 4

场景四:就地执行(IIFE)

c++ 复制代码
// 定义完立刻调用,常用于复杂初始化
const int result = [&] {
    if (condition_a) return 1;
    if (condition_b) return 2;
    return 0;
}();
0.1.3.8 C++14/17/20的扩展
  • C++14:泛型 lambda (参数用 auto)
c++ 复制代码
auto print = [](auto x) {
    std::cout << x << "\n";
};

print(42);       // int
print(3.14);     // double
print("hello");  // const char*
  • C++14:捕获时初始化(广义捕获)
c++ 复制代码
// 捕获一个移动语义的对象(unique_ptr 不能拷贝)
auto p = std::make_unique<int>(42);

auto f = [ptr = std::move(p)] {
    return *ptr;
};
// p 已经是 nullptr,所有权转移到 lambda 内部

// 捕获时计算新值
int x = 10;
auto g = [y = x * 2] { return y; };  // y == 20
  • C++17:[*this] 捕获对象副本
c++ 复制代码
struct Foo {
    int value = 42;
    
    auto get_lambda() {
        return [*this] { return value; };  // 捕获整个对象的副本
        // 即使 Foo 对象销毁,lambda 仍然安全
    }
};
  • C++20:模板 lambda
c++ 复制代码
// C++14 泛型 lambda 无法约束类型
// C++20 可以用模板参数
auto f = []<typename T>(T a, T b) {
    return a + b;
};

f(1, 2);      // ✅ 同类型 int
f(1, 2.0);    // ❌ 类型不同,编译错误(而不是隐式转换)
0.1.3.9 常见陷阱
  • 陷阱1:引用捕获的生命周期问题
c++ 复制代码
std::function<int()> make_lambda() {
    int x = 42;
    return [&x] { return x; };  // ❌ x 是局部变量,函数返回后销毁
}

auto f = make_lambda();
f();  // 未定义行为,x 已经不存在了

解决:返回值要用值捕获或者确保引用对象的生命周期够长

  • 陷阱2:for 循环中引用捕获循环变量
c++ 复制代码
std::vector<std::function<void()>> tasks;

for (int i = 0; i < 3; i++) {
    tasks.push_back([&i] {          // ❌ 引用捕获,i 最终是 3
        std::cout << i << "\n";
    });
}

for (auto& t : tasks) t();
// 在栈内存还没有被覆盖的情况下,输出三行 3,不是 0 1 2
// 由于循环变量 i 的生命周期在 for 循环结束就结束了,后续这块栈内存,没有被覆盖,这里的内容就还是 3,如果被覆盖了,就是其他值了

解决:使用值捕获。

  • 陷阱3:在类成员中捕获 this
c++ 复制代码
struct Foo {
    int value = 42;
    
    auto get_lambda() {
        return [this] { return value; };
        // 实际上捕获的是 this 指针
        // 如果 Foo 对象销毁,lambda 仍然访问 value 就是悬空指针
    }
};

解决:生命周期不确定时,使用[*this](c++17)复制整个对象

  • 陷阱4:值捕获是创建时拷贝
c++ 复制代码
int a = 10;

auto f = [a]() {
    return a;
};

a = 20;

f(); // 还是 10

0.1.4 右值引用 + move

0.1.4.1 左值和右值

左值(lvalue):有名字、有地址、能放在赋值表达式左边的

c++ 复制代码
int a = 10;
a = 20;     // a 是左值,可以放左边
int* p = &a; // 可以取地址

右值(rvalue):临时的、没有名字的、用完就消失的

c++ 复制代码
10;          // 字面量,右值
a + 1;       // 表达式结果,临时值,右值
func();      // 函数返回的临时对象(返回非引用时),右值

判断口诀:能不能取地址。能取地址的就是左值,不能就是右值。

c++ 复制代码
&a;          // ✅ 能取地址,左值
&10;         // ❌ 不能取地址,右值
&(a + 1);    // ❌ 不能取地址,右值
0.1.4.2 没有右值引用之前的问题

用一个 String 类演示:

c++ 复制代码
class String {
public:
    char* data;
    size_t size;

    // 构造函数
    String(const char* str) {
        size = strlen(str);
        data = new char[size + 1];
        memcpy(data, str, size + 1);
        std::cout << "构造: " << data << "\n";
    }

    // 拷贝构造函数
    String(const String& other) {
        size = other.size;
        data = new char[size + 1];
        memcpy(data, other.data, size + 1);  // 深拷贝,重新分配内存
        std::cout << "拷贝构造: " << data << "\n";
    }

    ~String() { delete[] data; }
};

现在执行这段代码:

c++ 复制代码
String make_string() {
    String s("hello");
    return s;  // 返回一个临时对象
}

String a = make_string();  // 接收临时对象

这个过程会发生什么:

c++ 复制代码
构造:       "hello"   ← make_string 内部构造 s
拷贝构造:   "hello"   ← s 拷贝到返回值(临时对象)
拷贝构造:   "hello"   ← 临时对象拷贝到 a
析构        ← 临时对象销毁
析构        ← s 销毁

核心矛盾 :临时对象马上就要销毁了,我们却还要把它的数据完整拷贝 一份给 a,拷贝完临时对象立刻析构。这是纯粹的浪费------能不能直接把临时对象的内存"转交"给 a,而不是拷贝?

0.1.4.3 右值引用&&

C++11引入右值引用,专门用来绑定右值:

c++ 复制代码
int a = 10;

int&  lref = a;    // 左值引用,绑定左值 ✅
int&  lref = 10;   // 左值引用,绑定右值 ❌ 编译错误

int&& rref = 10;   // 右值引用,绑定右值 ✅
int&& rref = a;    // 右值引用,绑定左值 ❌ 编译错误

const int& clref = 10;  // ❗️const 左值引用,可以绑定右值 ✅(特殊规则)

右值引用的作用:能够让我们识别出这是一个临时对象,可以安全地偷走它的资源。

0.1.4.4 核心------移动构造函数

有了右值引用,就可以给 String 加一个移动构造函数

c++ 复制代码
class String {
public:
    char* data;
    size_t size;

    // 拷贝构造:老老实实深拷贝
    String(const String& other) {
        size = other.size;
        data = new char[size + 1];
        memcpy(data, other.data, size + 1);
        std::cout << "拷贝构造\n";
    }

    // 移动构造:直接偷走资源
    String(String&& other) {
        size = other.size;
        data = other.data;    // 直接拿走指针,不分配新内存
        
        other.data = nullptr; // 把原对象置空,防止析构时 double free
        other.size = 0;
        std::cout << "移动构造\n";
    }

    ~String() {
        delete[] data;  // nullptr 时 delete 是安全的
    }
};

现在再执行 make_string() 内部流程就变成了如下:

c++ 复制代码
String a = make_string();

构造        ← 构造 s
移动构造    ← 把 s 的资源"偷"给返回值(不分配新内存)
移动构造    ← 把返回值的资源"偷"给 a
析构        ← 返回值析构(data 是 nullptr,什么都不做)
析构        ← s 析构(data 是 nullptr,什么都不做)

从两次拷贝(两次 new + memcpy)变成两次指针交换,性能差距在数据量大的时候极其显著

0.1.4.5 std::move ------把左值变成右值

问题来了:右值引用只能绑定右值,但有时候我们手里有个左值,想触发移动怎么办?

c++ 复制代码
String a("hello");
String b = a;             // a 是左值,调用拷贝构造
String b = std::move(a);  // 强制把 a 转成右值,调用移动构造

std::move 并不移动任何东西,它只是做了一个类型转换,把左值转换成右值引用,从而让编译器选择移动构造而不是拷贝构造。

std::move 的实现非常简单:

c++ 复制代码
template<typename T>
typename std::remove_reference<T>::type&& move(T&& t) {
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}
// 本质就是 static_cast<T&&>

关键点:std::move 之后,原变量进入"有效但未指定状态"

c++ 复制代码
String a("hello");
String b = std::move(a);

// a.data 现在是 nullptr
// a 还是合法对象,但内容已经被掏空
// 此后不应该再使用 a 的值,只能赋值或析构
std::cout << a.data;  // ❌ 逻辑错误,a 已经被掏空
a = String("world");  // ✅ 重新赋值是安全的
0.1.4.6 移动赋值运算符

除了移动构造,还需要移动赋值:

c++ 复制代码
class String {
public:
    // 移动赋值运算符
    String& operator=(String&& other) {
        if (this == &other) return *this;  // 自我赋值检查
        
        delete[] data;          // 释放自己原来的资源
        
        data = other.data;      // 偷走资源
        size = other.size;
        
        other.data = nullptr;   // 把原对象置空
        other.size = 0;
        
        std::cout << "移动赋值\n";
        return *this;
    }
};
c++ 复制代码
String a("hello");
String b("world");

b = std::move(a);  // 触发移动赋值
// b 拿到 "hello",a 被置空
0.1.4.7 五法则------这几个函数要一起考虑

C++ 有个原则:如果你需要自定义以下任何一个,通常五个都需要自定义:

c++ 复制代码
class String {
    String(const String&);             // 1. 拷贝构造
    String(String&&);                  // 2. 移动构造
    String& operator=(const String&);  // 3. 拷贝赋值
    String& operator=(String&&);       // 4. 移动赋值
    ~String();                         // 5. 析构函数
};
0.1.4.8 编译器的自动优化:RVO/NRVO

实际上现代编译器有返回值优化(RVO),很多场景下根本不会发生拷贝或移动:

c++ 复制代码
String make_string() {
    return String("hello");  // RVO:直接在调用方的内存里构造,没有任何拷贝/移动
}

String a = make_string();
// 实际只有一次构造,编译器把 make_string 内部的对象直接构造在 a 的地址上

但 RVO 不是万能的,以下情况 RVO 无法生效,需要移动语义兜底:

c++ 复制代码
String make_string(bool flag) {
    String s1("hello");
    String s2("world");
    return flag ? s1 : s2;  // 多个返回路径,NRVO 无法确定,退回到移动
}
0.1.4.9 转发引用(万能引用)------ && 的另一种身份

&& 出现在模板参数推导 的场景时,它不是右值引用,而是转发引用,既能绑定左值也能绑定右值:

c++ 复制代码
template<typename T>
void func(T&& x) {   // 这里的 && 是转发引用,不是右值引用
    // x 可以绑定左值或右值
}

int a = 10;
func(a);   // T 推导为 int&,x 的类型是 int& &&,折叠成 int&
func(10);  // T 推导为 int,x 的类型是 int&&

引用折叠规则

c++ 复制代码
&  +  &  →  &
&  +  && →  &
&& +  &  →  &
&& +  && →  &&
0.1.4.10 完美转发std::forward

有了转发引用,就能做完美转发------把参数原封不动地传给下一层函数,保留左值/右值属性:

c++ 复制代码
// 不用完美转发的问题
template<typename T>
void wrapper(T&& x) {
    func(x);  // ❌ x 在这里是左值(它有名字!)
              //    即使传入的是右值,这里也会调用 func 的左值版本
}

// 用完美转发
template<typename T>
void wrapper(T&& x) {
    func(std::forward<T>(x));  // ✅ 保留原始的左值/右值属性
}

int a = 10;
wrapper(a);   // x 是左值引用,forward 后仍然是左值
wrapper(10);  // x 是右值引用,forward 后仍然是右值

std::forward 的实现:

c++ 复制代码
template<typename T>
T&& forward(std::remove_reference_t<T>& x) {
    return static_cast<T&&>(x);
}
// T 是左值引用时 → 返回左值引用
// T 是非引用时   → 返回右值引用

std::remove_reference_t<T>& x 的含义:不管 T 是什么类型,先去掉引用,之后再变成左值引用。这里是为了确保参数统一是"左值表达式"。

c++ 复制代码
// 例如这里 T 是 int&
std::remove_reference_t<int&>& = int&
std::remove_reference_t<int>& = int&

关键在于 return static_cast<T&&>(x); 如果 T 本身是左值引用,如 int&,则实际最要转换的类型就是 int& &&,结合前边的引用这些,这里会这些成 &

实际应用场景,构造函数转发:

c++ 复制代码
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
    // 无论 args 是左值还是右值,都原封不动传给 T 的构造函数
}
0.1.4.11 std::move vs std::forward 对比
c++ 复制代码
std::move(x)        → 无条件转成右值引用
                      用于:明确知道 x 不再需要,想触发移动

std::forward<T>(x)  → 根据 T 的类型决定转成左值还是右值引用
                      用于:模板中转发参数,保留原始值类别
0.1.4.12 实际收益对比
c++ 复制代码
std::vector<std::string> v;

std::string s = "一段很长很长的字符串...";

v.push_back(s);             // 拷贝:分配新内存 + 复制所有字符
v.push_back(std::move(s));  // 移动:只交换指针,O(1)
v.emplace_back("直接构造"); // 更好:直接在 vector 内部构造,连移动都省了

容器扩容时也会用到移动:

c++ 复制代码
// vector 扩容时,如果元素有 noexcept 移动构造,就用移动而不是拷贝
// 所以移动构造应该声明 noexcept
String(String&& other) noexcept {  // ← 加上 noexcept
    data = other.data;
    other.data = nullptr;
}
0.1.4.13 总结
c++ 复制代码
值类别
├── 左值(lvalue):有名字、有地址
│     绑定到:T&(左值引用)
│
└── 右值(rvalue):临时的、即将消亡的
      绑定到:T&&(右值引用)
      
            │
            ▼
    如何"偷走"右值的资源?
    
    移动构造函数  String(String&&)
    移动赋值运算符 operator=(String&&)
            │
            ▼
    左值也想触发移动?
    
    std::move(x)  → 无条件转右值引用
            │
            ▼
    模板中如何保留值类别传递?
    
    T&&(转发引用)+ std::forward<T>(x)

核心记住两句话:

  • std::move 不移动,只是告诉编译器"这个东西你可以偷走"
  • 真正的移动发生在移动构造/移动赋值里,是你自己写的"偷资源"逻辑

0.2 总结

0.2.1 自动类型推导

工具 保留引用 保留 const 典型用途
auto ❌ 丢失 ❌ 丢失 简化局部变量、迭代器
auto& ✅ 强制引用 ✅ 保留 range-for、避免拷贝
decltype(expr) 精确推导表达式类型
decltype(auto) 模板返回值完美转发

核心记忆点

  • auto 推导"初始化值的类型",会剥除引用和顶层 const。
  • decltype(x) 看"声明类型",decltype((x)) 看"表达式值类别"------多一层括号触发 lvalue → T& 规则。
  • decltype(auto) = "用 decltype 的规则自动推",专为模板中的完美返回类型设计。

0.2.2 智能指针

指针类型 所有权语义 开销 最常见用途
unique_ptr 独占,不可拷贝 几乎零 默认首选,单一所有者
shared_ptr 共享,引用计数 原子操作 + 控制块 多处共享同一资源
weak_ptr 无所有权 shared_ptr 打破循环引用、观察者

选型口诀

复制代码
单一所有者          → unique_ptr(零开销,优先选)
需要共享            → shared_ptr(make_shared,一次分配)
需要观察/打破循环   → weak_ptr(lock() 后才能访问)
只是借用,不管理    → 裸指针或引用

四个高频陷阱

  1. 不要用同一裸指针构造两个 shared_ptr(double free)。
  2. 类内获取自身 shared_ptr 需继承 enable_shared_from_this
  3. std::move 后原 unique_ptrnullptr,不可再使用。
  4. shared_from_this 必须在 shared_ptr 管理的对象上调用,栈对象直接崩溃。

0.2.3 lambda 表达式

语法结构

c++ 复制代码
[捕获列表] (参数列表) mutable -> 返回类型 { 函数体 }

捕获方式速查

写法 含义 注意
[] 不捕获 无法访问外部变量
[x] 值捕获(副本,const) mutable 可解除 const
[&x] 引用捕获 注意对象生命周期
[=] 隐式值捕获所有 避免滥用
[&] 隐式引用捕获所有 避免滥用,生命周期风险
[this] 捕获 this 指针 对象销毁则悬空
[*this] 捕获对象副本(C++17) 生命周期安全

核心原则

  • 值捕获是创建时的快照,之后外部怎么变都不影响副本。
  • 引用捕获是别名,生命周期必须比 lambda 长,否则是悬空引用(UB)。
  • for 循环里异步存储 lambda → 用值捕获,不要引用捕获循环变量。
  • 无捕获的 lambda 可以退化为函数指针;有捕获的不行。
  • std::function 可以统一存储不同 lambda,但有类型擦除开销。

0.2.4 右值引用 + move

值类别

复制代码
左值(lvalue):有名字、有地址 → 绑定到 T&
右值(rvalue):临时、即将消亡 → 绑定到 T&&

判断口诀:能不能取地址,能取就是左值。

移动语义的本质

复制代码
拷贝 = 深拷贝一份新资源(new + memcpy)→ 开销大
移动 = 偷走指针,把原对象置空       → O(1)

移动语义的实现需要你手动写 移动构造 + 移动赋值 ,并遵守五法则(拷贝构造 / 拷贝赋值 / 移动构造 / 移动赋值 / 析构,只要自定义一个,通常五个都要写)。

std::move vs std::forward

作用 使用场景
std::move(x) 无条件转成右值引用 明确放弃 x,触发移动
std::forward<T>(x) 按 T 的类型决定左/右值 模板中转发参数,保留原始值类别

最重要的两句话

  • std::move 不移动任何东西,它只是类型转换,告诉编译器"你可以偷走这个"。
  • 真正的移动发生在你自己写的移动构造/移动赋值里,是你写的"偷指针、置 nullptr"逻辑。

完美转发模式

c++ 复制代码
template<typename T>
void wrapper(T&& x) {
    func(std::forward<T>(x));  // 原封不动传递左值/右值
}

T&& 在模板中是转发引用 (万能引用),配合 std::forward 实现完美转发,是 make_unique / emplace_back 等标准库设施的底层机制。


0.2.5 四大特性关联图

复制代码
auto / decltype
    └─→ 推导出正确类型,决定是值还是引用
              │
              ▼
        右值引用 T&&
              │
    ┌─────────┴──────────┐
    │                    │
std::move            std::forward
(左值→右值,触发移动)  (完美转发,保留值类别)
    │
    ▼
移动构造 / 移动赋值
(真正的"偷资源"逻辑)
    │
    ▼
智能指针的移动语义
unique_ptr 只能 move,不能 copy
shared_ptr 内部控制块用引用计数管理生命周期
lambda 的捕获列表本质是生成一个含成员的匿名类
    └─→ [ptr = std::move(p)] 把 unique_ptr 移动进 lambda

这四个特性在工程中高度协作:auto 推导类型,&& 识别右值,move/forward 触发或保留移动语义,智能指针和 lambda 则是移动语义最直接的受益者。

相关推荐
格林威1 小时前
面阵相机 vs 线阵相机:堡盟与Basler选型差异全解析 +C++ 实战演示
开发语言·c++·人工智能·数码相机·计算机视觉·视觉检测·工业相机
zzzsde1 小时前
【Linux】线程概念与控制(2)线程控制与核心概念
linux·运维·服务器·开发语言·算法
白夜11171 小时前
C++(不适合使用 CRTP情况)
开发语言·c++·笔记
宁静致远20212 小时前
ARM 架构 Ubuntu 20.04 / 22.04 触摸屏设备
linux·c++·ubuntu
栗少2 小时前
Python 入门教程(面向有 Java 经验的开发者)
java·开发语言·python
草莓熊Lotso2 小时前
Linux C++ 高并发编程:从原理到手撕,线程池全链路深度解析
linux·运维·服务器·开发语言·数据库·c++·mysql
Gh0st_Lx2 小时前
【8】分类任务原理
算法·分类·数据挖掘
WolfGang0073212 小时前
代码随想录算法训练营 Day45 | 图论 part03
算法·图论
小王师傅662 小时前
【Java结构化梳理】泛型-上
java·开发语言