本文围绕列表初始化、左右值引用、移动语义、移动构造与赋值等核心知识点展开,结合代码实例与底层原理,系统讲解C++11如何解决深拷贝效率问题、统一初始化语法,并优化容器与函数返回值性能。
一、C++11 发展历史
C++11 是 C++98 之后最重要的版本更新,2011 年 8 月 12 日正式采纳,之前曾用名"C++0x"。
版本迭代节奏:C++98(1998) → C++11(2011) → C++14(2014) → C++17(2017) → C++20(2020) → C++23(2023),之后每 3 年更新一次。
核心新增:移动语义、右值引用、列表初始化、std::initializer_list、Lambda 表达式、智能指针等。
二、列表初始化({} 初始化)
2.1 C++98 传统 {} 初始化
仅支持数组、结构体的初始化:
cpp
struct Point { int _x; int _y; };
int array1[] = {1, 2, 3, 4, 5}; // 数组初始化
int array2[5] = {0}; // 数组部分初始化
Point p = {1, 2}; // 结构体初始化
2.2 C++11 统一列表初始化
目标:实现一切对象皆可用 {} 初始化,统一初始化语法。
支持类型:内置类型、自定义类型、容器等。
语法特性:
可省略 =:Point p1{1,2}; / int x2{2};
单参数类型转换:Date d3{2025}; 等价于 Date d3 = 2025;
容器便捷初始化:vector<Date> v = {{2025,1,1}, {2024,7,25}};
编译器优化:自定义类型 {} 初始化本质是"构造临时对象 + 拷贝构造",编译器会优化为直接构造,避免拷贝。
cpp
#include <iostream>
#include <vector>
#include <map>
#include <string>
using namespace std;
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 支持的初始化
// 1. 内置类型支持
int x1 = { 2 }; // 列表初始化
int x2 = 2; // 传统赋值
int x3{ 2 }; // 省略 = 的列表初始化
// 2. 自定义类型支持
// 本质:先构造临时对象 Date(2025,1,1),再拷贝构造 d1
// 编译器优化后:直接在 d1 的内存上构造,**不会调用拷贝构造**
Date d1 = { 2025, 1, 1 };
Date d20(2025, 1, 1); // 直接构造,和上面优化后等价
// const 左值引用绑定临时对象,延长临时对象生命周期
const Date& d2 = { 2024, 7, 25 };
// 单参数构造的类型转换(C++98 就支持,C++11 用 {} 更统一)
Date d3 = { 2025 }; // C++11 列表初始化写法
Date d4 = 2025; // C++98 隐式类型转换写法
string str = "1111"; // 字符串也是单参数构造的类型转换
// 3. 省略 = 的列表初始化(C++11 特性)
Point p1{ 1, 2 };
Date d6{ 2024, 7, 25 };
const Date& d7{ 2024, 7, 25 };
// ❌ 不支持:只有 {} 初始化才能省略 =
// Date d8 2025;
// 4. 容器的列表初始化
vector<Date> v;
v.push_back(d1); // 左值:拷贝构造
v.push_back(Date(2025, 1, 1)); // 匿名对象:移动构造(C++11)
v.push_back({ 2025, 1, 1 }); // 更简洁:直接用 {} 构造临时对象,性价比更高
map<string, string> dict;
dict.insert({ "xxx", "yyyy" }); // pair 的列表初始化隐式转换
// 5. vector 列表初始化的不同写法
vector<int> v1{ 1,2,3,4 }; // 省略 =
vector<int> v2 = { 10,20,30,1,1,1,1,1,1,1,1,1 }; // 带 =
const vector<int>& v4 { 10,20,30,1,1,1,1,1,1,1,1,1 }; // 引用绑定临时对象
vector<int> v3({ 10,20,30,1,1,1,1,1,1,1,1,1 }); // 显式调用 initializer_list 构造
// 6. initializer_list 本质
initializer_list<int> il1 = { 10, 20, 30, 1, 1, 1, 1, 1, 1, 1, 1, 1 };
int aa1[] = { 10, 20, 30, 1, 1, 1, 1, 1, 1, 1, 1, 1 }; // 和数组类似,底层是连续内存
// 7. map 的嵌套列表初始化
map<string, string> dict2 = { { "xxx", "yyyy" }, { "sort", "zzzz" } };
return 0;
}
核心知识点总结
- C++98 vs C++11 初始化差异
C++98:仅支持数组、结构体的 {} 初始化,单参数构造支持隐式类型转换。
C++11:统一列表初始化,内置类型、自定义类型、容器都能用 {} 初始化,且可省略 =。
- 编译器优化(关键)
Date d1 = {2025,1,1}; 理论上是「构造临时对象 + 拷贝构造 d1」,但编译器会优化为直接构造 d1,完全跳过拷贝构造,运行时只会打印一次构造函数。
关闭优化(如 g++ -fno-elide-constructors)才能看到完整的「临时对象构造 + 拷贝构造」流程。
- std::initializer_list 作用
容器(vector/map 等)支持 {} 初始化,本质是实现了接受 initializer_list<T> 的构造函数。
initializer_list<T> 底层是一个只读数组,用两个指针标记首尾,支持范围 for 遍历。
- 引用绑定临时对象
const Date& d2 = {2024,7,25};:const 左值引用可以绑定右值临时对象,并延长临时对象生命周期到引用本身的生命周期。
const vector<int>& v4 { ... };:同理,引用绑定容器临时对象。
- 容器插入的性价比
v.push_back({2025,1,1}); 比 v.push_back(Date(2025,1,1)) 更简洁,本质都是构造临时对象后移动插入,代码更短、可读性更好。
2.3 std::initializer_list
本质:底层是一个数组,用两个指针指向数组首尾,支持迭代器遍历。
容器支持:STL 容器(vector/list/map 等)都实现了接受 initializer_list 的构造函数,从而支持 {x1,x2,x3...} 初始化。
示例:
cpp
vector<int> v1 = {1,2,3,4,5}; // 调用 initializer_list 构造函数
auto il = {10,20,30}; // il 类型为 std::initializer_list<int>
三、左值与右值
3.1 定义与核心区别
|----|----------------------------|------------------|
| 类型 | 定义 | 核心特征 |
| 左值 | 有持久存储、可取地址的表达式(变量名、指针解引用等) | 可出现在赋值号左右,能取地址 |
| 右值 | 字面常量、表达式求值产生的临时对象等,生命周期短暂 | 仅能出现在赋值号右侧,不能取地址 |
左值
左值:有持久存储、可以取地址的表达式,生命周期较长。
示例:p(指针)、b(变量)、*p(解引用)、s[0](数组元素)等。
特征:可以出现在赋值号左边,也能被 & 取地址。
右值
右值:临时产生、不能取地址的表达式,生命周期极短。
示例:字面常量 10、表达式 x+y、函数返回值 fmin(x,y)、临时对象 string("11111")。
特征:只能出现在赋值号右边,无法用 & 取地址。
cpp
// 左值示例
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0] = 'x';
cout << &c << endl; // 合法:左值可取地址
cout << (void*)&s[0] << endl; // 合法:左值可取地址
// 右值示例(均不能取地址)
double x = 1.1, y = 2.2;
10; // 字面常量
x + y; // 表达式结果
fmin(x, y); // 函数返回的临时对象
string("11111"); // 临时对象
// 以下代码均编译报错:右值不能取地址
// cout << &10 << endl;
// cout << &(x+y) << endl;
// cout << &(fmin(x, y)) << endl;
// cout << &string("11111") << endl;
命名来源:lvalue = "locator value"(可寻址),rvalue = "read value"(可读但不可寻址)。
3.2 左值引用与右值引用
左值引用(Type&):给左值取别名,只能绑定左值,const Type& 可绑定右值(延长临时对象生命周期)。只能绑定左值,给左值取别名。非常量左值引用不能绑定右值。
cpp
// 左值引用绑定左值
int& r1 = b; // r1 是 b 的别名
int*& r2 = p; // r2 是指针 p 的别名
int& r3 = *p; // r3 是 *p 的别名
string& r4 = s; // r4 是 string 对象 s 的别名
char& r5 = s[0]; // r5 是 s[0] 的别名
const 左值引用(const Type&):可以绑定右值,并延长临时对象的生命周期。绑定后不能修改对象,但可以安全访问。
cpp
// const 左值引用绑定右值(延长临时对象生命周期)
const int& rx1 = 10;
const double& rx2 = x + y;
const double& rx3 = fmin(x, y);
const string& rx4 = string("11111");
右值引用(Type&&):给右值取别名,C++11 新增,用于实现移动语义。不能直接绑定左值,但可通过 std::move() 将左值转为右值引用。
cpp
// 右值引用绑定右值
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
string&& rr4 = string("11111");
// 右值引用绑定被 move 的左值
int&& rrx1 = move(b);
int*&& rrx2 = move(p);
int&& rrx3 = move(*p);
string&& rrx4 = move(s);
string&& rrx5 = (string&&)s; // 强制类型转换,等价于 move(s)
注意:右值引用变量本身是左值(可取地址),若要继续传递右值属性需再次 move。
关键细节:右值引用变量本身是左值
右值引用变量(如 rr1)是一个有名字的变量,因此它本身是左值(可以取地址)。
如果要将右值引用变量继续作为右值传递,必须再次调用 move()。
cpp
int&& rr1 = 10;
cout << &rr1 << endl; // 合法:rr1 是左值,可取地址
int& r6 = rr1; // 合法:左值引用绑定左值 rr1
// int&& rrx6 = rr1; // 编译错误:rr1 是左值,不能直接绑定右值引用
int&& rrx6 = move(rr1); // 正确:move(rr1) 将 rr1 转为右值
模板函数中的引用匹配
cpp
template<class T>
void func(const T& x)
{}
该模板函数能接受左值、const 左值、右值,因为 const T& 是万能引用的前身,能匹配所有值类别。
这是C++11之前处理"既接受左值又接受右值"的常用方式,C++11之后则用T&&(万能引用)更灵活。
|--------------|-------------|---------------|
| 引用类型 | 可绑定对象 | 核心作用 |
| Type& | 左值 | 给左值取别名,可修改对象 |
| const Type& | 左值/右值 | 只读访问,延长右值生命周期 |
| Type&& | 右值/move(左值) | 实现移动语义,窃取右值资源 |
3.3 引用延长生命周期
const 左值引用 和 右值引用 都能延长临时对象的生命周期,但该对象无法被修改(const 引用)或需通过 move 操作。
cpp
const string& r2 = s1 + s1; // const 左值引用延长临时对象生命周期
string&& r3 = s1 + s1; // 右值引用延长临时对象生命周期
3.4 函数重载与参数匹配
C++11 支持按引用类型重载:
cpp
void f(int& x) { cout << "左值引用重载\n"; }
void f(const int& x) { cout << "const 左值引用重载\n"; }
void f(int&& x) { cout << "右值引用重载\n"; }
匹配规则:左值实参 → 匹配 f(int&) const 左值实参 → 匹配 f(const int&)
右值实参 / std::move(左值) → 匹配 f(int&&)
cpp
#include <iostream>
using namespace std;
// 重载版本1:匹配 非const 左值
void f(int& x)
{
cout << "左值引用重载 f(" << x << ")\n";
}
// 重载版本2:匹配 const 左值 / 无右值重载时的右值
void f(const int& x)
{
cout << "到 const 的左值引用重载 f(" << x << ")\n";
}
// 重载版本3:匹配 右值(包括字面量、move后的左值)
void f(int&& x)
{
cout << "右值引用重载 f(" << x << ")\n";
}
int main()
{
int i = 1;
const int ci = 2;
// 1. 传入左值 i
f(i); // 匹配 f(int&) → 输出:左值引用重载 f(1)
// 2. 传入 const 左值 ci
f(ci); // 匹配 f(const int&) → 输出:到 const 的左值引用重载 f(2)
// 3. 传入右值字面量 3
f(3); // 匹配 f(int&&) → 输出:右值引用重载 f(3)
// 若注释掉 f(int&&),则会降级匹配 f(const int&)
// 4. 传入 move(i) → 右值
f(std::move(i)); // 匹配 f(int&&) → 输出:右值引用重载 f(1)
// 5. 右值引用变量 x 本身是左值
int&& x = 1;
f(x); // x 是变量 → 左值 → 匹配 f(int&) → 输出:左值引用重载 f(1)
f(std::move(x)); // move(x) 转为右值 → 匹配 f(int&&) → 输出:右值引用重载 f(1)
return 0;
}
核心匹配规则总结
|--------------------|-------|-------------------|
| 实参类型 | 匹配优先级 | 最终调用版本 |
| 非const 左值 | 1 | f(int&) |
| const 左值 | 1 | f(const int&) |
| 右值(字面量/move(左值)) | 1 | f(int&&) |
| 右值(无 f(int&&) 时) | 2 | f(const int&) |
| 右值引用变量(如 x) | 1 | f(int&)(变量本身是左值) |
关键知识点
右值引用变量是左值:int&& x = 1; 中 x 是有名字的变量,属于左值,可被 int& 绑定。
std::move() 的作用:将左值转换为右值,让它能匹配右值引用重载。
重载优先级:右值引用重载优先级高于 const 左值引用,所以右值会优先匹配 f(int&&)。
四、移动构造与移动赋值
4.1 核心概念
目的:避免深拷贝类(如 string/vector)在返回值、传参时的昂贵拷贝,窃取右值对象的资源,提升效率。
移动构造函数:string(string&& s),参数为右值引用。
移动赋值运算符:string& operator=(string&& s),参数为右值引用。
4.2 gxy::string 类实现示例
cpp
#include <iostream> // 输入输出流,用于 cout 打印
#include <assert.h> // 断言,用于数组下标越界检查
#include <cstring> // C 风格字符串函数:strlen、strcpy
using namespace std;
// 自定义命名空间,防止命名冲突
namespace gxy
{
// 模拟实现 STL 中的 string 类(C++11 版本,支持移动语义)
class string
{
public:
// 迭代器类型重定义
typedef char* iterator; // 普通迭代器,指向的内容可修改
typedef const char* const_iterator; // const 迭代器,指向内容只读
// ------------------- 迭代器接口 -------------------
// 获取起始迭代器(指向第一个字符)
iterator begin()
{
return _str;
}
// 获取结束迭代器(指向最后一个字符的下一个位置)
iterator end()
{
return _str + _size;
}
// const 对象调用的起始迭代器(只读)
const_iterator begin() const
{
return _str;
}
// const 对象调用的结束迭代器(只读)
const_iterator end() const
{
return _str + _size;
}
// ------------------- 构造函数 -------------------
// 1. 普通构造函数
// 参数:C 风格字符串,默认值为空字符串
string(const char* str = "")
:_size(strlen(str)) // 有效字符长度 = 传入字符串长度
, _capacity(_size) // 初始容量 = 有效长度
{
cout << "string(char* str)-构造" << endl;
_str = new char[_capacity + 1]; // 开辟堆空间,+1 存放 '\0'
strcpy(_str, str); // 拷贝字符串内容
}
// ------------------- 拷贝构造(深拷贝) -------------------
// 2. 拷贝构造:用已存在的对象构造新对象
string(const string& s)
:_str(nullptr) // 先将指针置空,避免野指针
{
cout << "string(const string& s) -- 拷贝构造" << endl;
reserve(s._capacity); // 将当前对象容量扩容到和 s 一致
for (auto ch : s) // 范围 for 遍历 s,逐个字符尾插
{
push_back(ch);
}
}
// ------------------- 资源交换函数 -------------------
// 交换两个 string 对象的所有资源(指针、大小、容量)
void swap(string& ss)
{
::swap(_str, ss._str); // 交换字符数组指针(全局 swap)
::swap(_size, ss._size); // 交换有效长度
::swap(_capacity, ss._capacity); // 交换容量
}
// ------------------- 移动构造(C++11) -------------------
// 3. 移动构造:参数为右值引用,用于临时对象/即将被销毁的对象
string(string&& s)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s); // 直接交换资源,窃取 s 的内存,无数据拷贝,O(1)
}
// ------------------- 拷贝赋值运算符重载 -------------------
// 4. 拷贝赋值:深拷贝,适用于左值赋值
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 拷贝赋值" << endl;
if (this != &s) // 防止自赋值(自己给自己赋值)
{
_str[0] = '\0'; // 清空原有数据
_size = 0; // 有效长度置 0
reserve(s._capacity); // 扩容到目标容量
// 逐个拷贝字符
for (auto ch : s)
{
push_back(ch);
}
}
return *this; // 返回自身,支持连续赋值
}
// ------------------- 移动赋值运算符重载(C++11) -------------------
// 5. 移动赋值:参数为右值引用,直接交换资源
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s); // 直接窃取资源,效率极高
return *this;
}
// ------------------- 析构函数 -------------------
~string()
{
// 释放堆上的字符数组,防止内存泄漏
delete[] _str;
_str = nullptr; // 指针置空,避免野指针
}
// ------------------- 下标访问运算符重载 -------------------
// 支持 s[pos] 读写操作
char& operator[](size_t pos)
{
assert(pos < _size); // 断言检查:下标不能越界
return _str[pos]; // 返回对应位置字符的引用
}
// ------------------- 容量操作 -------------------
// 扩容函数:将容量调整到 n(只扩不缩)
void reserve(size_t n)
{
if (n > _capacity) // 只有新容量更大时才扩容
{
char* tmp = new char[n + 1]; // 申请新空间
if (_str) // 如果原指针不为空,拷贝旧数据
{
strcpy(tmp, _str);
delete[] _str; // 释放旧空间
}
_str = tmp; // 指向新空间
_capacity = n; // 更新容量
}
}
// ------------------- 数据修改接口 -------------------
// 尾插一个字符
void push_back(char ch)
{
// 容量不足时扩容:初始为4,满了则2倍扩容
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch; // 放入字符
++_size; // 有效长度+1
_str[_size] = '\0'; // 末尾补结束符,保证 C 字符串规范
}
// += 运算符重载:追加单个字符
string& operator+=(char ch)
{
push_back(ch); // 复用尾插接口
return *this; // 支持链式调用
}
// ------------------- 访问接口 -------------------
// 返回 C 风格字符串指针(const 保证不修改数据)
const char* c_str() const
{
return _str;
}
// 返回有效字符个数(不含 '\0')
size_t size() const
{
return _size;
}
private:
// 底层成员变量(模拟标准 string 实现)
char* _str = nullptr; // 指向堆区字符数组的指针
size_t _size = 0; // 有效字符数量(实际存储的字符个数)
size_t _capacity = 0; // 容量:当前可存储的最大有效字符数
};
}
// 测试主函数
int main()
{
// 用 const char* 构造对象 ------ 调用普通构造
gxy::string s1("hello");
// 用左值 s1 初始化 s2 ------ 调用拷贝构造
gxy::string s2 = s1;
// 用匿名临时对象(右值)初始化 s3 ------ 调用移动构造
// 先构造临时对象 → 临时对象是右值 → 触发移动构造,直接交换资源,不拷贝数据
gxy::string s3 = gxy::string("world");
// move(s1) 将 s1 转为右值 ------ 调用移动赋值
// std::move(s1) 把左值强转为右值引用 → 触发移动赋值,s2 窃取 s1 资源。
s2 = move(s1);
return 0;
}
输出结果
cpp
string(char* str)-构造 // s1("hello")
string(const string& s) -- 拷贝构造 // s2 = s1
string(char* str)-构造 // string("world") 临时对象构造
string(string&& s) -- 移动构造 // s3 接收临时对象
string& operator=(string&& s) -- 移动赋值 // s2 = move(s1)
核心知识点完整整理
一、核心成员变量
_str:指向堆区字符数组,存储实际字符串 _size:有效字符个数(不含 \0)
_capacity:已分配容量,满了需要扩容
二、构造与析构
-
普通构造:接收 const char*,初始化 _size、_capacity,开辟堆空间,使用 strcpy 拷贝字符串
-
拷贝构造(深拷贝)
参数:const string& s(左值引用) 行为:重新开辟内存,逐个字符拷贝,两个对象独立,互不影响
- 移动构造(C++11 重点)
参数:string&& s(右值引用) 行为:不新开内存,直接 swap 交换资源
意义:对临时对象/即将销毁对象,把资源"偷"过来,O(1) 复杂度
- 析构函数:释放 _str 指向的堆空间,避免内存泄漏
三、赋值运算符重载
-
拷贝赋值:深拷贝,清空原有内容,重新拷贝数据;自赋值判断 this != &s 防止错误
-
移动赋值:参数:string&& s;直接交换资源,效率极高,用于右值赋值场景
四、核心工具接口
-
reserve(n):扩容到 n,不改变有效数据,新空间 > 原有容量才执行
-
push_back(ch):尾插字符,满容则 2 倍扩容,维护 _size 与末尾 \0
-
operator[]:下标访问,支持读写,断言检查越界
-
迭代器 begin/end:普通/const 两个版本,支持范围for:for(auto ch : s)
五、移动语义核心价值(对比拷贝)
拷贝构造/赋值:O(n),开辟新空间 + 复制数据
移动构造/赋值:O(1),仅交换指针,无数据拷贝
适用场景:函数返回值、临时对象、std::move 后的左值
4.3 移动语义的价值
场景:函数返回局部对象、容器插入临时对象时,原本需要深拷贝,现在通过移动构造/赋值窃取资源,时间复杂度从 O(n) 降为 O(1)。
示例:vector 插入右值对象时调用移动构造,插入左值对象时调用拷贝构造。
五、右值引用解决传值返回问题
5.1 传统传值返回的问题
函数返回局部对象时,会经历:局部对象 → 临时对象 → 目标对象,两次拷贝构造(深拷贝类代价极高)
编译器优化(RVO/NRVO):会将多次拷贝合并为直接构造,但关闭优化(如 g++ -fno-elide-constructors)后可观察到完整拷贝/移动流程。
5.2 移动语义下的传值返回
若类实现了移动构造/赋值,返回局部对象时:
未优化:局部对象 → 临时对象(移动构造) → 目标对象(移动构造)
优化后:直接构造目标对象,无任何拷贝/移动。
示例:addStrings 函数返回 string 对象:
cpp
#include <algorithm> // 用于 reverse 反转函数
using namespace std;
// 大数加法解决方案类
class Solution {
public:
/**
* 字符串相加:实现两个大数的加法(数字以字符串形式传入,避免溢出)
* @param num1 数字字符串1(传值,会调用拷贝/移动构造)
* @param num2 数字字符串2(传值,会调用拷贝/移动构造)
* @return 相加后的结果字符串
*/
gxy::string addStrings(gxy::string num1, gxy::string num2) {
// 存储结果的字符串(最终为逆序,需反转)
gxy::string str;
// 从两个字符串末尾开始遍历(个位)
int end1 = num1.size() - 1, end2 = num2.size() - 1;
// 进位标记:记录上一位相加产生的进位,初始为0
int next = 0;
// 循环条件:任意一个字符串未遍历完 或 还有进位
while (end1 >= 0 || end2 >= 0)
{
// 取当前位数字,指针前移,无数字则取0
int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
// 当前位总和 = 数字1 + 数字2 + 进位
int ret = val1 + val2 + next;
// 更新进位:除以10取整
next = ret / 10;
// 当前位结果:取余10
ret = ret % 10;
// 将当前位字符追加到结果串
str += ('0' + ret);
}
// 循环结束后仍有进位,额外追加字符'1'
if (next == 1)
str += '1';
// 反转字符串:因为我们是从低位加到高位,存储顺序是逆序的
reverse(str.begin(), str.end());
// 传值返回:返回局部对象str
// C++11下优先调用移动构造,避免深拷贝
return str;
}
};
// ==================== 测试用例 ====================
// 测试1:接收返回值并赋值调用
int main()
{
// 定义接收结果的对象
gxy::string ret;
// 调用大数加法,返回值通过移动赋值给ret
ret = Solution().addStrings("111111111111111","222222222222222222222");
// 打印C风格字符串结果
cout << ret.c_str() << endl;
return 0;
}
// 测试2:移动语义与拷贝构造对比
int main()
{
// 构造字符串对象
gxy::string s1("11111111111111111");
// 拷贝构造:用左值s1初始化s3
gxy::string s3 = s1;
// 移动构造:用匿名临时对象(右值)初始化s4
gxy::string s4 = gxy::string("222222222");
// 移动构造:move将s1转为右值,转移资源
gxy::string s5 = move(s1);
return 0;
}
// 测试3:引用绑定临时对象(延长生命周期)
int main()
{
// const左值引用绑定临时对象,延长生命周期
const gxy::string& lr = gxy::string("111111");
// 右值引用绑定临时对象,延长生命周期
gxy::string&& rr = gxy::string("111111");
cout << "xxxxxxxxxxxxxxxxxxxxx" << endl;
return 0;
}
调用:string ret = addStrings("11111", "2222");
优化前:str → 临时对象(移动构造)→ ret(移动构造)
优化后:直接在 ret 内存上构造 str,无移动/拷贝。
核心注释说明
- 函数设计要点
传值传参:num1 / num2 为值传递,会触发拷贝/移动构造,函数内修改不影响外部实参
传值返回:返回局部对象 str,C++11 自动识别为右值,优先调用移动构造,性能远优于深拷贝
大数思路:模拟竖式加法,从低位加到高位,处理进位,最后反转得到正确顺序
- 关键逻辑
end1 / end2:双指针从字符串尾部向前遍历 next:进位变量,控制每一位的计算
reverse:修正逆序存储的结果 字符转数字:字符 - '0';数字转字符:数字 + '0'
- 测试用例用途
main1:演示函数返回值赋值调用,观察移动赋值
main2:对比拷贝构造、匿名对象移动构造、move 移动构造
main3:演示 const 左值引用 和 右值引用 绑定临时对象,延长生命周期
六、容器的右值引用接口与性能提升
6.1 STL 容器的接口更新
C++11 后,STL 容器的 push_back/insert 等接口新增右值引用版本:
cpp
void push_back(const T& val); // 左值版本:拷贝构造
void push_back(T&& val); // 右值版本:移动构造
实参是右值时,容器内部调用移动构造,窃取资源;实参是左值时,调用拷贝构造,深拷贝数据
6.2 自定义容器的移动支持
示例:gxy::list 容器的 push_back 重载:
cpp
namespace gxy {
// 双向链表的节点结构体
template<class T>
struct ListNode {
ListNode<T>* _next; // 指向后一个节点
ListNode<T>* _prev; // 指向前一个节点
T _data; // 存储的数据
// 左值版本构造函数:拷贝传入数据
ListNode(const T& data = T()) : _next(nullptr), _prev(nullptr), _data(data) {}
// 右值版本构造函数:移动传入数据(避免深拷贝,提高效率)
ListNode(T&& data) : _next(nullptr), _prev(nullptr), _data(move(data)) {}
};
// 链表迭代器模板:Ref是数据引用类型,Ptr是数据指针类型
template<class T, class Ref, class Ptr>
struct ListIterator {
typedef ListNode<T> Node;
typedef ListIterator<T, Ref, Ptr> Self; // 迭代器自身类型
Node* _node; // 指向当前节点的指针
// 用节点指针构造迭代器
ListIterator(Node* node) : _node(node) {}
// 前置++:移动到下一个节点,返回自身引用
Self& operator++() {
_node = _node->_next;
return *this;
}
// 解引用:返回节点数据的引用(Ref类型)
Ref operator*() { return _node->_data; }
// 不等比较:比较节点指针是否不同
bool operator!=(const Self& it) { return _node != it._node; }
};
// 双向链表类
template<class T>
class list {
typedef ListNode<T> Node;
public:
// 普通迭代器:Ref=T&,Ptr=T*
typedef ListIterator<T, T&, T*> iterator;
// const迭代器:Ref=const T&,Ptr=const T*
typedef ListIterator<T, const T&, const T*> const_iterator;
// 返回首节点迭代器(_head的下一个)
iterator begin() { return iterator(_head->_next); }
// 返回尾后迭代器(指向_head本身)
iterator end() { return iterator(_head); }
// 初始化空链表:创建哨兵节点,形成循环链表
void empty_init() {
_head = new Node(); // 哨兵节点,不存有效数据
_head->_next = _head; // 循环指向自己
_head->_prev = _head;
}
// 默认构造:初始化空链表
list() { empty_init(); }
// 尾插:左值版本(拷贝构造节点数据)
void push_back(const T& x) { insert(end(), x); }
// 尾插:右值版本(移动构造节点数据,避免深拷贝)
void push_back(T&& x) { insert(end(), move(x)); }
// 在pos位置前插入节点:左值版本
iterator insert(iterator pos, const T& x) {
Node* cur = pos._node; // 当前位置节点
Node* newnode = new Node(x); // 新节点(拷贝x)
Node* prev = cur->_prev; // 当前节点的前驱
// 双向链表插入逻辑:将新节点链接到prev和cur之间
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode); // 返回新节点迭代器
}
// 在pos位置前插入节点:右值版本(移动语义)
iterator insert(iterator pos, T&& x) {
Node* cur = pos._node; // 当前位置节点
Node* newnode = new Node(move(x)); // 新节点(移动x)
Node* prev = cur->_prev; // 当前节点的前驱
// 双向链表插入逻辑:将新节点链接到prev和cur之间
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode); // 返回新节点迭代器
}
private:
Node* _head; // 哨兵节点指针,链表头尾都依赖它实现循环
};
}
// 测试代码
int main() {
gxy::list<gxy::string> lt; // 实例化存储gxy::string的链表
gxy::string s1("11111111111111111111"); // 构造长字符串
lt.push_back(s1); // 传入左值s1 → 调用push_back(const T&) → 节点数据拷贝构造
lt.push_back(gxy::string("22222"));// 传入临时对象(右值)→ 调用push_back(T&&) → 节点数据移动构造
lt.push_back("33333"); // 字符串字面量隐式转为gxy::string右值 → 移动构造
lt.push_back(move(s1)); // move(s1)将左值转为右值 → 调用push_back(T&&) → 节点数据移动构造(s1变为有效但未定义状态)
return 0;
}
核心知识点总结
-
移动语义优化:ListNode(T&& data) 和 insert(T&& x) 利用右值引用和 std::move,将临时对象或 move 后的对象资源转移到新节点,避免了 T(如 gxy::string)的深拷贝,大幅提升插入效率。左值版本仍保留拷贝语义,保证代码兼容性。
-
双向链表结构:使用哨兵节点 _head 实现循环链表,简化边界处理(begin() 是 _head->_next,end() 是 _head)。insert 函数是核心插入逻辑,尾插 push_back 本质是在 end() 前插入。
-
迭代器设计:通过模板参数 Ref/Ptr 实现普通迭代器和 const 迭代器的复用,解引用时返回对应引用类型。迭代器本质是对节点指针的封装,++ 操作直接跳转节点。
-
测试用例意图:覆盖了左值、临时右值、字面量右值、move 后的右值四种插入场景,验证移动构造和拷贝构造的调用逻辑,体现 C++11 移动语义在容器中的实际价值。
6.3 杨辉三角示例(传值返回与拷贝代价)
版本1
cpp
class Solution {
public:
// 传值返回,vector 深拷贝代价大,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 后自动调用移动构造,避免深拷贝
}
};
问题:vector<vector<int>> 是深拷贝类,传值返回时会拷贝整个二维数组,C++11 之前只能用输出型参数解决;优点:写法直观,符合直觉;缺点:数据量大时拷贝代价高(旧C++会深拷贝,C++11后支持移动返回优化)
C++11 解决:vector 实现了移动构造,返回时会自动调用移动构造,将 vv 的资源窃取到返回值,避免拷贝。
详细拆解
- 为什么会触发移动构造?
vv 是函数内部的局部变量,return vv; 意味着它即将被销毁。
编译器会识别到这种"即将死亡"的局部对象,将其视为右值。
对于 vector 这类管理堆内存的容器,C++11 提供了移动构造函数:
cpp
vector(vector&& other) noexcept;
它会直接"偷"走 other(也就是这里的 vv)的内部资源(数组指针、容量、大小),而不是重新分配内存并拷贝所有元素。
- 移动构造 vs 深拷贝
|--------------|----------------|-----------------|
| 操作 | 时间复杂度 | 开销 |
| 深拷贝(C++98) | O(N),N 为所有元素总数 | 极大,数据量大时非常慢 |
| 移动构造(C++11+) | O(1) | 极小,仅复制几个内部指针和变量 |
- 更进一步:编译器优化(NRVO)
在很多现代编译器(GCC、Clang、MSVC)开启优化后,甚至连移动构造都不会调用,直接执行 NRVO(命名返回值优化):
编译器会直接在函数外部的返回值内存地址上构造 vv,完全跳过了"局部对象创建 → 移动/拷贝 → 局部对象销毁"的整个过程。这是比移动构造更极致的优化,效率和用引用传参的版本几乎一样。
版本2
cpp
// 输出型参数版本(通过引用接收vector,无拷贝,效率更高)
void generate(int numRows, vector<vector<int>>& vv) {
// 重置传入的vector大小,直接操作参数,不创建局部变量遮蔽
vv.resize(numRows);
// 每一行初始化为全1
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];
}
}
}
};
直接在实参对象上修改,零拷贝,效率高;没有返回值,结果通过参数带出
左值引用 vector<vector<int>>& vv:引用就是变量别名,不产生新对象; 函数内修改 = 函数外实参修改
cpp
// 主函数:测试两个generate函数(有效入口,其余main注释)
int main()
{
// 调用传值返回版本,接收返回结果
vector<vector<int>> ret1 = Solution().generate(100);
// 调用输出型参数版本
vector<vector<int>> ret2;
Solution().generate(100, ret2);
return 0;
}
代码解释
cpp
vector<vector<int>> ret1 = Solution().generate(100);
generate 内部的局部变量 vv,在 return vv 时:
• C++11 以前:会用拷贝构造,把整个二维 vector 完整复制一份,开销很大
• C++11 以后:vv 是即将销毁的局部对象,编译器把它当成右值,自动调用移动构造;,编译器自动隐式当做 std::move(vv) 处理,把 vv 从左值转为右值,从而匹配移动构造/移动赋值,而不是拷贝。
只转移内部指针、大小、容量,O(1) 开销,不拷贝数据; 等价于:return std::move(vv);(编译器自动帮你做了)
移动构造只认右值:vector<vector<int>> ret1 = 函数返回的右值;
等号右边是右值 → 调用移动构造;等号右边是左值 → 调用拷贝构造
和下面这句对比
cpp
vector<vector<int>> ret2;
Solution().generate(100, ret2);
用的是左值引用,直接在 ret2 上修改;全程没有构造、拷贝、移动,和移动构造完全无关
总结
ret1 接收返回值:用到了移动构造,是 C++11 对"返回大对象"的核心优化
ret2 引用传参:没有任何资源转移,效率最高,和移动构造无关
-
列表初始化:统一了 C++ 初始化语法,std::initializer_list 是容器支持 {} 初始化的核心。
-
右值引用:是实现移动语义的基础,核心是区分"可持久的左值"和"临时的右值"。
-
移动语义:通过窃取右值对象资源,避免深拷贝,大幅提升 string/vector 等类在传参、返回值场景下的性能。
-
编译器优化:RVO/NRVO 会进一步消除移动/拷贝,直接构造目标对象,是性能优化的重要手段。
-
容器升级:STL 容器新增右值引用接口,配合移动语义,实现了高效的临时对象插入。
6.4 类型分类
C++11 对表达式的值类别进行了更细致的划分,核心是将右值拆分为:
• 纯右值 (prvalue, pure rvalue):字面常量、求值结果为字面量/不具名临时对象的表达式。
例子:42、true、nullptr、str.substr(1,2)、str1+str2、a++、a+b 等。
C++11 中纯右值等价于 C++98 的右值。
• 将亡值 (xvalue, expiring value):返回右值引用的函数/转换函数的调用表达式。
例子:std::move(x)、static_cast<X&&>(x)。
• 泛左值 (glvalue, generalized lvalue):包含左值 (lvalue) 和将亡值 (xvalue)。
表达式值类别树形结构
cpp
expression
├── glvalue (泛左值)
│ ├── lvalue (左值)
│ └── xvalue (将亡值)
└── rvalue (右值)
├── xvalue (将亡值)
└── prvalue (纯右值)
6.5 引用折叠
核心规则
C++ 不能直接定义"引用的引用"(如 int& && r = i; 会报错),但通过模板/typedef 可间接构成,此时遵循引用折叠规则:
• 右值引用的右值引用 → 折叠为右值引用 (&& && → &&)
• 其他所有组合 → 折叠为左值引用 (& &, & &&, && & → &)
代码示例(typedef 演示)
cpp
int main()
{
typedef int& lref; // lref = int&
typedef int&& rref; // rref = int&&
int n = 0;
lref& r1 = n; // int& & → int&
lref&& r2 = n; // int& && → int&
rref& r3 = n; // int&& & → int&
rref&& r4 = 1; // int&& && → int&&
}
一句话记忆:只要出现一个 &,结果就是左值引用;只有两个 && 相遇,才会折叠成右值引用。
模板函数示例
cpp
// 无论 T 是什么,f1 实例化后总是左值引用
template<class T>
void f1(T& x) {}
// 万能引用:可实例化为左值/右值引用,取决于实参
template<class T>
void f2(T&& x) {}
调用与实例化结果
cpp
// 模板函数 f1:参数为左值引用 T&
// 无论 T 被实例化为哪种类型,经过引用折叠后,参数最终一定是左值引用
template<class T>
void f1(T& x)
{}
// 模板函数 f2:参数为万能引用 T&&
// 根据实参类型,经过引用折叠后,参数可以是左值引用 或 右值引用
template<class T>
void f2(T&& x)
{}
int main()
{
typedef int& lref; // lref 代表 int&(左值引用)
typedef int&& rref; // rref 代表 int&&(右值引用)
int n = 0; // n 是一个左值
// -------------------------- 引用折叠演示 --------------------------
lref& r1 = n; // 展开为 int& & → 折叠为 int&(左值引用)
lref&& r2 = n; // 展开为 int& && → 折叠为 int&(左值引用)
rref& r3 = n; // 展开为 int&& & → 折叠为 int&(左值引用)
rref&& r4 = 1; // 展开为 int&& && → 折叠为 int&&(右值引用)
// -------------------------- f1 模板调用与实例化 --------------------------
// 1. T = int:无折叠,实例化为 void f1(int& x)
f1<int>(n); // 合法:左值 n 绑定到 int&
//f1<int>(0); // 报错:右值 0 无法绑定到非 const 左值引用 int&
// 2. T = int&:折叠为 int& & → int&,实例化为 void f1(int& x)
f1<int&>(n); // 合法
//f1<int&>(0); // 报错:右值无法绑定到 int&
// 3. T = int&&:折叠为 int&& & → int&,实例化为 void f1(int& x)
f1<int&&>(n); // 合法
//f1<int&&>(0); // 报错:右值无法绑定到 int&
// 4. T = const int&:折叠为 const int& & → const int&,实例化为 void f1(const int& x)
f1<const int&>(n); // 合法:左值 n 绑定到 const int&
f1<const int&>(0); // 合法:const 左值引用可以绑定右值 0
// 5. T = const int&&:折叠为 const int&& & → const int&,实例化为 void f1(const int& x)
f1<const int&&>(n); // 合法
f1<const int&&>(0); // 合法:const 左值引用可以绑定右值 0
// -------------------------- f2 模板调用与实例化 --------------------------
// 1. T = int:无折叠,实例化为 void f2(int&& x)(右值引用)
//f2<int>(n); // 报错:左值 n 无法绑定到右值引用 int&&
f2<int>(0); // 合法:右值 0 绑定到 int&&
// 2. T = int&:折叠为 int& && → int&,实例化为 void f2(int& x)(左值引用)
f2<int&>(n); // 合法:左值 n 绑定到 int&
//f2<int&>(0); // 报错:右值 0 无法绑定到非 const 左值引用 int&
// 3. T = int&&:折叠为 int&& && → int&&,实例化为 void f2(int&& x)(右值引用)
//f2<int&&>(n); // 报错:左值 n 无法绑定到右值引用 int&&
f2<int&&>(0); // 合法:右值 0 绑定到 int&&
return 0;
}
核心规律总结
- f1(T& x):
无论 T 是 int、int& 还是 int&&,最终参数类型都折叠为 int&(或 const int&)。
只有当参数是 const 左值引用时,才能绑定右值。
f1(T& x) 的参数是 T&(左值引用),不管 T 是什么,最终都会被折叠成左值引用:
|---------------|------------------|-------------|------------------------|
| 模版实参 | 展开后类型 | 折叠结果 | 最终函数签名 |
| int | int& | int& | void f1(int& x) |
| int& | int& & | int& | void f1(int& x) |
| int&& | int&& & | int& | void f1(int& x) |
| const int& | const int& & | const int& | void f1(const int& x) |
| const int&& | const int&& & | const int& | void f1(const int& x) |
结论:
只要是 f1(T& x),最终参数一定是左值引用。
非 const 的左值引用(int&)不能绑定右值(比如 0、临时对象),所以 f1<int>(0) 会报错。
只有 const 左值引用(const int&)可以绑定右值,所以 f1<const int&>(0) 是合法的。
- f2(T&& x)(万能引用):
传入左值 → T 推导为 U& → 折叠为 U&(左值引用)。
传入右值 → T 推导为 U → 折叠为 U&&(右值引用)。 这是实现完美转发的基础。
f2(T&& x) 的参数是 T&&,这就是 C++ 里的万能引用,它的最终类型完全由传入的实参是左值还是右值决定:
1) 传入左值(比如变量 n)
编译器推导 T = int& 展开:int& && 引用折叠:& && → & 最终参数类型:int&(左值引用)
函数签名:void f2(int& x)
2) 传入右值(比如字面量 0、std::move(n))
编译器推导 T = int 展开:int&& 引用折叠:&& && → && 最终参数类型:int&&(右值引用)
函数签名:void f2(int&& x)
结论:万能引用 T&& 会自动适配实参的类型:实参是左值 → 变成左值引用;实参是右值 → 变成右值引用。这让一个模板函数就能同时处理左值和右值,而不用写两个重载版本。
Function(T&& t) 模板推导示例
cpp
// 万能引用模板函数
// T&& 是万能引用,可接收左值/右值,T 的类型由传入实参决定
template<class T>
void Function(T&& t)
{
// 定义局部变量 a
int a = 0;
// 关键点:T 是推导出来的类型
// 1. 传入左值 → T = int& / const int& → T x = a 等价于 int& x = a(引用)
// 2. 传入右值 → T = int / const int → T x = a 等价于 int x = a(普通变量)
T x = a;
// x++ 被注释原因:
// 若 T 是 const int / const int&,x 带 const 修饰,自增会编译报错
//x++;
// 打印 a 的地址
cout << &a << endl;
// 打印 x 的地址
// 左值场景:x 是引用 → &x == &a
// 右值场景:x 是新变量 → &x != &a
cout << &x << endl << endl;
}
int main()
{
// 10 是纯右值
// 推导:T = int
// 实例化:void Function(int&& t)
Function(10);
int a;
// a 是左值
// 推导:T = int&
// 引用折叠:int& && → int&
// 实例化:void Function(int& t)
Function(a);
// std::move(a) 将左值转为右值引用,属于右值(将亡值)
// 推导:T = int
// 实例化:void Function(int&& t)
Function(std::move(a));
const int b = 8;
// b 是 const 左值
// 推导:T = const int&
// 引用折叠:const int& && → const int&
// 实例化:void Function(const int& t)
// 内部 T x = a → const int& x = a
// x++ 报错:const 对象不可修改
Function(b);
// std::move(b) 是 const 右值(将亡值)
// 推导:T = const int
// 实例化:void Function(const int&& t)
// 内部 T x = a → const int x = a
// x++ 报错:const 对象不可修改
Function(std::move(b));
return 0;
}
核心规律总结
- 万能引用 T&& 推导规则
实参是左值 → T = 类型& 实参是右值 → T = 类型
- T x = a 的两种行为
T = int& → int& x = a(引用,同地址)
T = int → int x = a(拷贝,不同地址)
T = const int& → const int& x = a(只读引用)
T = const int → const int x = a(只读拷贝)
-
x++ 报错的唯一原因:推导后 x 带有 const 限定,不能修改。
-
地址观察: 传左值:&a == &x 传右值:&a != &x
6.6 完美转发
问题背景:在 Function(T&& t) 中,变量 t 本身是左值(具名变量),若直接传递给下层函数 Fun(t),会始终匹配左值版本,丢失了原实参的右值属性。
完美转发 就是要保留实参原本的左值/右值属性,将其精确传递给下层函数。
std::forward 原理
std::forward 是模板函数,通过引用折叠实现类型精确转发:
cpp
template <class T>
T&& forward(typename remove_reference<T>::type& arg) noexcept;
template <class T>
T&& forward(typename remove_reference<T>::type&& arg) noexcept;
• 若实参是左值 → T 推导为 U& → forward 返回 U&(左值引用)
• 若实参是右值 → T 推导为 U → forward 返回 U&&(右值引用)
代码示例
cpp
// 四个重载函数,分别匹配四种参数类型
// 匹配:普通左值引用(非const左值)
void Fun(int& x) { cout << "左值引用" << endl; }
// 匹配:const左值引用(const左值 + 右值都可以绑定)
void Fun(const int& x) { cout << "const 左值引用" << endl; }
// 匹配:普通右值引用(非const右值)
void Fun(int&& x) { cout << "右值引用" << endl; }
// 匹配:const右值引用(const右值)
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// 万能引用模板函数
// T&&:既能接收左值,也能接收右值
template <class T>
void Function(T&& t)
{
// std::forward<T>(t):完美转发
// 保持实参原本的值类型(左值/右值、const属性),转发给Fun
// 如果直接写 Fun(t),t是具名变量,永远是左值,只会匹配左值版本
Fun(forward<T>(t));
}
int main()
{
// 10 是纯右值
// 推导 T = int
// forward 转发为 int&&
Function(10); // 输出:右值引用
int a;
// a 是普通左值
// 推导 T = int&
// 引用折叠后为 int&,forward 转发为 int&
Function(a); // 输出:左值引用
// std::move(a) 将左值强转为右值(将亡值)
// 推导 T = int
// forward 转发为 int&&
Function(std::move(a)); // 输出:右值引用
const int b = 8;
// b 是 const 左值
// 推导 T = const int&
// forward 转发为 const int&
Function(b); // 输出:const 左值引用
// std::move(b) 是 const 右值(将亡值)
// 推导 T = const int
// forward 转发为 const int&&
Function(std::move(b)); // 输出:const 右值引用
return 0;
}
输出结果
cpp
右值引用
左值引用
右值引用
const 左值引用
const 右值引用
Function(T&& t) 是万能引用,能接住所有左值/右值、const/非const
forward<T>(t) 是完美转发,把参数原本的类型属性原封不动传给下一层函数
如果不用 forward,直接 Fun(t),所有调用都会匹配左值引用版本,丢失右值属性
List.h 代码示例
cpp
#pragma once
#include <assert.h>
#include <initializer_list>
#include <utility> // std::forward / std::move
namespace gxy
{
// 双向链表节点结构体
template<class T>
struct list_node
{
T _data; // 节点存储的数据
list_node<T>* _next; // 指向后一个节点
list_node<T>* _prev; // 指向前一个节点
// 默认构造:编译器自动生成,用于哨兵节点
list_node() = default;
// 模板构造函数:万能引用 + 完美转发
// 接收任意类型 X(左值/右值),将 data 完美转发给 T 的构造函数
template<class X>
list_node(X&& data)
: _data(std::forward<X>(data)) // 保留左值/右值属性,触发拷贝/移动构造
, _next(nullptr)
, _prev(nullptr)
{}
};
// 链表迭代器模板:Ref 是数据引用类型,Ptr 是数据指针类型
template<class T, class Ref, class Ptr>
struct list_iterator
{
typedef list_node<T> Node;
typedef list_iterator<T, Ref, Ptr> Self; // 迭代器自身类型
Node* _node; // 指向当前节点的指针
// 用节点指针构造迭代器
list_iterator(Node* node) : _node(node) {}
// 解引用:返回节点数据的引用(Ref 类型)
Ref operator*() { return _node->_data; }
// 箭头运算符:返回数据指针,用于访问结构体成员(如 it->_a1)
Ptr operator->() { return &_node->_data; }
// 前置++:移动到下一个节点,返回自身引用
Self& operator++()
{
_node = _node->_next;
return *this;
}
// 前置--:移动到前一个节点,返回自身引用
Self& operator--()
{
_node = _node->_prev;
return *this;
}
// 后置++:返回旧迭代器,自身后移
Self operator++(int)
{
Self tmp(*this);
_node = _node->_next;
return tmp;
}
// 后置--:返回旧迭代器,自身前移
Self operator--(int)
{
Self tmp(*this);
_node = _node->_prev;
return tmp;
}
// 不等比较:比较节点指针
bool operator!=(const Self& s) const { return _node != s._node; }
// 相等比较:比较节点指针
bool operator==(const Self& s) const { return _node == s._node; }
};
// 双向链表类
template<class T>
class list
{
typedef list_node<T> Node;
public:
// 普通迭代器:Ref=T&, Ptr=T*
typedef list_iterator<T, T&, T*> iterator;
// const 迭代器:Ref=const T&, Ptr=const T*
typedef list_iterator<T, const T&, const T*> const_iterator;
// 返回首节点迭代器(哨兵节点的下一个)
iterator begin() { return _head->_next; }
// 返回尾后迭代器(指向哨兵节点本身)
iterator end() { return _head; }
// const 版本:返回首节点迭代器
const_iterator begin() const { return _head->_next; }
// const 版本:返回尾后迭代器
const_iterator end() const { return _head; }
// 初始化空链表:创建哨兵节点,形成循环链表
void empty_init()
{
_head = new Node; // 哨兵节点(无有效数据)
_head->_next = _head; // 循环指向自己
_head->_prev = _head;
_size = 0; // 元素个数初始为 0
}
// 默认构造:初始化空链表
list() { empty_init(); }
// 初始化列表构造:用 initializer_list 初始化链表
list(std::initializer_list<T> il)
{
empty_init();
for (auto& e : il) { push_back(e); }
}
// 拷贝构造:深拷贝另一个链表
list(const list<T>& lt)
{
empty_init();
for (auto& e : lt) { push_back(e); }
}
// 赋值运算符:拷贝交换法(现代写法)
list<T>& operator=(list<T> lt)
{
swap(lt); // 交换 *this 和临时对象 lt,lt 销毁时释放旧资源
return *this;
}
// 析构函数:清空所有节点,释放哨兵节点
~list()
{
clear(); // 删除所有有效节点
delete _head; // 释放哨兵节点
_head = nullptr;
}
// 清空链表:删除所有有效节点,保留哨兵节点
void clear()
{
auto it = begin();
while (it != end()) { it = erase(it); }
}
// 交换两个链表的资源(O(1))
void swap(list<T>& lt)
{
std::swap(_head, lt._head); // 交换哨兵节点指针
std::swap(_size, lt._size); // 交换元素个数
}
// -------------------------- 万能引用 + 完美转发 --------------------------
// 尾插:万能引用版本,接收任意类型 X(左值/右值)
template<class X>
void push_back(X&& x)
{
insert(end(), std::forward<X>(x)); // 完美转发给 insert
}
// 头插:左值版本(拷贝构造)
void push_front(const T& x) { insert(begin(), x); }
// 插入:万能引用版本,在 pos 前插入节点
template<class X>
iterator insert(iterator pos, X&& x)
{
Node* cur = pos._node; // 当前位置节点
Node* prev = cur->_prev; // 当前节点的前驱
// 新节点:完美转发 x,触发 T 的拷贝/移动构造
Node* newnode = new Node(std::forward<X>(x));
// 双向链表插入逻辑:将新节点链接到 prev 和 cur 之间
newnode->_next = cur;
cur->_prev = newnode;
newnode->_prev = prev;
prev->_next = newnode;
++_size; // 元素个数 +1
return newnode; // 返回新节点迭代器
}
// -----------------------------------------------------------------------
// 尾删:删除最后一个节点
void pop_back() { erase(--end()); }
// 头删:删除第一个节点
void pop_front() { erase(begin()); }
// 删除 pos 位置的节点,返回下一个节点的迭代器
iterator erase(iterator pos)
{
assert(pos != end()); // 不能删除尾后迭代器
Node* prev = pos._node->_prev; // 被删节点的前驱
Node* next = pos._node->_next; // 被删节点的后继
// 双向链表删除逻辑:跳过被删节点
prev->_next = next;
next->_prev = prev;
delete pos._node; // 释放被删节点
--_size; // 元素个数 -1
return next; // 返回后继节点迭代器
}
// 返回元素个数
size_t size() const { return _size; }
// 判断链表是否为空
bool empty() const { return _size == 0; }
private:
Node* _head; // 哨兵节点指针(循环链表的核心)
size_t _size; // 链表中有效元素的个数
};
// 测试用结构体:用于验证 operator-> 的用法
struct AA
{
int _a1 = 1;
int _a2 = 1;
};
}
cpp
#include "List.h"
int main()
{
// 定义存储 gxy::string 的双向链表
gxy::list<gxy::string> lt;
// 1. 构造左值字符串 s1
gxy::string s1("11111111111");
// 传入左值 s1
// push_back 万能引用推导 X=gxy::string&
// forward 转发为左值,节点构造触发 string 拷贝构造
lt.push_back(s1);
// 2. 构造左值字符串 s2
gxy::string s2("33333333333");
// move(s2) 将左值强转为右值引用
// push_back 万能引用推导 X=gxy::string&&
// forward 转发为右值,节点构造触发 string 移动构造(高效,无深拷贝)
lt.push_back(move(s2));
// 3. 传入字符串字面量,隐式构造临时 gxy::string(右值)
// push_back 万能引用推导 X=const char(&)[N],最终转发为右值
// 节点构造直接移动临时对象,效率极高
lt.push_back("22222222222");
return 0;
}
关键调用链路(对应 List 里的 forward 设计)
- lt.push_back(s1)
X = bit::string& forward<X>(x) → 左值 new Node(左值) → string 拷贝构造
- lt.push_back(move(s2))
X = bit::string&& forward<X>(x) → 右值 new Node(右值) → string 移动构造
- lt.push_back("22222222222")
字面量构造临时 string(右值) forward 转发为右值 new Node(右值) → string 移动构造
为什么这里必须用 forward?
因为你用了万能引用 X&&:形参 x 是具名变量,本身永远是左值。不用 forward<X>(x),所有插入都会走拷贝构造。用了 forward,才能保留实参原本的左值/右值属性,让移动语义真正生效。
核心设计亮点解析 💡
- 万能引用 + std::forward 的极致优化
list_node::list_node(X&& data):用模板万能引用 X&& 接收任意左值/右值,通过 std::forward<X>(data) 完美转发给 T::_data 的构造函数。左值 → 触发 T 的拷贝构造,右值 → 触发 T 的移动构造,避免不必要的深拷贝。
list::push_back(X&& x) + list::insert(iterator pos, X&& x):同样用万能引用 + std::forward,将参数类型信息原封不动传递给节点构造,实现了一套代码同时支持左值/右值插入,替代了之前的两个重载版本。
- 迭代器设计
通过模板参数 Ref/Ptr 复用同一套迭代器代码,实现了普通迭代器和 const 迭代器:
iterator:Ref=T&, Ptr=T* → 可修改数据。
const_iterator:Ref=const T&, Ptr=const T* → 只读数据。
operator->() 特殊处理:返回数据指针,支持 it->_a1 这种直观写法(编译器会自动补全为 it.operator->()->_a1)。
- 循环链表与哨兵节点
用 _head 作为哨兵节点,形成循环链表,简化了边界处理:begin() = _head->_next(第一个有效节点)。end() = _head(尾后迭代器)。空链表时 _head->_next == _head,无需额外判断。
- 现代 C++ 特性
拷贝交换法:operator=(list<T> lt) 利用传值调用的临时对象,自动完成旧资源的释放,安全且高效。
初始化列表构造:支持 list<int> lt = {1,2,3,4} 这种直观写法。
std::swap:高效交换两个链表的资源,时间复杂度 O(1)。
七、 可变参数模板
7.1 基本语法及原理
C++11 支持可变参数模板,即支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包:
模板参数包:表示零或多个模板参数 函数参数包:表示零或多个函数参数
核心语法
cpp
// 1. 基础形式:按值传递参数包
template <class ...Args>
void Func(Args... args) {}
// 2. 左值引用传递参数包
template <class ...Args>
void Func(Args&... args) {}
// 3. 右值引用传递参数包(完美转发常用)
template <class ...Args>
void Func(Args&&... args) {}
语法规则
模板参数列表:class... 或 typename... 表示接下来的参数是零或多个类型列表
函数参数列表:类型名后接 ... 表示接下来的形参是零或多个对象列表
引用折叠规则:函数参数包可以是左值引用或右值引用,实例化时遵循引用折叠规则
本质原理:编译器会实例化对应类型和个数的多个函数,本质是模板实例化的扩展
参数包大小计算
使用 sizeof... 运算符计算参数包中参数的个数:
cpp
#include <iostream>
#include <string>
using namespace std;
template <class ...Args>
void Print(Args&&... args)
{
// 输出参数包的参数数量
cout << sizeof...(args) << endl;
}
int main()
{
double x = 2.2;
Print(); // 输出 0(0个参数)
Print(1); // 输出 1(1个参数)
Print(1, string("xxxxx"));// 输出 2(2个参数)
Print(1.1, string("xxxxx"), x); // 输出 3(3个参数)
return 0;
}
编译期实例化原理
编译器会结合引用折叠规则,实例化出对应重载函数:
cpp
// 对应 Print()
void Print();
// 对应 Print(1)
void Print(int&& arg1);
// 对应 Print(1, string("xxxxx"))
void Print(int&& arg1, string&& arg2);
// 对应 Print(1.1, string("xxxxx"), x)
void Print(double&& arg1, string&& arg2, double&& arg3);
本质:可变参数模板是语法糖,底层是编译器自动生成不同参数数量的函数模板,替代手动编写:
cpp
template <class T1>
void Print(T1&& arg1);
template <class T1, class T2>
void Print(T1&& arg1, T2&& arg2);
template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3);
// ... 无限扩展
7.2 包扩展(参数包展开)
对于参数包,除了计算大小,唯一能做的操作就是扩展,扩展模式用于处理每个元素,将参数包分解为构成元素,通过在模式右侧放 ... 触发扩展。
递归展开方式(最常用)
通过递归函数模板,逐个取出参数包的第一个参数,剩余参数继续传递:
cpp
#include <iostream>
#include <string>
using namespace std;
// 递归终止条件:参数包为空时调用
void ShowList()
{
cout << endl;
}
// 递归展开:取出第一个参数,剩余参数包继续传递
template <class T, class ...Args>
void ShowList(T x, Args... args)
{
cout << x << " ";
ShowList(args...); // 剩余参数包继续展开
}
// 可变参数模板入口:接收任意参数包,调用递归展开
template <class ...Args>
void Print(Args... args)
{
ShowList(args...);
}
int main()
{
Print(); // 输出换行
Print(1); // 输出 "1 " + 换行
Print(1, string("xxxxx"));// 输出 "1 xxxxx " + 换行
Print(1, string("xxxxx"), 2.2); // 输出 "1 xxxxx 2.2 " + 换行
return 0;
}
编译期推导过程
以 Print(1, string("xxxxx"), 2.2) 为例,编译器会推导为:
cpp
// 最终实例化的函数链
void ShowList(double x)
{
cout << x << " ";
ShowList();
}
void ShowList(string x, double z)
{
cout << x << " ";
ShowList(z);
}
void ShowList(int x, string y, double z)
{
cout << x << " ";
ShowList(y, z);
}
void Print(int x, string y, double z)
{
ShowList(x, y, z);
}
函数调用展开方式
将参数包中的每个元素传入另一个函数,再组合成新的参数包:
cpp
#include <iostream>
#include <string>
using namespace std;
// 单参数模板:处理单个参数并打印
template <class T>
const T& GetArg(const T& x)
{
cout << x << " ";
return x;
}
// 可变参数模板:仅作为参数包的"容器",不做实际操作
template <class ...Args>
void Arguments(Args... args)
{}
// 可变参数模板:入口函数,触发包扩展
template <class ...Args>
void Print(Args... args)
{
// 关键:包扩展 → 将每个 args 传入 GetArg,再组合成新参数包
Arguments(GetArg(args)...);
}
int main()
{
// 调用 Print,参数包为 {int, string, double}
Print(1, string("xxxxx"), 2.2);
return 0;
}
当调用 Print(1, string("xxxxx"), 2.2) 时,编译器会做以下推导:
-
模板实例化:Print 被实例化为:void Print(int, string, double);
-
包扩展展开:代码 Arguments(GetArg(args)...); 会被展开为:
cpp
Arguments(
GetArg(1), // 处理 int 参数
GetArg(string("xxxxx")), // 处理 string 参数
GetArg(2.2) // 处理 double 参数
);
- 函数调用顺序:为了构造 Arguments 的参数包,编译器会按顺序执行 GetArg(1)、GetArg(string("xxxxx"))、GetArg(2.2),从而依次输出 1 、xxxxx 、2.2 。
编译期推导过程
cpp
void Print(int x, string y, double z)
{
Arguments(GetArg(x), GetArg(y), GetArg(z));
}
注意:可变参数模板是编译期展开,不支持运行期遍历(如 for 循环遍历参数包),必须通过模板扩展机制处理。
GetArg 的作用:既是参数处理函数,也是包扩展的"模式",每个参数都会被传入并执行打印。
Arguments 的作用:仅作为参数包的接收方,本身不做任何操作,目的是触发包扩展的语法。
编译期行为:整个展开过程发生在编译期,运行时只是顺序执行 GetArg 调用,没有循环或动态遍历。
7.3 emplace 系列接口(可变参数模板的典型应用)
1. emplace_back vs push_back
假设我们要往链表里放一个 string 对象:
cpp
gxy::list<string> lt;
// 写法1:用 push_back
string s("hello");
lt.push_back(s); // 拷贝构造
lt.push_back(string("world")); // 先构造临时string,再移动构造
// 写法2:用 emplace_back
lt.emplace_back("hello"); // 直接构造!
核心区别:
push_back:你得先造好一个对象(不管是变量还是临时对象),再把它拷贝/移动到容器里。
emplace_back:你直接把构造对象需要的参数传给它,它会在容器的内存里就地构造一个对象,完全省掉了拷贝/移动的步骤。
底层到底发生了什么?
我们以 lt.emplace_back("apple", 10)(构造 pair<string, int>)为例:
- 调用 emplace_back
cpp
template <class... Args>
void emplace_back(Args&&... args)
{
insert(end(), std::forward<Args>(args)...);
}
它把参数包 "apple", 10 完美转发给 insert。
- 调用 insert
cpp
template <class... Args>
iterator insert(iterator pos, Args&&... args)
{
Node* newnode = new Node(std::forward<Args>(args)...);
// ... 链表指针操作
}
这里 new Node(...) 会调用 ListNode 的可变参数构造函数。
- 调用 ListNode 可变构造
cpp
template <class... Args>
ListNode(Args&&... args)
:_data(std::forward<Args>(args)...) // 关键!
{}
_data 是 pair<string, int>,所以 std::forward<Args>(args)... 会被展开成:
_data("apple", 10); // 直接调用 pair<string, int> 的构造函数!
✅ 最终结果:pair<string, int> 对象直接在 ListNode::_data 的内存地址上构造完成,没有任何临时对象、没有任何拷贝/移动。
为什么说 emplace 更高效?
|------|----------------------|----------------|
| 操作 | push_back | emplace_back |
| 构造次数 | 2次:先构造对象 → 再拷贝/移动到容器 | 1次:直接在容器内存构造对象 |
| 临时对象 | 可能产生 | 完全避免 |
| 适用场景 | 已经有现成对象要放入容器 | 直接用构造参数创建新对象 |
举个极端例子:
cpp
// 构造一个复杂的大对象,需要很多参数
struct BigObj {
BigObj(int a, string b, double c, bool d) { /* 很耗时的构造 */ }
};
gxy::list<BigObj> lst;
// push_back:必须先造一个临时对象
lst.push_back(BigObj(1, "test", 3.14, true));
// 流程:构造临时BigObj → 移动构造到容器 → 临时对象销毁
// emplace_back:直接传参数,一步到位
lst.emplace_back(1, "test", 3.14, true);
// 流程:直接在容器内存里构造BigObj → 结束
emplace_back 少了一次对象移动和临时对象销毁,在频繁插入大对象时,性能优势非常明显。
注意事项(容易踩坑)
- 参数要严格匹配构造函数
cpp
// pair<string, int> 的构造函数需要 (const string&, int)
lp.emplace_back("apple", 10); // ✅ 正确,"apple" 会隐式转成 string
lp.emplace_back(string("apple"), 10); // ✅ 也正确
// lp.emplace_back(10, "apple"); // ❌ 错误,参数顺序反了
- 不要和 push_back 混用逻辑
push_back 接收的是对象(const T& 或 T&&); emplace_back 接收的是构造 T 的参数
- 不是所有场景都更快
如果已经有一个现成的对象,push_back(std::move(obj)) 和 emplace_back(std::move(obj)) 效率几乎一样。只有在需要当场构造新对象时,emplace 才体现优势。
总结
emplace 系列接口就是让你把"构造对象的参数"直接传给容器,由容器在内部内存里帮你把对象造出来,从而避免了临时对象的创建和拷贝/移动,让代码更简洁、运行更快。
2. 接口实现
C++11 后 STL 新增 emplace 系列接口,均为可变参数模板,功能兼容 push/insert,且支持直接传入构造参数,在容器空间上直接构造对象,效率更高。
核心接口
cpp
// 在容器末尾构造元素
template <class... Args>
void emplace_back(Args&&... args);
// 在指定位置构造元素
template <class... Args>
iterator emplace(const_iterator position, Args&&... args);
核心优势:避免临时对象的拷贝/移动构造,直接在容器内存上构造对象;支持传递构造函数的参数包,无需手动创建临时对象;推荐优先使用 emplace 系列替代 push/insert
示例代码(自定义 list 容器实现)
cpp
// List.h
namespace gxy
{
// 链表节点
template<class T>
struct ListNode
{
ListNode<T>* _next;
ListNode<T>* _prev;
T _data;
// 拷贝构造节点
ListNode(T& data)
:_next(nullptr)
,_prev(nullptr)
,_data(move(data)) // 移动构造数据
{}
// 可变参数构造:接收任意参数包,通过 std::forward<Args>(args)...
// 完美转发给 T 的构造函数,直接在节点内存上构造 T 对象,避免临时对象的产生。
template <class... Args>
ListNode(Args&&... args)
:_next(nullptr)
,_prev(nullptr)
,_data(std::forward<Args>(args)...) // 完美转发参数包
{}
};
// 迭代器
template<class T, class Ref, class Ptr>
struct ListIterator
{
typedef ListNode<T> Node;
typedef ListIterator<T, Ref, Ptr> Self;
Node* _node;
ListIterator(Node* node) :_node(node) {}
// 迭代器自增/自减
Self& operator++()
{
_node = _node->_next;
return *this;
}
Self& operator--()
{
_node = _node->_prev;
return *this;
}
// 解引用与比较
Ref operator*() { return _node->_data; }
bool operator!=(const Self& it) { return _node != it._node; }
};
// 链表容器
template<class T>
class list
{
typedef ListNode<T> Node;
public:
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
iterator begin() { return iterator(_head->_next); }
iterator end() { return iterator(_head); }
// 初始化空链表
void empty_init()
{
_head = new Node();
_head->_next = _head;
_head->_prev = _head;
}
list() { empty_init(); }
// 普通 push_back(拷贝/移动构造)
void push_back(const T& x) { insert(end(), x); }
void push_back(T&& x) { insert(end(), move(x)); }
// 插入函数(基础版:接收左值)
iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* newnode = new Node(x); // 拷贝构造节点
Node* prev = cur->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
// 插入函数(基础版:接收右值)
iterator insert(iterator pos, T&& x)
{
Node* cur = pos._node;
Node* newnode = new Node(move(x)); // 移动构造节点
Node* prev = cur->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
// ========== 可变参数 emplace 接口 ==========
template <class... Args>
void emplace_back(Args&&... args)
{
insert(end(), std::forward<Args>(args)...);
}
template <class... Args>
iterator insert(iterator pos, Args&&... args)
{
Node* cur = pos._node;
// 直接用参数包构造 T 对象,避免临时对象
Node* newnode = new Node(std::forward<Args>(args)...);
Node* prev = cur->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
private:
Node* _head;
};
}
可变参数 emplace 接口
cpp
// ========== 可变参数 emplace 接口 ==========
template <class... Args>
void emplace_back(Args&&... args)
{
insert(end(), std::forward<Args>(args)...);
}
template <class... Args>
iterator insert(iterator pos, Args&&... args)
{
Node* cur = pos._node;
// 直接用参数包构造 T 对象,避免临时对象
Node* newnode = new Node(std::forward<Args>(args)...);
Node* prev = cur->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
核心优势:
-
直接构造:接收 T 构造函数的参数包,直接在节点内存上构造 T 对象,完全避免临时对象的拷贝/移动。
-
完美转发:std::forward<Args>(args)... 保持参数的右值/左值属性,最大化效率。
-
泛型兼容:支持任意数量、任意类型的构造参数,适配 T 的所有构造函数。
• 对比 push_back:
push_back:T obj → 拷贝/移动到节点 → 两次构造。
emplace_back:直接在节点构造 T → 一次构造,性能更优。
使用示例
cpp
int main()
{
list<gxy::string> lt;
gxy::string s1("111111111111");
gxy::string s2("111111111111");
// 1. emplace_back(左值)
lt.emplace_back(s1);
// 走可变参构造+完美转发,s1 为左值
// _data 调用 string 拷贝构造,无非法 move,安全
cout << "*********************************" << endl;
// 2. push_back(左值)
lt.push_back(s1);
// 走 insert(const T&) -> new Node(s1)
// 匹配 ListNode(T& data),内部执行 _data(move(data))
// 强行将左值转为右值,依赖 string 实现,可能是移动/拷贝,写法不标准
cout << "*********************************" << endl;
// 3. emplace_back(右值)
lt.emplace_back(move(s1));
// 走可变参构造+完美转发,保持右值属性
// _data 调用 string 移动构造,高效且标准
cout << "*********************************" << endl;
// 4. push_back(右值)
lt.push_back(move(s2));
// 走 insert(T&&) -> new Node(move(x))
// 匹配 ListNode(T& data),_data(move(data)) 为合法右值移动
cout << "*********************************" << endl;
// 5. emplace_back(构造参数) ------ emplace 核心优势
lt.emplace_back("111111111111");
// 直接将 const char* 转发给 string 构造函数
// 在节点 _data 内存就地构造,无临时对象、无拷贝/移动开销
cout << "*********************************" << endl;
// 6. push_back(字符串字面量)
lt.push_back("111111111111");
// 先隐式构造临时 string 对象
// 再将临时对象移动/拷贝到节点,比 emplace 多一次临时对象开销
cout << "*********************************" << endl;
return 0;
}
cpp
int main()
{
gxy::list<pair<gxy::string, int>> lt1;
pair<gxy::string, int> kv("苹果", 1);
// 1. emplace_back(左值pair)
lt1.emplace_back(kv);
// 行为:拷贝构造 pair 到节点
// 底层:转发 kv → _data(kv) → pair 的拷贝构造
cout << "*********************************" << endl;
// 2. emplace_back(右值pair)
lt1.emplace_back(move(kv));
// 行为:移动构造 pair 到节点
// 底层:转发 move(kv) → _data(move(kv)) → pair 的移动构造
cout << "*********************************" << endl;
// 3. emplace_back(构造pair的参数包) ✨核心优势✨
lt1.emplace_back("苹果", 1 );
// 行为:直接在节点内存构造 pair 对象
// 底层:转发 "苹果" 和 1 → _data("苹果", 1)
// 直接调用 pair<string, int>(const char*, int) 构造函数,无临时对象
cout << "*********************************" << endl;
// 4. push_back(初始化列表)
lt1.push_back({ "苹果", 1 });
// 行为:先构造临时 pair,再移动到节点
// 底层:隐式构造 pair<string, int>("苹果", 1) → 移动构造到 _data
// 对比 emplace_back:多了一次临时 pair 的构造和销毁
cout << "*********************************" << endl;
return 0;
}
结论:当传入已存在的 pair 对象时,emplace_back 和 push_back 效率一致。当传入构造 pair 的多个参数(如 "苹果", 1)时,emplace_back 可以直接转发参数包给 pair 的构造函数,这是 push_back 做不到的,push_back 必须先构造一个临时 pair 对象。
一句话总结
push_back:接收的是已经构造好的对象,把它拷贝/移动到容器里。
emplace_back:接收的是构造对象的参数,直接在容器内部内存里构造对象。
什么时候用 emplace_back?
-
需要当场构造新对象,且构造需要多个参数时(比如 pair、自定义复杂对象)。
-
传入构造参数(如字符串字面量)时,比 push_back 更高效。
什么时候用 push_back?
-
已经有现成的对象,直接移动进去(push_back(move(obj)))。
-
简单类型(如 int),两者效率几乎无差别。
编译期原理
编译器会根据参数包生成对应重载函数,例如:
cpp
// 对应 lt.emplace_back("11111111111")
void emplace_back(const char* s)
{
insert(end(), std::forward<const char*>(s));
}
// 对应 lt1.emplace_back("苹果", 1)
iterator insert(iterator pos, const char* arg1, int arg2)
{
Node* newnode = new Node(std::forward<const char*>(arg1), std::forward<int>(arg2));
// ... 链表插入逻辑
}
核心总结
-
参数包:可变参数模板的核心,分为模板参数包和函数参数包
-
基本语法:class...Args 定义模板参数包,Args...args 定义函数参数包
-
包扩展:通过递归或函数调用模式,在编译期分解参数包
-
关键运算符:sizeof...(args) 计算参数包大小
-
完美转发:std::forward<Args>(args)... 保持参数的右值/左值属性
-
典型应用:STL emplace 系列接口,直接在容器上构造对象,提升效率
-
本质:编译器自动生成不同参数数量的函数模板,是泛型编程的强大工具
八、新的类功能
8.1 默认的移动构造和移动赋值
- C++类默认成员函数
原有6个默认成员函数:构造函数、析构函数、拷贝构造函数、拷贝赋值重载、取地址重载、const取地址重载(后两个实际用途较少)。
C++11 新增:移动构造函数、移动赋值运算符重载。
- 默认移动构造生成规则
条件:未手动实现移动构造函数,且未实现析构函数、拷贝构造函数、拷贝赋值重载中的任意一个。
行为:
内置类型成员:按字节拷贝(浅拷贝)。
自定义类型成员:若该成员实现了移动构造,则调用其移动构造;否则调用拷贝构造。
- 默认移动赋值生成规则
条件:未手动实现移动赋值运算符重载,且未实现析构函数、拷贝构造函数、拷贝赋值重载中的任意一个。
行为:与默认移动构造完全一致。
- 关键约束:若手动提供了移动构造或移动赋值,编译器将不会自动生成拷贝构造和拷贝赋值。
cpp
class Person {
public:
Person(const char* name = "", int age = 0)
: _name(name), _age(age)
{}
// 若注释掉拷贝构造/赋值,编译器会自动生成移动构造/赋值
// Person(const Person& p) : _name(p._name), _age(p._age) {}
// Person& operator=(const Person& p) { ... }
private:
gxy::string _name;
int _age;
};
int main() {
Person s1;
Person s2 = s1; // 调用拷贝构造
Person s3 = std::move(s1); // 调用移动构造
Person s4;
s4 = std::move(s2); // 调用移动赋值
return 0;
}
8.2 成员变量声明时给缺省值
作用:为初始化列表提供默认值。
规则:若未在初始化列表显式初始化,则自动使用该缺省值初始化成员变量。
8.3 default 和 delete
- default:显式指定生成默认函数
场景:手动实现了某些默认函数(如拷贝构造),导致编译器不再生成其他默认函数(如移动构造),可使用 default 强制生成。
Person(Person&& p) = default; // 强制编译器生成默认移动构造
- delete:禁止生成默认函数
作用:指示编译器不生成对应函数的默认版本,被 delete 修饰的函数称为删除函数。
对比 C++98:C++98 需将函数设为 private 且只声明不定义,C++11 直接用 delete 更简洁。
Person(const Person& p) = delete; // 禁止拷贝构造
8.4 final 与 override
final:修饰类时,类不能被继承;修饰虚函数时,该函数不能被子类重写。
override:显式标记子类函数为重写的基类虚函数,编译器会检查是否匹配基类虚函数签名,避免写错函数名/参数。
九、STL 中的变化
- 新容器
常用:unordered_map、unordered_set(哈希表实现,查询/插入平均 O(1))。
了解:array(固定大小数组)、forward_list(单向链表)、bitset(位容器)。
- 新接口
右值引用相关:push/insert/emplace 系列接口(支持移动语义,避免拷贝)、移动构造/移动赋值。
初始化列表:initializer_list 版本构造函数,支持用 {} 初始化容器。
范围 for 遍历:for (auto& e : container) 简化遍历。
其他:cbegin/cend(常量迭代器)等,需查阅文档。
十、Lambda 表达式
10.1 Lambda 表达式语法
本质:匿名函数对象,可定义在函数内部,无实际类型,通常用 auto 或模板参数接收。
格式:[capture-list] (parameters) -> return_type { function_body }
capture-list\]:捕捉列表,必写(即使为空 \[\]),用于捕捉上下文变量供 lambda 内部使用。
(parameters):参数列表,与普通函数一致,无参数可省略 ()。
-\> return_type:返回值类型,可省略,由编译器自动推导。
{function_body}:函数体,必写(即使为空 {})。
```cpp
// 完整写法
auto add1 = [](int x, int y)->int { return x + y; };
// 省略返回值类型
auto add2 = [](int x, int y) { return x + y; };
// 无参数,省略参数列表
auto func1 = [] { cout << "hello world" << endl; };
// 引用参数
auto swap1 = [](int& x, int& y) {
int tmp = x; x = y; y = tmp;
};
```
1. 完整写法 auto add1 = \[\](int x, int y)-\>int { return x + y; };
\[\]:捕捉列表(空,表示不捕捉任何外部变量)
(int x, int y):参数列表,和普通函数一样指定参数类型
-\>int:显式返回值类型,明确告诉编译器返回 int
{ return x + y; }:函数体,实现加法逻辑
auto add1:用 auto 接收这个匿名函数对象,后续可像函数一样调用:add1(1, 2) → 返回 3
2. 省略返回值类型 auto add2 = \[\](int x, int y) { return x + y; };
当函数体只有单一 return 语句时,编译器可以自动推导返回值类型,所以 -\>int 可以省略
调用方式和 add1 完全一样:add2(3, 4) → 返回 7
注意:如果函数体有多条语句或没有 return,就不能省略返回值类型
3. 无参数,省略参数列表 auto func1 = \[\] { cout \<\< "hello bit" \<\< endl; };
没有参数时,参数列表 () 可以完全省略,直接写 \[\] { ... }
调用方式:func1() → 输出 hello world 这是 lambda 最简洁的写法之一
4. 引用参数
```cpp
auto swap1 = [](int& x, int& y) {
int tmp = x; x = y; y = tmp;
};
```
参数用 int\&(左值引用),表示可以修改传入的实参(和普通函数的引用参数作用一致)
调用示例:
int a = 1, b = 2;
swap1(a, b); // 交换后 a=2, b=1
这里也可以用 auto swap1 = \[\](auto\& x, auto\& y) { ... } 实现泛型版本,支持交换任意类型
💡 核心总结
• lambda 本质是匿名函数对象,用 auto 接收后可像普通函数一样调用
• 语法简化规则:
1. 单一 return → 可省略 -\>返回值类型
2. 无参数 → 可省略 ()
3. 捕捉列表 \[\] 永远不能省略;引用参数 T\& 可以修改外部变量,值参数 T 只会拷贝
### 10.2 捕捉列表
1. 捕捉规则:lambda 内部默认只能使用自身函数体、参数及全局/静态变量,外层局部变量必须通过捕捉列表获取。
2. 捕捉方式
|--------|--------------|-----------------|
| 捕捉方法 | 语法 | 说明 |
| 显式值捕捉 | \[x, y\] | 拷贝捕捉 x、y,内部不可修改 |
| 显式引用捕捉 | \[\&x,\& y\] | 引用捕捉 x、y,内部可修改 |
| 隐式值捕捉 | \[=\] | 自动拷贝捕捉所有使用的外层变量 |
| 隐式引用捕捉 | \[\&\] | 自动引用捕捉所有使用的外层变量 |
| 混合捕捉1 | \[=, \&x\] | 默认值捕捉,仅 x 引用捕捉 |
| 混合捕捉2 | \[\&, x\] | 默认引用捕捉,仅 x 值捕捉 |
```cpp
#include