【c++进阶】c++11下类的新变化以及Lambda函数和封装器

关注我,学习c++不迷路:

个人主页:爱装代码的小瓶子

专栏如下:

  1. c++学习
  2. Linux学习

后续会更新更多有趣的小知识,关注我带你遨游知识世界

期待你的关注。

在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函数:)
  • [4 .包装器function:](#4 .包装器function:)
  • [5. bind包装器](#5. bind包装器)
  • 最后的总结:

1. 前言:

在上一篇文章中,我们已经讲述了c++11的新特性万能可变模板以及他的底层原理。

本篇文章主要讲述三个方向:

  1. 类的新变化:这个主要是对c++和c++11所引起的变化进行总结。
  2. Lambda函数:这个是c++11新引入的特性函数。
  3. 封装器:也是c++11引进的的特点。

我将由浅入深,完成对这些知识的讲解。


2. c++11:类的新变化:

2-1 c++11之前的类的默认成员函数:

在c++11之前,我们已经有了六个默认成员函数:

  1. 构造函数:用来"初始化"一个对象。在你创建对象时自动调用,保证对象一出生就处于一个合法的状态。
  2. 析构函数:完成对类资源的释放。在对象销毁时自动调用,比如释放动态内存、关闭文件、释放锁等。
  3. 拷贝构造函数:同样也是用来完成对对象的初始化,用一个已有的对象来构造一个新对象(拷贝初始化)。
  4. 拷贝赋值重载:用一个已有对象给另一个已有对象赋值(对象已经存在)。注意的是原本的对象已经初始化,已经存在了。
  5. 取地址重载:决定"取对象的地址"(即 &a)时做什么,默认返回对象的真实地址。
  6. 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之后:移动拷贝构造和移动赋值重载:

  1. 移动拷贝构造:利用左值的特点,直接交换内部的资源,完成对该对象的构造,相比与普通的拷贝构造。效率更高。
  2. 移动赋值重载:与上面的不一样的是:该对像已经完成初始化,但是需要左值完成对对象的赋值,也是利用左值的特点完成快速资源交换。

这两个默认函数极大的提高了c++11的开发效率。在特定的情况下,比如传值返回的函数。提高效率。

第一,在这里,你如果写了这两个函数,编译器就默认不会生成原本的拷贝构造和赋值重载。

第二,这两个默认成员函数对内置类型都是逐字节拷贝(其实写了&& 算是万能引用了)

第三,对自定义类型则是如果完成了移动构造就是移动构造,没有实现就是拷贝构造

2-3 两个关键词 default 和delete:

我在图片上已经标出了两个关键词:

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

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

或者:

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

2-4:编译器生成默认函数的苛刻条件:

在这里我们还想讲讲什么时候编译器会自动生成默认成员函数,或者我们可以来县以下:编译器并不是"不想帮你生成",而是它非常胆小和保守。只要它发现生成了这个函数可能导致逻辑错误(如浅拷贝崩溃)、资源泄漏、或者根本做不到(如成员禁止这种操作),它就会直接拒绝生成(声明为 deleted)或者干脆不声明。

在这里我们将分成四组,来完成对编译器行为进行讲解:

2-4-1 默认构造函数(较为简单):

有三种情况会导致不会生成默认构造函数:

  1. 引用成员 且没有类内初始化器:引用(int&)必须在初始化时绑定到一个对象。默认构造函数并不知道要把引用绑定给谁,所以它生成不了。
  2. . Const 成员 且没有类内初始化器,且该类型没有默认构造. Const 成员 且没有类内初始化器,且该类型没有默认构造。
  3. 成员或基类"不可默认构造"
    如果类里有一个成员 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):

如果类的任何非静态数据成员或基类的析构函数是:

  1. 被删除的(=delete);
  2. 无法访问的(比如是 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):

  1. 成员不可拷贝:某个成员的拷贝构造被删除了(比如 std::unique_ptr)。
  2. 成员的拷贝构造是私有的/不可访问的。
  3. 类声明了移动操作:如果你写了 移动构造 或 移动赋值,编译器会直接把 拷贝构造 标记为删除(C++11 规定:想移动就得显式声明拷贝)。

拷贝赋值的苛刻条件(被 deleted):

除了包含上述拷贝构造的条件外,还有两个著名的"天敌":

  1. 引用成员:引用一旦绑定就不能改。默认的赋值操作是 a.ref = b.ref,这试图让引用重新绑定,这是非法的。
cpp 复制代码
struct E {
    int& ref;
    // operator= 被删除
};
  1. Const 成员:const 变量初始化后不能改。默认的赋值操作试图修改 const 成员,非法。
cpp 复制代码
struct F {
    const int x;
    // operator= 被删除
};

2-4-4 Class(Class&&) operator=(Class&&))

