@TOC
Lambda表达式+可变模板参数
github地址
💡 前言
在 C++98 中,为了实现简单的 排序、过滤或回调逻辑 ,我们往往需要编写额外的 仿函数类 。
这种方式虽然功能齐全,但显得冗长、啰嗦、缺乏直观性。
自 C++11 起,语言迎来了质的飞跃 ------
Lambda 表达式 的引入让这一切变得简洁灵活:
你无需再定义命名类,就能在算法调用处直接编写逻辑,使代码更清晰、更贴近人类思维。
与此同时,C++11 对 类机制 进行了深度扩展:
新增了 移动语义 、=default 与 =delete 等关键字,使得资源转移更高效 、对象行为更安全 、类设计更现代化。
📘 本文主要内容:
- ✅ Lambda 表达式 的语法结构、捕获方式与底层机制
- ⚙️ C++11 新类特性:默认函数的生成规则与语义设计
- 🧩
=default与=delete的典型用法与实战场景
通过本文的学习,你将理解 C++11 如何在"性能"与"可维护性"之间取得完美平衡,并能编写出既高效又优雅的现代 C++ 代码。
一、Lambda表达式
1. 场景引入
在
C++98中,如果想要对一个数据集合中的元素进行排序 ,可以使用std::sort方法
- 对内置类型排序 :默认为升序
cpp
int array[] = {4, 1, 8, 5, 3, 7, 0, 9, 2, 6};
// 默认按照小于比较,排出来结果是升序
std::sort(array, array + sizeof(array) / sizeof(array[0]));
// 如果需要降序,需要改变元素的比较规则,使用仿函数控制
std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>()); // 降序控制
- 对自定义类型排序 :需要用户定义排序时的比较规则
cpp
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
- 有如上商品类,需要对多个商品类对象进行排序 ,我们可以使用
std::sort(v.begin(), v.end());方法,但默认写法不支持排序,因为该类没有重载比较运算符
cpp
std::vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 },
{ "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 }
};
// std::sort(v.begin(), v.end()); // 默认不支持排序,因为该类没有重载比较运算符
以上场景sort函数的使用要求该自定义类型实现比较运算符的重载 :即operator<或operator>函数,但存在如下问题:
operator<或operator>函数在类中都只能实现一次,如果要对商品的多种不同属性排序 ,如第一次对价格排序,第二次对评价排序,第三次按照名字排序,该情况下operator<或operator>无法满足需求
此时可以使用仿函数来控制排序逻辑 :以下为用于比较的仿函数:
cpp
// 仿函数
// 价格升序和降序
struct ComparePriceGreater {
bool operator()(const Goods& gl, const Goods& gr) {
return gl._price > gr._price;
}
};
struct ComparePriceLess {
bool operator()(const Goods& gl, const Goods& gr) {
return gl._price < gr._price;
}
};
// 评价升序和降序
struct CompareEvaluateGreater {
bool operator()(const Goods& gl, const Goods& gr) {
return gl._evaluate > gr._evaluate;
}
};
struct CompareEvaluateLess {
bool operator()(const Goods& gl, const Goods& gr) {
return gl._evaluate < gr._evaluate;
}
};
- 第一次按照价格排序,第二次按照评价排序。
cpp
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 },
{ "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 }
};
// operator< 或 operator> 不能解决问题
// 针对以上问题,C++98中 只有仿函数能解决问题
std::sort(v.begin(), v.end(), ComparePriceGreater()); // 价格降序
std::sort(v.begin(), v.end(), ComparePriceLess()); // 价格升序
std::sort(v.begin(), v.end(), CompareEvaluateGreater()); // 评价降序
std::sort(v.begin(), v.end(), CompareEvaluateLess()); // 评价升序

