【C++11】Lambda表达式+新的类功能
@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方法
- 对内置类型排序:默认为升序
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>()); // 降序控制
- 对自定义类型排序:需要用户定义排序时的比较规则
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());方法,但默认写法不支持排序,因为该类没有重载比较运算符
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>无法满足需求
此时可以使用仿函数来控制排序逻辑:以下为用于比较的仿函数:
// 仿函数
// 价格升序和降序
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;}
};
- 第一次按照价格排序,第二次按照评价排序。
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()); // 评价升序

但不可避免,别人会使用仿函数写出可读性极差的代码,给我们带来阅读困扰,如:
- 仿函数类命名极差
- 代码中无注释
// 无注释且命名极差的仿函数
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表达式**,代替仿函数简化控制逻辑:
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和返回类型可选:
[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表达式简单使用举例:
// 最简单的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
// 传值捕获
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表达式。
// 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可以被修改,但是值传递捕获,依然是外部变量的拷贝,因此没有完成交换

引用捕获
- 使用引用捕获可以完成交换
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;

传值和引用混合捕获
- 无需列举变量,一键捕获所有外部变量
[=]() { }:值捕获当前局部域所有外部变量[&]() { }:引用捕获当前局部域所有外部变量
// 仅引用捕获, 捕获所有变量
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;

-
值捕获与引用捕获混合:
-
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;
值捕获与引用捕获的区别
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关键字可以允许修改这些副本(注意:修改的是副本,不影响外部变量本身)。int 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表达式底层
代码样例
class Rate {
public:Rate(double rate) : _rate(rate){}double operator()(double money, int year){return money * _rate * year;}
private:double _rate;
};
int main()
{// 函数对象double rate = 0.49;Rate r1(rate);r1(10000, 2);// lambdaauto 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算法的使用,无需预先定义函数或函数对象。
std::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编程)中非常方便。
// 模拟一个按钮点击回调 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. 验证 移动构造 和 移动赋值 的生成条件
#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: 主动要求编译器生成默认版本的成员函数
- 解决因手动实现其他函数导致默认函数被抑制的问题
场景实例:
- 若手动实现了拷贝构造函数,编译器会默认抑制移动构造函数的生成。
- 此时若仍需要编译器生成默认移动构造,可显式声明:
class MyClass
{
public://1.手动实现拷贝构造,抑制了默认移动构造MyClass(const MyClass& other) { /* 自定义逻辑 */ }//2.用 =default 显式要求编译器生成默认移动构造MyClass(MyClass&& other) = default;
};
核心价值:
让开发者在 “需要自定义部分函数(如:拷贝构造)” 的同时,保留编译器自动生成的其他默认函数(如:移动构造),避免手动实现所有函数的冗余。
典型场景:拷贝语义需要自定义,但移动语义仍然安全
有时候,你必须手写拷贝构造,是因为默认拷贝不安全(比如成员是指针),
但编译器生成的移动构造其实是安全的。
例如:
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且不实现,调用时才会报错(但报错在链接阶段,不直观):
class NonCopyable
{
private:// 声明但不实现,外部调用会触发链接错误NonCopyable(const NonCopyable&);
};
- C++11 新写法(推荐):用
=delete直接标记函数为 “删除”,编译阶段即可报错,更高效且语义清晰:
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 引入 |
示例:
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++”
以上就是本文的所有内容了,如果觉得文章对你有帮助,欢迎 点赞⭐收藏 支持!如有疑问或建议,请在评论区留言交流,我们一起进步
分享到此结束啦
一键三连,好运连连!你的每一次互动,都是对作者最大的鼓励!
征程尚未结束,让我们在广阔的世界里继续前行!🚀


