C++11(上)
Hello,小伙伴们!又到了咱们一起捣鼓代码的时间啦!💪 把生活调成热情模式,带着满满的能量钻进编程的奇妙世界吧——今天也要写出超酷的代码,冲鸭!🚀
我的博客主页:喜欢吃燃面
我的专栏:《C语言》,《C语言之数据结构》,《C++》,《Linux学习笔记》
感谢你点开这篇博客呀!真心希望这些内容能给你带来实实在在的帮助~ 如果你有任何想法或疑问,非常欢迎一起交流探讨,咱们互相学习、共同进步,在编程路上结伴成长呀!
一.现代C++
1.分类
C++ 以 C++11 为界,分为传统 C++(C++11 之前)和现代 C++(C++11 及之后),核心差异集中在特性完善度、安全性、易用性和性能优化上。
2.传统 C++(C++98/03 为主)
- 核心定位:C++ 首个正式标准及修正版,确立基础语法和 STL 框架。
- 关键局限:依赖裸指针(易内存泄漏)、无自动类型推导/Lambda/移动语义、模板繁琐、标准库功能简单(无哈希容器、元组)。
3. 现代 C++(C++11 及后续标准:C++14/17/20/23 等)
- 核心定位:弥补传统 C++ 不足,在保留底层控制能力的同时,强化安全性、易用性和性能。
- 核心优势:智能指针(内存自动化管理)、自动类型推导、Lambda 表达式、移动语义(提升效率)、丰富标准库(哈希容器、optional 等)。