但不可避免,别人会使用仿函数 写出可读性极差的代码 ,给我们带来阅读困扰,如:
- 仿函数类命名极差
- 代码中无注释
cpp
// 无注释且命名极差的仿函数
struct Compare1 {
bool operator()(const Goods& gl, const Goods& gr) {
return gl._price > gr._price;
}
};
struct Compare2 {
bool operator()(const Goods& gl, const Goods& gr) {
return gl._price < gr._price;
}
};
struct Compare3 {
bool operator()(const Goods& gl, const Goods& gr) {
return gl._evaluate > gr._evaluate;
}
};
struct Compare4{
bool operator()(const Goods& gl, const Goods& gr) {
return gl._evaluate < gr._evaluate;
}
};
// 排序逻辑
std::sort(v.begin(), v.end(), Compare1()); // 这种写法可读性极差
std::sort(v.begin(), v.end(), Compare2());
std::sort(v.begin(), v.end(), Compare3());
std::sort(v.begin(), v.end(), Compare4());
总结:
- 万一别人写的仿函数命名为
Compare1,Compare2等不规范的命名,如果再没有代码注释,可读性极差 - 比较对象的比较条目多了 之后,对多个条目排序需要写多个仿函数 ,一方面使代码略显臃肿 ,一方面增加阅读困难
综上:
- 随着
C++语法的发展,对于控制比较逻辑来说,仿函数的写法显得太复杂笨重了!
- 每次仅仅为了实现一个比较算法,都要重新去写一个类.
- 如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名
这些都给编程者带来了极大的不便。
因此,在C++11语法中出现了**Lambda表达式**
- 使用**
Lambda表达式**,代替仿函数简化控制逻辑:
cpp
vector<Goods> v = {
{ "苹果", 2.1, 5 }, { "香蕉", 3, 4 },
{ "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 }
};
// 直接使用 Lambda 表达式解决比较问题
std::sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr)->bool {
return gl._price > gr._price;
}); // 价格降序
std::sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr)->bool {
return gl._price < gr._price;
}); // 价格升序
std::sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr)->bool {
return gl._evaluate > gr._evaluate;
}); // 评价降序
std::sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr)->bool {
return gl._evaluate < gr._evaluate;
}); // 评价升序
Lambda表达式 本质是一个==匿名函数对象 ==,接下来介绍Lambda表达式的语法
2. Lambda的语法
Lambda表达式的基本语法格式 如下,包含四个部分,其中捕获列表 和函数体 是必需的,参数列表、mutable和返回类型可选:
cpp
[capture-list] (parameter-list) mutable -> return-type { Function Body }
- 捕获列表 (Capture List) :
- 总是出现在
Lambda函数的开始位置,编译器根据==[]==判断代码是否为Lambda函数。 - 捕捉列表能够捕捉上下文中的变量供
Lambda函数使用,一般捕获范围是父作用域 - 捕获列表 (Capture List)定义了
Lambda表达式如何访问其所在作用域中的外部变量。
- 总是出现在
- 参数列表 (Parameter List) :
- 类似于普通函数的参数列表,定义
lambda接收的参数。如无需传参,可连同()一起省略
- 类似于普通函数的参数列表,定义
-
mutable :默认情况下,按值捕获 的
lambda函数是一个const成员函数,无法对参数进行修改。mutable可以取消其常量性。-
使用该修饰符时,参数列表不可省略(即使参数为空)。
默认情况下,
lambda的operator()是const成员函数,因此不能修改其值捕获的副本。- 不是"值捕获的变量不可修改",而是"值捕获的副本不能在默认情况下修改"。
- 如果加上
mutable,值捕获的副本就能修改;但无论如何,外部变量本身不会受副本影响。
-
-
返回类型 (return-type) :用追踪返回类型形式声明
Lambda函数返回值的类型,通常可以省略。返回值可以省略的情况:- 返回值类型明确时:可省略,由编译器对返回类型进行推导。
- 没有返回值时:可省略
-
函数体 (Function Body) :
lambda要执行的代码逻辑。在该函数体内,可使用其参数和所有捕获到的变量 -
注意 :在
lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。- 因此
C++11中最简单的lambda函数为:[]{};但该lambda函数不能做任何事情
- 因此
Lambda表达式简单使用举例:
cpp
// 最简单的lambda表达式,什么都不做,没有任何意义
[]{};
// 一个打印Hello World的无参lambda,通过函数调用操作符 () 立即执行
[](){ std::cout << "Hello, World!" << std::endl; }();
// 省略参数列表和返回值类型,返回值类型由编译器推导为int
int a = 3, b = 4;
[=]{ return a + 3; }; // 值捕获
// 定义一个 lambda 对象,省略了返回值类型,无返回值类型
auto fun1 = [&](int c){ b = a + c; }; // 引用捕获, 形参为 c
fun1(10) // 通过函数调用操作符() 传参后调用执行
cout << a <<" " << b << endl;
//各部分都很完善的lambda函数
auto fun2 = [=, &b](int c)->int { return b += a + c; }; // b 变量引用捕获,其余变量值捕获
cout << fun2(10) << endl;
// 将一个lambda赋值给auto变量接受,然后调用
auto greet = [](){ std::cout << "Hello, World!" << std::endl; };
greet(); // 调用输出 Hello, World!
- 通过上述例子可以看出,
Lambda表达式实际上可以理解为无名函数 ,该函数无法直接调用 。如果想要直接调用,可借助auto将其赋值给一个函数对象,通过该对象调用
3. 捕获列表详解
捕获列表 是 Lambda 表达式最独特的特性,它让Lambda能够访问外部变量。
Lambda 捕获的范围:
- Lambda 只能捕获Lambda定义处可见的自动变量(automatic variables) ,也就是当前Lambda所处的局部作用域里的局部变量。
- 它不能直接捕获:
- 全局变量 / 全局静态变量(因为它们在全局可访问,不需要捕获)。
- 形参以外的其他函数内部的局部变量(作用域不在可见范围)。
- Lambda 捕获方式灵活多样,下表总结了主要的捕获方式及其含义:
| 捕获方式 | 含义 |
|---|---|
[] |
不捕获任何外部变量。 |
[&] |
以引用方式 捕获所有外部变量 (小心悬挂引用)。 |
[=] |
以值方式 捕获所有外部变量 (捕获的是副本,修改副本不影响原值)。默认情况下,lambda 的 operator() 是 const 成员函数,因此不能修改其值捕获的副本 |
[var] |
仅以值方式 捕获变量 var。 |
[&var] |
仅以引用方式 捕获变量 var。 |
[=, &var] |
默认以值方式捕获所有变量,但变量 var以引用方式捕获。 |
[&, var] |
默认以引用方式捕获所有变量,但变量 var以值方式捕获。 |
[this] |
捕获所在类的 this指针,从而可以访问类成员。 |
传值捕获与mutable
cpp
// 传值捕获
int a = 1, b = 3;
double rate = 2.5555;
auto add1 = [rate](int x, int y) { return (x + y) * rate; };
cout << add1(a, b) << endl;
以上代码为传值捕获 ,将(x + y)的结果乘rate后返回
mutable的使用:以下是一个用于交换 的Lambda表达式。
cpp
// mutable的用法
int c = 8, d = 9;
cout << c << " " << d << endl;
auto swap1 = [c, d]() mutable ->void {
int tmp = c;
c = d;
d = tmp;
};
cout << c << " " << d << endl;
此处为值捕获 ,lambda函数捕获列表中的形参 c、d是外部c,d的副本 ,传值捕获默认无法修改形参 ,因此默认无法完成交换
- 这里形参列表 后使用了
mutable修饰:默认情况下,按值捕获 的lambda函数 是const函数,无法对参数进行修改 。mutable 可以取消其常量性。 - mutable 让捕获到的
c和d可以被修改,但是值传递捕获,依然是外部变量的拷贝,因此没有完成交换

