C++11的发展
目录
- 1. 列表初始化
- 1.1 C++98传统的{}
- 1.2 C++11中的{}
- 2. 右值引用和移动语义
- 2.1 左值和右值
- 2.1.1 左值(lvalue /loactor value)
- 2.1.2 右值(rvalue /read value)
- 2.2 左值引用和右值引用
- 2.3 右值引用的使用
- 2.3.1 左值引用问题
- 2.3.2 移动构造和移动赋值
- 2.4 右值引用和移动语义在传参中的提效
- 2.4.1 容器新增右值引用版本接口
- 2.4.2 万能引用与引用折叠规则
- 2.4.2.1 万能引用
- 2.4.2.2 引用折叠
- 2.1 左值和右值
一、基础定位
C++11 是继 C++98 后改动最大的主版本,曾用代号C++0x,原计划 2010 年前发布,最终 ISO 在 2011 年 8 月 12 日正式采纳,标准号 ISO/IEC 14882:2011。 C++03 到 C++11 间隔 8 年,是 C++ 史上最长版本空档;自此 C++ 固定为每 3 年更新一个标准(C++14/17/20/23)。
二、核心新增特(PPT 清单)
语言层面
移动语义(右值引用、移动构造 / 移动赋值,和你自定义 string 代码对应)
统一初始化、
auto/decltype类型推导Lambda 匿名函数、
constexpr常量表达式
标准库 & 底层
多线程库 + 标准化内存模型
正则表达式、智能指针、哈希表、
std::array
1.. 列表初始化
1.1 C++98传统的{}
一、C++98 大括号{}的局限
C++98 中{}仅能用于数组、结构体聚合初始化,场景割裂、语法不统一:
- 仅数组 / 原生聚合体能用
{},普通类、string、vector不能用; - 存在窄转换隐式允许,编译不报错;
- 初始化语法碎片化:
()、=、{}混用,阅读混乱; - 无法初始化内置变量,必须用
=。
#include <vector> #include <string> struct Stu { int id; }; int main() { // 1. 数组支持{} int arr[3] = {1,2,3}; // 2. 结构体聚合支持{} Stu s = {1001}; // C++98 不允许:vector/string 不能直接用{}批量初始化 // vector<int> v = {1,2,3}; // 编译报错 // string str{"abc"}; // 编译报错 // 只能用传统括号/等号 std::vector<int> v(3, 0); std::string str = "test"; // 危险:窄转换无警告 char c = 1024; // int转char,溢出,编译器不拦截 return 0; }1.2 C++11中的{}
- C++11 引入统一初始化语法,任何类型都能用 {} 初始化,也叫列表初始化,核心是
std::initializer_list支撑容器批量赋值。 语法通用形式:
类型 变量{参数...}; 类型 变量 = {参数...};- 全类型统一语法:内置类型、结构体、类、STL 容器全部通用一套
{},不用区分()/=; - 禁止隐式窄转换:高精度→低精度数值转换(int→char、double→int)直接编译报错,规避溢出 bug;
- 容器快速批量初始化:标准库全部支持
initializer_list,一行完成容器填充; - 内置类型零初始化:
int a{}自动初始化为 0,避免随机垃圾值; - 区分构造歧义(最典型:vector (数量,值) vs vector {元素 1, 元素 2})
vector<int> v(3, 5):3 个 5vector<int> v{3, 5}:元素 3、5
#define _CRT_SECURE_NO_WARNINGS 1 #include <iostream> #include <vector> #include <map> using namespace std; #include <assert.h> struct Point { int _x; int _y; }; class Date { public: Date(int year = 1, int month = 1, int day = 1) :_year(year) , _month(month) , _day(day) { cout << "Date(int year, int month, int day)" << endl; } Date(const Date& d) :_year(d._year) , _month(d._month) , _day(d._day) { cout << "Date(const Date& d)" << endl; } private: int _year; int _month; int _day; }; int main() { // C++98支持的 int a1[] = { 1, 2, 3, 4, 5 }; int a2[5] = { 0 }; Point p = { 1, 2 }; // C++11支持的 // 内置类型支持 int x0 = 1; int x1 = { 2 }; int x2{ 2 }; // 自定义类型支持 Date d0(2025, 5, 19); // 构造+拷贝构造->优化直接构造 Date d1 = { 2025, 5, 19 }; Date d3 { 2025, 5, 19 }; //Date d2 = 2025; Date d2 { 2025 }; vector<Date> v; // 使用场景 v.push_back(2025); v.push_back({ 2025, 5, 19 }); // 这里d2引用的是{ 2024, 7, 25 }构造的临时对象 const Date& d4 = { 2024, 7, 25 }; return 0; }2. 右值引用和移动语义
- C++98 的 C++ 语法里已经存在引用语法,C++11 新增了右值引用这一语法特性;C++11 发布之后,我们之前学习的引用就被称作左值引用。不论是左值引用还是右值引用,本质都是为对象取别名。
2.1 左值和右值
2.1.1 左值(lvalue /loactor value)
左值是代表数据的表达式(变量名、解引用指针等),具备持久内存状态,可以获取地址; 既可放在赋值符号=左侧,也可放在右侧; 加const修饰的左值禁止赋值,但依旧能取地址。
核心特征
- 内存常驻,拥有合法内存地址,支持
&取地址; - 赋值双向通用:
a = 10、int b = a都合法; - const 左值:可读、可取地址,不可修改。
#include <iostream> using namespace std; int main() { int a = 10; // a是普通左值 int* p = &a; *p = 20; // *p 解引用指针,属于左值 // 左值可放赋值左右两侧 a = 30; int b = a; // const修饰左值:可取地址,不能赋值 const int c = 100; cout << &c << endl; // 合法,取地址 // c = 200; // 编译报错,const左值禁止修改 return 0; }2.1.2 右值(rvalue /read value)
右值是代表数据的表达式,分为两类:字面常量、表达式生成的临时对象;仅能出现在赋值符号右侧,不能放在左侧,无法取地址。
核心特征
- 无持久内存,多存于寄存器 / 临时栈,不支持
&取地址; - 只能读取数值,不能被赋值;
- 包含纯右值(字面量、运算临时结果)、C++11 新增将亡值(
std::move转换后的临时对象)。
#include <string> using namespace std; string getTemp() { return "临时字符串"; // 返回匿名临时对象,右值 } int main() { int a = 10; // 1. 字面量是右值 a = 100; // 100 = a; 报错,右值不能放在赋值左边 // &100; 报错,右值无法取地址 // 2. 算术表达式结果为右值 int b = a + 20; // &(a + 20); 报错 // 3. 函数返回临时对象是右值 string s = getTemp(); // &getTemp(); 报错 return 0; }2.2 左值引用和右值引用
Type& r1 = x; Type&& rr1 = y; 第⼀个语句就是左值引用,左值引用就是给左值取别名,第二个就是右值引用,同样的道理,右值引用就是给右值取别名。
Type& r1 = x; // 左值引用:给左值取别名 Type&& rr1 = y; // 右值引用:给右值取别名左 / 右值引用绑定规则
1. 普通左值引用Type&只能绑定左值,不能直接绑定右值
int b = 1; int& r1 = b; // 合法,绑定左值b int& err = 10; // 编译报错,10是右值,普通左值引用不能绑右值2. const 左值引用const Type&
万能绑定:左值、右值都可以绑定
const int& rx1 = 10; // 绑定字面量右值 const double& rx2 = x + y; // 绑定运算临时右值 const string& rx4 = string("111"); // 绑定临时对象右值3. 右值引用Type&&
只能绑定纯右值 /move 转换后的将亡值,不能直接绑定普通左值
// 合法:绑定纯右值 int&& rr1 = 10; double&& rr2 = x + y; string&& rr4 = string("111"); // 报错:直接绑定普通左值 int&& err = b; // 合法:std::move把左值转为将亡值,再绑定右值引用 int&& rrx1 = move(b); string&& rrx4 = move(s);4. 变量表达式都是左值属性;右值被右值引用绑定后,该右值引用变量是左值。
int&& rr1 = 10; // &rr1 可以取地址,rr1自身是左值 cout << &rr1 << endl; int& r6 = rr1; // 合法:左值引用绑定左值rr1 // int&& rrx6 = rr1; // 报错:rr1是左值,不能直接绑右值引用 int&& rrx6 = move(rr1);// 必须再次move转为右值才能绑定5. std::move 详解
template <class _Ty> remove_reference_t<_Ty>&& move(_Ty&& _Arg) { return static_cast<remove_reference_t<_Ty>&&>(_Arg); }作用说明:
- move 是标准库函数模板,核心逻辑是强制类型转换;
remove_reference_t剥离变量原本的引用修饰,再强转为对应类型的右值引用;- 功能:接收任意左值,转换为右值,从而可以被右值引用绑定,触发移动语义;
int b = 1; int&& rrx1 = move(b); // move(b) 将左值b转为将亡右值 string s("111"); string&& rrx4 = move(s);示例代码:
#include<iostream> #include<string> #include<cmath> using namespace std; int main() { // 1. 定义各类左值(可取地址) int* p = new int(0); int b = 1; const int c = b; *p = 10; string s("111111"); s[0] = 'x'; double x = 1.1, y = 2.2; // ========== 左值引用:绑定各类左值 ========== int& r1 = b; int*& r2 = p; int& r3 = *p; string& r4 = s; char& r5 = s[0]; // ========== 右值引用:绑定纯右值 ========== int&& rr1 = 10; double&& rr2 = x + y; double&& rr3 = fmin(x, y); string&& rr4 = string("111111"); // ========== const左值引用:万能绑定 ========== const int& rx1 = 10; const double& rx2 = x + y; const double& rx3 = fmin(x, y); const string& rx4 = string("111111"); // ========== move转换左值,绑定右值引用 ========== int&& rrx1 = move(b); int*&& rrx2 = move(p); int&& rrx3 = move(*p); string&& rrx4 = move(s); string&& rrx5 = (string&&)s; // 等价move强制转换 // 验证:所有带名字变量都是左值,可以取地址 cout << &b << endl; cout << &r1 << endl; cout << &rr1 << endl; // 右值引用变量rr1是左值,直接绑定报错,需要再次move int& r6 = rr1; // int&& rrx6 = rr1; // 编译错误 int&& rrx6 = move(rr1); delete p; return 0; }2.3 右值引用的使用
2.3.1 左值引用问题
1. 无法用左值引用返回局部对象
函数内的局部变量在函数执行结束后会被析构销毁。
同理vector<vector<int>>& generate(...)返回局部vv也是非法野引用。
2. C++98 的妥协方案:输出型参数
既然不能引用返回局部对象,只能额外传一个外部容器作为输出参数,函数内部直接填充外部对象,规避拷贝 + 野引用问题。 以generate举例改造:
C++98这里的传值返回拷贝代价就太大了,C++11之后效率就很高,不用担心效率 vector<vector<int>> generate(int numRows) { vector<vector<int>> vv(numRows); for (int i = 0; i < numRows; ++i) { vv[i].resize(i + 1, 1); } for (int i = 2; i < numRows; ++i) { for (int j = 1; j < i; ++j) { vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1]; } } return vv; } C++11之前,得通过输出型参数改善效率 void generate(int numRows, vector<vector<int>>& vv) { //vector<vector<int>> vv(numRows); vv.resize(numRows); for (int i = 0; i < numRows; ++i) { vv[i].resize(i + 1, 1); } for (int i = 2; i < numRows; ++i) { for (int j = 1; j < i; ++j) { vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1]; } } }2.3.2 移动构造和移动赋值
一、拷贝构造的痛点
1. 拷贝构造Type(const Type& x)
参数是const 左值引用,逻辑:完整深拷贝堆资源
- 分配一块全新堆内存;
- 把源对象所有数据逐字节复制;
- 两个对象各自持有独立堆内存,析构时各自释放。
2. 存在巨大性能缺陷
对于string/vector这类存储堆内存的容器: 函数返回局部对象时会连续多次深拷贝,字符串越长、容器越大,内存分配 / 拷贝 / 释放开销越高(PPT 中addStrings函数 C++98 场景会两次深拷贝、两次析构释放堆内存)。
3. 移动语义诞生目的
临时对象用完立刻销毁,没必要完整拷贝它的堆资源;直接窃取临时对象内部的堆指针,仅交换几个内置成员变量,无堆内存操作,O (1) 常数开销。
二、移动构造函数
Type(Type&& x)- 本质是构造函数,和拷贝构造
Type(const Type& x)构成重载; - 第一个参数必须是本类类型的右值引用
Type&&; - 若存在额外参数,其余参数必须给默认值。
string 移动构造源码解析:
// 移动构造 string(string&& s) { cout << "string(string&& s) -- 移动构造" << endl; swap(s); // 交换this与s内部的_str/_size/_capacity } void swap(string& s) { ::swap(_str, s._str); ::swap(_size, s._size); ::swap(_capacity, s._capacity); }执行逻辑:
- 新对象初始
_str是空指针; - 和传入的临时对象
s交换三个成员:堆指针、有效长度、容量; - 资源完成转移:新对象拿到堆内存,原临时对象
s内部指针变为空; - 原临时对象析构时,
delete[]空指针无任何内存释放开销。
触发场景(仅右值会调用):
- 函数值返回局部对象(
addStrings中return str;,局部str是将亡值); std::move(左值)转换后的对象;- 匿名临时对象
bit::string("123")。
有无移动构造的运行对比:
场景 1:类只有拷贝构造
执行string ret = bit::addStrings("11111","222222222");
- 函数内部局部
str创建; return str触发拷贝构造:分配新堆内存,复制字符串;- 函数局部
str析构:释放原有堆内存; - 外部
ret持有拷贝出来的堆内存; - 日志打印两次
拷贝构造、两次析构,两次堆内存分配 + 释放。
#include <iostream> #include <cstring> // --------------------- 版本1:只有拷贝构造,无移动构造 --------------------- class MyStringCopyOnly { private: char* _str; size_t _size; public: // 构造函数 MyStringCopyOnly(const char* s) { _size = std::strlen(s); _str = new char[_size + 1]; std::strcpy(_str, s); std::cout << "[构造] 堆内存分配: " << (void*)_str << "\n"; } // 拷贝构造函数(深拷贝) MyStringCopyOnly(const MyStringCopyOnly& other) { _size = other._size; _str = new char[_size + 1]; std::strcpy(_str, other._str); std::cout << "[拷贝构造] 新堆内存分配: " << (void*)_str << "\n"; } // 拷贝赋值 MyStringCopyOnly& operator=(const MyStringCopyOnly& other) { if(this == &other) return *this; delete[] _str; _size = other._size; _str = new char[_size + 1]; std::strcpy(_str, other._str); std::cout << "[拷贝赋值] 新堆内存分配: " << (void*)_str << "\n"; return *this; } // 析构 ~MyStringCopyOnly() { std::cout << "[析构] 释放堆内存: " << (void*)_str << "\n"; delete[] _str; } // 禁止移动构造、移动赋值 MyStringCopyOnly(MyStringCopyOnly&&) = delete; MyStringCopyOnly& operator=(MyStringCopyOnly&&) = delete; };日志:
========= 场景1:只有拷贝构造,无移动构造 ========= [构造] 堆内存分配: 0x55f87c2b22a0 [拷贝构造] 新堆内存分配: 0x55f87c2b22d0 [析构] 释放堆内存: 0x55f87c2b22a0 [析构] 释放堆内存: 0x55f87c2b22d0场景 2:类实现移动构造
同样代码,return str触发移动构造:
- 仅交换
_str/_size/_capacity三个指针 / 数字,不新分配堆; - 局部
str析构时内部_str已为空,无需释放堆; - 日志打印两次
移动构造,无多余堆内存操作,性能大幅提升。
// --------------------- 版本2:带移动构造函数 --------------------- class MyStringMoveAble { private: char* _str; size_t _size; public: // 构造函数 MyStringMoveAble(const char* s) { _size = std::strlen(s); _str = new char[_size + 1]; std::strcpy(_str, s); std::cout << "[构造] 堆内存分配: " << (void*)_str << "\n"; } // 拷贝构造 MyStringMoveAble(const MyStringMoveAble& other) { _size = other._size; _str = new char[_size + 1]; std::strcpy(_str, other._str); std::cout << "[拷贝构造] 新堆内存分配: " << (void*)_str << "\n"; } // 移动构造!!核心:资源转移,不分配内存 MyStringMoveAble(MyStringMoveAble&& other) noexcept { // 直接偷对方的资源 _str = other._str; _size = other._size; // 置空源对象,源对象析构不会释放这块堆内存 other._str = nullptr; other._size = 0; std::cout << "[移动构造] 仅转移指针, 无新堆分配! 接管内存:" << (void*)_str << "\n"; } // 拷贝赋值 MyStringMoveAble& operator=(const MyStringMoveAble& other) { if(this == &other) return *this; delete[] _str; _size = other._size; _str = new char[_size + 1]; std::strcpy(_str, other._str); std::cout << "[拷贝赋值] 新堆内存分配: " << (void*)_str << "\n"; return *this; } // 移动赋值 MyStringMoveAble& operator=(MyStringMoveAble&& other) noexcept { if(this == &other) return *this; delete[] _str; _str = other._str; _size = other._size; other._str = nullptr; other._size = 0; std::cout << "[移动赋值] 仅转移指针,无新堆分配\n"; return *this; } // 析构 ~MyStringMoveAble() { if(_str != nullptr) { std::cout << "[析构] 释放堆内存: " << (void*)_str << "\n"; delete[] _str; } else { std::cout << "[析构] 对象资源已被移走, 无需释放堆内存\n"; } } };========= 场景2:实现移动构造函数 ========= [构造] 堆内存分配: 0x55f088e282a0 [移动构造] 仅转移指针, 无新堆分配! 接管内存:0x55f088e282a0 [析构] 对象资源已被移走, 无需释放堆内存 [析构] 释放堆内存: 0x55f088e282a02.4 右值引用和移动语义在传参中的提效
2.4.1 容器新增右值引用版本接口
// C++98,接收左值,只能拷贝 void push_back (const value_type& val); // C++11,接收右值,可以移动 void push_back (value_type&& val);- 传入左值对象:
lt.push_back(s1)调用const T&版本重载,触发拷贝构造,深拷贝堆内存,开销大。 - 传入右值对象(
std::move(s1)、临时对象):lt.push_back(move(s1))调用T&&版本重载:- 如果你的类实现了移动构造函数:资源所有权转移,只交换指针,不开辟新堆内存,性能高;
- 如果没有实现移动构造:右值引用版本只能降级调用拷贝构造,依然深拷贝,移动优化失效。
2.4.2 万能引用与引用折叠规则
2.4.2.1 万能引用
万能引用唯一形式:函数模板中T&&,并且T需要由实参推导得出。
template<class T> void Function(T&& t); // 万能引用- 传左值进去 → 通过引用折叠,实例化出左值引用版本
void Function(int& t) - 传右值进去 → 实例化出右值引用版本
void Function(int&& t)
2.4.2.2 引用折叠
规则:
两个引用叠加,只要有一个左值引用 (&),折叠结果就一定是左值引用
示例:
template<class T> void f1(T& x); // T&永远左值引用,不能接收右值0 template<class T> void f2(T&& x);//万能引用,左值→左值引用;右值→右值引用