
关注我,学习c++不迷路:
专栏如下:
后续会更新更多有趣的小知识,关注我带你遨游知识世界

期待你的关注。

在2026年里,我会改变我写博客的技巧,向优秀的博主学习,新的一年里,一起加油:
文章目录
- [1. 前言:](#1. 前言:)
- [2. c++11:类的新变化:](#2. c++11:类的新变化:)
-
- [2-1 c++11之前的类的默认成员函数:](#2-1 c++11之前的类的默认成员函数:)
- [2-2 c++11之后:移动拷贝构造和移动赋值重载:](#2-2 c++11之后:移动拷贝构造和移动赋值重载:)
- [2-3 两个关键词 default 和delete:](#2-3 两个关键词 default 和delete:)
- 2-4:编译器生成默认函数的苛刻条件:
-
- [2-4-1 默认构造函数(较为简单):](#2-4-1 默认构造函数(较为简单):)
- [2-4-2 析构函数(较为简单)](#2-4-2 析构函数(较为简单))
- [2-4-3 Class(const Class&) operator=(const Class&)](#2-4-3 Class(const Class&) operator=(const Class&))
- [2-4-4 Class(Class&&) operator=(Class&&))](#2-4-4 Class(Class&&) operator=(Class&&)))
- [2-5 总结与经验:](#2-5 总结与经验:)
- [3. Lambda函数:](#3. Lambda函数:)
-
- [3-1 简单的定义和样式:](#3-1 简单的定义和样式:)
- [3-2 lambda函数的使用:](#3-2 lambda函数的使用:)
- [3-3 lambda函数的本质:](#3-3 lambda函数的本质:)
- [4 .包装器function:](#4 .包装器function:)
- [5. bind包装器](#5. bind包装器)
- 最后的总结:
1. 前言:
在上一篇文章中,我们已经讲述了c++11的新特性万能可变模板以及他的底层原理。
本篇文章主要讲述三个方向:
- 类的新变化:这个主要是对c++和c++11所引起的变化进行总结。
- Lambda函数:这个是c++11新引入的特性函数。
- 封装器:也是c++11引进的的特点。
我将由浅入深,完成对这些知识的讲解。
2. c++11:类的新变化:
2-1 c++11之前的类的默认成员函数:
在c++11之前,我们已经有了六个默认成员函数:
- 构造函数:用来"初始化"一个对象。在你创建对象时自动调用,保证对象一出生就处于一个合法的状态。
- 析构函数:完成对类资源的释放。在对象销毁时自动调用,比如释放动态内存、关闭文件、释放锁等。
- 拷贝构造函数:同样也是用来完成对对象的初始化,用一个已有的对象来构造一个新对象(拷贝初始化)。
- 拷贝赋值重载:用一个已有对象给另一个已有对象赋值(对象已经存在)。注意的是原本的对象已经初始化,已经存在了。
- 取地址重载:决定"取对象的地址"(即 &a)时做什么,默认返回对象的真实地址。
- const取地址重载:这个与第五个不同的是:如果是const对象就完成,就使用这哥函数。
这些都是默认函数,如果不写,编译器就会自动生成。
大多数普通类:只写构造和析构就够用了。拷贝构造和拷贝赋值:只有当你"管理资源"(比如有 new/delete)时,才需要认真考虑"三法则/五法则"(写了其中一个,一般要把其他几个也写好)。
而对于取地址重载 & const 取地址重载:基本上不用写,知道它们存在、知道默认行为是返回地址即可。
一表总结c++11之前的六大默认函数:
| 函数 | 什么时候自动调用 | 默认会给你吗? | 最典型的需要自己写的场景 |
|---|---|---|---|
| 构造函数 | 对象创建时 A a; A a(10); |
有默认构造(如果你没写任何构造) | 需要特定初始化逻辑、依赖关系等 |
| 析构函数 | 对象销毁时(离开作用域、delete) | 有默认析构 | 管理资源(内存、文件、锁等)需要释放 |
| 拷贝构造函数 | 拷贝初始化:A a2(a1);、传参、返回 |
默认逐成员拷贝 | 管理资源需要深拷贝,避免浅拷贝问题 |
| 拷贝赋值运算符 | 对象赋值:a1 = a2; |
默认逐成员赋值 | 管理资源需要深拷贝,注意自赋值 |
| 取地址重载 | &a |
默认返回真实地址 | 很少自己写,除非特殊设计 |
| const 取地址重载 | &ca(ca 是 const 对象) |
默认返回 const 地址 | 很少自己写,除非特殊设计 |
2-2 c++11之后:移动拷贝构造和移动赋值重载:
- 移动拷贝构造:利用左值的特点,直接交换内部的资源,完成对该对象的构造,相比与普通的拷贝构造。效率更高。
- 移动赋值重载:与上面的不一样的是:该对像已经完成初始化,但是需要左值完成对对象的赋值,也是利用左值的特点完成快速资源交换。
这两个默认函数极大的提高了c++11的开发效率。在特定的情况下,比如传值返回的函数。提高效率。
第一,在这里,你如果写了这两个函数,编译器就默认不会生成原本的拷贝构造和赋值重载。
第二,这两个默认成员函数对内置类型都是逐字节拷贝(其实写了&& 算是万能引用了)
第三,对自定义类型则是如果完成了移动构造就是移动构造,没有实现就是拷贝构造
2-3 两个关键词 default 和delete:

我在图片上已经标出了两个关键词:
- default :明示告诉编译器我会使用这个函数的默认成员函数。这里可能是由于你自己实现了其他的默认函数(后面我们会讲什么时候会总动生成默认函数)。如果你没有写这个函数你可能会会报错:详见图片情况1。
- delete:显式"删除"函数,禁止调用,只要在函数声明尾部加上这个,你无论怎么调用,他都是错误的。它相当于告诉编译器:"把这个函数标记为'已删除',任何地方调用它都直接编译错误"。比起 =default,=delete 能用得更广。普通的函数也能用上。

只需要做出下面的变化就可以了:

或者:

我们可以打印来试试,打开调试:

2-4:编译器生成默认函数的苛刻条件:
在这里我们还想讲讲什么时候编译器会自动生成默认成员函数,或者我们可以来县以下:编译器并不是"不想帮你生成",而是它非常胆小和保守。只要它发现生成了这个函数可能导致逻辑错误(如浅拷贝崩溃)、资源泄漏、或者根本做不到(如成员禁止这种操作),它就会直接拒绝生成(声明为 deleted)或者干脆不声明。
在这里我们将分成四组,来完成对编译器行为进行讲解:
2-4-1 默认构造函数(较为简单):
有三种情况会导致不会生成默认构造函数:
- 引用成员 且没有类内初始化器:引用(int&)必须在初始化时绑定到一个对象。默认构造函数并不知道要把引用绑定给谁,所以它生成不了。
- . Const 成员 且没有类内初始化器,且该类型没有默认构造. Const 成员 且没有类内初始化器,且该类型没有默认构造。
- 成员或基类"不可默认构造"
如果类里有一个成员 Member m;,而 Member 类没有默认构造函数(或者被删除了),那么当前类的默认构造也无法生成。
三种情况的代码如下:
cpp
struct A {
int& ref; // 没有写 int& ref = someVar;
// A() = delete; // 编译器被迫删除
};
cpp
struct B {
const int x;
// B() = delete; // 没法初始化 x
};
cpp
struct NoDefault {
NoDefault(int); // 只有带参构造
};
struct C {
NoDefault m;
// C() = delete; // m 无法默认构造
};
2-4-2 析构函数(较为简单)
析构对应的就是完成资源释放。那么他在实战中只要能保证它销毁时能正确调用成员和基类的析构即可。
编译器无法生成只要两个:
苛刻条件(导致被 deleted):
如果类的任何非静态数据成员或基类的析构函数是:
- 被删除的(=delete);
- 无法访问的(比如是 private 的,且当前类不是它的友元)。
那么编译器不敢生成析构函数,因为如果生成了,对象销毁时就会出错。
cpp
struct NoDestroy {
~NoDestroy() = delete; // 禁止销毁
};
struct D {
NoDestroy nd;
// ~D() = delete; // 因为无法调用 nd.~NoDestroy()
};
2-4-3 Class(const Class&) operator=(const Class&)
是最容易出坑的地方。拷贝操作的生成逻辑是:只要你没声明移动操作,我通常就声明一个拷贝操作。但是,生成出来是否"合法"(是否被 deleted),条件非常苛刻。
拷贝构造的苛刻条件(被 deleted):
- 成员不可拷贝:某个成员的拷贝构造被删除了(比如 std::unique_ptr)。
- 成员的拷贝构造是私有的/不可访问的。
- 类声明了移动操作:如果你写了 移动构造 或 移动赋值,编译器会直接把 拷贝构造 标记为删除(C++11 规定:想移动就得显式声明拷贝)。
拷贝赋值的苛刻条件(被 deleted):
除了包含上述拷贝构造的条件外,还有两个著名的"天敌":
- 引用成员:引用一旦绑定就不能改。默认的赋值操作是 a.ref = b.ref,这试图让引用重新绑定,这是非法的。
cpp
struct E {
int& ref;
// operator= 被删除
};
- Const 成员:const 变量初始化后不能改。默认的赋值操作试图修改 const 成员,非法。
cpp
struct F {
const int x;
// operator= 被删除
};
2-4-4 Class(Class&&) operator=(Class&&))
这也是生成条件最苛刻的一组。编译器非常不愿意帮你生成移动函数,因为它默认认为你的类可能像老式 C++ 类一样,只是简单的内存拷贝,移动并不安全。
自动生成的门槛(必须同时满足):
- 完全没有手动声明:没有声明拷贝构造、拷贝赋值、移动构造、移动赋值、析构函数。
- 需要的成员都支持移动:所有成员和基类都有可用的移动操作。
苛刻条件(导致被 deleted):
- 成员不可移动:某个成员的移动操作被删除或不可访问。
- 移动析构/拷贝有副作用:(这个比较复杂,通常不用记,标准规定如果拷贝构造不是 constexpr 或抛异常等可能影响移动生成的判定)。
最重要的现象------"抑制":
如果你写了析构函数、拷贝构造或拷贝赋值 中的任何一个,编译器就不会生成移动操作(不是删除,是根本不声明) 。这时如果你尝试 std::move(obj),它会退而求其次调用拷贝操作。
cpp
struct G {
~G() { /* 手动写了析构 */ }
// 此时:移动构造 和 移动赋值 不会被生成!
// 哪怕你的成员看起来非常适合移动。
};
2-5 总结与经验:

实战建议:如何面对这些苛刻条件?
-
不要猜,用 =default 和 =delete 明确表达意图。
如果编译器没生成你要的函数(报错说使用了 deleted function),不要纠结为什么没生成,直接检查成员。
- 如果成员是 unique_ptr,你就知道不能拷贝,必须 =delete 拷贝操作。
- 如果成员是引用,你就知道不能赋值。
-
遵循 Rule of Zero(零法则)。
尽量不要在类里直接管理资源(不用裸指针 new)。用 std::string, std::vector, std::unique_ptr 代替。这些标准库类的默认成员函数行为都是完美的。你的类里只要不写这些函数,编译器生成的也就是完美的。
-
一旦写了析构或拷贝,就要考虑"五法则"。
如果你写了析构函数(比如为了 free 内存),一定要记得:
- 编译器已经不会给你生成移动操作了。
- 你很可能需要显式地 =default 拷贝构造,或者 =delete 它。
这种"苛刻条件"的设计初衷是为了安全:宁可编译不通过,也不能生成一个运行时崩溃的函数。
3. Lambda函数:
3-1 简单的定义和样式:
先来看一小端代码:
cpp
//例1:简单的lambda函数:
//1.[]为捕获列表:现在为空,没有捕捉函数外面的变量。
//2.()里面的为自定义变量,类似于普通函数的参数列表。
//3.返回值类型:这里省略。
//4.{}里面为函数体。正常使用
auto sum = [](int a, int b) {return a + b; };
std:: cout << sum(1,2) << std::endl;
这里我们就可以看到一个Lambda函数了,似乎和正常函数很相似:
捕获列表 \] ( 参数列表 ) → 返回类型 { 函数体 } \[捕获列表\](参数列表) \\rightarrow 返回类型 \\{ 函数体 \\} \[捕获列表\](参数列表)→返回类型{函数体}
我们对照上面的lambda函数对照就可以知道:这里的捕获列表为空。没有从外面进行捕捉,()里面的是函数参数列表,里面自己定义了a和b两个变量。
返回类型省略,这里应该是int。最后是函数体。

我们可以看到和正常函数是一致的。
### 3-2 lambda函数的使用:
#### 示例1:直接使用,写了就立马调用:
```cpp
[]() {
std::cout << "hello world" << std::endl;
}();
```
记住后面一个(),这个不能省略,这就表示立马调用。我们也可以从这里看到lambda函数似乎本身不是一个函数,只是长的很像普通函数(后面我们会讲这是什么)
#### 示例2:捕捉外面的变量的值,需要当作参照(只读):
```cpp
int a = 10;
auto isB = [a](int b) {
b = a;
std::cout << a << std::endl;
};
isB(1111);
std::cout << a << std::endl;
```
这里结果两个都是10,如果尝试对a进行修改呢?

#### 实例3:尝试引用传入变量:
```cpp
int a = 10;
auto isB = [&a](int b) {
b = a;
a = 100;
std::cout << a << std::endl;
};
isB(1111);
std::cout << a << std::endl;
```
此时我们可以惊奇的发现:这里似乎又可以改变。和正常函数一样的。
#### 示例4:排序中使用:
在这里按照绝对值来完成排序。这样写的更加美观,也更容易让人弄懂。
```cpp
vector