引用捕获
- 使用引用捕获可以完成交换
cpp
int c = 8, d = 9;
cout << c << " " << d << endl;
// 引用捕获
auto swap2 = [&c, &d]()->void {
auto tmp = c;
c = d;
d = tmp;
};
swap2(); // 调用Lambda表达式
cout << c << " " << d << endl;

传值和引用混合捕获
- 无需列举变量,一键捕获所有外部变量
[=]() { }:值捕获 当前局部域所有外部变量[&]() { }:引用捕获 当前局部域所有外部变量
cpp
// 仅引用捕获, 捕获所有变量
int a = 1, b = 2, c = 3, d = 4;
cout << a << " " << b << " " << c << " " << d << endl;
// auto func1 = [=]() { // 一键捕获所有外部变量 值捕获
auto func1 = [&]() { // 一键捕获所有外部变量 引用捕获
++a;
++b;
++c;
++d;
};
func1();
cout << a << " " << b << " " << c << " " << d << endl;

-
值捕获与引用捕获混合:
cpp
int a = 1, b = 2, c = 3, d = 4;
const int e = 6;
cout << a << " " << b << " " << c << " " << d << e << endl;
// 引用捕获 父作用域所有变量,值捕获 c 变量
cout << &e << endl;
auto func1 = [&, c]() {
++a;
++b;
//++c; // 值捕获的 c 不可修改
++d;
cout << &e << endl;
cout << typeid(e).name() << endl;
};
func1();
cout << a << " " << b << " " << c << " " << d << e << endl;

