C++11,发展历史浅谈,初始化列表和对象初始化,右值引用和移动语义

C++11 右值引用与移动语义

主题:C++11 新特性之右值引用(Rvalue Reference)与移动语义(Move Semantics)

关联知识链:C++ 标准演进 → 左值/右值 → 引用类型 → 移动构造/移动赋值 → 完美转发 → 智能指针


一、C++11 的发展历史

1.1 C++11 的里程碑意义

  • C++11 是 C++ 标准化的重要里程碑,距 C++98 标准(1998-12-14 发布)过去 13 年后才完成一次完整的大版本更新
  • 开发代号 "C++0x"(因标准编号 14882 于 200x 年间制定而得名),2004 年工作组重启标准修订(JTC3),至 2011 年 12 月正式发布,历经多轮工作草案与投票
  • C++11 引入大量新特性,优化了语言表达,显著提升了 C++ 的灵活性与表达能力,是近 20 年来对 C++ 影响最深远的修订
  • C++ 标准由 ISO/IEC JTC1/SC22 下属的 WG21 委员会 负责制定与维护

1.2 各版本发布时间线

标准 发布时间 ISO 编号 主要特性
C++98 1998-12-14 ISO/IEC 14882:1998 第一个 ISO/IEC C++ 标准:类与模板、命名空间、异常、RTTI、STL、std::string、iostream
C++11 2011-12 ISO/IEC 14882:2011 右值引用、移动语义、auto、lambda、enum class、智能指针、nullptr、范围 for、变参模板
C++14 2014-12-12 ISO/IEC 14882:2014 对 C++11 的修订(非大版本):泛型 lambda、变量模板、std::make_unique、return 自动推导、数字字面量分隔符 '
C++17 2017-12-11 ISO/IEC 14882:2017 结构化绑定、if 带初始化语句、折叠表达式、std::optional/any/variant、std::string_view、并行算法(TS 转正)、inline 变量
C++20 2020-11-11 ISO/IEC 14882:2020 模块(Modules)、协程(coroutine)、概念(concepts)、三向比较运算符 <=>、ranges、consteval/constinit
C++23 2023-10 ISO/IEC 14882:2023 反射、std::expected/expected<T,E>、std::print/format、简化 #include、std::mdspan
C++26 进行中 --- 虚拟继承继承器、std::generator、atomic<std::shared_ptr> 等

要点:

  • C++11 是"大版本",C++14 定位是修订版(修 bug + 小特性),C++17/20/23 恢复大版本节奏,此后基本保持约 3 年一个大版本
  • 每个版本都是对前一版本的超集(基本保持向后兼容),老代码新标准可编译

1.3 C++11 新特性速览

C++11 相比旧版本变化很大,主要围绕旧有标准进行优化、语法简化与类型明确化:

  • 右值引用(T&&)与移动语义 ------ 本篇主角
  • auto 关键字与类型推导 :函数参数/返回值、decltype
  • lambda 表达式 :[](参数) -> 返回类型 { 函数体 },可捕获上下文
  • 强类型枚举(enum class) :枚举值不泄漏到外层作用域,默认类型为 int
  • 智能指针 :std::unique_ptr / std::shared_ptr / std::weak_ptr + std::make_unique(C++14)
  • nullptr 取代 NULL:类型安全的空指针,可重载
  • 初始化列表(std::initializer_list) :统一初始化语法 {...}
  • 范围 for 循环 :for (auto x : container)
  • 变参模板(Variadic Templates) :template<class... Args>
  • constexpr:编译期常量表达式,C++11 中函数体必须单条 return
  • static_assert:编译期断言
  • thread_local:线程局部存储
  • alignas / alignof:内存对齐
  • = default / = delete:显式控制编译器生成行为
  • explicit 扩展 :explicit operator bool()
  • noexcept:异常安全承诺,影响编译器优化与容器行为

二、C++11 中的初始化列表与对象初始化

2.1 初始化列表(initializer_list)

  • C++11 为复杂类型的管理提供了更友好的语法,初始化列表是其中一项
  • 底层实现:一个只读 的 T 数组 + 起止迭代器,编译器用临时对象实现
  • 核心成员函数:begin() / end() / size() / max_size() / empty()
cpp 复制代码
class Complex {
public:
    double real, imag;
    Complex(double r, double i) : real(r), imag(i) {}
};

