C++11(曾用名 C++0x)是 C++ 语言自 1998 年标准化以来最重大的一次更新,被称为 “现代 C++” 的基石 —— 它解决了 C++98 的诸多痛点,引入了大量提升开发效率、性能和安全性的特性,同时为后续 C++14/17/20 奠定了基础。
本文将按语言核心特性、标准库增强、并发编程、其他实用特性四大模块,结合示例详细讲解 C++11 的核心新内容。
1. 自动类型推导(
auto)核心用途
替代冗长的显式类型声明,让编译器根据初始化表达式推导变量类型,减少代码冗余且不损失类型安全。
关键规则
auto变量必须初始化(编译器需要推导依据);- 不能用于函数参数(除非结合模板)、类成员变量;
- 推导时会忽略顶层
const/volatile(如需保留需显式声明)。示例
#include <iostream> #include <vector> using namespace std; int main() { // 替代冗长的迭代器类型 vector<string> vec = {"a", "b", "c"}; for (auto it = vec.begin(); it != vec.end(); ++it) { // 推导为vector<string>::iterator cout << *it << " "; } auto x = 10; // int const auto z = x; // const int auto y = 3.14; // double auto* p = &x; // int* auto& ref = x; // int& return 0; }2. 空指针常量(
nullptr)核心问题
C++98 的
NULL是宏(#define NULL 0),本质是int类型,会导致类型歧义(比如重载f(int)和f(char*)时,f(NULL)会匹配f(int)而非f(char*))。解决方案
nullptr是专属的空指针类型(std::nullptr_t),能精准匹配指针类型。示例
#include <iostream> using namespace std; void f(int) { cout << "int" << endl; } void f(char*) { cout << "char*" << endl; } int main() { f(NULL); // 匹配f(int),不符合预期 f(nullptr); // 匹配f(char*),正确 return 0; }3. 范围
for循环核心用途
简化容器 / 数组的遍历,无需手动管理迭代器或索引,语法更直观。
语法
for (元素类型 变量 : 容器/数组) { 循环体 }示例
#include <iostream> #include <vector> using namespace std; int main() { int arr[] = {1, 2, 3, 4}; // 遍历数组(值拷贝) for (int num : arr) { cout << num << " "; } vector<int> vec = {5, 6, 7}; // 遍历容器(引用,可修改元素) for (auto& num : vec) { num *= 2; // 修改原容器元素 } for (const auto& num : vec) { // const引用避免拷贝,推荐 cout << num << " "; // 输出10 12 14 } return 0; }4. 统一列表初始化(
{})核心问题
C++98 的初始化语法混乱(括号、等号、聚合初始化混用),且无法禁止 “窄化转换”(比如
int转char丢失数据)。解决方案
{}初始化(列表初始化)统一所有类型的初始化方式,且禁止窄化转换。示例
#include <iostream> #include <vector> #include <string> using namespace std; struct Person { string name; int age; }; int main() { // 内置类型 int a{10}; // 等价于int a = 10 // int b{3.14}; // 编译错误:窄化转换(double→int) // 容器 vector<int> vec{1, 2, 3}; // 直接初始化,无需push_back // 自定义类型(聚合初始化) Person p{"Alice", 20}; // 动态数组 int* arr = new int[3]{1, 2, 3}; delete[] arr; return 0; }
5. 右值引用与移动语义(核心难点)
核心问题
传统 C++ 中,临时对象(右值)的拷贝会产生大量性能开销(比如
std::string的深拷贝)。关键概念
- 左值:有名称、可取地址的对象(如变量
x),生命周期由程序员控制;;- 右值:无名称、不可取地址的临时对象(如
10、x+y、函数返回的临时值),生命周期通常仅在当前表达式,用完即销毁;- 右值引用:
T&&,专门绑定右值,允许 “移动” 而非 “拷贝” 资源。C++11 中右值又细分为两类(不影响核心判定,但能更精准理解):
- 纯右值(prvalue):基础类型的临时值(如
10、x+y)、字面量(除字符串字面量);- 将亡值(xvalue):即将销毁的对象(如函数返回的临时对象、
std::move转换后的左值),是右值引用的核心操作对象。以下是左值 / 右值(纯右值 / 将亡值)的完整分类对照表,覆盖所有核心场景,包含判定依据、地址验证和避坑备注,可直接对照参考:
示例表达式 / 对象 类型(左 右) 核心判定依据 地址验证( &表达式是否合法)补充说明(避坑 / 关键备注) 基础变量 / 常量 int a = 10;中的a左值 有持久名称,生命周期可控,可修改 &a; // 合法最基础的左值,所有具名变量均为左值 const double b = 3.14;中的b左值 有持久名称,可取地址; const仅限制修改,不改变左值属性&b; // 合法const 左值不可修改,但仍是左值 std::string str = "hello";中的str左值 自定义类型具名对象,有地址,生命周期可控 &str; // 合法所有 STL 容器具名对象均为左值 int arr[5];中的arr左值 数组名代表数组首地址,有固定存储位置,可取地址 &arr; // 合法数组名是左值,但不能赋值( arr = {}非法)struct A {int x;} obj;中的obj左值 自定义结构体具名对象,有地址 &obj; // 合法所有用户自定义类型具名对象均为左值 表达式类左值 ++a(前置自增)左值 返回 a的左值引用(int& operator++()),指向原对象int a=1; &(++a); // 合法前置自增 / 减均返回左值 --a(前置自减)左值 同前置自增,返回左值引用 int a=1; &(--a); // 合法a = 10(赋值表达式)左值 赋值运算符返回左值引用( int& operator=(int)),指向aint a; &(a=10); // 合法赋值表达式结果是左值(可链式赋值 a=b=c)obj.x(类成员变量)左值 通过具名对象访问成员,有地址 A obj; &(obj.x); // 合法结构体 / 类的成员变量(具名访问)均为左值 *p(解引用表达式)左值 解引用指向具体对象,有地址(即使 p是指针)int a=1; int* p=&a; &(*p); // 合法等价于 &a,解引用结果必为左值arr[0](数组下标)左值 下标运算符返回左值引用,指向数组元素 int arr[5]; &(arr[0]); // 合法数组所有下标访问结果均为左值 std::cin/std::cout左值 标准流对象有持久名称,存储在全局区,可取地址 &std::cin; // 合法所有标准库具名全局对象均为左值 特殊左值(易混淆) 字符串字面量 "hello world"左值 存储在只读数据段,有固定地址,生命周期贯穿程序 &"hello"; // 合法唯一例外:字面量中仅字符串字面量是左值 函数名 func(void func(){})左值 函数名代表函数入口地址,有固定存储位置 &func; // 合法函数指针的基础,函数名是左值 具名 lambda 变量 auto f = [](){};中的f左值 有持久名称,存储在栈上,可取地址 &f; // 合法匿名 lambda 是右值,具名 lambda 是左值 左值引用变量 int& ref = a;中的ref左值 有名称,绑定左值,可取地址(等价于 &a)&ref; // 合法引用变量本身是左值(无论左 / 右值引用) 纯右值(prvalue,基础临时值) 数值字面量 10/3.14/true纯右值 无名称,临时值,存储在寄存器 / 临时栈帧,无持久地址 &10; // 编译错误所有基础类型字面量(除字符串)均为纯右值 字符字面量 'a'/'\n'纯右值 无名称,临时值,不可寻址 &'a'; // 编译错误算术表达式 a+b/a*3/b/2纯右值 结果是临时值,无名称,表达式结束后销毁 int a=1,b=2; &(a+b); // 编译错误所有算术运算结果均为纯右值 逻辑 / 比较表达式 a>b/!a/a&&b纯右值 结果是临时布尔值,无名称,不可寻址 int a=1,b=2; &(a>b); // 编译错误所有逻辑 / 比较运算结果均为纯右值 后置自增 / 减 a++/a--纯右值 返回自增 / 减前的临时拷贝,无名称,不可寻址 int a=1; &(a++); // 编译错误后置返回临时值(右值),前置返回引用(左值) 取地址表达式 &a纯右值 结果是临时指针值,无名称,不可寻址 int a=1; &(&a); // 编译错误取地址结果是纯右值 空指针常量 nullptr纯右值 无名称,临时常量,不可寻址 &nullptr; // 编译错误NULL是宏(0),也是纯右值枚举值 enum Color{Red};中的Red纯右值 无名称,临时常量,不可寻址 &Red; // 编译错误枚举值均为纯右值 类型转换临时值 static_cast<double>(10)纯右值 转换结果是临时值,无名称,不可寻址 &static_cast<double>(10); // 编译错误所有显式类型转换(非引用)结果均为纯右值 将亡值(xvalue,即将销毁的对象) 函数返回的临时对象 getStr()(std::string getStr(){return "hello";})将亡值 函数返回非引用类型,生成临时对象,无名称,表达式结束后销毁 &getStr(); // 编译错误触发移动构造的核心场景 std::move(a)(a是左值)将亡值 std::move强制将左值转为右值引用,对象即将被移动int a=1; &std::move(a); // 编译错误std::move仅改类型,不移动资源匿名 lambda [](){cout<<"hello";}将亡值 无名称,临时对象,执行后销毁 &[](){}; // 编译错误具名 lambda 是左值,匿名是将亡值 列表初始化临时对象 {1,2,3}将亡值 编译器生成临时 initializer_list,无名称,表达式结束后销毁&{1,2,3}; // 编译错误传给 initializer_list参数时的底层载体匿名类对象 A()(struct A{})将亡值 无名称,临时构造,表达式结束后析构 &A(); // 编译错误匿名对象均为将亡值 STL 临时容器 std::vector<int>{1,2,3}将亡值 无名称的临时容器,无持久地址,表达式结束后销毁 &std::vector<int>{1,2,3}; // 编译错误临时容器对象是将亡值,触发移动语义 边界易混淆案例 右值引用变量 int&& t = 10;中的t左值 变量有名称,可取地址;仅绑定的对象是右值 &t; // 合法右值引用变量本身是左值(核心避坑点) const int&& t = 10;中的t左值 常量右值引用变量仍有名称,是左值 &t; // 合法所有引用变量(无论类型)本身都是左值 函数返回左值引用 int& func(){static int a=1;return a;}中的func()左值 返回引用,指向持久对象(static),可取地址 &func(); // 合法返回引用 = 左值,返回值 = 右值 右值引用(T&&)核心定义与语法
语法:
类型&& 引用名 = 右值对象;本质:专门给无名称、不可取地址的右值(纯右值 / 将亡值) 起别名,允许接管右值的资源(而非拷贝)。
场景 是否合法 示例 说明 绑定纯右值(如 10、3.14) ✅ 合法 int&& a = 10;直接绑定临时数值 绑定将亡值(如函数返回的临时对象) ✅ 合法 std::string&& s = getStr();(getStr 返回 std::string)绑定即将销毁的临时对象 直接绑定左值 ❌ 非法 int x=10; int&& a = x;右值引用不能直接绑定左值 绑定 std::move 转换后的左值 ✅ 合法 int x=10; int&& a = std::move(x);std::move 将左值强制转为右值引用类型 const 右值引用(const T&&) 语法合法,但无实用价值 const int&& a = 10;右值本身即将销毁,const 修饰后无法移动,几乎不用 右值引用变量本身是左值!
- 左值引用(
T&):有名字、可取地址,本身是左值,指向一块持久的内存(比如变量int a = 10; int& ref = a;,ref指向a的栈内存)。- 右值引用(
T&&):有名字、可取地址,本身也是左值,指向一块临时内存(比如int&& rref = 10;,rref指向栈上的临时内存)。- 二者的共性:都是 “有名字的引用”,本质上都是一个 “指向内存地址的指针(编译器层面)”,都能操作内存中的数值。
右值引用变量(如
int&& a = 10;中的a)有名称、可取地址,因此变量本身是左值void func(int&) { cout << "左值引用参数" << endl; } void func(int&&) { cout << "右值引用参数" << endl; } int main() { int&& a = 10; // a是右值引用变量,绑定右值10 func(a); // 输出:左值引用参数(a本身是左值) func(std::move(a)); // 输出:右值引用参数(std::move将a转为右值) return 0; }编译器在语法分析阶段,会为每个表达式标记 “左值属性”(lvalue)或 “右值属性”(rvalue):
- 具名变量 / 引用变量 / 解引用表达式等 → 标记为
lvalue;- 临时值 / 匿名对象 /
std::move表达式等 → 标记为rvalue。
左值转换成右值std::move()
std::move()是C++11引入的一个工具函数,它本身不移动任何数据,而是将一个左值转换为右值引用,从而允许移动语义的发生。
#include <utility> // 包含move template<typename T> typename std::remove_reference<T>::type&& move(T&& arg) noexcept;
move对于内置类型无效,移动就是拷贝。
对于没有移动构造函数的类,move会调用拷贝构造函数。
移动后对象的状态是有效但未指定的,可以安全析构和重新赋值。
移动操作应该标记为noexcept。
谨慎使用move,避免意外移动全局对象、静态对象或函数参数。
将对象标记为"可移动",允许编译器使用移动构造函数或移动赋值运算符,而不是拷贝操作
class Buffer { private: char* data; size_t size; public: // 移动构造函数 Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) { other.data = nullptr; // 重要:将原对象置空 other.size = 0; } // 移动赋值运算符 Buffer& operator=(Buffer&& other) noexcept { if (this != &other) { delete[] data; // 释放当前资源 data = other.data; size = other.size; other.data = nullptr; other.size = 0; } return *this; } }; Buffer buf1(1024); Buffer buf2 = std::move(buf1); // 高效移动,无深拷贝问题:buf1被移动后还能正常使用吗?
答案:不能正常使用,因为它的内部指针已经被置为空。但是,它仍然处于一个有效的状态(可以安全析构,也可以被重新赋值)。也就是说,移动后的对象(buf1)处于一个有效但不确定的状态,除了重新赋值或者让它销毁,我们不能对它有任何关于其内容的假设。为什么能高效移动?
因为移动操作只是复制了指针和大小,然后将原对象的指针置空,避免了深拷贝(即避免了分配新内存和复制数据)。这通常比深拷贝快得多,特别是当数据很大时。注意:移动操作后,原对象的资源已经被转移,所以原对象不能再使用这些资源。在上面的例子中,buf1的data变成了nullptr,所以你不能通过buf1访问之前的内存。但是,你可以给buf1重新赋值,或者让它销毁(析构函数会处理空指针)。
右值引用的原理:
1. 纯右值(prvalue)的底层存储
纯右值(如
10、3.14、a+b)是 “临时值”,底层存储分两种情况:
- 小类型(int/double/char):直接存储在CPU 寄存器(无内存地址),编译器为了让右值引用能绑定,会自动将寄存器中的值 “拷贝到栈上的临时内存”,再让右值引用指向该临时内存;示例:
int&& rref = 10;底层步骤:
- 编译器在栈上分配临时内存(如地址
0x7ffeefbff5c8);- 将
10写入该临时内存;- 右值引用
rref(底层指针)指向该临时内存地址。- 大类型(如临时 std::string):直接存储在栈帧的临时区域(函数调用栈 / 表达式栈),有内存地址,但生命周期仅在当前表达式。
2. 将亡值(xvalue)的底层存储
将亡值(如函数返回的临时对象、
std::move后的左值)是 “即将销毁的对象”,底层存储:
- 函数返回的临时对象:存储在调用者栈帧的临时区域(而非被调用函数栈帧,避免栈销毁导致悬空);
std::move后的左值:仍存储在原左值的内存地址(仅编译器标记为 “可被移动”)。核心结论:
1. 右值的底层仍有内存地址(编译器兜底分配),右值引用的本质是 “指向这些临时 / 即将销毁内存的指针”,但编译器对这些指针的绑定、生命周期、语义触发有特殊规则;
2. 右值引用(或 const 左值引用)直接绑定函数返回的临时对象时,临时对象不会 “自动销毁”(析构被推迟),其生命周期与引用一致;
3. 小类型会改变存储位置(因为小,所有拷贝也不消耗性能), 但是大类型临时对象的存储位置(调用者栈帧)本身只要栈帧未销毁就一直存在,生命周期延长只是 “推迟析构”,而非 “改变存储位置”;
移动构造函数 & 移动赋值运算符(右值引用的核心落地)
移动构造函数(Move Constructor)和移动赋值运算符(Move Assignment Operator)是 C++11 基于右值引用设计的核心特性,二者的目标是接管右值对象的资源(如堆内存、文件句柄等),替代传统的 “深拷贝”,彻底解决临时对象的拷贝开销问题。
移动构造函数T(T&& other)
移动赋值运算符T& operator=(T&& other)简单来说:
- 移动构造:创建新对象时,“偷” 走右值对象的资源(而非拷贝);
- 移动赋值:已有对象赋值时,“偷” 走右值对象的资源(先释放自身旧资源,再接管新资源)。
核心背景:为什么需要移动构造 / 赋值?
传统的拷贝构造(
const T&)和拷贝赋值(const T& operator=)会做深拷贝(重新分配内存 + 拷贝数据),比如std::string/std::vector的堆内存拷贝,开销极大;而移动构造 / 赋值仅做浅拷贝(指针赋值) + 置空源对象,几乎无开销。
操作 拷贝(const T&) 移动(T&&) 内存分配 分配新内存(如 new char[])无分配,直接接管源对象内存 数据拷贝 逐字节拷贝堆内存数据 仅拷贝指针(8 字节,64 位) 源对象状态 不变(仍持有原资源) 置空(避免析构时重复释放资源) 性能 O (n)(n 为数据大小) O (1)(常数时间) 移动构造函数(Move Constructor)
1. 定义与语法规则
语法格式
class 类名 { public: // 移动构造函数(核心:参数是 非const 右值引用) 类名(类名&& other) noexcept { // noexcept 是关键! // 步骤1:接管 other 的资源 this->m_ptr = other.m_ptr; this->m_size = other.m_size; // 步骤2:置空 other 的资源(必须!避免析构时重复释放) other.m_ptr = nullptr; other.m_size = 0; } };核心规则(必遵守)
规则 说明 参数类型 必须是 类名&&(非 const 右值引用)—— 因为要修改源对象(置空资源)noexcept声明建议 / 必须加:容器(如 vector)扩容时,只有移动构造标记noexcept,才会优先用移动而非拷贝不能是 constconst 类名&&无意义:无法修改源对象,无法置空资源编译器默认生成 满足条件时(无自定义拷贝构造 / 拷贝赋值 / 析构),编译器生成默认移动构造(浅拷贝) 2. 触发条件(何时调用移动构造?)
当用右值对象初始化新对象时,自动触发移动构造:
- 函数返回的临时对象(将亡值);
std::move转换后的左值;- 匿名对象(如
MyString("hello"))。
移动赋值运算符(Move Assignment Operator)
1. 定义与语法规则
移动赋值是 “已有对象” 赋值右值时调用,核心是先释放自身旧资源,再接管右值的新资源。
语法格式
class 类名 { public: // 移动赋值运算符 类名& operator=(类名&& other) noexcept { // 步骤0:处理自赋值(避免自己赋值自己) if (this == &other) { return *this; } // 步骤1:释放自身旧资源(避免内存泄漏) delete[] this->m_ptr; // 步骤2:接管 other 的资源 this->m_ptr = other.m_ptr; this->m_size = other.m_size; // 步骤3:置空 other 的资源 other.m_ptr = nullptr; other.m_size = 0; return *this; // 返回自身,支持链式赋值 } };// ---------------------- 移动构造函数 ---------------------- string(string&& s) { cout << "string(const string&& s) -- 移动构造" << endl; this->swap(s); } // ---------------------- 移动赋值运算符 ---------------------- string& operator=(string&& s) { cout << "string& operator=(string&& s) -- 移动赋值" << endl; this->swap(s); return *this; }移动构造用
swap的性能是「O (1)」,拷贝构造是「O (n)」
std::string的swap是资源交换操作,不是拷贝:
string内部是通过 “指针 + 长度 + 容量” 管理堆内存的(比如指向字符数组的指针char* buf);swap只是交换两个string的指针、长度、容量这几个成员变量(相当于交换 3 个变量的值),是常数时间(O (1)),几乎没有开销;- 而拷贝构造函数是 “深拷贝”:需要新分配一块和原字符串一样大的堆内存,再把原字符串的每个字符都复制过去(时间复杂度是
O(n),n是字符串长度)。核心规则(对比移动构造)
规则 说明 返回值 必须返回 类名&(支持链式赋值,如a = b = c)自赋值检查 必须加:避免 s = std::move(s)时,先释放自身资源导致悬空释放旧资源 必须先释放自身堆内存,否则会内存泄漏 noexcept声明同移动构造,建议 / 必须加 编译器默认生成 同移动构造:无自定义拷贝构造 / 赋值 / 析构时,编译器生成默认移动赋值 2. 触发条件(何时调用移动赋值?)
已有对象赋值右值对象时触发:
s = getTempString();(临时对象赋值);s = std::move(other);(左值转右值赋值)。
完整示例 (移动构造和移动赋值)
#include <iostream> #include <cstring> // strcpy、strlen #include <utility> // std::move using namespace std; // 自定义字符串类:演示移动构造/移动赋值 class MyString { private: char* _str; // 指向堆上分配的字符串内存(核心资源) size_t _size; // 字符串长度(不含'\0') public: // 1. 构造函数:分配堆内存,初始化字符串 MyString(const char* s = "") { _size = strlen(s); _str = new char[_size + 1]; // +1 存'\0' strcpy(_str, s); cout << "构造函数 [" << (void*)_str << "]:" << _str << endl; } // 2. 析构函数:释放堆内存(避免内存泄漏) ~MyString() { if (_str) { // 只释放非空指针 cout << "析构函数 [" << (void*)_str << "]:" << _str << endl; delete[] _str; _str = nullptr; _size = 0; } } // 3. 拷贝构造(深拷贝):为新对象分配独立内存 // 左值引用参数 + const(保证不修改原对象) MyString(const MyString& other) { // 步骤1:分配和原对象一样大的内存 _size = other._size; _str = new char[_size + 1]; // 步骤2:逐字节复制字符串(深拷贝核心) strcpy(_str, other._str); cout << "拷贝构造 [" << (void*)_str << "]:拷贝自 [" << (void*)other._str << "]" << endl; } // 4. 拷贝赋值(深拷贝):先释放自己的内存,再深拷贝 MyString& operator=(const MyString& other) { if (this != &other) { // 防止自赋值(a = a) // 步骤1:释放当前对象的旧内存 delete[] _str; // 步骤2:分配新内存 + 深拷贝数据 _size = other._size; _str = new char[_size + 1]; strcpy(_str, other._str); cout << "拷贝赋值 [" << (void*)_str << "]:拷贝自 [" << (void*)other._str << "]" << endl; } return *this; } // 5. 移动构造(浅拷贝/资源接管):偷原对象的指针,掏空原对象 // 右值引用参数 + noexcept(容器优先调用noexcept的移动构造) MyString(MyString&& other) noexcept { // 步骤1:直接接管原对象的资源(指针+长度) _str = other._str; _size = other._size; // 步骤2:掏空原对象(关键!避免析构时双重释放) other._str = nullptr; other._size = 0; cout << "移动构造 [" << (void*)_str << "]:接管自 [" << (void*)&other._str << "]" << endl; } // 6. 移动赋值(浅拷贝/资源接管):释放自己内存,偷原对象指针 MyString& operator=(MyString&& other) noexcept { if (this != &other) { // 防止自赋值 // 步骤1:释放当前对象的旧内存 delete[] _str; // 步骤2:接管原对象的资源 _str = other._str; _size = other._size; // 步骤3:掏空原对象 other._str = nullptr; other._size = 0; cout << "移动赋值 [" << (void*)_str << "]:接管自 [" << (void*)&other._str << "]" << endl; } return *this; } // 辅助函数:打印字符串(方便测试) void print() const { if (_str) { cout << "字符串:" << _str << " | 地址:" << (void*)_str << " | 长度:" << _size << endl; } else { cout << "字符串:空 | 地址:nullptr | 长度:0" << endl; } } }; // 测试函数:接收MyString,触发拷贝/移动 MyString createString(const char* s) { MyString temp(s); // 构造临时对象 return temp; // 返回临时对象(右值) } int main() { cout << "===== 测试1:拷贝构造(左值)=====" << endl; MyString a("Hello World"); // 构造函数 MyString b = a; // 左值 → 拷贝构造(深拷贝) b.print(); cout << "\n===== 测试2:移动构造(右值/临时对象)=====" << endl; MyString c = createString("I am a temp string"); // 临时对象 → 移动构造 c.print(); cout << "\n===== 测试3:移动赋值(std::move转换左值为右值)=====" << endl; MyString d("Old string"); d = std::move(c); // std::move把c转为右值 → 移动赋值 d.print(); c.print(); // c已被掏空,变成空字符串 cout << "\n===== 测试结束 =====" << endl; return 0; }输出结果
===== 测试1:拷贝构造(左值)===== 构造函数 [0x55f8b8a7f2a0]:Hello World 拷贝构造 [0x55f8b8a7f2d0]:拷贝自 [0x55f8b8a7f2a0] 字符串:Hello World | 地址:0x55f8b8a7f2d0 | 长度:11 ===== 测试2:移动构造(右值/临时对象)===== 构造函数 [0x55f8b8a7f300]:I am a temp string 移动构造 [0x55f8b8a7f300]:接管自 [0x7ffeefbff5d8] 字符串:I am a temp string | 地址:0x55f8b8a7f300 | 长度:16 ===== 测试3:移动赋值(std::move转换左值为右值)===== 构造函数 [0x55f8b8a7f330]:Old string 移动赋值 [0x55f8b8a7f360]:接管自 [0x7ffeefbff5e0] 字符串:I am a temp string | 地址:0x55f8b8a7f300 | 长度:16 字符串:空 | 地址:nullptr | 长度:0 ===== 测试结束 ===== 析构函数 [0x55f8b8a7f360]:I am a temp string 析构函数 [0x55f8b8a7f300]:(null) 析构函数 [0x55f8b8a7f2d0]:Hello World 析构函数 [0x55f8b8a7f2a0]:Hello World关键:移动赋值先释放
s1的旧内存,再接管右值的内存,无深拷贝开销。移动构造 vs 移动赋值(核心对比)
维度 移动构造函数 移动赋值运算符 调用时机 新对象初始化( MyString s = 右值;)已有对象赋值( s = 右值;)自身资源处理 无需释放(新对象无旧资源) 必须先释放自身旧资源 自赋值检查 可选(新对象不可能是自身) 必须(避免释放自身资源) 返回值 无(构造函数无返回值) 必须返回 类名&(链式赋值)核心逻辑 接管资源 + 置空源对象 释放旧资源 → 接管新资源 → 置空源对象
引用折叠与万能模板
引用折叠的核心定义
C++ 语法本身不允许直接定义 “引用的引用”(比如
int& &、int& &&这类写法),但在模板推导、auto类型推导、类型别名(typedef/using)等场景下,会间接产生 “引用的引用”。此时编译器会按照 “引用折叠规则”,将这种非法的复合引用类型折叠成一个合法的引用类型 —— 这就是引用折叠。引用折叠的核心规则(只有 4 条)
引用折叠的规则可以总结为:只要有一个是左值引用(
&),最终结果就是左值引用;只有全部是右值引用(&&),结果才是右值引用。
复合引用类型 折叠后的最终类型 核心逻辑 左值引用 + 左值引用 ( T& &)T&含左值引用 → 左值引用 左值引用 + 右值引用 ( T& &&)T&含左值引用 → 左值引用 右值引用 + 左值引用 ( T&& &)T&含左值引用 → 左值引用 右值引用 + 右值引用 ( T&& &&)T&&全是右值引用 → 右值引用 补充:C++ 中,有名字的右值引用是左值(比如函数参数
T&& param,param本身是左值),这也是引用折叠能发挥作用的重要前提。引用折叠的典型应用场景(附代码示例)
引用折叠不是孤立的规则,它是模板推导、完美转发的底层支撑,以下是最常见的应用场景:
场景 1:模板参数推导中的引用折叠
模板参数
T&&被称为 “万能引用”(仅当T是模板参数时),其最终类型由传入的实参类型决定,核心就是引用折叠:#include <iostream> #include <type_traits> // 用于类型判断 using namespace std; // 万能引用模板函数 template <typename T> void func(T&& param) { // 判断param的最终类型 if (is_lvalue_reference<decltype(param)>::value) { cout << "param 最终是左值引用\n"; } else if (is_rvalue_reference<decltype(param)>::value) { cout << "param 最终是右值引用\n"; } } int main() { int a = 10; func(a); // 传入左值,T推导为int&,T&& → int& && → 折叠为int&(左值引用) func(20); // 传入右值,T推导为int,T&& → int&&(右值引用) func(std::move(a)); // 传入右值,T推导为int,T&& → int&&(右值引用) return 0; }输出结果:
param 最终是左值引用 param 最终是右值引用 param 最终是右值引用
场景 2:类型别名(
typedef/using)中的引用折叠当类型别名本身是引用类型,再叠加引用时,也会触发引用折叠:
#include <iostream> #include <type_traits> using namespace std; int main() { // 定义左值引用的类型别名 using IntLRef = int&; // IntLRef&& → int& && → 折叠为int&(左值引用) IntLRef&& a = 10; // 注意:这里a是左值引用,绑定到右值10(C++11起允许) cout << boolalpha; // 输出true/false而非1/0 cout << "a 是左值引用:" << is_lvalue_reference<decltype(a)>::value << endl; // true // 定义右值引用的类型别名 using IntRRef = int&&; // IntRRef&& → int&& && → 折叠为int&&(右值引用) IntRRef&& b = 20; cout << "b 是右值引用:" << is_rvalue_reference<decltype(b)>::value << endl; // true return 0; }引用折叠的核心价值
- 解决了 “引用的引用” 的语法合法性问题,让模板推导、万能引用成为可能;
- 是
std::forward(完美转发)的底层基础,保证参数能以原始类型(左值 / 右值)传递;- 让右值引用的语义在复杂场景(如模板、嵌套类型)中保持一致。
完美转发(std::forward)
完美转发是 C++11 及以后的核心特性,主要用于模板编程中提升性能(减少拷贝)和简化代码, 模板函数转发参数时,右值引用可能被降级为左值,导致无法正确触发移动语义。
完美转发的核心目标:原样转发参数,保留其左值 / 右值属性,避免拷贝和重载匹配错误;
实现三要素:万能引用(T&&,模板推导场景) + 引用折叠规则 + std::forward<T>();
std::forward<T>(完美转发)能保留参数的原始类型(左值 / 右值),仅在模板中生效。示例
#include <iostream> #include <utility> using namespace std; // 目标函数:区分左值/右值参数 void target(int& x) { cout << "接收左值:" << x << endl; } void target(int&& x) { cout << "接收右值:" << x << endl; } // 转发函数:万能引用 + std::forward template <typename T> void forward_func(T&& param) { // std::forward<T> 利用引用折叠,还原param的原始类型 target(std::forward<T>(param)); /*如果不使用完美转发, 无论param是左值引用还是右值引用, 最终param都是一个左值,只会匹配函数void target(int& x), 无法调用forward_func内部的右值引用函数target(int&& x);*/ } int main() { int a = 10; forward_func(a); // 转发左值 → 调用target(int&) forward_func(20); // 转发右值 → 调用target(int&&) forward_func(std::move(a)); // 转发右值 → 调用target(int&&) return 0; }输出结果:
接收左值:10 接收右值:20 接收右值:10
个人的深刻理解:
过去的很多时间里,我都一直在思考, 移动构造和赋值构造的本质区别, 我明白左值引用和右值引用其本质没太大区别, 其特质几乎一模一样, 我们使用左值引用引用左值, 我们用右值引用捕获右值, 这些右值有的在栈上很快就要被操作系统抛弃, 有的只是程序运行过程中的过眼云烟, 但是右值引用捕获它们, 为它们赋予了新的生命周期, 维护住了其所在的内存空间, 直到右值引用死去. 我十分不理解, 既然这两个引用如此相像为啥就不能混用, 为啥右值引用不能直接当成左值引用;后来我明白了:其实两者可以随便混用, 但是具体怎么用是取决于你的需求, 而不是它的功能, 移动构造的精髓不是调用了一个右值引用, 而是利用右值引用(也可以是右值) 去掠夺右值的元素, 如果你不掠夺其元素, 只是和拷贝构造一样复制其元素, 是没有性能提升的;移动构造只转移指针,不复制数据, 堆上分配的数据物理位置永远不会变, C++移动语义的核心是 "偷窃而不复制"(steal, don't copy)。它允许我们将昂贵的深拷贝转换为廉价的指针复制,前提是我们接受原对象会被"掏空"(但仍然是类型安全的)。
1. 当我们使用左值引用时, 我们是在复制左值引用引用的对象, 有时是深拷贝有时是浅拷贝, 但是我们在操作过左值引用后, 左值引用引用的对象, 必然不会有影响, 可以正常使用;
2. 但是我们在操作右值引用时, 我们不再是单纯的复制右值引用引用的对象, 我们是完全的掠夺右值引用引用的对象的资源, 如果这个对象里有一个指针指向一个内存空间起始点, 然后有一个size变量告诉此空间长度, 如果用拷贝构造, 我们需要自己开空间, 自己创建一个size, 然后复制对方size到自己这来, 然而移动构造, 是直接把别人的指针拷贝到我们这来, 把别人的指针直接置空(防止我和对方对这个指针两次free), 这个操作相当于直接把对面的指针抢过来, 同样的size也会被抢过来;如果对方内部还有那种特别大的对象时, 还需要再次把对方的内部大型元素用move转为右值(如果对方没有移动构造那么move会按拷贝构造处理), 再抢过来;这种野蛮的抢夺, 会导致右值引用引用的对象, 被操作后被破坏无法正常使用, 其资源被掏空,仅保留基础可操作能力(可析构、可赋值)
掠夺一个对象是非常快的, O(1)的时间复杂度, 有点像独享的浅拷贝(浅拷贝的缺点就是会导致不同对象公用内存, 可能导致各种错误, 如果独享就不会有错误), 但是拷贝一个对象的速度取决于对象大小, 如果很大就会很慢;
所以我们需要自己选择到底是要掠夺一个对象还是拷贝一个对象, 如果我们需要掠夺A, 那么就要用右值引用引用这个对象A, 然后操作这个右值引用, 如果要拷贝一个对象B, 那就用左值引用引用这个对象B;
对象A和对象B类型是不是左右值,不重要,可以通过move把一个左值变成右值, 然后掠夺一个左值; 也可以把一个右值用右值引用捕获, 然后把这个右值引用 当作左值引用处理, 复制, 但是这样做是罕见的, 因为右值本身不可以被操作, 即使被掠夺, 也没事, 如果你复制它, 他留下来也没用.
在这个基础上理解完美转发就很简单了: 某个函数用的是万能模板T&&, 如果我们想要执行复制, 访问之类的操作, 那就传入一个左值引用T& 那么变量就按T&处理, 执行复制, 访问之类的操作, 不会掠夺; 如果我们想要掠夺一个对象, 那就传入右值引用T&&, 万能模板就按T&&处理, 问题出在这了:如果我们引用一个右值, 无论是左值引用还是右值引用都是左值, 那么你后续如果函数内部有一个嵌套函数需要执行某种操作, 由于无论你一开始是想要掠夺还是复制, 进来后都是左值, 就无法判断你要执行什么操作了,(注意并不是左值不可以掠夺,掠夺是一种行为,之前移动构造就是通过操作左值完成掠夺的),而且通常接受左值引用的函数是不会掠夺的.因此执行掠夺的函数(必须接受一个右值), 就无法执行, 因为没有右值能传进嵌套函数, 这个时候如果你把这个变量用move强转,又会导致所有的变量到此处都会变成右值, 接受左值的函数就无法被触发, 所以引入了完美转发, 使用完美转发意味着, 它会使得右值引用恢复你想要的掠夺属性, 左值引用保持你想要的复制属性; func(std::forward<T>(param));如果param是左值引用就是左值, 如果是右值引用就是右值, func通常会重载一个右值引用func和左值引用func;
其实即使没有完美转发也可以通过写两个函数一个接受左值引用一个接受右值引用, 来实现功能;
用右值引用接住函数返回的右值后,对象在栈上的 “外壳”(本体)地址可能变(取决于优化),但里面真正占内存的核心数据(比如字符串的字符、数组的元素)都存在堆上,这部分数据的物理地址不会变;移动构造之所以能做到 O (1) 的高速,是因为它只把 “指向堆数据的指针” 从一个对象转给另一个(逻辑上改变了其位置),而不是复制整个堆数据;比如把右值存到 set、再转到 vector 的过程,不用移动构造就会反复拷贝堆数据,性能很差,而移动构造只是不断更换堆数据的 “主人”(从无主→set 元素→vector 元素),全程不拷贝核心数据 , 但是外壳可能会更换。
RVO/NRVO 优化:
编译器预判到函数要返回一个临时对象,直接在调用方的栈帧里预留一块内存,让被调用函数的返回对象直接构造在这块内存里
#include <iostream> #include <string> using namespace std; int main() { string* ptr = nullptr; { string s = "scope test"; ptr = &s; cout << "内层作用域s的栈地址:" << ptr << endl; // 0x7ffee8a29a00 } // 内层栈帧销毁,s的地址失效 string new_s = move(*ptr); // 移动构造(仅演示地址变化,实际ptr已失效) cout << "外层new_s的栈地址:" << &new_s << endl; // 0x7ffee8a29a40(地址不同) return 0; }因为编译器有返回值优化(RVO/NRVO) :编译器会直接在调用方的栈帧中构造函数返回的对象,跳过 “函数栈帧→调用方栈帧” 的移动 / 拷贝步骤,此时对象的栈地址就 “不变”。
比如开启 RVO 后,上面场景 1 的输出可能变成:
函数内s的栈地址:0x7ffee9b29a40 main中右值引用的栈地址:0x7ffee9b29a40 // 地址相同(编译器优化)
noexcept 关键词
noexcept是 C++11 引入的异常规范关键字,用来声明函数是否会抛出异常(noexcept= 不抛,noexcept(false)= 可能抛);标记为
noexcept的函数如果抛异常,程序会直接终止(调用std::terminate),而非触发try-catch;核心价值是帮助编译器优化、明确语义,尤其对 STL 容器的移动操作至关重要(决定是否用高效的移动语义)。
对比 C++11 之前的
throw()(比如void func() throw();),noexcept更高效、语义更清晰,也是现在 C++ 标准推荐的写法(throw()在 C++17 中已被弃用)。1. 无参数形式(最常用)
// 声明:这个函数绝对不会抛出任何异常 void func() noexcept; // 反之:这个函数可能抛出异常(等价于不写noexcept) void func() noexcept(false);
noexcept等价于noexcept(true),是 “函数不抛异常” 的承诺;- 如果一个标记为
noexcept(true)的函数违反承诺(内部抛出了异常,且没有在函数内捕获),程序会直接终止(调用std::terminate()),不会触发外层的try-catch。2. 带参数形式(编译期条件判断)
参数是一个编译期可求值的布尔表达式,用来动态判断函数是否不抛异常:
#include <utility> #include <string> // 只有当 std::move(s) 不抛异常时,这个函数才是noexcept的 void move_string(std::string& s) noexcept(noexcept(std::move(s))) { std::string tmp = std::move(s); }这种写法常用于模板编程,让函数的异常规范 “跟随” 其内部调用的函数。
为什么需要 noexcept?(核心价值)
1. 帮助编译器做优化
如果编译器知道一个函数不会抛异常,就可以省略很多异常处理的额外代码(比如栈展开、异常对象构造等),提升程序运行效率。
2. 明确语义,避免调用者踩坑
调用者看到
noexcept标记的函数,就知道无需为这个函数写try-catch,代码逻辑更清晰;反之,看到无noexcept的函数,就知道需要考虑异常处理。3. 影响 STL 容器的行为(最实用的场景)
STL 容器(比如
vector、map)在某些操作(如扩容、移动元素)时,会根据 “移动构造函数是否是noexcept” 来选择行为:
- 如果移动构造函数是
noexcept:容器会优先使用移动语义(高效,不拷贝);- 如果移动构造函数不是
noexcept:容器会退而求其次使用拷贝构造(低效,但更安全,避免移动过程中抛异常导致数据丢失)。实战示例:noexcept 的用法和效果
#include <iostream> #include <stdexcept> using namespace std; // 声明:绝对不抛异常 void safe_func() noexcept { cout << "这个函数很安全,不会抛异常" << endl; } // 声明:可能抛异常(等价于不写noexcept) void risky_func() noexcept(false) { throw runtime_error("出问题了!"); // 抛出异常 } int main() { // 测试noexcept函数抛异常的后果 try { safe_func(); // 正常执行 risky_func(); // 抛出异常,被catch捕获 } catch (const exception& e) { cout << "捕获异常:" << e.what() << endl; } // 故意让noexcept函数抛异常(违反承诺) try { // 匿名函数,标记为noexcept,但内部抛异常 auto bad_func = []() noexcept { throw runtime_error("noexcept函数抛异常了!"); }; bad_func(); // 程序直接终止,不会走到catch } catch (const exception& e) { cout << "不会执行到这里" << endl; } return 0; }运行结果:
这个函数很安全,不会抛异常 捕获异常:出问题了! terminate called after throwing an instance of 'std::runtime_error' what(): noexcept函数抛异常了! Aborted (core dumped)可以看到:违反
noexcept承诺的函数会直接终止程序,而非进入catch块。noexcept 的常见使用场景
- 析构函数:C++11 起,析构函数默认是
noexcept(true)(除非显式声明为noexcept(false)),这是因为析构函数抛异常会导致程序终止(比如栈展开时析构函数抛异常)。- 移动构造 / 移动赋值函数:为了让 STL 容器优先使用移动语义,建议给移动构造 / 赋值加
noexcept。- 性能敏感的简单函数:比如数学计算、数据拷贝等确定不抛异常的函数,加
noexcept让编译器优化。- 模板函数:用带参数的
noexcept(表达式)让异常规范跟随模板参数的特性。
7. Lambda 表达式(匿名函数)
核心用途
定义临时的匿名函数,避免编写冗余的函数对象(如
std::less),尤其适合算法回调。语法
[capture](parameters) mutable noexcept -> return_type { // 函数体 }
- 捕获列表:绑定外部变量(
[]空捕获、[=]值捕获、[&]引用捕获、[this]捕获当前对象、[x, &y]混合捕获);mutable:允许修改值捕获的变量;noexcept:声明函数不抛出异常;- 返回类型:可省略(编译器自动推导)。
示例
#include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { vector<int> vec = {3, 1, 4, 1, 5}; // 排序(降序) sort(vec.begin(), vec.end(), [](int a, int b) { return a > b; }); // 遍历(值捕获外部变量) int sum = 0; for_each(vec.begin(), vec.end(), [&sum](int num) { sum += num; cout << num << " "; // 输出5 4 3 1 1 }); cout << "\n总和:" << sum << endl; // 14 return 0; }
8. constexpr(编译期常量)
核心用途
替代
#define和普通const,允许函数 / 变量在编译期计算,提升性能。关键规则
constexpr变量:编译期确定值,存储在只读内存;constexpr函数:满足条件时(无副作用、返回值依赖参数),编译期执行。#include <iostream> using namespace std; // 编译期计算阶乘 constexpr int factorial(int n) { return n <= 1 ? 1 : n * factorial(n - 1); } int main() { constexpr int f5 = factorial(5); // 编译期计算为120 int n = 5; // int f6 = factorial(n); // 运行期执行(n是变量) cout << f5 << endl; // 120 return 0; }
9. 强类型枚举(enum class)
核心问题
传统
enum存在作用域污染(枚举值暴露在全局)、类型不安全(可隐式转换为int)。解决方案
enum class(强枚举):作用域封闭、不可隐式转换、可指定底层类型。#include <iostream> using namespace std; // 传统枚举(问题) enum Color { Red, Green, Blue }; // 枚举值在全局作用域 // int x = Red; // 隐式转换为int,不安全 // 强类型枚举 enum class Direction { Left, Right }; // 作用域封闭 // int y = Direction::Left; // 编译错误:不可隐式转换 Direction dir = Direction::Right; // 指定底层类型 enum class Week : char { Mon, Tue, Wed }; int main() { if (dir == Direction::Right) { cout << "Right" << endl; } return 0; }
10. 默认 / 删除函数(=default/=delete)
核心用途
显式控制编译器生成的默认函数(如拷贝构造、析构),解决 “隐式生成的函数不符合预期” 的问题。
#include <iostream> using namespace std; class NonCopyable { public: // 显式生成默认构造函数(比手动写更高效) NonCopyable() = default; // 禁用拷贝构造(禁止对象拷贝) NonCopyable(const NonCopyable&) = delete; // 禁用赋值运算符 NonCopyable& operator=(const NonCopyable&) = delete; // 析构函数默认 ~NonCopyable() = default; }; int main() { NonCopyable a; // NonCopyable b = a; // 编译错误:拷贝构造被删除 return 0; }
11. 可变参数模板(Variadic Templates)
核心用途
定义接受任意数量、任意类型参数的模板,是实现泛型工具(如
std::tuple、printf)的基础。语法
template <typename... Args>(参数包),配合Args... args(展开参数包)。示例
#include <iostream> using namespace std; // 递归终止函数 void print() { cout << endl; } // 可变参数模板函数 template <typename T, typename... Args> void print(T first, Args... rest) { cout << first << " "; print(rest...); // 递归展开参数包 } int main() { print(1, "hello", 3.14, 'a'); // 输出1 hello 3.14 a return 0; }
12. decltype(类型推导)
核心用途
推导表达式的类型(补充
auto),尤其适合模板中推导返回类型。示例
#include <iostream> #include <vector> using namespace std; int main() { int x = 10; const int& ref = x; decltype(x) y = 20; // int decltype(ref) z = x; // const int& decltype((x)) w = x; // int&(表达式x是左值,加括号后推导为引用) // 推导函数返回类型 vector<int> vec; decltype(vec.begin()) it = vec.begin(); // vector<int>::iterator return 0; }
13. 委托构造
委托构造的核心语法是:在构造函数的初始化列表中,调用同一个类的另一个构造函数,而非直接初始化成员变量。被调用的构造函数称为 “目标构造函数”,发起调用的称为 “委托构造函数”。
#include <iostream> #include <string> using namespace std; class Person { private: string name; int age; string gender; public: // 1. 基础构造函数(目标构造函数):负责核心初始化逻辑 Person(string n, int a, string g) : name(n), age(a), gender(g) { // 真正初始化成员 cout << "基础构造函数执行(初始化成员)" << endl; } // 2. 委托构造函数1:委托给基础构造(默认性别) Person(string n, int a) : Person(n, a, "Unspecified") { // 委托调用↑ cout << "带姓名+年龄的委托构造函数执行" << endl; } // 3. 委托构造函数2:委托给上面的构造(默认年龄) Person(string n) : Person(n, 0) { // 委托调用↑ cout << "带姓名的委托构造函数执行" << endl; } // 4. 委托构造函数3:委托给上面的构造(默认姓名) Person() : Person("Unknown") { // 委托调用↑ cout << "默认委托构造函数执行" << endl; } // 辅助函数:打印信息 void showInfo() { cout << "Name: " << name << ", Age: " << age << ", Gender: " << gender << endl; } }; // 测试代码 int main() { cout << "=== 创建默认对象 ===" << endl; Person p1; p1.showInfo(); cout << "\n=== 创建带姓名的对象 ===" << endl; Person p2("Alice"); p2.showInfo(); return 0; }
二、标准库增强
C++11 大幅扩展和优化了标准库,重点解决 “易用性” 和 “性能” 问题。
1. 新容器
| 容器 | 用途 | 优势 |
|---|---|---|
std::array | 固定大小数组 | 替代 C 数组,支持迭代器、边界检查(at()),更安全 |
std::forward_list | 单向链表 | 比std::list更轻量,内存占用少 |
std::unordered_map/set | 哈希表 | 平均 O (1) 查找,替代std::map/set(红黑树,O (logn)) |
std::tuple | 元组 | 存储任意数量 / 类型的元素,替代std::pair(仅 2 个元素) |
示例(std::array/std::tuple)
#include <iostream>
#include <array>
#include <tuple>
using namespace std;
int main() {
// std::array
array<int, 3> arr = {1, 2, 3};
cout << arr.at(1) << endl; // 2(边界检查,越界抛异常)
// std::tuple
tuple<string, int, double> t("Alice", 20, 95.5);
cout << get<0>(t) << " " << get<1>(t) << endl; // Alice 20
return 0;
}
2. emplace系列函数
核心用途
直接在容器内构造对象(而非先构造临时对象再拷贝 / 移动),减少开销,替代push_back/insert。
示例
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main() {
vector<string> vec;
// push_back:先构造临时string,再移动/拷贝
vec.push_back(string("hello"));
// emplace_back:直接在容器内构造string,无临时对象
vec.emplace_back("world");
for (const auto& s : vec) {
cout << s << " "; // hello world
}
return 0;
}
| 维度 | insert | emplace |
|---|---|---|
| 传参方式 | 传「已构造的对象」(如临时对象、变量) | 传「对象的构造参数」(如字符串字面量、数值) |
| 底层逻辑 | 先构造对象 → 再拷贝 / 移动到容器 | 直接在容器内存位置构造对象 |
| 性能开销 | 多一次拷贝 / 移动(临时对象) | 仅就地构造,无额外开销 |
| 适用场景 | 插入已有对象(变量) | 插入新对象(无现成对象,仅知构造参数) |
3. 智能指针(内存管理革命)
C++11 废弃了不安全的auto_ptr,引入三类智能指针,实现RAII(资源获取即初始化),自动管理堆内存,避免内存泄漏。
| 智能指针 | 所有权 | 核心用途 |
|---|---|---|
std::unique_ptr | 独占所有权 | 替代裸指针,不支持拷贝,仅支持移动 |
std::shared_ptr | 共享所有权 | 引用计数,多对象共享资源 |
std::weak_ptr | 弱引用 | 解决shared_ptr循环引用问题 |
示例(unique_ptr/shared_ptr)
#include <iostream>
#include <memory>
using namespace std;
class Test {
public:
Test() { cout << "Test构造" << endl; }
~Test() { cout << "Test析构" << endl; }
};
int main() {
// unique_ptr(独占)
unique_ptr<Test> p1(new Test());
// unique_ptr<Test> p2 = p1; // 编译错误:禁止拷贝
unique_ptr<Test> p2 = move(p1); // 移动,p1变为空
// shared_ptr(共享)
shared_ptr<Test> p3 = make_shared<Test>(); // 推荐make_shared(更高效)
cout << "引用计数:" << p3.use_count() << endl; // 1
shared_ptr<Test> p4 = p3;
cout << "引用计数:" << p3.use_count() << endl; // 2
return 0; // 自动析构,无内存泄漏
}
循环引用问题(weak_ptr解决)
#include <iostream>
#include <memory>
using namespace std;
struct Node {
shared_ptr<Node> next; // 共享指针
weak_ptr<Node> prev; // 弱指针(解决循环引用)
~Node() { cout << "Node析构" << endl; }
};
int main() {
auto n1 = make_shared<Node>();
auto n2 = make_shared<Node>();
n1->next = n2;
n2->prev = n1; // 弱引用不增加计数
return 0; // 引用计数归0,正常析构
}
4. 时间库(std::chrono)
核心用途
精准处理时间和计时,替代传统的time()/clock()。
核心组件
duration:时长(如seconds、milliseconds);time_point:时间点;clock:时钟(system_clock系统时间、steady_clock单调时钟)。
示例(计时)
#include <iostream>
#include <chrono>
#include <thread>
using namespace std;
using namespace chrono;
int main() {
// 开始计时
auto start = steady_clock::now();
// 模拟耗时操作
this_thread::sleep_for(100ms);
// 结束计时
auto end = steady_clock::now();
duration<double, milli> elapsed = end - start;
cout << "耗时:" << elapsed.count() << " 毫秒" << endl; // ~100ms
return 0;
}
5. 字符串工具
std::to_string:数值转字符串;std::stoi/stol/stod:字符串转数值。
示例
#include <iostream>
#include <string>
using namespace std;
int main() {
string s1 = to_string(123); // "123"
string s2 = to_string(3.14); // "3.140000"
int x = stoi("456"); // 456
double y = stod("7.89"); // 7.89
return 0;
}
6. std::initializer_list
std::initializer_list 是 C++11 新增的轻量级、只读容器类型(头文件 <initializer_list>),专门用于表示 “同类型常量值的有序列表”,是 {} 列表初始化的底层实现载体—— 所有通过 {} 进行的列表初始化,本质上都是编译器将 {} 内的元素封装为 std::initializer_list 对象,再传递给接收该类型的函数 / 构造函数。
简单说:{1,2,3} 这种写法本身不是一个 “类型”,编译器会自动把它转换成 std::initializer_list<int> 类型的临时对象,供函数 / 构造函数接收。
#include <iostream>
#include <vector>
#include <map>
#include <initializer_list> // 显式包含头文件(部分编译器可隐式包含,但建议显式写)
using namespace std;
int main() {
// 1. vector 列表初始化
vector<int> vec{1, 2, 3, 4}; // 编译器将{1,2,3,4}转为initializer_list<int>,传给vector的构造函数
for (int num : vec) {
cout << num << " "; // 输出:1 2 3 4
}
cout << endl;
// 2. map 列表初始化(元素是pair,因此列表内是pair的{}初始化)
map<string, int> mp{{"Alice", 20}, {"Bob", 25}};
// 等价于:mp(initializer_list<pair<const string, int>>{pair{"Alice",20}, pair{"Bob",25}})
for (auto& p : mp) {
cout << p.first << ":" << p.second << " "; // 输出:Alice:20 Bob:25
}
cout << endl;
// 3. 空列表初始化
vector<double> empty_vec{};
cout << "空列表size:" << empty_vec.size() << endl; // 输出:0
return 0;
}
三、并发编程(C++11 首次标准化)
C++11 首次将并发编程纳入标准库,无需依赖平台特定 API(如 POSIX Threads、Windows Threads)。
1. 线程(std::thread)
核心用途
创建和管理线程,支持join()(等待线程结束)、detach()(分离线程)。
#include <iostream>
#include <thread>
using namespace std;
void printHello(int n) {
for (int i = 0; i < n; ++i) {
cout << "Hello from thread: " << this_thread::get_id() << endl;
}
}
int main() {
thread t(printHello, 3); // 创建线程,传递参数3
t.join(); // 等待线程结束(必须调用join/detach,否则程序崩溃)
cout << "Main thread end" << endl;
return 0;
}
2. 互斥量(std::mutex)与lock_guard
核心用途
保护共享资源,避免多线程竞争(数据竞争未定义行为)。
lock_guard:RAII 封装互斥量,自动加锁 / 解锁,避免手动解锁遗漏。
示例(线程安全计数器)
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int count = 0;
mutex mtx;
void increment(int n) {
for (int i = 0; i < n; ++i) {
lock_guard<mutex> lock(mtx); // 自动加锁
++count;
} // 自动解锁(lock_guard析构)
}
int main() {
thread t1(increment, 100000);
thread t2(increment, 100000);
t1.join();
t2.join();
cout << "Count: " << count << endl; // 200000(正确)
return 0;
}
3. 原子操作(std::atomic)
核心用途
替代互斥量,实现无锁的线程安全操作,更高效。
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
atomic<int> count(0); // 原子变量
void increment(int n) {
for (int i = 0; i < n; ++i) {
++count; // 原子操作,无需互斥量
}
}
int main() {
thread t1(increment, 100000);
thread t2(increment, 100000);
t1.join();
t2.join();
cout << "Count: " << count << endl; // 200000
return 0;
}
4. 异步操作(std::async/std::future)
核心用途
异步执行函数,获取结果(无需手动管理线程)。
#include <iostream>
#include <future>
using namespace std;
int calculate(int x) {
return x * x;
}
int main() {
// 异步执行calculate(10),返回future
future<int> fut = async(calculate, 10);
// 主线程做其他事
cout << "Doing other work..." << endl;
// 获取异步结果(阻塞直到完成)
int res = fut.get();
cout << "Result: " << res << endl; // 100
return 0;
}
四、其他实用特性
1. 静态断言(static_assert)
编译期断言,替代运行时的assert,提前发现错误。
static_assert(sizeof(int) == 4, "int must be 4 bytes"); // 编译期检查
2. 对齐支持(alignas/alignof)
控制内存对齐,用于高性能 / 硬件相关编程。
alignas(16) int arr[4]; // 数组按16字节对齐
cout << alignof(arr) << endl; // 16
3. SFINAE(模板替换失败非错误)
模板替换失败时不报错,而是跳过该模板,用于类型萃取(如判断类型是否有某个成员)。
#include <iostream>
#include <type_traits>
using namespace std;
// 检测类型是否有size()成员函数
template <typename T>
auto has_size(T&& t) -> decltype(t.size(), true_type{}) {
return true_type{};
}
template <typename T>
false_type has_size(T&& t) {
return false_type{};
}
int main() {
cout << has_size(string("hello")).value << endl; // 1
cout << has_size(123).value << endl; // 0
return 0;
}




3041

被折叠的 条评论
为什么被折叠?