值捕获与引用捕获的区别
cpp
int x = 10;
auto value_lambda = [x] { std::cout << x << std::endl; }; // 值捕获,捕获的是x的副本(此时为10)
auto ref_lambda = [&x] { std::cout << x << std::endl; }; // 引用捕获,捕获的是x的引用
x = 20; // 修改外部x的值
value_lambda(); // 输出:10,因为值捕获的是副本,不受外部x修改的影响
ref_lambda(); // 输出:20,因为引用捕获的是引用,会反映外部x的变化
-
mutable关键字 :默认情况下,以值方式捕获的变量在lambda函数体内是只读的 (const)。使用mutable关键字可以允许修改这些副本(注意:修改的是副本,不影响外部变量本身)。cppint x = 10; auto lambda = [x]() mutable { x += 1; // 没有mutable则编译错误;有mutable则允许修改值捕获的副本 std::cout << "Inside: " << x << std::endl; }; lambda(); // 输出:Inside: 11 std::cout << "Outside: " << x << std::endl; // 输出:Outside: 10 (外部x未改变)
4. Lambda表达式与仿函数
Lambda表达式之间不能相互赋值
Lambda表达式对象之间不能相互赋值,即使看起来类型相同。


Lambda表达式对象之间不能相互赋值 的本质原因是底层类型不同
<lambda_1>和<lambda_2>类型不同。Lambda 的底层是编译器生成了重载了()的类 ,本质是生成了仿函数对象 ,因此类型不同,不能赋值
仿函数和Lambda表达式底层
代码样例
cpp
class Rate {
public:
Rate(double rate) : _rate(rate)
{}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
cpp
int main()
{
// 函数对象
double rate = 0.49;
Rate r1(rate);
r1(10000, 2);
// lambda
auto r2 = [=](double monty, int year)->double { return monty * rate * year; };
r2(10000, 2);
}
main函数调用:
- Lambda 和仿函数 都执行了相同的逻辑:
- 执行的操作都是
money * _rate * year
- 执行的操作都是
底层汇编
我们来查看以上仿函数调用 和Lambda表达式调用 的底层汇编
-
仿函数:
-
Lambda表达式:
-
可以看到,仿函数和Lambda底层都是调用了operator()
-
因此:
Lambda编译后就是一个仿函数- 捕捉列表的本质就是在进行函数传参
5. 注意事项总结
- 父作用域 指包含
lambda函数的语句块 - 语法上捕捉列表可由多个捕捉项组成,并以逗号分割 。如:
[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
- 捕捉列表不允许变量重复传递 ,否则会导致编译错误。如:
[=, a]:编译报错,=已经以值传递方式捕捉了所有变量,再值捕获a重复
- 在块作用域以外的
Lambda函数捕捉列表必须为空。 - 在块作用域中 的
lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。 - lambda表达式之间不能相互赋值
C++ 中 Lambda 表达式**值捕获默认不能修改的设计哲学**:
C++ 设计者希望 lambda 默认是"无副作用"的可重入函数对象。
因此值捕获的变量默认是 const,要修改必须显式 mutable,以示"我知道我在改状态"。
6. Lambda表达式的常见应用场景
-
STL算法中的谓词:Lambda极大地简化了STL算法的使用,无需预先定义函数或函数对象。
cppstd::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // 使用lambda查找第一个偶数 auto it = std::find_if(numbers.begin(), numbers.end(), [](int n) { return n % 2 == 0; }); // 使用lambda对所有元素进行平方操作 std::transform(numbers.begin(), numbers.end(), numbers.begin(), [](int n) { return n * n; }); // 使用lambda计数大于5的元素 int count = std::count_if(numbers.begin(), numbers.end(), [](int n) { return n > 5; }); // 使用lambda降序排序 std::sort(numbers.begin(), numbers.end(), [](int a, int b) { return a > b; }); -
作为回调函数:在异步操作、事件处理(如GUI编程)中非常方便。
cpp// 模拟一个按钮点击回调 button.onClick( []() { std::cout << "Button clicked!" << std::endl; } ); // 在线程中执行任务 std::thread t([]{ std::cout << "Running in a thread." << std::endl; }); t.join();
二、新的类功能
1. 新增的默认成员函数
C++98 的类中,包含6个默认成员函数。默认成员函数就是我们不显式写,编译器会默认生成的成员函数

到了 C++11 标准,默认成员函数新增 移动构造函数 和 移动赋值运算符重载 ,进一步丰富了类的资源转移能力。
1. 默认移动构造函数的生成规则
生成规则:
如果你没有自己手动实现移动构造函数,且没有手动实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动构造。
默认移动构造函数完成什么工作:
默认生成的移动构造函数,对于内置类型成员 会执行逐成员按字节拷贝 ,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
总结如下:
我们不写,编译器自动生成默认移动构造函数,需同时满足两个条件:
- 类中 没有手动实现移动构造函数
- 类中 没有手动实现析构函数、拷贝构造函数、拷贝赋值重载函数的任意一个(这三者只要有一个手动实现,就会抑制默认移动构造的生成)
默认移动构造函数的行为:
- 对 内置类型成员 (如:
int、double、指针等):直接按字节拷贝 ,本质是浅拷贝 - 对自定义类型成员 :检查该成员是否实现了移动构造
- 若实现,则调用其移动构造
- 若未实现,则调用其拷贝构造
2. 默认移动赋值运算符重载的生成规则
生成规则:
如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。
默认赋值运算符重载完成什么工作:
默认生成的移动构造函数,对于内置类型成员 会执行逐成员按字节拷贝 ,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
总结如下:
我们不写,编译器自动生成默认移动赋值移动赋值函数,需同时满足两个条件:
- 类中 没有手动实现移动赋值重载函数
- 类中 没有手动实现析构函数、拷贝构造函数、拷贝赋值重载函数的任意一个(任一手动实现,都会抑制默认移动赋值的生成)
默认移动赋值运算符重载的行为:
- 对 内置类型成员 (如:
int、double、指针等):直接按字节拷贝 ,本质是浅拷贝 - 对自定义类型成员 :检查该成员是否实现了移动赋值
- 若实现,则调用其移动赋值
- 若未实现,则调用其拷贝赋值
注:整体逻辑与默认移动构造高度相似,只是操作对象从"构造新对象"变为"赋值给已有对象"
3. 移动语义与拷贝语义的互斥性
PS:"如果你手动提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。"
若手动提供了 移动构造函数 或 移动赋值重载函数 ,说明当前类是深拷贝的类,默认的拷贝构造函数和拷贝赋值重载函数无法实现深拷贝 ,因此 不会再自动生成默认的拷贝构造函数和拷贝赋值重载函数
(反之,若手动实现了拷贝语义相关函数,默认移动语义函数也不会自动生成,需开发者按需抉择 。)
4. 验证 移动构造 和 移动赋值 的生成条件
cpp
#include <iostream>
#include <utility> // 用于std::move
using namespace std;
namespace mySpace
{
class string
{
private:
const char* _str;
public:
string(const char* str = "")
: _str(str)
{
cout << "mySpace::string 构造函数" << endl;
}
string(const string& other)
: _str(other._str) // 浅拷贝指针
{
cout << "mySpace::string 拷贝构造" << endl;
}
string(string&& other) noexcept
: _str(move(other._str))
{
other._str = nullptr; // 避免悬挂指针
cout << "mySpace::string 移动构造" << endl;
}
string& operator=(const string& other)
{
if (this != &other)
{
_str = other._str;
cout << "mySpace::string 拷贝赋值" << endl;
}
return *this;
}
string& operator=(string&& other) noexcept
{
if (this != &other)
{
_str = move(other._str);
other._str = nullptr; // 避免悬挂指针
cout << "mySpace::string 移动赋值" << endl;
}
return *this;
}
~string()
{
cout << "mySpace::string 析构函数" << endl;
}
};
}
class Person
{
private:
mySpace::string _name; // 字符串类型的姓名
int _age; // 整型的年龄
public:
Person(const char* name = "", int age = 0)
: _name(name) // 调用mySpace::string的构造函数
, _age(age) // 直接初始化int成员
{
cout << "Person 构造函数" << endl;
}
//注意:根据规则,手动实现拷贝构造会抑制默认移动构造的生成
Person(const Person& p)
: _name(p._name) // 调用mySpace::string的拷贝构造
, _age(p._age) // 拷贝int成员
{
cout << "Person 拷贝构造" << endl;
}
//注意:根据规则,手动实现拷贝赋值会抑制默认移动赋值的生成
Person& operator=(const Person& p)
{
if (this != &p) // 防止自赋值
{
_name = p._name; // 调用mySpace::string的拷贝赋值
_age = p._age; // 赋值int成员
cout << "Person 拷贝赋值" << endl;
}
return *this;
}
// 注意:根据规则,手动实现析构函数会抑制默认移动构造和移动赋值的生成
~Person()
{
cout << "Person 析构函数" << endl;
}
/* 注意:
* 1. 由于我们手动实现了"拷贝构造、拷贝赋值和析构函数"
* 2. 根据C++规则,编译器不会自动生成默认的"移动构造"和"移动赋值"
* 3. 如果需要移动语义,必须手动实现
* 所以:如果你想看到默认生成的"移动构造"和"移动赋值",可以将person类中手动实现的"拷贝构造、拷贝赋值和析构函数"注释掉
*/
};
int main()
{
/*--------------------测试:默认构造函数--------------------*/
cout << "Person s1;" << endl;
Person s1; // 调用Person的构造函数,使用默认参数
/*--------------------测试:拷贝构造--------------------*/
cout << "\nPerson s2 = s1;" << endl;
Person s2 = s1; // 调用Person的拷贝构造函数
/*--------------------测试:移动构造--------------------*/
cout << "\nPerson s3 = move(s1);" << endl;
Person s3 = move(s1);
/* 说明:
* 1. 由于Person类手动实现了拷贝构造和析构函数
* 2. 编译器不会生成默认移动构造,这里实际会调用拷贝构造
*/
/*--------------------测试:拷贝赋值--------------------*/
cout << "\ns2 = s1;" << endl;
s2 = s1; // 调用Person的拷贝赋值运算符
/*--------------------测试:移动赋值--------------------*/
cout << "\ns3 = move(s1);" << endl;
s3 = move(s1);
/* 说明:
* 1. 由于Person类手动实现了拷贝赋值和析构函数
* 2. 编译器不会生成默认移动赋值,这里实际会调用拷贝赋值
*/
cout << "\n程序结束,对象开始析构" << endl;
return 0;
}

5. 一个类何时需要手动实现移动构造?
- 类内的成员类型 vs 是否需要手动移动语义
| 成员变量类型 | 特性 | 默认移动语义是否可用 | 是否建议手动实现 |
|---|---|---|---|
| 仅内置类型 | 按值拷贝即可 | ✅ 是 | ❌ 不需要 |
| 仅 STL 容器 / 智能指针 | 自带移动构造/赋值 | ✅ 是 | ❌ 不需要 |
| 含裸指针(堆内存) | 无法安全移动 | ❌ 否 | ✅ 必须手动实现 |
| 含文件句柄/Socket等系统资源 | 独占资源 | ❌ 否 | ✅ 必须手动实现 |
| 含自定义类型,且该类型未定义移动语义 | 会退化为拷贝 | ⚠️ 不推荐 | ✅ 建议手动实现 |
| 含自定义类型,且该类型已定义移动语义 | 可完美转移 | ✅ 是 | ❌ 不需要 |
2. 类的成员变量缺省值
-
成员变量声明时设置缺省值,作用是供构造函数的初始化列表使用。
-
若构造函数未在初始化列表里显式对该成员初始化,初始化列表就会用这个缺省值来初始化成员。
3. 强制生成默认函数的关键字default
用 =default 显式生成默认函数
=default : 主动要求编译器生成默认版本的成员函数
- 解决因手动实现其他函数导致默认函数被抑制的问题
场景实例:
- 若手动实现了拷贝构造函数 ,编译器会默认抑制移动构造函数的生成。
- 此时若仍需要编译器生成默认移动构造,可显式声明:
cpp
class MyClass
{
public:
//1.手动实现拷贝构造,抑制了默认移动构造
MyClass(const MyClass& other)
{
/* 自定义逻辑 */
}
//2.用 =default 显式要求编译器生成默认移动构造
MyClass(MyClass&& other) = default;
};
核心价值:
让开发者在 "需要自定义部分函数(如:拷贝构造)" 的同时,保留编译器自动生成的其他默认函数(如:移动构造),避免手动实现所有函数的冗余。
典型场景:拷贝语义需要自定义,但移动语义仍然安全
有时候,你必须手写拷贝构造,是因为默认拷贝不安全(比如成员是指针),
但编译器生成的移动构造其实是安全的。
例如:
cpp
class Buffer
{
std::unique_ptr<char[]> _data;
size_t _size;
public:
// 自定义拷贝构造(unique_ptr不能拷贝)
Buffer(const Buffer& other)
: _data(new char[other._size])
, _size(other._size)
{
memcpy(_data.get(), other._data.get(), _size);
}
// 👇 让编译器默认生成移动构造
Buffer(Buffer&&) = default;
Buffer& operator=(Buffer&&) = default;
};
✅为什么这样写是合理的?
- 确实需要手写拷贝逻辑 (深拷贝
unique_ptr所指内容,但unique_ptr本身不支持拷贝); - 但
unique_ptr的 移动语义 是完全安全的; - 所以
= default让编译器自动生成高效的移动版本。
📦 结论:
"我手写拷贝,是为了深拷;
我 default 移动,是为了性能。"
4. 禁止生成默认函数的关键字delete
用 =delete 显式删除默认函数
=delete :主动禁止编译器生成默认版本的成员函数
- 替代 C++98 中 "将函数声明为
private且不实现" 的蹩脚写法
如果要禁止一个类的拷贝构造 ,C++98 和C++11 有不同风格的做法:
- C++98 旧写法(不推荐) :若要禁止拷贝构造,需将其声明为
private且不实现,调用时才会报错(但报错在链接阶段,不直观):
cpp
class NonCopyable
{
private:
// 声明但不实现,外部调用会触发链接错误
NonCopyable(const NonCopyable&);
};
- C++11 新写法(推荐) :用
=delete直接标记函数为 "删除",编译阶段即可报错 ,更高效且语义清晰:
cpp
class NonCopyable
{
public:
// 用 =delete 显式禁止编译器生成拷贝构造
NonCopyable(const NonCopyable&) = delete;
};
int main()
{
NonCopyable a;
// 编译报错:调用了被删除的函数(拷贝构造)
NonCopyable b = a;
}
典型应用场景:
- 禁止对象拷贝 (如 :
std::unique_ptr需独占资源) - 禁止某些无意义的默认函数 (如:禁止基本类型的隐式转换构造)
5. =default 和 =delete 对比
| 对比项 | =default |
=delete |
|---|---|---|
| 功能 | 显式要求编译器 生成默认版本 的函数 | 显式要求编译器 禁用(删除) 该函数 |
| 主要目的 | 保留编译器自动生成的行为,防止被其他定义抑制 | 禁止某些函数被使用(例如拷贝、赋值、隐式转换等) |
| 典型使用场景 | 当类已定义了其他特殊成员函数(例如析构或拷贝构造)导致移动构造或默认构造被抑制时,希望仍然自动生成默认版本 | 防止误用(例如禁止拷贝、禁止隐式类型转换、禁止特定构造方式) |
| 使用语法 | ClassName(const ClassName&) = default; |
ClassName(const ClassName&) = delete; |
| 作用时间 | 编译期,指示编译器自动生成函数体 | 编译期,指示编译器删除函数定义(不能被调用) |
| 是否可以应用于所有成员函数 | ✅ 可以应用于构造函数、析构函数、拷贝/移动构造、赋值运算符等默认生成的函数 | ✅ 可以用于任何函数(包括普通函数与模板),不仅限于默认成员函数 |
| 是否影响函数重载解析 | ❌ 不影响重载选择(函数依然存在) | ✅ 被删除的函数仍参与重载解析,但最终无法调用(会导致编译错误) |
| 常见用途举例 | 强制生成移动构造函数:MyClass(MyClass&&) = default; |
禁止拷贝构造函数:MyClass(const MyClass&) = delete; |
| 与手动定义的区别 | 告诉编译器"你帮我生成" | 告诉编译器"别生成也别用" |
| 语义意图 | ✅ "我希望保留默认行为" | ❌ "我希望彻底禁止此行为" |
| 出现版本 | C++11 引入 | C++11 引入 |
示例:
cpp
class Example
{
public:
Example() = default; // 显式要求生成默认构造函数
Example(const Example&) = delete; // 禁止拷贝
Example(Example&&) = default; // 强制生成移动构造
Example& operator=(Example&&) = delete; // 禁止移动赋值
};
✅
=default:想让编译器帮你生成。❌
=delete:不想让编译器帮你生成,也不想别人用。
🚀 结语
C++11 的到来,让 C++ 从传统的"面向对象"语言,正式迈入了"现代化与高抽象化"的时代。
- Lambda 表达式 ------ 让算法逻辑更简洁自然
- 移动语义 ------ 让资源转移更高效安全
=default/=delete------ 让类行为更可控、更清晰
掌握这些特性,不仅仅是语法层面的提升,更意味着你已经踏入了现代 C++ 思维方式 的门槛。
当你能够熟练地在项目中使用 Lambda 与移动语义 时,也就真正理解了什么是------
💬 "高效与优雅并存的现代 C++"
以上就是本文的所有内容了,如果觉得文章对你有帮助,欢迎 点赞⭐收藏 支持!如有疑问或建议,请在评论区留言交流,我们一起进步
分享到此结束啦
一键三连,好运连连!你的每一次互动,都是对作者最大的鼓励!
征程尚未结束,让我们在广阔的世界里继续前行!🚀