int main() {
    Complex c{1.0, 2.0};          // 列表初始化

    int sum = 0;
    for (int i : {1, 2, 3, 4, 5}) // 展开为临时 initializer_list<int>
    {
        sum += i;
    }
}
  • 成员函数的对应关系:构造函数 Complex、析构函数 ~Complex、数据成员 real/imag、迭代器 begin/end、容量查询 size/max_size/empty

2.2 成员初始化顺序的坑

  • 成员按声明顺序初始化,而不是初始化列表的书写顺序
  • 初始化列表不能跳过的成员:引用成员、const 成员、没有默认构造函数的类类型成员、静态成员
  • 正确写法是把初始化放进列表,而不是在构造函数体内赋值(赋值前构造已完成,浪费且可能出错)
cpp 复制代码
class A {
public:
    A(int x, int y) : y_(y), x_(x) { }  // 书写顺序 x_ 在前
private:
    int x_;   // 但按声明顺序,x_ 先构造!
    int y_;
};

2.3 构造函数中的引用参数

  • 常用 const T& 接收外部对象:避免拷贝,同时不承诺修改
  • 注意 const T& 既可以绑定左值也可以绑定右值(右值允许绑定到 const 左值引用,且延长临时对象生命周期)
  • T&& 只能绑定非 const 右值;const T&& 可绑定 const 右值(实际中很少使用)
cpp 复制代码
class Date {
public:
    Date(const string& data);  // 左值右值均可接收
    void print();
private:
    int year, month, day;
};

2.4 拷贝初始化 vs 直接初始化的区别

cpp 复制代码
std::string s1("hello");      // 直接初始化:调 string(const char*)
std::string s2 = "hello";    // 看似拷贝初始化,实际也调 string(const char*)
std::string s3 = s1;         // 拷贝初始化:调拷贝构造(C++11 中若 s1 是右值则可能调移动构造)
  • 只有当目标类型与源类型不同(或源是字面量/转换而来的表达式)时,才会走"先临时对象再拷贝"的路径(且可能被 RVO 优化掉)

三、右值引用和移动语义

3.1 左值和右值

左值(lvalue)

  • 表达式求值结果可以被取地址 (&expr 合法),可以放在赋值语句左侧
  • 表示一个有名称的内存位置,通常与变量名关联
  • 例子:变量 a、数组元素 arr[0]、函数返回的引用、++x(前置自增返回左值引用)、x = y 整体是左值(返回 x 的引用)

右值(rvalue)

  • 表达式求值结果不能被取地址,通常无法放在赋值号左侧
  • 表示临时对象或无名称的内存位置,表达式求值结束后通常被销毁
  • 例子:字面量 1、std::string("abc")、x + y、x++(后置自增返回临时值)、std::move(x)(结果是右值)

判断技巧:能取地址 &expr 的就是左值;不能的就是右值。

cpp 复制代码
int a = 10;
&a;          // OK,a 是左值
&10;         // 编译错误,10 是右值
&(a + b);    // 编译错误,结果是右值

补充(C++17 更精确的三分法,面试加分项):

  • prvalue(纯右值):字面量、非引用类型返回的函数返回值
  • xvalue (将亡值):std::move 的结果、引用类型函数的返回值------"快要死掉"的对象,移动语义的优先目标
  • lvalue:变量名等
  • 右值 = prvalue + xvalue

临时对象生命周期延长:

cpp 复制代码
const std::string& s = "hello";  // 临时 string 被 const 引用延长到 s 的作用域
std::string&& t = "hello";       // 右值引用也能延长(C++11 起)

3.2 左值引用和右值引用

  • C++11 之前,引用(&)只能绑定左值,临时对象的销毁时机固定,编译器必须为"通过值传递"做防御性拷贝
  • C++11 引入右值引用 T&&,专门用于绑定右值,让"转移资源"成为可能
  • const T& 左值引用仍可绑定右值(延长临时对象生命周期),但只能读不能写
引用类型 记号 可绑定对象 典型用途
左值引用 T& 仅左值 修改对象、传参避免拷贝
const 左值引用 const T& 左值 + 右值 只读传参
右值引用 T&& 仅非 const 右值 移动语义、完美转发
const 右值引用 const T&& 仅 const 右值 实际场景很少

注意:右值引用变量本身是左值------因为引用变量有名字、可取地址:

cpp 复制代码
int&& r = 5;     // 绑定右值
int& lr = r;     // 合法!r 是命名变量,本身就是左值

3.3 左值引用与右值引用的区别

左值引用(&):

  • 只能绑定到左值(加 const 后也可绑定右值)
  • 用于传递对象引用、允许修改对象

右值引用(&&):

  • 只能绑定到右值(临时对象、表达式结果、std::move 结果)
  • 用于实现移动语义,高效转移资源
  • 配合 std::move 可把左值显式转换为右值引用

3.4 std::move 与 std::forward

std::move

cpp 复制代码
template <class T>
typename remove_reference<T>::type&& move(T&& arg) {
    return static_cast<remove_reference<T>::type&&>(arg);
}
  • 本质:无条件把参数 cast 为右值引用 (剥掉引用后重新加 &&),本身不执行任何移动操作,只是"贴标签"
  • 标签贴完后,编译器才有机会选择移动构造/移动赋值
  • 移动后源对象处于合法但未知 的状态:成员可读不可依赖(指针类成员一般为 nullptr),不应再解引用或重复移动

常见误区:

  • "move 就是快拷贝" ------ 错,move 是资源转移,源对象被掏空
  • "调用了 std::move 就一定走移动" ------ 不一定:对象是 const、参数按值且没有移动构造、目标没有移动操作符时,仍会拷贝或直接构造

std::forward

cpp 复制代码
template <class T>
T&& forward(remove_reference_t<T>& arg) noexcept {
    return static_cast<T&&>(arg);
}
  • 本质:保留参数原有值类别的条件 cast(模板推导 + 引用折叠)
  • 用于模板函数中把参数"原样"转发给另一个函数,不丢失引用性与 const 性
  • 必须配合万能引用(转发引用) T&&(T 是模板参数时)使用
  • std::move(x) 永远得到右值;std::forward<T>(x) 在 T 被推导为 A& 时得到左值、推导为 A 时得到右值
cpp 复制代码
template <class T>
void process(T&& arg) {
    do_something(std::forward<T>(arg));  // 左值进来还是左值,右值进来还是右值
}

引用折叠规则(forward 的数学基础)

T 推导结果 T&& 折叠 结果
U& U& && U&(左值)
U U && U&&(右值)

规则:两个左值引用叠一起还是左值引用,其余都是右值引用。

3.5 使用场景

左值引用:

  • 函数参数:需要修改对象、或避免大型对象拷贝时
  • 函数返回对象引用

右值引用:

  • 移动构造函数参数(T(T&&))
  • 移动赋值运算符参数(T& operator=(T&&))
  • 模板中支持完美转发(变参模板)
  • 资源独占类的传递(unique_ptr 只能移动)

3.6 移动构造与移动赋值(资源窃取)

移动语义允许对象之间转移资源而非复制。核心操作:

cpp 复制代码
#include <iostream>
#include <cstring>

class MyString {
    char* data;
    size_t size_;
public:
    MyString() : data(nullptr), size_(0) {}

    MyString(const char* str)
        : size_(std::strlen(str)), data(new char[size_ + 1]) {
        std::strcpy(data, str);
        std::cout << "ctor: " << data << std::endl;
    }

    // 拷贝构造:深拷贝
    MyString(const MyString& other)
        : size_(other.size_), data(new char[other.size_ + 1]) {
        std::strcpy(data, other.data ? other.data : "");
        std::cout << "copy ctor: " << data << std::endl;
    }

    // 移动构造:窃取资源,源对象置空
    MyString(MyString&& other) noexcept
        : data(other.data), size_(other.size_) {
        other.data = nullptr;   // 防止 double free
        other.size_ = 0;
        std::cout << "move ctor: " << data << std::endl;
    }

    // 移动赋值
    MyString& operator=(MyString&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            size_ = other.size_;
            other.data = nullptr;
            other.size_ = 0;
            std::cout << "move assign: " << data << std::endl;
        }
        return *this;
    }

    ~MyString() {
        std::cout << "dtor, data = " << (data ? data : "(null)") << std::endl;
        delete[] data;
    }
};

int main() {
    MyString s1("Hello World");        // ctor
    MyString s2(std::move(s1));       // move ctor,s1 被掏空
    s1 = MyString("again");           // 临时对象右值 → move assign
    // 析构顺序:s1(此时持有 "again") → s2(持有 "Hello World")
}

