C++11特性(一)
2026/9/6 23:54:41 网站建设 项目流程

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 引用折叠

一、基础定位

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 清单)

  1. 语言层面

  • 移动语义(右值引用、移动构造 / 移动赋值,和你自定义 string 代码对应)

  • 统一初始化、auto/decltype类型推导

  • Lambda 匿名函数、constexpr常量表达式

  1. 标准库 & 底层

  • 多线程库 + 标准化内存模型

  • 正则表达式、智能指针、哈希表、std::array

1.. 列表初始化

1.1 C++98传统的{}

一、C++98 大括号{}的局限

C++98 中{}仅能用于数组、结构体聚合初始化,场景割裂、语法不统一:

  1. 仅数组 / 原生聚合体能用{},普通类、stringvector不能用;
  2. 存在窄转换隐式允许,编译不报错;
  3. 初始化语法碎片化:()={}混用,阅读混乱;
  4. 无法初始化内置变量,必须用=
  • #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 个 5
    • vector<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修饰的左值禁止赋值,但依旧能取地址。

核心特征

  1. 内存常驻,拥有合法内存地址,支持&取地址;
  2. 赋值双向通用:a = 10int b = a都合法;
  3. 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)

右值是代表数据的表达式,分为两类:字面常量、表达式生成的临时对象;仅能出现在赋值符号右侧,不能放在左侧,无法取地址

核心特征

  1. 无持久内存,多存于寄存器 / 临时栈,不支持&取地址;
  2. 只能读取数值,不能被赋值;
  3. 包含纯右值(字面量、运算临时结果)、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); }

作用说明:

  1. move 是标准库函数模板,核心逻辑是强制类型转换
  2. remove_reference_t剥离变量原本的引用修饰,再强转为对应类型的右值引用;
  3. 功能:接收任意左值,转换为右值,从而可以被右值引用绑定,触发移动语义;
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); }

执行逻辑:

  1. 新对象初始_str是空指针;
  2. 和传入的临时对象s交换三个成员:堆指针、有效长度、容量;
  3. 资源完成转移:新对象拿到堆内存,原临时对象s内部指针变为空;
  4. 原临时对象析构时,delete[]空指针无任何内存释放开销。

触发场景(仅右值会调用):

  1. 函数值返回局部对象addStringsreturn str;,局部str是将亡值);
  2. std::move(左值)转换后的对象;
  3. 匿名临时对象bit::string("123")

有无移动构造的运行对比:

场景 1:类只有拷贝构造

执行string ret = bit::addStrings("11111","222222222");

  1. 函数内部局部str创建;
  2. return str触发拷贝构造:分配新堆内存,复制字符串;
  3. 函数局部str析构:释放原有堆内存;
  4. 外部ret持有拷贝出来的堆内存;
  5. 日志打印两次拷贝构造、两次析构,两次堆内存分配 + 释放。
#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触发移动构造

  1. 仅交换_str/_size/_capacity三个指针 / 数字,不新分配堆;
  2. 局部str析构时内部_str已为空,无需释放堆;
  3. 日志打印两次移动构造,无多余堆内存操作,性能大幅提升。
// --------------------- 版本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 [析构] 对象资源已被移走, 无需释放堆内存 [析构] 释放堆内存: 0x55f088e282a0

2.4 右值引用和移动语义在传参中的提效

2.4.1 容器新增右值引用版本接口

// C++98,接收左值,只能拷贝 void push_back (const value_type& val); // C++11,接收右值,可以移动 void push_back (value_type&& val);
  1. 传入左值对象:lt.push_back(s1)调用const T&版本重载,触发拷贝构造,深拷贝堆内存,开销大。
  2. 传入右值对象(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);//万能引用,左值→左值引用;右值→右值引用

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询