这也是生成条件最苛刻的一组。编译器非常不愿意帮你生成移动函数,因为它默认认为你的类可能像老式 C++ 类一样,只是简单的内存拷贝,移动并不安全

自动生成的门槛(必须同时满足):

  1. 完全没有手动声明:没有声明拷贝构造、拷贝赋值、移动构造、移动赋值、析构函数。
  2. 需要的成员都支持移动:所有成员和基类都有可用的移动操作。

苛刻条件(导致被 deleted):

  1. 成员不可移动:某个成员的移动操作被删除或不可访问。
  2. 移动析构/拷贝有副作用:(这个比较复杂,通常不用记,标准规定如果拷贝构造不是 constexpr 或抛异常等可能影响移动生成的判定)。

最重要的现象------"抑制":

如果你写了析构函数、拷贝构造或拷贝赋值 中的任何一个,编译器就不会生成移动操作(不是删除,是根本不声明) 。这时如果你尝试 std::move(obj),它会退而求其次调用拷贝操作。

cpp 复制代码
struct G {
    ~G() { /* 手动写了析构 */ } 
    // 此时:移动构造 和 移动赋值 不会被生成!
    // 哪怕你的成员看起来非常适合移动。
};

2-5 总结与经验:

实战建议:如何面对这些苛刻条件?

  1. 不要猜,用 =default 和 =delete 明确表达意图。

    如果编译器没生成你要的函数(报错说使用了 deleted function),不要纠结为什么没生成,直接检查成员。

    • 如果成员是 unique_ptr,你就知道不能拷贝,必须 =delete 拷贝操作。
    • 如果成员是引用,你就知道不能赋值。
  2. 遵循 Rule of Zero(零法则)。

    尽量不要在类里直接管理资源(不用裸指针 new)。用 std::string, std::vector, std::unique_ptr 代替。这些标准库类的默认成员函数行为都是完美的。你的类里只要不写这些函数,编译器生成的也就是完美的。

  3. 一旦写了析构或拷贝,就要考虑"五法则"。

    如果你写了析构函数(比如为了 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。最后是函数体。 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/c83b66cbab484444b09d4fc9bb4d476b.png) 我们可以看到和正常函数是一致的。 ### 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进行修改呢? ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/f1e4f2904f0c411ba7bda49a5c9f0611.png) #### 实例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 v1 = {1,-344445,6,11,-23,4,2,445}; sort(v1.begin(), v1.end(), [](int a, int b) { return abs(a) > abs(b); }); for (auto e : v1) { cout << e << " "; } ``` #### 一些容易错误的点: ```cpp int a = 10; auto isB = [a](int b) mutable{ a = 100; b = a; std::cout << b << std::endl; }; isB(1111); std::cout << a << std::endl; ``` 我们可以看到加了mutable这个关键词是可以改变捕获列表的捕获变量的,但是他只是a 的一个副本,无法改变全局变量的a。 ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/c01d7e60780341098e202714dedf0616.png) 第二个: ```cpp auto sum = [](int a, int b) {return a + b; }; std:: cout << sum(1,2) << std::endl; ``` 在这个函数中:auto和返回类型不是一个东西:这里的auto是一个非常复杂的名字,在这里必须要这么写。而这里的返回类型是一个int类型。其本质不是一个东西。 ### 3-3 lambda函数的本质: 其本质是一个类,但是不是由你来写,而是通过编译器来完成构建:这个类的名字是独一无二的,就算你写了一个一模一样的,但是背后的类都是不一致的。继续以sum函数来举例: ```cpp // 1. 编译器先偷偷生成一个类 class __CompilerGeneratedName_X123 { public: // 重载了 () 操作符,这使得对象可以像函数一样被调用 int operator()(int a, int b) const { return a + b; } // 如果是无捕获的 Lambda,甚至可以转换为函数指针(这也是特例) // using FunctionPointer = int(*)(int, int); // operator FunctionPointer() const ... }; // 2. 然后用这个类实例化一个对象 __CompilerGeneratedName_X123 sum = __CompilerGeneratedName_X123(); ``` 这就是一个类,名字是读一读二的,这也就解释了为什么auto那里为什么只能用auto。 这里调用`sum(1,2)`就相当于调用`sum.operator()(1, 2)`。是不是很有意思? ## 4 .包装器function: 在这里我们暂时只讲functional里面的function这个包装器: 介绍如下: ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/ce5cab8999394f129fde084c1402d4f7.png) 一个类,它能够把任意类型的可调用对象(例如普通函数、lambda、带有 **call** 的函数对象、functor 等)包装起来,变成一个可拷贝(copyable)的对象,并且这个类的类型只取决于它的调用签名(call signature),而与被包装的那个可调用对象的具体类型无关。 我们来慢慢看看 ```cpp int sum(int a, int b) { return a + b; } auto Div = [](int a, int b) { assert(b); return a / b; }; struct mul { int operator()(int a, int b) { return a * b; } }; struct math { static int mul(int a, int b) { return a * b; } }; int main() { function op; //1 .尝试使用加法: op = sum; cout << op(1, 2) << endl; //2 .尝试使用除法: op = Div; cout << op(4, 2) << endl; //3. 尝试乘法: op = mul(); cout << op(2, 2) << endl; op = math::mul; cout << op(2, 2) << endl; } ``` 这里就是一个简单的例子,上面四个函数分别是:普通函数,lambda函数,仿函数,和静态成员函数。 如何进行封装呢?传入他们的指针,似乎不太行,仿函数和lambda函数都是类,无法通过指针来完成指引,根本装不下去。这是一个问题。所以我们引出了这个function这个来完成封装。要求封装,我们只要要求返回值类型和传入值相同就可以了。 ```cpp struct math { int add(int a, int b) { return a * b; } }; int main() { math m1; function func = &math::add; cout << func(&m1,1, 10) << endl; } ``` 如果不是静态成员函数,我们在调用的时候就需要需要传入指针,此时我们在前面还需要传入m1的地址,需要给结构体里面的this使用。 ## 5. bind包装器 与function相似,这里多了一个占位符,这个主要是用来固定函数的参数的位置的。 我们废话不说多,直接来看一段程序: ```cpp void print(int a, int b, int c) { cout << a << " " << b << " " << c << endl; } int main() { //1。按照情况来说应该是:10,11,12; auto print1 = bind(print, _1, _2,_3); print1(10, 11, 12); //2. 按照情况来说应该是:12,10,11; auto print2 = bind(print, _3, _1, _2); print2(10, 11, 12); //3. 同理,应该是 auto print3 = bind(print, _1, _1, _1); print3(10); } ``` 要用这个占位符号,需要使用using namespace std::placeholders,同时我们来讲一下占位符,10,11,12传入了三个数,其中10就是第一个符号,一直对应_1,那么无论_1怎么变化,他都是第一个,传进print的时候有需要看位置了,此时_1如果在第一个位置,就传入a,按照位置而定。还是比较简单的。无需要写那么复杂。 利用这个特性,我们可以尝试写出: ```cpp auto func1 = [](double rate, double money, int year)->double { double ret = money; for (int i = 0; i < year; i++) { ret += ret * rate; } return ret - money; }; int main() { function func3_1_5 = bind(func1, 0.015, _1, 3); function func5_1_5 = bind(func1, 0.015, _1, 5); function func10_1_5 = bind(func1, 0.015, _1, 10); function func3_2_5 = bind(func1, 0.025, _1, 3); function func5_2_5 = bind(func1, 0.025, _1, 5); function func10_2_5 = bind(func1, 0.025, _1, 10); cout << func3_1_5(10000) << endl; } ``` 这样就比较的巧妙的使用了占位符,完成符合逻辑的银行利润计算。 *** ** * ** *** ## 最后的总结: 在这几篇c++11的新特性的轰炸下:我们总结类的六个默认成员函数,最后还讲了什么是lambda函数,以及最后的封装器。这些语法糖更加利于程序员去写代码。是c++生命力强大的根基。 如果你已经看到这里了真的很厉害了。大家一起加油! ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/f5dea7c555234985b244d003c867bdb4.png)

相关推荐
乌萨奇也要立志学C++2 小时前
【Linux】线程同步 条件变量精讲 + 生产者消费者模型完整实现
java·linux·运维
澄澈青空~2 小时前
病毒木马侵入系统内核的底层运作机理
java·linux·服务器
m0_748250032 小时前
C++ 标准库概述
开发语言·c++
FAFU_kyp2 小时前
Rust 所有权(Ownership)学习
开发语言·学习·rust
superman超哥2 小时前
Rust 异步性能的黑盒与透视:Tokio 监控与调优实战
开发语言·后端·rust·编程语言·rust异步性能·rust黑盒与透视·tokio监控与调优
lkbhua莱克瓦242 小时前
进阶-存储对象2-存储过程上
java·开发语言·数据库·sql·mysql
杨杨杨大侠2 小时前
深入理解 LLVM:从编译器原理到 JIT 实战
java·jvm·编译器
Mr -老鬼2 小时前
Rust 知识图谱 -进阶部分
开发语言·后端·rust