5.1 默认的移动构造和移动赋值
1. 默认成员函数概述
- C++98 的 6 个默认成员函数:构造函数、析构函数、拷贝构造函数、拷贝赋值重载、取地址重载、const取地址重载。
- 核心重点:前 4 个函数最为重要,后 2 个用处不大。
- 默认行为:如果不手动编写,编译器会自动生成默认版本。
- C++11 新增 2 个默认成员函数:移动构造函数、移动赋值运算符重载。
2. 默认移动构造函数的生成与规则
- 生成条件:如果没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,编译器会自动生成一个默认移动构造。
- 执行规则:
- 内置类型成员:执行逐成员按字节拷贝(浅拷贝)。
- 自定义类型成员:检查该成员是否实现了移动构造。如果实现了,则调用其移动构造;如果没有实现,则退而调用其拷贝构造。
3. 默认移动赋值运算符重载的生成与规则
- 生成条件:如果没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,编译器会自动生成一个默认移动赋值。
- 执行规则:与默认移动构造完全类似:
- 内置类型成员:执行逐成员按字节拷贝赋值。
- 自定义类型成员:检查该成员是否实现了移动赋值。如果实现了,则调用其移动赋值;如果没有实现,则退而调用其拷贝赋值。
4. 移动与拷贝函数的互斥规则
- 如果你手动提供了移动构造或者移动赋值,编译器将不会自动提供拷贝构造和拷贝赋值。
classPerson{public:Person(constchar*name="",intage=0):_name(name),_age(age){}/* 注释掉拷贝构造、拷贝赋值和析构函数, 编译器会自动生成默认的拷贝和移动操作 *//* Person(const Person& p) : _name(p._name) , _age(p._age) {} *//* Person& operator=(const Person& p) { if (this != &p) { _name = p._name; _age = p._age; } return *this; } *//* ~Person() {} */private:bit::string _name;// 自定义类型int_age;// 内置类型};intmain(){Person s1;Person s2=s1;// 调用编译器默认生成的拷贝构造Person s3=std::move(s1);// 调用编译器默认生成的移动构造Person s4;s4=std::move(s2);// 调用编译器默认生成的移动赋值return0;}5.2 default 和 delete 关键字
C++11 引入了=default和=delete关键字,让程序员能够更精细、更直观地控制类中默认成员函数的生成与使用。
1.=default:显式指定生成默认函数
- 使用场景:当你需要某个默认函数,但由于某些原因(例如自定义了拷贝构造),编译器不再自动生成它时,可以使用
=default显式要求编译器生成该函数的默认版本。 - 典型示例:根据 C++11 规则,如果类中提供了拷贝构造函数,编译器就不会自动生成移动构造函数。此时,可以使用
=default显式指定移动构造函数的生成:MyClass(MyClass&&)=default;// 显式要求编译器生成默认移动构造- 核心优势:
- 明确意图:明确表明你的意图,提高代码可读性,让其他开发者知道这是有意生成的。
- 保持性能:显式默认化的特殊成员函数仍被认为是“平凡的(trivial)”,没有额外的性能开销,且类依然可以保持为 POD 类型。
2.=delete:显式删除函数(禁止生成/调用)
- 使用场景:如果想要限制某些默认函数的生成,禁止对象进行某种操作。
- C++98 的旧做法:将函数设置为
private并且只声明不定义。- 缺点:代码不够直观;错误检测被延迟到链接阶段(而非编译阶段);类的成员函数和友元函数依然可以调用它,只是链接时会报错。
- C++11 的新做法:在函数声明后加上
=delete,指示编译器不生成对应函数的默认版本。被=delete修饰的函数称为删除函数(deleted function)。
核心优势:MyClass(constMyClass&)=delete;// 显式删除拷贝构造
编译期报错:任何尝试调用删除函数的代码都会导致编译错误,错误信息比 C++98 的链接错误更清晰。
绝对禁止:使用 =delete 删除的函数无法通过任何方法调用,即使是成员函数或友元函数中的代码也无法调用。
适用范围广:不仅适用于成员函数,还可以用于非成员函数、运算符重载以及模板函数(例如禁止特定类型的隐式转换或模板实例化)。
5.4 final 与 override
1.override:显式标记重写基类虚函数
- 核心作用:用于派生类中,显式声明并强制校验该函数是否真正重写了基类的虚函数。
- 解决的问题:防止因函数名拼写错误、参数列表不匹配、返回值类型错误或漏写
virtual等疏忽,导致“看似重写,实则未重写(变成隐藏或新增函数)”的隐性错误。 - 核心优势:将原本运行时的多态失效问题,提前到编译期暴露并报错,极大降低了调试成本,提高了代码安全性。
2.final:禁止继承或禁止重写
- 作用于类:标记在类名后,表示该类是“最终类”,禁止被其他类继承(终止继承链)。
- 作用于虚函数:标记在虚函数声明后,表示该函数是“最终实现”,禁止在任何派生类中被重写。
- 核心优势:
- 保障安全与明确意图:保护核心类或关键逻辑不被意外扩展或篡改。
- 编译器优化:明确告知编译器该函数无拓展、无重写,编译器可执行去虚拟化优化(消除虚函数查表间接寻址开销,直接静态绑定调用),提升代码执行效率。
6. STL 中的一些变化
1. 新增容器
- 核心重点:新增的容器中最具实际价值的是
unordered_map和unordered_set(基于哈希表实现,查找效率极高)。这两个容器在前面已经进行了非常详细的讲解。 - 其他新增容器:如
std::array、std::forward_list、std::tuple等,大家只需了解其基本概念即可。
2. 容器的新接口
- 核心重点:最重要的新接口与右值引用和移动语义相关。主要包括:
push_back/insert的右值引用版本(支持移动构造,减少拷贝)。emplace系列接口(如emplace_back,支持传入参数包直接在容器内存中构造对象,性能最优)。- 支持
std::initializer_list版本的构造函数(支持使用花括号{}进行统一初始化)。 - 以上核心接口在前面均已详细讲解。
- 其他小改动:还有一些无关痛痒的接口,如
cbegin()/cend()(返回 const 迭代器)、crbegin()/crend()(返回 const 反向迭代器)等,在实际开发中遇到时查阅文档即可。
3. 遍历方式革新
- 范围 for 循环(Range-based for loop):极大地简化了容器的遍历操作,无需再手动编写复杂的迭代器或下标,该特性在容器部分也已详细讲解。
7. Lambda 表达式
7.1 Lambda 表达式语法
1. 本质与类型
- 本质:Lambda 表达式本质上是一个匿名函数对象(闭包)。与普通函数不同的是,它可以直接定义在函数内部。
- 类型接收:从语法使用层面而言,Lambda 表达式没有显式的类型名。因此,我们一般使用
auto关键字或者模板参数来定义对象,以接收 Lambda 对象。
2. 语法格式
[capture-list](parameters)->return_type{function_body}各部分详细说明
[capture-list](捕获列表):- 位置:总是出现在 Lambda 函数的开始位置,编译器正是根据
[]来判断接下来的代码是否为 Lambda 表达式。 - 作用:用于捕捉上下文(外部作用域)中的变量,供 Lambda 函数体内部使用。
- 方式:支持传值捕捉和传引用捕捉(具体细节在 7.2 中细讲)。
- 注意:即使捕获列表为空,
[]也绝对不能省略。
- 位置:总是出现在 Lambda 函数的开始位置,编译器正是根据
(parameters)(参数列表):- 作用:与普通函数的参数列表功能类似。
- 省略规则:如果不需要传递参数,可以连同
()一起省略。
-> return_type(返回值类型):- 作用:使用追踪返回类型的形式声明函数的返回值类型。
- 省略规则:如果没有返回值,或者返回值类型明确可由编译器自动推导,此部分通常可以省略。
{ function_body }(函数体):- 作用:包含 Lambda 的具体实现逻辑,与普通函数完全类似。
- 内部访问:在函数体内,除了可以使用传入的参数外,还可以使用所有通过捕获列表捕捉到的外部变量。
- 注意:即使函数体为空,
{}也绝对不能省略。
7.2 捕获列表详解
捕获的必要性:
Lambda 表达式默认只能使用其函数体内部定义的变量和参数列表中的参数。如果需要在 Lambda 内部使用外层作用域中的变量,就必须通过捕获列表进行捕获。捕获方式分类:
- 显式捕获:在捕获列表中明确指定变量及其捕获方式,多个变量之间用逗号分隔。例如
[x, y, &z]表示x和y按值捕获,z按引用捕获。 - 隐式捕获:在捕获列表中仅写
=或&。[=]表示隐式值捕获,[&]表示隐式引用捕获。编译器会自动捕获 Lambda 函数体内实际使用到的外部变量。 - 混合捕获:结合隐式捕获与显式捕获使用。
[=, &x]:默认按值捕获,唯独x按引用捕获。[&, x, y]:默认按引用捕获,唯独x和y按值捕获。- 语法规则:使用混合捕获时,第一个元素必须是
&或=。当默认捕获为&时,后续显式捕获的变量必须是值捕获;当默认捕获为=时,后续显式捕获的变量必须是引用捕获。
- 显式捕获:在捕获列表中明确指定变量及其捕获方式,多个变量之间用逗号分隔。例如
捕获的作用域与限制:
- Lambda 表达式只能捕获定义在它之前的局部变量。
- 静态局部变量和全局变量不需要也不能被捕获,Lambda 内部可以直接访问它们。
- 如果 Lambda 表达式定义在全局作用域中,其捕获列表必须为空。
mutable关键字与常量性:- 默认情况下,Lambda 的函数调用运算符是
const的,这意味着按值捕获的变量在 Lambda 内部是只读的,无法被修改。 - 在参数列表后加上
mutable关键字可以解除这一限制,允许修改按值捕获的变量副本(注意:修改的仅是副本,不会影响外部原始变量)。 - 注意:一旦使用了
mutable关键字,即使 Lambda 没有参数,参数列表()也绝对不能省略。
- 默认情况下,Lambda 的函数调用运算符是
7.3 Lambda 的应用
1. 替代传统可调用对象
在学习 Lambda 表达式之前,C++ 中常用的可调用对象主要是函数指针和仿函数(函数对象):
- 函数指针:类型定义较为繁琐,且无法携带状态。
- 仿函数:需要额外定义一个类并重载
operator(),代码量较大,相对麻烦。 - Lambda 的优势:使用 Lambda 定义可调用对象,既简单又方便,可以直接在需要的地方内联编写逻辑,极大地提高了代码的可读性和开发效率。
2. 广泛的应用场景
除了配合 STL 算法(如sort、find_if等)使用外,Lambda 在其他场景中也极为好用:
- 多线程编程:在创建线程(如
std::thread)时,直接定义线程的执行逻辑。 - 智能指针:在创建智能指针(如
std::shared_ptr)时,使用 Lambda 定制专属的删除器(Deleter)。
3. 实战案例:配合 STL 进行自定义排序
在需要实现多种自定义比较逻辑时,Lambda 的优势尤为明显。以下是一个商品排序的对比示例:
传统仿函数做法:
structGoods{string _name;// 名字double_price;// 价格int_evaluate;// 评价Goods(constchar*str,doubleprice,intevaluate):_name(str),_price(price),_evaluate(evaluate){}};// 需要为每一种比较规则单独定义一个结构体structComparePriceLess{booloperator()(constGoods&gl,constGoods&gr){returngl._price<gr._price;}};structComparePriceGreater{booloperator()(constGoods&gl,constGoods&gr){returngl._price>gr._price;}};intmain(){vector<Goods>v={{"苹果",2.1,5},{"⾹蕉",3,4},{"橙⼦",2.2,3},{"菠萝",1.5,4}};// 类似这样的场景,我们实现仿函数对象或者函数指针⽀持商品中// 不同项的⽐较,相对还是⽐较⿇烦的,那么这⾥lambda就很好⽤了sort(v.begin(),v.end(),ComparePriceLess());sort(v.begin(),v.end(),ComparePriceGreater());sort(v.begin(),v.end(),[](constGoods&g1,constGoods&g2){returng1._price<g2._price;});sort(v.begin(),v.end(),[](constGoods&g1,constGoods&g2){returng1._price>g2._price;});sort(v.begin(),v.end(),[](constGoods&g1,constGoods&g2){returng1._evaluate<g2._evaluate;});sort(v.begin(),v.end(),[](constGoods&g1,constGoods&g2){returng1._evaluate>g2._evaluate;});return0;}7.4 Lambda 的底层原理
1. 本质:编译器生成的仿函数对象
Lambda 表达式和范围for循环一样,本质上都是语法糖。从汇编指令层面来看,底层压根就没有 Lambda 和范围for这样的概念。范围for底层被展开为迭代器操作,而Lambda 底层则是一个仿函数(Functor)对象。
2. 编译器的转换过程
当我们编写一个 Lambda 表达式时,编译器会在后台自动生成一个对应的匿名仿函数类。具体转换规则如下:
- 匿名类名:编译器会按照一定的内部规则生成一个唯一的类名(类似于
__Lambda123的形式),以保证不同的 Lambda 表达式生成的类名互不冲突。 operator()重载:Lambda 的参数列表、返回值类型和函数体,会被直接转换为该匿名仿函数类中operator()的参数、返回类型和函数体。这使得 Lambda 对象可以像普通函数一样被调用。- 捕获列表与成员变量:捕获列表中的变量,本质上会被生成为该仿函数类的成员变量。
- 按值捕获的变量,会成为类中的普通成员变量。
- 按引用捕获的变量,会成为类中的引用成员变量。
- 构造函数:编译器会自动为该匿名类生成一个构造函数。捕获列表中的变量就是构造函数的实参,用于在创建 Lambda 对象时初始化其内部的成员变量。
- 隐式捕获的处理:对于隐式捕获(如
[=]或[&]),编译器会在编译期分析 Lambda 函数体,自动识别出实际使用了哪些外部变量,并将它们作为实参传递给构造函数。