C++11 学习笔记:统一初始化、右值引用与完美转发

markdown 复制代码
# 拥抱现代C++:C++11核心特性全景梳理

C++11 是"现代 C++"的开端。初学时总觉得知识点零散,直到我把它们串成一条线,才真正理解。这篇博客从统一初始化开始,把右值引用(重点)、`std::move`、完美转发、lambda、可变参数模板、新类功能、包装器一一理清,希望能帮到同样在路上的你。

## 1. 统一初始化与 `std::initializer_list`

### 1.1 大括号 `{}` 一统江湖

C++11 之前,初始化方式杂乱:小括号、等号、大括号混用。现在,**大括号初始化** 可以用于一切场合,并禁止窄化转换。

```cpp
int a{10};
int b = {20};                  // 也可
std::vector<int> v{1,2,3,4};
std::map<int,std::string> m{{1,"one"},{2,"two"}};
struct Point { int x,y; };
Point p{3,4};                 // 聚合初始化

int x = 3.14;                 // 仅警告
int y{3.14};                  // 编译错误:窄化转换

1.2 幕后英雄 std::initializer_list

当用 {} 初始化容器时,编译器会生成一个 std::initializer_list,它是一种轻量只读容器。

cpp 复制代码
#include <initializer_list>
#include <iostream>
#include <vector>

class MyVec {
public:
    MyVec(std::initializer_list<int> list) {
        for(auto& e : list)
            data.push_back(e);
    }
    void print() {
        for(auto e : data) std::cout << e << " ";
    }
private:
    std::vector<int> data;
};

MyVec v = {10, 20, 30};   // 调用 initializer_list 构造函数
v.print();                // 10 20 30

理解它,你就明白为什么 STL 容器都能直接用 {...} 填值了。

2. 新类功能

几个最常用的语法增强:

  • = default= delete

    显式要求编译器生成或禁用特殊成员函数。

    cpp 复制代码
    class NoCopy {
    public:
        NoCopy() = default;
        NoCopy(const NoCopy&) = delete;  // 禁止拷贝
    };
  • overridefinal

    覆写虚函数时加 override,让编译器帮你检查签名;final 阻止进一步覆写或继承。

    cpp 复制代码
    struct Base {
        virtual void f() const;
    };
    struct Derived : Base {
        void f() const override;  // 签名不匹配就报错
    };
  • 委托构造

    一个构造函数调用另一个构造函数,减少重复代码。

    cpp 复制代码
    class MyClass {
        int a, b;
    public:
        MyClass() : MyClass(0,0) {}
        MyClass(int x, int y) : a(x), b(y) {}
    };
  • 继承构造

    using Base::Base; 直接"拿来"基类构造函数。

    cpp 复制代码
    struct Base {
        Base(int);
    };
    struct Derived : Base {
        using Base::Base;  // Derived 也可用 Derived(5)
    };

3. 右值引用(重点)

右值引用是 C++11 最大的变革,支撑起了移动语义完美转发。我们沿着"值类别转换"这条线,一步步看清它的全貌。

3.1 左值、右值

  • 左值 :有名字、能取地址、生命周期较长(如变量 int a)。
  • 右值 :字面量、临时对象、表达式产生的无名结果(如 5std::string("hello")a+b)。

右值引用的语法是 T&&,它可以绑定到右值。这让我们有机会"窃取"临时对象的资源,而不是笨重地拷贝。

3.2 移动构造与移动赋值

假设一个管理动态内存的类:

cpp 复制代码
class Buffer {
    size_t sz;
    char* data;
public:
    Buffer(size_t n) : sz(n), data(new char[n]) {}
    
    // 拷贝构造(昂贵)
    Buffer(const Buffer& other) : sz(other.sz), data(new char[other.sz]) {
        std::copy(other.data, other.data+sz, data);
    }
    
    // 移动构造(便宜)
    Buffer(Buffer&& other) noexcept : sz(other.sz), data(other.data) {
        other.sz = 0;
        other.data = nullptr;   // 源对象置于安全状态
    }
    
    ~Buffer() { delete[] data; }
};

调用时机:

cpp 复制代码
Buffer getBuffer() {
    Buffer tmp(1024);
    return tmp;             // 返回时优先移动构造(也可能被 NRVO 优化)
}
Buffer b = getBuffer();    // 移动构造
Buffer b2 = std::move(b);  // 显式移动(之后不要再用 b,除非重新赋值)

移动语义的意义:临时对象的资源直接"过户",避免深拷贝,大幅提升性能。

3.3 std::move 的本质与正确用法

std::move 不执行任何移动操作,它只做一件事:将左值强制转换为右值引用,让你能够匹配移动构造函数。

cpp 复制代码
int x = 10;
int&& r = std::move(x);   // 仅是类型转换,x 的值仍是 10

对基础类型,移动即拷贝;但对 std::stringstd::vector 或含指针的类,就会触发资源窃取:

cpp 复制代码
std::string s1 = "hello";
std::string s2 = std::move(s1);
// s1 现在处于"有效但未指定状态"(通常为空),不应再使用

什么时候用?

  • 不要对局部返回值使用 std::move,它会抑制 NRVO 优化。

    cpp 复制代码
    std::string getStr() {
        std::string s = "hello";
        return s;               // 正确,编译器会做 NRVO
        // return std::move(s); // 错误!反而降低性能
    }
  • 当你确实要交出左值的所有权时,才显式使用

    cpp 复制代码
    std::vector<std::string> v;
    std::string s = "test";
    v.push_back(std::move(s));  // s 的资源被移入容器,s 变为空
  • const 对象使用 std::move 基本无效const T&& 无法匹配移动构造函数,最终退化为拷贝。

std::movestd::forward 的区别

std::move std::forward
作用 无条件转为右值引用 有条件转发:左值→左值,右值→右值
场景 主动交出所有权 模板中保持参数原始值类别
典型代码 std::move(obj) std::forward<T>(obj)

一句话:想交出所有权用 move,想原封不动转发用 forward

3.4 值类别的转换与保持

上面几个概念背后有一条隐藏的逻辑线:左值和右值的"身份"如何转换与保持

  1. const T& 能"接纳"右值

    常量左值引用可以绑定到右值,并延长其生命期。它不是转换了右值的类别,而是获得了绑定右值的权利。

    cpp 复制代码
    const int& ref = 10;  // 10 是右值,但 ref 是左值引用

    很多只读参数因此写成 const T&,既能接受左值,又能接受右值,避免拷贝。

  2. std::move 让左值"充当"右值

    std::move 返回右值引用,使左值能被移动构造"窃取"。但原变量本身仍然是左值。

    cpp 复制代码
    int a = 5;
    int&& r = std::move(a);  // r 是右值引用,但 a 依然是左值
  3. 右值引用变量本身是左值 ------ 所以需要 forward 保持"原籍"

    这是最容易犯错的地方:有名字的右值引用是左值

    cpp 复制代码
    int&& r = 10;
    // r 有名字,可以取地址,所以 r 是左值!

    如果你把 r 传给另一个函数,它会匹配到左值版本,失去原本的右值属性。为了保持它最初的"右值性",必须用 std::forward 进行完美转发:

    cpp 复制代码
    template<class T>
    void wrapper(T&& arg) {
        use(std::forward<T>(arg));  // 保持 arg 的原始值类别
    }

这四条规则串在一起,就是 C++11 值类别体系的精髓:const& 接纳右值,&&move 创造右值,forward 保持右值

3.5 生命周期与资源安全

移动后,源对象处于 "有效但未指定状态" 。通常可以安全析构或赋予新值,但不应再使用其原有内容。这就是为什么移动构造要将源指针置为 nullptr

3.6 引用折叠与类型推导

模板中 T&& 是万能引用,推导规则:

  • 传入左值 int&T 推导为 int&,折叠:int& &&int&
  • 传入右值 int&&T 推导为 int,结果 int&&

引用折叠规则 :只有 && + && 才能折叠成 &&,其他组合全折叠为 &。这是完美转发的底层基础。

3.7 完美转发:std::forward

我们需要一个函数,能把参数的值类别原封不动地传递给另一个函数。结合万能引用和 std::forward 即可:

cpp 复制代码
template<typename T, typename Arg>
std::unique_ptr<T> factory(Arg&& arg) {
    return std::unique_ptr<T>(new T(std::forward<Arg>(arg)));
}
  • Arg&& 是万能引用,可匹配任何实参。
  • std::forward<Arg>(arg):如果 arg 原本是左值,转发为左值;原本是右值,转发为右值。

引用折叠与 std::forward 的配合,实现了参数的零开销中转。

4. 可变参数模板

C++11 允许模板参数列表接受任意数量参数:

cpp 复制代码
void print() {}  // 递归终止

template<typename T, typename... Args>
void print(T first, Args... rest) {
    std::cout << first << " ";
    print(rest...);
}

print(1, 2.5, "hello", 'c');  // 输出 1 2.5 hello c

Args... 是参数包,通过递归展开依次处理。它让 std::tupleemplace_back 等成为可能。

5. Lambda 表达式

语法:[capture](params) -> ret { body }

捕获方式:

  • [=]:按值捕获所有(副本,默认不可改,除非加 mutable
  • [&]:按引用捕获所有(修改会影响外部)
  • [a, &b]a 按值,b 按引用
  • [this]:捕获当前对象指针

常见用例:

cpp 复制代码
std::vector<int> v{3,1,4,1,5};
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });

int threshold = 3;
auto it = std::find_if(v.begin(), v.end(), 
                       [threshold](int x) { return x > threshold; });

lambda 的底层是一个匿名的仿函数类,捕获列表是其成员变量。

6. 包装器:std::functionstd::bind

6.1 std::function

通用多态函数包装器,可存储任意可调用物:函数、lambda、仿函数、成员函数等。

cpp 复制代码
#include <functional>
int add(int a, int b) { return a+b; }

std::function<int(int,int)> func;
func = add;                        // 函数指针
func = [](int a,int b){ return a-b; };  // lambda
std::cout << func(4,2);            // 2

存储成员函数时需传入对象指针或配合 std::bind

6.2 std::bind

将参数与可调用物绑定,生成新的可调用对象,并可通过占位符调整参数顺序。

cpp 复制代码
using namespace std::placeholders;
auto minus = [](int a, int b) { return a - b; };
auto five_minus = std::bind(minus, 5, _1);
std::cout << five_minus(3);  // 5 - 3 = 2

// 绑定成员函数
struct Foo {
    void display(int x) { std::cout << x; }
};
Foo obj;
auto f = std::bind(&Foo::display, &obj, _1);
f(100);  // 打印 100

7. 结语

这篇笔记从统一初始化开始,到右值引用的移动语义、std::move、值类别转换与完美转发(重点),再到可变参数模板、lambda、新类功能和包装器,把 C++11 的核心骨架搭了出来。

相关推荐
magic_now1 小时前
Modbus RTU 与 TCP 学习笔记
笔记·学习·tcp/ip
轻闲一号机1 小时前
【语音】笔记
前端·笔记·算法
叶子野格1 小时前
《C语言学习:位运算》17
c语言·开发语言·c++·学习·visual studio
aWty_1 小时前
实分析入门(12)--可测函数
学习·数学·算法·实变函数
词元Max2 小时前
4.1 监督学习入门:线性回归与分类
学习·分类·线性回归
-To be number.wan2 小时前
计算机组成原理 | 位扩展、字扩展与片选逻辑
学习·计算机组成原理
小烤箱2 小时前
ROS2 学习资源与学习方法
学习·ros·学习方法·ros2
晚风吹红霞2 小时前
C++ stack 和 queue 完全指南:适配器模式与双端队列的奥秘
c++·算法·适配器模式
casual~2 小时前
十六届蓝桥杯国赛个人题解
经验分享·学习·算法·蓝桥杯