要点:

  • 移动构造的参数是右值引用
  • 通常用于转移资源(指针、句柄、缓冲区),而不是复制
  • 源对象必须置为可安全析构 的状态(一般为 nullptr + 大小为 0),否则会 double free
  • 移动操作一般要标记 noexcept,否则 vector 扩容时会回退为拷贝

移动后源对象的状态约定 :合法但未知。标准库保证移动后的 unique_ptr 必为 nullptr,vector 必为空;自写类应约定源对象置空(指针 = nullptr、标志位归零)。

3.7 Rule of 3 / Rule of 5 / Rule of 0

编译器默认生成六个特殊成员函数:默认构造、拷贝构造、拷贝赋值、移动构造(C++11)、移动赋值(C++11)、析构。

C++11 的抑制规则:

你手写了什么 编译器停止自动生成
拷贝构造 / 拷贝赋值 / 析构函数 任意一个 移动构造、移动赋值(不会自动生成)
移动构造 / 移动赋值 任意一个 拷贝赋值(拷贝构造仍生成)
默认构造 无(C++11 中默认构造不影响其他特例)
  • Rule of 3(C++03):手写析构、拷贝构造、拷贝赋值三者之一,三者都要手写
  • Rule of 5 (C++11):涉及动态资源的类,五个特例(析构 + 拷贝构造 + 拷贝赋值 + 移动构造 + 移动赋值)要么全手写、要么用 = delete / = default 明确表态
  • Rule of 0:优先把资源交给智能指针,类就不需要手写任何特例

3.8 完美转发与变参模板

cpp 复制代码
template <class T, class U>
void forwarder(T&& a, U&& b) {
    call(std::forward<T>(a), std::forward<U>(b));  // 原样转发
}

forwarder(1, std::string("x"));  // 调用 call(1, 右值string),类别不丢失

完整示例:手写 make_unique(C++14 标准库实现几乎就是这段):

cpp 复制代码
template <class T, class... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
  • 完美转发让模板函数能把参数"原样"传给另一函数,不丢失引用/常量性
  • 依赖右值引用 + std::forward + 万能引用(转发引用)三者配合
  • 典型应用:std::make_unique、std::forward_list::push_front、构造函数转发成员

3.9 常见坑与调试技巧

常见坑清单

  • std::move 不移动任何东西,它只是 cast
  • 右值引用变量本身是左值:int&& r = 5; int& lr = r; 合法
  • const 对象不能被移动(除非显式 const_cast,一般不建议);对 const 左值调用 std::move 得到 const T&&,只能绑定拷贝构造
  • 有用户声明的析构函数时,移动特例不会被自动合成,容器扩容会退化为拷贝
  • 移动构造函数应声明 noexcept,否则 vector 扩容搬迁时选择拷贝(异常安全考虑)
  • std::move(x) 之后再用 x 是 UB 边缘行为(合法但未知状态)
  • 万能引用 T&& 只有在 T 是模板参数(auto 推导或显式模板参数)时才是"转发引用";T 为具体类型时它就是普通右值引用

调试技巧:打印构造/移动/析构的轨迹

cpp 复制代码
class Debug {
public:
    Debug(const char* who) { printf("ctor(%s)\n", who); }
    Debug(const Debug& o) { printf("copy(%.4s)\n", o.name_); }
    Debug(Debug&& o) noexcept { printf("move(%.4s)\n", o.name_); }
    ~Debug() { printf("dtor(%.4s)\n", name_); }
private:
    const char* name_;
};

int main() {
    Debug a("a");
    Debug b(std::move(a));   // 输出 move(a)
}

观察 vector 扩容:

cpp 复制代码
std::vector<std::string> v;
v.reserve(2);
v.push_back(std::string("x"));
for (int i = 0; i < 20; ++i)
    v.push_back(std::to_string(i));  // 多次扩容,观察 copy/move 次数
// 加 -O0 可看到移动;若类有用户定义析构且 move 未 noexcept,则全走拷贝

3.10 总结

  • 左值 = 有名字的内存位置(可取地址);右值 = 临时对象/表达式结果(不可取地址)
  • 左值引用 & 绑定左值;右值引用 && 绑定右值,是移动语义的载体
  • 移动语义 = 移动构造 + 移动赋值,通过右值引用转移资源、规避深拷贝
  • std::move 是"贴标签",std::forward 是"保留原标签",两者都不是真正的移动
  • 完美转发 = 右值引用 + std::forward + 万能引用,在模板中无损传递参数
  • 资源类设计遵循 Rule of 5 / Rule of 0,移动操作加 noexcept

