欢迎来到我的频道 【点击跳转专栏】
码云链接 【点此转跳】
文章目录
- [1. 可变参数模板](#1. 可变参数模板)
- [1.1 基本语法及原理](#1.1 基本语法及原理)
- [1.2 包扩展(难点非重点 选择性学习)](#1.2 包扩展(难点非重点 选择性学习))
- [1.2.1 编译期通过递归展开处理参数包](#1.2.1 编译期通过递归展开处理参数包)
- [1.2.2 编译期 vs 运行期](#1.2.2 编译期 vs 运行期)
- [1.2.3 利用参数包展开在函数调用中"副作用"地处理每个参数](#1.2.3 利用参数包展开在函数调用中“副作用”地处理每个参数)
- [1.2.4 与递归版本对比](#1.2.4 与递归版本对比)
- [1.3 empalce系列接⼝](#1.3 empalce系列接⼝)
- [1.3.1 emplace_back 与 push_back 在处理初始化列表时的重要区别(加餐部分)](#1.3.1 emplace_back 与 push_back 在处理初始化列表时的重要区别(加餐部分))
- [1.3.2 模拟实现了list的emplace和emplace_back接⼝](#1.3.2 模拟实现了list的emplace和emplace_back接⼝)
- [2. C++11 类的新功能](#2. C++11 类的新功能)
- [2.1 默认的移动构造和移动赋值](#2.1 默认的移动构造和移动赋值)
- [2.2 成员变量声明时给缺省值](#2.2 成员变量声明时给缺省值)
- [2.3 defult和delete](#2.3 defult和delete)
- [2.4 final与override](#2.4 final与override)
- [2.5 委托构造函数](#2.5 委托构造函数)
- [2.6 继承构造函数](#2.6 继承构造函数)
- [3. C++11 STL的变化](#3. C++11 STL的变化)
- [3.1 forward_list 和 list的区别](#3.1 forward_list 和 list的区别)
1. 可变参数模板
1.1 基本语法及原理
- C++11支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函数参数。
template void Func(Args... args) {}template void Func(Args&... args) {}template void Func(Args&&... args) {}- 我们用省略号来指出一个模板参数或函数参数的表示一个包,在模板参数列表中,
class...或 typename...指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟...指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则。 - 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
- 这里我们可以使用
sizeof...运算符去计算参数包中参数的个数。
cpp
#include <iostream>
#include <string>
using namespace std;
// ────────────────────────────────────────────────────────
// 可变参数模板函数:Print
// Args... 是模板参数包(零个或多个类型)
// args... 是函数参数包(零个或多个实参)
// 使用右值引用形式 (Args&&...) 实现"万能引用",支持左值/右值完美转发
// ────────────────────────────────────────────────────────
template <class ...Args>
void Print(Args&&... args)
{
// sizeof...(args) 是 C++11 新增运算符,用于计算参数包中参数的个数
cout << sizeof...(args) << endl;
}
int main()
{
double x = 2.2; // 左值变量
Print(); // 参数包为空 → 0 个参数
Print(1); // 传入字面量(右值)→ 1 个参数
Print(1, string("xxxxx")); // 两个右值 → 2 个参数
Print(1.1, string("xxxxx"), x); // 前两个是右值,x 是左值 → 3 个参数
return 0;
}
// ────────────────────────────────────────────────────────
// 【原理】编译器在实例化时,会根据实际调用生成具体的函数版本
// 并结合"引用折叠规则"确定每个参数的实际类型:
// - 字面量(如 1, "xxx")是纯右值 → 推导为 T&&
// - 命名变量(如 x)是左值 → 推导为 T&(因万能引用 + 引用折叠)
//
// 因此,上述四次调用等价于以下四个具体函数被隐式生成:
// ────────────────────────────────────────────────────────
void Print(); // 空参数包
void Print(int&& arg1); // 1 是 int 字面量 → 右值引用
void Print(int&& arg1, string&& arg2);
// 1 → int&&, string("xxxxx") 是临时对象 → string&&
void Print(double&& arg1, string&& arg2, double& arg3);
// 1.1 → double&&(右值)
// string("xxxxx") → string&&(右值)
// x 是左值变量 → 虽然形参写成 double&&,但因万能引用 + 引用折叠,
// 实际推导为 double&(即 arg3 是左值引用)
// ────────────────────────────────────────────────────────
// 如果没有可变参数模板,要实现同样功能,
// 我们必须手动重载多个模板函数(数量固定,不灵活):
// ────────────────────────────────────────────────────────
void Print(); // 0 参数
template <class T1>
void Print(T1&& arg1); // 1 参数
template <class T1, class T2>
void Print(T1&& arg1, T2&& arg2); // 2 参数
template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3); // 3 参数
// ... 还需继续写 4 参数、5 参数...... 极其繁琐!
// ────────────────────────────────────────────────────────
// ✅ 可变参数模板的意义:
// - 在"类型泛化"的基础上,叠加"参数数量泛化";
// - 让泛型编程真正支持任意数量、任意类型的参数;
// - 是实现日志系统、printf 封装、make_shared、emplace 等高级功能的基础。
// ────────────────────────────────────────────────────────
| 概念 | 说明 |
|---|---|
| 参数包(Parameter Pack) | Args... 表示零或多个模板参数,args... 表示零或多个函数参数 |
| 万能引用(Universal Reference) | T&& 在模板中若 T 是推导类型,则可能是左值或右值引用 |
| 引用折叠 | 决定 T&& 最终是左值还是右值引用(如 double& && → double&) |
sizeof... |
编译期运算符,返回参数包中元素个数 |
| 编译实例化 | 可变参数模板不是运行时循环,而是编译期展开为多个具体函数 |
1.2 包扩展(难点非重点 选择性学习)
引子: 比如说 想要打印可变参数模版里的参数 有个大聪明想到了如下错的离谱方法
cpp
template <class ...Args>
void Print(Args... args)
{
// 可变参数模板编译时解析
// 下⾯是运⾏获取和解析,所以不⽀持这样⽤
cout << sizeof...(args) << endl;
for (size_t i = 0; i < sizeof...(args); i++)
{
cout << args[i] << " ";
}
cout << endl;
}
如果我们确实想逐个打印我们所写的每一个参数,就需要扩展每一包,也就是包展开
1.2.1 编译期通过递归展开处理参数包
cpp
#include <iostream>
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(1, 2, "hello", 3.14);
return 0;
}
- 对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个包时,我们还要提供⽤于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应⽤模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(...)来触发扩展操作。底层的实现细节如图1 所⽰。
(图 1 ⬆️)- 下面是这个流程的详细解释!
第一步:调用 Print(1, 2, "hello", 3.14)
-
Print是一个可变参函数模板,接受任意数量、任意类型的参数。 -
它直接调用
ShowList(args...),即把所有参数原样转发给ShowList。 -
所以这等价于调用:
cppShowList(1, 2, "hello", 3.14);
第二步:递归展开 ShowList
C++ 的可变参数模板在编译期通过递归展开 处理参数包。每次匹配"至少一个参数"的版本,直到参数包为空,匹配无参的 ShowList()。
我们一步步展开:
第 1 层调用:
cpp
ShowList(1, 2, "hello", 3.14)
-
匹配模板:
template<class T, class... Args> void ShowList(T x, Args... args) -
推导:
T = intArgs... = {int, const char*, double}→ 即(2, "hello", 3.14)
-
执行:
cppcout << 1 << " "; // 输出: "1 " ShowList(2, "hello", 3.14); // 递归
第 2 层调用:
cpp
ShowList(2, "hello", 3.14)
-
T = int -
Args... = {const char*, double} -
执行:
cppcout << 2 << " "; // 输出追加: "2 " ShowList("hello", 3.14);
第 3 层调用:
cpp
ShowList("hello", 3.14)
-
T = const char* -
Args... = {double} -
执行:
cppcout << "hello" << " "; // 输出追加: "hello " ShowList(3.14);
第 4 层调用:
cpp
ShowList(3.14)
-
T = double -
Args... = {}(空参数包) -
执行:
cppcout << 3.14 << " "; // 输出追加: "3.14 " ShowList(); // 递归调用无参版本
第 5 层调用(终止条件):
cpp
ShowList()
-
匹配非模板的普通函数
void ShowList() -
执行:
cppcout << endl; // 输出换行 -
函数返回,递归结束。
✅ 最终输出结果
依次输出的内容为:
1 2 hello 3.14
注意:
- 每个值后都有一个空格(包括最后一个
3.14后面)。 - 然后换行。
所以完整输出是(用 · 表示空格):
1·2·hello·3.14·\n
Print(1, 2, "hello", 3.14)
↓
ShowList(1, 2, "hello", 3.14)
↓
cout << "1 "; → ShowList(2, "hello", 3.14)
↓
cout << "2 "; → ShowList("hello", 3.14)
↓
cout << "hello "; → ShowList(3.14)
↓
cout << "3.14 "; → ShowList()
↓
cout << '\n';
1.2.2 编译期 vs 运行期
-
参数包展开发生在编译期:编译器会为每一层生成具体的函数实例(instantiation)。
-
实际生成的函数类似:
cppvoid ShowList<int, int, const char*, double>(int, int, const char*, double); void ShowList<int, const char*, double>(int, const char*, double); void ShowList<const char*, double>(const char*, double); void ShowList<double>(double); void ShowList(); // 非模板 -
所有递归调用都是静态分发(static dispatch),没有运行时开销(这个可以理解成编译时期的递归)。
-
所以有些"小天才"们想到的这种结束递归的方式就是错误的了,因为那种
if判断已经是运行期要做的事情了。
cpp
template<class T ,class ...Args>
void ShowList(T x,Args... args)
{
cout<< x << " ";
if(sizeof...(args) == 0)
return;
//这是运行时候的事情! 所以这么结束递归是错误的!!
ShowList(args...);
}
1.2.3 利用参数包展开在函数调用中"副作用"地处理每个参数
cpp
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)
{
Arguments(GetArg(args)...);
}
int main()
{
double x = 2.2;
Print(1.1, string("xxxxx"), x); // 包里有3个参数
return 0;
}
🔍 核心思想
- 不使用递归 ,而是利用参数包展开在函数调用中"副作用"地处理每个参数。
GetArg被设计成:打印参数 + 返回引用。Arguments(...)是一个"哑函数 "(do-nothing function),它的唯一作用是接收展开后的参数列表,从而强制编译器对每个GetArg(args)进行求值。
💡 关键点:函数参数的求值顺序在 C++17 起是确定的(从左到右),所以输出顺序是可靠的(C++17 之前是未指定的,但实践中多数编译器也是从左到右)。
第一步:main() 中调用 Print
cpp
double x = 2.2;
Print(1.1, string("xxxxx"), x);
- 传入三个参数:
1.1→ 类型doublestring("xxxxx")→ 临时std::string对象x→double类型的变量(值为2.2)
所以 Print 的模板参数推导为:
cpp
Args = {double, std::string, double}
于是 Print 实例化为:
cpp
void Print<double, std::string, double>(double, std::string, double)
第二步:Print 内部调用
cpp
Arguments(GetArg(args)...);
这里的 args... 是 (1.1, string("xxxxx"), x)。
GetArg(args)... 是一个参数包展开表达式,它会展开为:
cpp
Arguments(
GetArg(1.1),
GetArg(string("xxxxx")),
GetArg(x)
);
✅ 这就是关键:每个
GetArg都会被调用一次,且其副作用(打印)会生效。
第三步:依次执行 GetArg(按参数顺序)
GetArg(1.1)
T = double- 执行:
cout << 1.1 << " ";→ 输出1.1 - 返回
const double&(绑定到字面量1.1的临时对象,合法,因为函数返回前临时对象仍存活)
GetArg(string("xxxxx"))
T = std::string- 执行:
cout << "xxxxx" << " ";→ 输出xxxxx - 返回
const std::string&(绑定到临时 string 对象)
GetArg(x)
x是double变量,值为2.2- 执行:
cout << 2.2 << " ";→ 输出2.2 - 返回
const double&(引用x本身)
📌 注意:虽然
GetArg返回了引用,但Arguments并不使用这些返回值------它们只是被"丢弃"。但函数调用本身必须发生 ,所以cout副作用一定会执行。
第四步:调用 Arguments(...)
展开后实际调用的是:
cpp
Arguments(
/* 返回值1 */,
/* 返回值2 */,
/* 返回值3 */
);
而 Arguments 的定义是:
cpp
template <class ...Args>
void Arguments(Args... args) {}
→ 它什么也不做,直接返回。
但它迫使编译器实例化这个函数并求值所有实参 ,从而触发 GetArg 的调用。
main()
↓
Print(1.1, "xxxxx", x)
↓
Arguments( GetArg(1.1), GetArg("xxxxx"), GetArg(x) )
↓ ↓ ↓
cout<<1.1 cout<<"xxxxx" cout<<2.2
↓ ↓ ↓
返回引用 返回引用 返回引用
↓
Arguments( ref1, ref2, ref3 ) → 什么都不做,返回
↓
程序结束
输出:
1.1 xxxxx 2.2
(每个值后有一个空格,包括最后一个)
⚠️ 重要细节说明
- 为什么需要
Arguments? 如果没有Arguments,只写:cpp GetArg(args)...; // ❌ 错误!不能直接展开语句这是语法错误 。C++
不允许在普通语句中直接展开参数包。必须在支持展开的上下文中使用,例如:
- 函数调用的实参列表(✅ 本例)
- 初始化列表
{ expr... }- 模板实参
<Types...>所以
Arguments是一个"语法容器",让展开合法。
- 求值顺序
- C++17 起 :函数实参的求值顺序是从左到右(标准规定)。
- C++14 及以前 :求值顺序是未指定(unspecified),理论上可能乱序。 但在几乎所有主流编译器(GCC、Clang、MSVC)中,即使 C++14 也按从左到右求值,所以实践中通常安全。
如果你用的是 C++17 或更高(推荐),输出顺序是严格保证的。
- 返回引用的作用
GetArg返回const T&主要是为了:
- 避免不必要的拷贝(尤其对大对象如
string)- 让返回值能作为
Arguments的实参(否则如果返回void就无法组成参数列表)如果
GetArg返回void,那么Arguments(GetArg(args)...)会变成
Arguments(void, void, ...),这是非法的。
1.2.4 与递归版本对比
| 特性 | 递归版本(ShowList) | 展开+哑函数版本(GetArg+Arguments) |
|---|---|---|
| 是否递归 | 是 | 否 |
| 模板实例化深度 | O(n) | O(1)(仅 Arguments 一个实例) |
| 编译速度 | 较慢(多层实例化) | 较快 |
| 可读性 | 直观 | 稍显技巧性 |
| C++ 标准要求 | C++11 | C++11 |
| 控制分隔符 | 困难(尾随空格) | 同样困难 |
1.3 empalce系列接⼝
template <class... Args> void emplace_back (Args&&... args);template <class... Args> iterator emplace (const_iterator position, Args&&... args);
- C++11以后STL容器新增了
emplace系列的接口,emplace系列的接口均为模板可变参数,功能上兼容push和insert系列,但是emplace还支持新玩法,假设容器为container,emplace还支持直接插入构造T对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造T对象。 emplace_back总体而言是更高效,推荐以后使用emplace系列替代insert和push系列
cpp
#include <list>
// emplace_back 总体而言是更高效,推荐以后使用 emplace 系列替代 insert 和 push 系列
int main()
{
// 定义一个存储 bit::string 类型的 list 容器
std::list<bit::string> lt;
// 情况1:传入左值(具名变量)
// 行为与 push_back 相同:调用 bit::string 的拷贝构造函数
bit::string s1("111111111111");
lt.emplace_back(s1);
std::cout << "**************************************" << std::endl;
// 情况2:传入右值(通过 std::move 转换)
// 行为与 push_back 相同:调用 bit::string 的移动构造函数
lt.emplace_back(std::move(s1));
std::cout << "**************************************" << std::endl;
// 情况3:直接传递构造 bit::string 所需的参数(这里是 const char* 字面量)
//直接把构造string参数包往下传,直接用string参数包构造string
// emplace_back 会在 list 内部直接用这些参数构造 bit::string 对象
// 避免了先构造临时对象再拷贝/移动的过程 ------ 这是 push_back 无法做到的
lt.emplace_back("111111111111");
std::cout << "**************************************" << std::endl;
// 定义一个存储 pair<bit::string, int> 的 list
std::list<std::pair<bit::string, int>> lt1;
// 情况4:传入已构造好的 pair 左值
// 先构造 pair kv,再将它拷贝进 list 节点(等效于 push_back)
std::pair<bit::string, int> kv("苹果", 1);
lt1.emplace_back(kv);
std::cout << "**************************************" << std::endl;
// 情况5:传入右值 pair
// 移动构造进 list 节点(等效于 push_back + move)
lt1.emplace_back(std::move(kv));
std::cout << "**************************************" << std::endl;
/////////////////////////////////////////////////////////////
// 情况6:直接传递构造 pair 所需的两个参数(const char*, int)
// 直接把构造pair参数包往下传,直接用pair参数包构造pair
// emplace_back 会直接在 list 节点内存中调用 pair 的构造函数:
// pair<bit::string, int>("苹果", 1)
// 这避免了先创建临时 pair 再移动/拷贝的开销 ------ push_back 无法实现此优化
lt1.emplace_back("苹果", 1);
std::cout << "**************************************" << std::endl;
return 0;
}
⚠️:
emplace系列 可以理解成
- 当参数是
左值的时候 它和push_back一样都是走拷贝构造构造(如 情况1、4 解释一下 因为s1是左值 资源无法转移)- 当参数是
将亡值时 走移动构造 这点在现代编译器下和push_back也是一样的。(情况2 、5)- 但是当参数是
纯右值的时候 会直接在编译期间把构造参数包往下传(这也是可变参数模版特有的特征) 然后优化成直接构造 这是push_back做不到的!!(情况 3 6)
1.3.1 emplace_back 与 push_back 在处理初始化列表时的重要区别(加餐部分)
cpp
std::list<std::pair<bit::string, int>> lt1;
lt1.emplace_back({ "苹果", 1 });// 不支持,形参是模板,无法推导形参类型
lt1.push_back({ "苹果", 1 }); // 隐式类型转换传参
lt1.push_back({ "苹果", 1 });------ 可以编译通过
cpp
lt1.push_back({ "苹果", 1 });
-
push_back的函数签名通常是:cppvoid push_back(const T& value); void push_back(T&& value);其中
T = std::pair<bit::string, int>。 -
当传入
{ "苹果", 1 }时,编译器会尝试将这个花括号初始化列表 隐式转换为T类型。 -
由于
std::pair支持用两个元素的初始化列表构造(即pair(const T1&, const T2&)),因此这里会隐式构造一个临时的pair对象 ,然后调用push_back(T&&)(移动)或push_back(const T&)(拷贝)。 -
✅ 所以这行代码是合法的。
lt1.emplace_back({ "苹果", 1 });------ 无法编译
cpp
// lt1.emplace_back({ "苹果", 1 }); // 不支持!
-
emplace_back的签名是模板可变参数:cpptemplate<class... Args> void emplace_back(Args&&... args); -
问题在于:
{ "苹果", 1 }是一个"匿名初始化列表",它没有具体类型。 -
模板参数推导无法从
{...}推导出Args...的具体类型 ,因为{...}不是一个表达式,而是一种语法结构。 -
因此,编译器报错:"无法推导模板参数" 或 "initializer list cannot deduce template arguments"。
📌 这就是为什么
emplace_back不能直接接受初始化列表作为参数,除非你显式指定类型。
cpp
std::vector<std::vector<int>> v;
v.emplace_back(std::initializer_list<int>{1, 2, 3}); // ✅ 显式指定类型
// 或简写为:
v.emplace_back(std::vector<int>{1, 2, 3}); // ✅ 构造临时对象(但失去emplace优势)
| 写法 | 是否可行 | 原因 |
|---|---|---|
push_back({ "苹果", 1 }) |
✅ 可行 | 初始化列表 → 隐式构造临时 pair → 移动/拷贝进容器 |
emplace_back({ "苹果", 1 }) |
❌ 不可行 | 模板无法从 {...} 推导参数类型 |
emplace_back("苹果", 1) |
✅ 可行 | 参数直接用于就地构造 pair,高效 |
1.3.2 模拟实现了list的emplace和emplace_back接⼝
- 这里把参数包不断往下传递,最终在结点的构造中直接去匹配容器存储的数据类型
T的构造,所以达到了前面说的emplace支持直接插入构造T对象的参数,这样有些场景会更高效一些,可以直接在容器空间上构造T对象。 - 传递参数包过程中,如果是
Args&&... args的参数包,要用完美转发参数包,方式如下
std::forward<Args>(args)...,否则编译时包扩展后右值引用变量表达式就变成了左值。
cpp
// emplace_back:在 list 尾部就地构造一个元素
template <class... Args>
void emplace_back(Args&&... args)
{
// 调用通用 emplace 接口,在 end() 位置(即尾后位置)插入
// end() 返回指向链表末尾之后的哨兵节点(通常为头节点)
emplace(end(), std::forward<Args>(args)...);
}
// emplace:在指定迭代器 pos 位置前插入一个就地构造的元素
template <class... Args>
void emplace(iterator pos, Args&&... args)
{
// 获取 pos 对应的底层节点指针
Node* cur = pos._node; // pos 指向的节点(新节点将插入到它前面)
Node* prev = cur->_prev; // pos 前一个节点
// 关键:使用完美转发,将参数包直接传递给 Node 中 data 成员的构造函数
// Node 的构造函数会调用 T(args...) 来初始化其存储的数据(T 即容器元素类型)
Node* newnode = new Node(std::forward<Args>(args)...);
// 将新节点插入到 prev 和 cur 之间
// 链表连接:prev <-> newnode <-> cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
// 容器大小加一
++_size;
}
⚠️:同时注意
list_node(Node)的构造函数必须支持完美转发 其实更重要的是不要忘记写
cpp
template <class T>
struct Node {
T data;
Node* _next;
Node* _prev;
// 可变参构造函数:用传入的参数直接构造 data
template <class... Args>
Node(Args&&... args) : data(std::forward<Args>(args)...), _next(nullptr), _prev(nullptr) {}
};
2. C++11 类的新功能
2.1 默认的移动构造和移动赋值
- 原来C++类中,有6个默认成员函数:构造函数/析构函数/拷贝构造函数/拷贝赋值重载/取地址重载/const取地址重载 ,最后重要的是前4个,后两个用处不大,默认成员函数就是我们不写编译器会生成一个默认的。C++11新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
- 如果你没有自己实现移动构造函数 ,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个 。(条件还是比较苛刻的)那么编译器会自动生成一个默认移动构造。
- 默认生成的移动构造函数 ,对于内置类型成员 会执行逐成员按字节拷贝 ,自定义类型成员 ,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果你没有自己实现移动赋值重载函数 ,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。
- 默认生成的移动赋值函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
⚠️:很多人可能就不理解了 为什么 自动生成移动构造 的条件为何如此苛刻??
其实答案很简单 移动构造 对于内置类型都是逐字节拷贝的(因为移动资源和直接拷贝基本消耗没区别) 而我们一般在自定义类型不写拷贝构造析构函数 等 说明我们的成员对象 都是浅拷贝类型 ! 而这种情况都是逐字节拷贝(哪怕移动构造;当我们主动写析构 拷贝构造 的时候也就说明有深拷贝类型了(动态开辟内存) 这个时候我们肯定要手动实现移动构造的 所以 这个条件可以说是十分的合理!!
cpp
//完全没有写的必要!!
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
private:
bit::string _name;//调用string自己的移动构造!!
int _age;
};
2.2 成员变量声明时给缺省值
该内容已经在博主的 类和对象(下)章节的再度解析构造函数部分详细写了 如果有需要可以 直接点击转跳 或者直接参考这张图
2.3 defult和delete
- C++11可以让你更好的控制要使⽤的默认函数。假设你要使⽤某个默认的函数,但是因为⼀些原因这个函数没有默认⽣成。⽐如:我们提供了拷⻉构造,就不会⽣成移动构造了,那么我们可以使⽤
default关键字显⽰指定移动构造⽣成。
当我们 手动写一个析构函数的时候 此时 它就不会再生成默认的移动构造和移动赋值了!
cpp
class Person
{
public:
Person(const char* name = "张三", int age = 10)
:_name(name)
, _age(age)
{}
~Person()
{}
private:
bit::string _name;
int _age = 1;
};
int main()
{
Person s1;
cout<<"*******************************************************"<<endl;
Person s2 = s1;
cout<<"*******************************************************"<<endl;
Person s3 = std::move(s1);
cout<<"*******************************************************"<<endl;
Person s4;
s4 = std::move(s2);
return 0;
}
我们查看结果,发现很多原本移动构造、赋值的部分 全部变成了拷贝构造、赋值
如果想要解决这种情况 我们就需要手动生成默认移动构造 不过注意!defult手动生成的默认移动构造和赋值 可能会对本来自动生成的拷贝构造、赋值造成影响 如图:
所以最好都写一遍:
cpp
class Person
{
public:
Person(const char* name = "张三", int age = 10)
:_name(name)
, _age(age)
{}
Person(const Person& p) = default;
Person(Person&& p) = default;
Person& operator=(const Person& p) = default;
Person& operator=(Person && p) = default;
~Person()
{}
private:
bit::string _name;
int _age = 1;
};
int main()
{
Person s1;
cout<<"*******************************************************"<<endl;
Person s2 = s1;
cout<<"*******************************************************"<<endl;
Person s3 = std::move(s1);
cout<<"*******************************************************"<<endl;
Person s4;
s4 = std::move(s2);
return 0;
}
此时结果:
- 如果能想要限制某些默认函数的⽣成,在C++98中,是该函数设置成
private,并且只声明补丁,不实现,这样只要其他⼈想要调⽤就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指⽰编译器不⽣成对应函数的默认版本,称=delete修饰的函数为删除函数。
比如说流对象就不支持拷贝
cpp
Person(const Person& p) = delete;//此时不再支持拷贝构造!
2.4 final与override
这个我在继承多态章节就讲到了 有需要的可以参考博主以前的博客
继承中讲到final的部分 点击转跳
多态中讲到final与override的部分 点击转跳
2.5 委托构造函数
- C++中的委托构造函数(Delegating Constructor)是C++11引入的特性,允许一个构造函数调用同类中的其他构造函数,从而减少代码重复并提高可维护性。
- 被委托的构造函数必须初始化所有成员变量,因为委托构造函数后不能再重复初始化。
cpp
#include <iostream>
using namespace std;
class Example {
public:
// 👑 主构造函数(目标构造函数)
// 接收两个参数,负责真正初始化成员变量 _x 和 _y
Example(int a, int b)
: _x(a)
, _y(b)
{
cout << "目标构造函数\n";
}
// ❌ 以下代码是错误的:
//
// Example(int a)
// : Example(a, 0) // ✅ 委托调用另一个构造函数
// , _y(1) // ❌ 错误!委托构造后不能再初始化成员
// {
// cout << "委托构造函数\n";
// }
//
// 📌 规则:一旦使用委托构造(即在初始化列表中调用本类其他构造函数),
// 就不能再对任何成员变量进行额外初始化(包括默认初始化),
// 否则编译器会报错。
// ✅ 正确的委托构造函数
// 只接受一个参数 a,将 b 默认设为 0,并委托给上面的双参构造函数
// 成员变量 _x 和 _y 的初始化完全由被委托的构造函数完成
Example(int a)
: Example(a, 0) // 委托给 Example(int, int)
{
cout << "委托构造函数\n";
// 注意:这里只能写普通语句(如打印、逻辑处理等),
// 不能再次初始化 _x 或 _y!
}
// 成员变量
int _x;
int _y;
};
int main()
{
cout << "=== 创建对象1 ===\n";
Example(1, 2); // 直接调用双参构造函数
cout << "\n=== 创建对象2 ===\n";
Example(1); // 调用单参委托构造函数 → 内部委托给双参构造函数
return 0;
}
易错点:
2.6 继承构造函数
- 继承构造函数是C++11引入的一项特性,它允许派生类直接继承基类的构造函数 ,而不需要手动重新定义它们。这一特性显著简化了派生类的编写,特别是在基类有多个构造函数的情况下。
- 派生类继承基类的普通构造函数,特殊的拷贝构造函数/移动构造函数不继承。
- 继承构造函数中派生类自己的成员变量如果有缺省值会使用缺省值初始化,如果没有缺省值那么跟之前类似,内置类型成员不确定,自定义类型成员使用默认构造初始化。
格式:
cpp
using ClassX::ClassX;
cpp
class Base {
public:
Base(int x, double d)
:_x(x)
, _d(d)
{}
Base(int x)
:_x(x)
{}
Base(double d)
:_x(d)
{}
protected:
int _x = 0;
double _d = 0;
};
// 传统的派生类实现构造
//class Derived : public Base {
//public:
// Derived(int x) : Base(x) {}
// Derived(double d) : Base(d) {}
// Derived(int x, double d) : Base(x, d) {}
//};
// C++11继承基类的所有构造函数
// 1、没有成员变量的派生类
// 2、成员变量都有缺省值,并且我们就想用这个缺省值初始化
class Derived : public Base {
public:
using Base::Base;
protected:
int _i;
string _s;
};
int main()
{
//Derived d;
Derived d1(1);
Derived d2(1.1);
Derived d3(2, 2.2);
return 0;
}
3. C++11 STL的变化
- STL中容器的新接⼝也不少,最重要的就是右值引⽤ 和移动语义 相关的
push/insert/emplace系列接⼝和移动构造和移动赋值 ,还有initializer_list版本的构造 等,这些前⾯都讲过了,还有⼀些⽆关痛痒的如cbegin/cend等需要时查查⽂档即可。 - 容器的范围for遍历,这个没必要再多说了吧。
- 唯一要稍微说说的也就
forward_list其实这个并不是很重要 就稍微讲讲。
3.1 forward_list 和 list的区别
- 底层数据结构
| 容器 | 底层实现 |
|---|---|
std::list |
双向循环链表 (每个节点有 prev 和 next 指针) |
std::forward_list |
单向链表 (每个节点只有 next 指针) |
✅
forward_list是 C++11 新增的,设计目标是极致节省内存。
- 内存开销
list节点大小 :
sizeof(T) + 2 * sizeof(void*)(数据 + 前驱指针 + 后继指针)forward_list节点大小 :
sizeof(T) + sizeof(void*)(数据 + 下一节点指针)
💡 对于小对象(如
int),forward_list的内存开销可能比list少近 50%。
- 迭代器类型与遍历方向
| 特性 | list |
forward_list |
|---|---|---|
| 迭代器类型 | 双向迭代器(Bidirectional Iterator) | 前向迭代器(Forward Iterator) |
支持 ++it |
✅ | ✅ |
支持 --it |
✅ | ❌ |
反向遍历(rbegin/rend) |
✅ | ❌ |
⚠️
forward_list不能反向遍历 ,也不能使用需要双向迭代器的算法(如reverse、sort等,除非容器自己提供成员函数)。
- 插入与删除操作
| 操作 | list |
forward_list |
|---|---|---|
| 在任意位置插入/删除 | ✅(需迭代器) | ✅(在当前位置的后一个插入删除) |
push_front() / pop_front() |
✅ | ✅ |
push_back() / pop_back() |
✅ | ❌ 不支持! |
emplace_back() |
✅ | ❌ 没有 back 相关接口 |
📌 关键限制:
forward_list没有尾部指针只有头指针,因此:
- 无法高效访问或操作尾部;
- 插入到末尾需要从头遍历到倒数第二个节点(O(n)),如果强行实现
push_back,效率极低。
- 特殊接口差异
| 功能 | list |
forward_list |
|---|---|---|
size() |
✅ O(1)(C++11 起) | ❌ 没有 size() 成员函数! (需手动 std::distance(begin(), end()),O(n)) |
💡
forward_list为了节省空间,故意省略了size(),强调"零开销抽象"。
- 适用场景
| 场景 | 推荐容器 |
|---|---|
| 需要频繁在头部和尾部插入/删除 | list |
| 只在头部操作,且内存敏感 | forward_list |
| 需要反向遍历或随机前后移动迭代器 | list |
| 实现栈(stack)或前向遍历队列 | forward_list(更省内存) |
| 元素很多且对象较小(如嵌入式系统) | forward_list |
- 代码示例对比
cpp
#include <list>
#include <forward_list>
int main() {
std::list<int> lst = {1, 2, 3};
lst.push_back(4); // ✅
lst.pop_back(); // ✅
auto it = lst.end();
--it; // ✅ 双向迭代
std::forward_list<int> flst = {1, 2, 3};
flst.push_front(0); // ✅
// flst.push_back(4); // ❌ 不存在!
// flst.pop_back(); // ❌ 不存在!
// --flst.begin(); // ❌ 不支持反向移动
}
| 特性 | std::list |
std::forward_list |
|---|---|---|
| 链表类型 | 双向 | 单向 |
| 内存开销 | 较高(两个指针) | 更低(一个指针) |
支持 push_back / pop_back |
✅ | ❌ |
支持 size() |
✅(O(1)) | ❌(需 O(n) 计算) |
| 迭代器类型 | 双向 | 前向 |
| 反向遍历 | ✅ | ❌ |
| 适用场景 | 通用双向操作 | 内存敏感、仅前向操作 |