二.列表初始化
C++11开始希望通过列表统一初始化
1.列表初始化
C++11 引入统一初始化(Uniform Initialization) 机制,核心目标是通过列表初始化(List Initialization) 统一不同场景下的初始化语法,解决传统 C++ 中初始化方式混乱、歧义等问题。其核心特点和作用如下:
1.1 统一语法:一种语法适配多种场景
传统 C++ 中,初始化方式因对象类型不同而差异较大(如变量、数组、结构体、类对象等各有语法)。C++11 用花括号括号 {} 作为统一标记,覆盖几乎所有初始化场景:
#include <vector> // 用于std::vector容器
#include <iostream>
// 定义结构体
struct Point {
int x;
int y;
};
int main() {
// 1. 变量初始化
int x{5}; // 列表初始化整数x
// 2. 数组初始化
int arr[]{1, 2, 3}; // 列表初始化数组(可省略数组长度,由编译器推导)
// 3. 结构体对象初始化
Point p{10, 20}; // 列表初始化结构体成员x=10, y=20
// 4. 容器初始化
std::vector<int> vec{1, 2, 3}; // 列表初始化vector容器
// 5. 动态内存初始化(动态数组)
int* ptr = new int[3]{1, 2, 3}; // 列表初始化动态分配的数组
// 释放动态内存
delete[] ptr;
return 0;
}
1.2 解决歧义与安全性提升
- 避免“最令人头疼的解析”:传统 C++ 中,
A a();会被解析为函数声明而非对象初始化,而A a{};明确表示默认初始化对象。 - 禁止窄化转换(Narrowing Conversion):列表初始化会编译报错,避免隐式的精度丢失或范围溢出,例如:
这比传统的int x{3.14}; // 错误:double 转 int 是窄化转换 short s{100000}; // 错误:超出 short 范围int x = 3.14;(隐式截断为 3)更安全。
1.3 对标准库的强化
统一初始化大幅简化了容器、智能指针等的使用,例如:
#include <map>
#include <memory> // 用于智能指针
#include <iostream>
// 定义结构体Point
struct Point {
int x;
int y;
};
int main() {
// 直接初始化std::map
std::map<std::string, int> dict{{"one", 1}, {"two", 2}, {"three", 3}};
// 智能指针初始化(std::unique_ptr)
std::unique_ptr<Point> p{new Point{1, 2}}; // 用列表初始化Point对象,再交给智能指针管理
// 智能指针会自动释放内存,无需手动delete
return 0;
}
1.4 总结
C++11 的统一初始化通过 {} 列表语法,实现了“一处语法,多处适用”,既简化了代码,又减少了歧义,同时通过禁止窄化转换提升了类型安全性。这一特性成为现代 C++ 中初始化的推荐方式,后续标准(如 C++17、C++20)进一步完善了其细节,使其成为核心语法之一。
2.列表初始化和std::initializer_list的区别
列表初始化(List Initialization)和 std::initializer_list 是 C++11 中关联紧密但本质不同的概念,前者是一种初始化语法,后者是支持这种语法的标准库类型。
2.1 本质与定位
-
列表初始化:是一种语法规则,表现为用花括号
{}包裹初始化值(如T obj{arg1, arg2};),用于统一各种场景下的初始化方式(变量、数组、类对象、容器等)。它是编译器层面的语法支持,不依赖特定类型。 -
std::initializer_list:是 C++ 标准库定义的一种模板类型(
template<typename T> class initializer_list),用于封装一个同类型元素的临时列表(如{1, 2, 3}可被隐式转换为initializer_list<int>)。它是一种数据容器,用于在函数/构造函数与初始化列表之间传递数据。
2.2 作用场景
-
列表初始化:适用于几乎所有初始化场景,核心是简化语法并提升安全性:
- 初始化基本类型:
int a{5}; - 初始化数组:
int arr{1, 2, 3}; - 初始化结构体/类对象:
struct S{int x; int y;}; S s{1, 2}; - 初始化标准容器:
std::vector<int> vec{1, 2, 3};(此时会隐式用到initializer_list)。
- 初始化基本类型:
-
std::initializer_list:主要用于函数或构造函数的参数,接收通过列表初始化传递的一组同类型值。例如:
- 标准容器(如
vector、map)的构造函数都重载了接收initializer_list的版本,因此才能直接用{...}初始化。 - 自定义类也可通过它支持列表初始化:
class MyList { public: // 接收 initializer_list 的构造函数 MyList(std::initializer_list<int> list) { for (auto val : list) { /* 处理元素 */ } } }; MyList ml{1, 2, 3}; // 调用上述构造函数,list 封装 {1,2,3}
- 标准容器(如
2.3 依赖关系
列表初始化是更上层的语法,而 std::initializer_list 是实现列表初始化在类/函数参数传递场景中的关键工具:
- 当用
{a, b, c}初始化一个类对象时,编译器会优先尝试匹配接收initializer_list的构造函数(若存在),否则再尝试其他构造方式(如聚合初始化)。 - 若没有
initializer_list,列表初始化仍可用于基础类型、数组、聚合类(如简单结构体),但无法直接初始化需要处理动态元素的类(如容器)。
2.4 关键区别总结
| 维度 | 列表初始化(List Initialization) | std::initializer_list |
|---|---|---|
| 本质 | 语法规则(用 {} 初始化) |
标准库模板类型(封装临时列表) |
| 作用 | 统一初始化语法,提升安全性 | 传递列表初始化中的元素给函数/构造函数 |
| 依赖 | 不依赖特定类型,由编译器直接支持 | 需显式在代码中使用(如函数参数) |
| 适用场景 | 所有初始化场景(变量、数组、对象等) | 仅用于接收列表参数的函数/构造函数 |
简言之:列表初始化是“怎么写”的语法,std::initializer_list 是“怎么接收和处理这些值”的工具,前者依赖后者实现对复杂类型(如容器、自定义类)的列表初始化支持。
三.右值引用和移动语义
C++11开始引入右值引用和移动语义。
1.左值和右值
核心结论是:左值是可被取地址、能放在赋值号左边的表达式,右值是仅表示数据、只能放在赋值号右边的临时值或字面量,存储在寄存器中的变量。
1.1 核心定义与区别
- 左值(lvalue):有明确内存地址,可被修改(const左值除外),能出现在赋值号左侧。
- 右值(rvalue):无持久内存地址,是临时数据(如字面量、表达式结果、函数返回的临时对象),只能出现在赋值号右侧。
1.2 典型例子
- 左值:变量(
int a)、数组元素(arr[0])、解引用表达式(*ptr)。 - 右值:字面量(
10、"abc")、表达式结果(a+3)、临时对象(func()返回的非引用值)。
2.左值引用和右值引用
核心结论:左值引用(T&)绑定左值,右值引用(T&&)绑定右值,二者是C++中用于绑定不同类型表达式的引用类型,核心作用是控制对象的生命周期和优化拷贝。
2.1 核心定义与绑定规则
- 左值引用(T&):专门绑定可寻址的左值,绑定后成为左值的别名。
- 右值引用(T&&):专门绑定临时的右值(字面量、表达式结果、临时对象),绑定后可延长右值的生命周期。
2.2 关键区别与示例
| 特性 | 左值引用(T&) | 右值引用(T&&) |
|---|---|---|
| 绑定对象 | 左值(变量、数组元素等) | 右值(字面量、临时对象等) |
| 可修改性 | 非const版本可修改绑定对象 | 可修改绑定的右值(临时对象) |
| 典型示例 | int a=10; int& ref=a; |
int&& ref=10; int&& ref=a+3; |
| 核心用途 | 传递参数、避免拷贝(输入输出) | 移动语义、完美转发(优化性能) |
2.3 关键补充
- const左值引用(
const T&)是特殊情况,可同时绑定左值和右值(如const int& ref=10;),但无法修改绑定对象。- 右值引用绑定后,临时右值的生命周期会延长至与引用相同,避免了临时对象的销毁与重建。
- 移动语义依赖右值引用,通过
std::move可将左值强制转为右值,供右值引用绑定(如int a=5; int&& ref=std::move(a);)。
3.右值引用的使用场景
右值引用(T&&)的核心价值在于优化对象生命周期管理和减少不必要的拷贝。
3.1 移动语义(避免深拷贝,提升性能)
当对象包含堆内存(如动态数组、字符串)时,传统的拷贝构造/赋值会对堆内存进行深拷贝(耗时且浪费资源)。而右值引用允许我们“窃取”右值(临时对象)的资源,实现移动构造和移动赋值,仅转移资源所有权而非拷贝。
示例:
class String {
private:
char* data;
public:
// 构造函数
String(const char* str) {
data = new char[strlen(str) + 1];
strcpy(data, str);
}
// 移动构造函数(参数为右值引用)
String(String&& other) noexcept {
data = other.data; // 直接窃取资源
other.data = nullptr; // 原对象放弃资源所有权
}
// 移动赋值运算符
String& operator=(String&& other) noexcept {
if (this != &other) {
delete[] data; // 释放当前资源
data = other.data; // 窃取右值资源
other.data = nullptr;
}
return *this;
}
~String() { delete[] data; }
};
// 使用场景:临时对象作为右值,触发移动构造
String getString() {
return String("临时字符串"); // 返回的临时对象是右值
}
int main() {
String s = getString(); // 调用移动构造,而非拷贝构造
return 0;
}
效果:移动操作避免了对堆内存的深拷贝,性能提升显著(尤其对大对象)。
3.2 函数参数传递(区分左值/右值,优化参数处理)
通过右值引用作为函数参数,可以区分传入的是左值还是右值,从而对右值参数进行特殊处理(如直接复用资源)。
示例:
// 重载函数:分别处理左值和右值
void process(String& s) {
cout << "处理左值:" << s << endl; // 需保留原对象,可能执行拷贝
}
void process(String&& s) {
cout << "处理右值:" << s << endl; // 可直接移动资源,无需拷贝
}
int main() {
String a("左值对象");
process(a); // 调用左值引用版本
process(getString()); // 调用右值引用版本(临时对象是右值)
return 0;
}
3. 3 延长临时对象生命周期
右值引用绑定到临时对象(右值)时,会延长该临时对象的生命周期,使其与引用的生命周期一致,避免过早销毁。
示例:
int&& ref = 10 + 20; // 临时值30被右值引用绑定,生命周期延长
cout << ref << endl; // 合法,输出30
关键注意事项
- 右值引用本身是左值(可被取地址),因此在嵌套传递时需用
std::forward保持右值属性。 std::move仅将左值强制转为右值引用,不移动资源,实际移动由移动构造/赋值实现,而且move的返回值才是右值引用。- 右值引用主要用于临时对象或即将销毁的对象,避免对左值使用
std::move(可能导致原对象资源被窃取)。
这些场景的核心目标是减少拷贝开销,尤其在处理大对象(如容器、字符串)时,右值引用能显著提升程序性能。
4.右值引用和移动语义解决传值返回的问题
在C++中,函数“传值返回”对象时,传统机制存在冗余拷贝的性能问题,而右值引用与移动语义的结合,从根本上解决了这一问题。
4 .1 传统传值返回的核心问题:冗余拷贝
当函数返回一个对象(尤其是包含堆内存的大对象,如std::string、std::vector)时,传统流程会产生多次深拷贝,导致性能浪费。具体过程如下:
- 函数内部创建局部对象:例如
String func() { String s("hello"); return s; },函数内的s是局部对象。 - 返回时拷贝到临时对象:由于局部对象
s在函数结束后会销毁,编译器会先将s拷贝到一个“临时对象”(右值)中。 - 临时对象拷贝到接收变量:调用者用
String a = func();接收时,临时对象会再次拷贝到a中,之后临时对象销毁。
对于包含堆内存的对象(如String类持有char*),每次拷贝都是深拷贝(重新分配堆内存并复制数据),两次拷贝的成本极高。
// 传统String类(仅展示拷贝构造)
class String {
char* data;
public:
String(const char* str) { /* 分配堆内存并复制 */ }
// 拷贝构造(深拷贝)
String(const String& other) {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data); // 耗时的深拷贝
}
};
String getString() {
String s("test");
return s; // 第一步:拷贝s到临时对象(深拷贝1)
}
int main() {
String a = getString(); // 第二步:临时对象拷贝到a(深拷贝2)
return 0;
}
即使编译器有返回值优化(RVO/NRVO) 可以省略部分拷贝,但优化有严格限制(如返回值必须是局部对象且与返回类型一致),无法覆盖所有场景(如条件分支返回不同对象时)。
4.2 右值引用+移动语义:用“资源转移”替代“拷贝”
右值引用(T&&)和移动语义的引入,允许在传值返回时直接转移对象资源(而非拷贝),彻底消除冗余拷贝。核心逻辑如下:
- 返回的局部对象被视为右值:函数返回局部对象时,该对象即将被销毁(生命周期结束),编译器会将其识别为右值(临时对象也是右值)。
- 触发移动构造/移动赋值:如果类定义了以右值引用为参数的移动构造函数,则返回时会调用移动构造而非拷贝构造,直接“窃取”右值的资源(如堆内存指针)。
- 资源转移而非拷贝:移动操作仅修改指针指向,无需重新分配内存和复制数据,成本极低。
示例:移动语义解决传值返回的拷贝问题
class String {
char* data;
public:
String(const char* str) { /* 分配堆内存 */ }
// 移动构造函数(右值引用参数)
String(String&& other) noexcept {
data = other.data; // 直接窃取资源(指针赋值)
other.data = nullptr; // 原对象放弃资源所有权
}
// 移动赋值运算符(同理)
String& operator=(String&& other) noexcept { /* 转移资源 */ }
};
String getString() {
String s("test");
return s; // s是局部对象,返回时被视为右值,触发移动构造(无深拷贝)
}
int main() {
String a = getString(); // 临时右值(被移动后的s)再移动到a(仍无深拷贝)
return 0;
}
效果:两次深拷贝被替换为两次移动操作(仅指针赋值),性能提升显著(尤其对大对象)。
4.3 关键细节:为何能触发移动?
- 函数返回的局部对象是“将亡值”(右值的一种):局部对象在返回后立即销毁,符合右值“临时、即将消亡”的特性,因此编译器会将其转为右值。
- 右值引用是移动语义的“钥匙”:移动构造函数的参数类型为
T&&,只能接收右值,因此编译器会优先调用移动构造而非拷贝构造(当参数为右值时)。 - 与返回值优化(RVO)的关系:若RVO生效,编译器会直接在接收变量的内存地址上构造对象,连移动操作都省略;若RVO不生效(如条件分支返回不同对象),则自动 fallback 到移动语义,确保性能仍优于拷贝。
总结
右值引用通过识别“即将销毁的对象”(右值),结合移动语义实现资源的高效转移,彻底解决了传值返回时的冗余拷贝问题。无论是编译器优化生效与否,都能保证对象传递的低开销,这也是现代C++中大规模使用值语义(而非指针)的重要基础。
5.引用折叠
引用折叠(Reference Collapsing)是C++11为支持右值引用引入的类型推导规则,用于解决“引用的引用”(如T& &、T&& &等)在编译时的类型简化问题。其核心是:当代码中出现“引用的引用”时,编译器会按照特定规则将其折叠为单一引用类型。
5.1 为什么需要引用折叠?
在模板编程或auto类型推导中,可能会推导出“引用的引用”这种看似矛盾的类型。
template <typename T>
void func(T&& param) {} // T的类型需推导
int main() {
int a = 0;
int& ref_a = a;
func(ref_a); // 传入左值引用,T会被推导为int&,此时param的类型为int& &&(引用的引用)
}
直接写int& &&是语法错误,但模板推导可能产生这种中间类型,因此需要引用折叠规则来简化为合法类型。
5.2 引用折叠的核心规则
“右右得右,其余得左”:即只有当两个引用都是右值引用(&&)时,折叠结果才是右值引用(&&);其他任何组合(左值引用与左值引用、左值引用与右值引用)都折叠为左值引用(&)。
具体规则如下:
| 原始类型(引用的引用) | 折叠后的类型 |
|---|---|
T& & |
T& |
T& && |
T& |
T&& & |
T& |
T&& && |
T&& |
5.3 典型应用场景
引用折叠是万能引用(Forwarding Reference) 的实现基础。
万能引用的类型推导(区分左值/右值)
万能引用(形式为T&&,仅在模板参数推导或auto推导时生效)的本质依赖引用折叠。当传入左值时,推导结果折叠为左值引用;传入右值时,折叠为右值引用。
示例:
template <typename T>
void func(T&& param) {
// param的类型由引用折叠决定
}
int main() {
int a = 10; // a是左值
int& ref_a = a; // ref_a是左值引用(左值)
func(a); // 传入左值:T推导为int&,param类型为int& && → 折叠为int&(左值引用)
func(ref_a); // 传入左值引用(左值):同上,param为int&
func(20); // 传入右值:T推导为int,param类型为int&& → 折叠为int&&(右值引用)
func(std::move(a)); // 传入右值:同上,param为int&&
}
5.4 关键注意事项
- 引用折叠仅发生在类型推导场景(模板参数推导、
auto推导),直接声明int& &是语法错误。 - 万能引用(
T&&)并非总是右值引用:只有当T被推导为非引用类型时,才是右值引用;若T被推导为左值引用,会通过折叠成为左值引用。 - 引用折叠是编译器的隐式行为,无需手动干预,但理解其规则是掌握移动语义的前提。
总结
引用折叠是C++处理“引用的引用”的核心规则,通过“右右得右,其余得左”的逻辑,解决了模板推导中类型冲突的问题,为万能引用提供了底层支持,是现代C++泛型编程的重要基础。
更多推荐



所有评论(0)