四、主动回忆清单(自测)

  • C++11 何时发布?开发代号是什么?由哪个委员会维护?
  • C++11 与 C++98 间隔了多少年?为什么这么久?(修订重启、工作组流程)
  • 左值和右值的判断标准是什么?举 3 个各自例子
  • x++ 和 ++x 分别返回什么值类别?
  • T&、const T&、T&&、const T&& 分别能绑定哪些值类别?
  • 为什么 int&& r = 5; int& lr = r; 合法?
  • 写出 std::move 的源码,并说明它是否真的"移动"了资源
  • 引用折叠的两种情况分别是什么?
  • std::forward 为什么必须配合模板参数使用?对具体类型 std::forward<int>(x) 合法吗?
  • 移动构造完成后,源对象应处于什么状态?为什么必须把指针置 nullptr?
  • const 对象为什么不能被移动?
  • 移动构造函数加 noexcept 有什么实际影响(vector 扩容行为)?
  • Rule of 5 是哪五个特例?写出抑制规则表
  • 什么情况下写了移动构造但编译器仍然生成的是拷贝赋值?
  • 手写 make_unique,说明 std::forward<Args>(args)... 每个部分的含义

五、知识链串联

资源管理演进:

复制代码
裸指针(new/delete,易泄漏、易 double free)
      ↓
拷贝构造/拷贝赋值(深拷贝,O(n) 昂贵)
      ↓
移动构造/移动赋值(资源窃取,O(1) 转移)
      ↓
std::move / std::forward(控制值类别、完美转发)
      ↓
智能指针(unique_ptr 天然移动、shared_ptr 控制块 + weak_ptr 防循环)
      ↓
容器(vector 扩容时借助移动 + noexcept 回退机制搬迁元素)

上一篇:深拷贝三件套 → 本篇:移动语义 → 下一篇:智能指针


六、面试高频追问

  1. 问 :string s("hi"); string t = s; 这里发生了什么?

    答 :s 是左值,const string& 形参绑定左值,走拷贝构造;若 string 有 move 且 s 是右值(如 string t = make_string(...))则走移动。

  2. 问 :std::move 能省掉一次拷贝吗?

    答 :move 本身不拷贝不移动;它让表达式变为 xvalue,若目标类实现了移动构造/赋值则编译器选用移动(O(1) 指针转移),替代 O(n) 深拷贝。

  3. 问 :为什么 unique_ptr 不能拷贝只能移动?

    答 :独占所有权语义,拷贝会导致两个指针 delete 同一块内存;拷贝构造/赋值被 = delete,只保留移动。

  4. 问 :容器扩容为什么优先移动、要求 noexcept?

    答:扩容需要搬元素到新内存。若移动可能抛异常,搬迁一半时旧内存析构 + 新内存析构会造成双析构或资源混乱;拷贝是"安全兜底",因此 move 不 noexcept 时回退到拷贝。

  5. 问 :C++17 前值类别为什么改成了 prvalue / xvalue / lvalue?

    答 :C++11 的"左值/右值"二分无法表达 std::move 结果的优先级(将亡值最优先移动、临时值次之)。C++17 三分法把"右值"拆成 prvalue 与 xvalue,让绑定规则(xvalue 优先于 prvalue 绑定 T&&)更精确。

任何的批评和建议欢迎指出,我们共同进步!

相关推荐
动词ing39 分钟前
【题目练习】外观数列
算法
学逆向的44 分钟前
C 宏函数
c语言·开发语言·网络安全
Code_Solitude1 小时前
C语言:指针题型总结
c语言·开发语言
曹牧1 小时前
Java:`tools.jar`
java·开发语言·jar
IT枫斗者枫哥1 小时前
Spring AI文档更新后仍命中旧内容:核对分片ID、来源和版本
java·人工智能
suhan1 小时前
【数据结构】六、图:4.图的遍历(深度优先算法DFS、广度优先算法BFS)
数据结构·算法·排序算法
suhan1 小时前
【数据结构】六、图:3.十字链表、邻接多重表、边集数组
数据结构·算法·排序算法
aibo8603081 小时前
基于 Spring Boot + Vue3 的爱心众筹管理系统设计与实现
java·spring boot·后端