一. C++11的发展历史

        

        C++11 是 C++ 的第⼆个主要版本,并且是从 C++98 起的最重要更新。它引⼊了⼤量更改,标准化了既 有实践,并改进了对 C++ 程序员可⽤的抽象。在它最终由 ISO 在 2011 年 8 ⽉ 12 ⽇采纳前,⼈们曾使 ⽤名称“C++0x”,因为它曾被期待在 2010 年之前发布。C++03 与 C++11 期间花了 8 年时间,故⽽这 是迄今为⽌最⻓的版本间隔。从那时起,C++ 有规律地每 3 年更新⼀次。

        c++11和c++20变化还是比较大的,其他的变化都较小。


     二.   列表初始化

  2.1 C++98传统的{}

        C++98中⼀般数组和结构体可以⽤{}进⾏初始化,看下图。

        

        这是我们c++11中允许的,数组肯定没问题,这个结构体就是把_x初始化为1,_y初始化成2。

2.2 C++11中的{}

C++11以后想统⼀初始化⽅式,试图实现⼀切对象皆可⽤{}初始化,{}初始化也叫做列表初始化。
内置类型⽀持,⾃定义类型也⽀持,⾃定义类型本质是类型转换,中间会产⽣临时对象,最后优化
了以后变成直接构造。
{}初始化的过程中,可以省略掉=
C++11列表初始化的本意是想实现⼀个⼤统⼀的初始化⽅式,其次他在有些场景下带来的不少便
利,如容器push/inset多参数构造的对象时,{}初始化会很⽅便
        
        我们定义了一个日期类,接下来我们来使用一些c++11中的新语法。
        
        内置类型也是可以用{}的,如图我们的x,自定义类型也可以如图d1,因为{}的本质就是调用构造函数产生了一个临时对象,然后在使用拷贝构造构造这个函数,但是编译器优化后直接是调用构造了,所以我们不能用引用直接接收它,必须加上const,调用一个参数的构造函数的时候,可以不用加{},在c++11更新之后,连等于号都可以省略掉,如图我们的x2和d6,d7.
        {}的运用让我们也方便了很多,如图我们的vector函数,你可以传入一个有名对象,也可以传入匿名对象,但是都没有直接使用一个{}方便。
        

2.3 C++11中的std::initializer_list

        

上⾯的初始化已经很⽅便,但是对象容器初始化还是不太⽅便,⽐如⼀个vector对象,我想⽤N个

值去构造初始化,那么我们得实现很多个构造函数才能⽀持, vector<int> v1 = {1,2,3};vector<int> v2 = {1,2,3,4,5};

C++11库中提出了⼀个std::initializer_list的类, auto il = { 10, 20, 30 }; // the type of il is an initializer_list ,这个类的本质是底层开⼀个数组,将数据拷⻉过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。

容器⽀持⼀个std::initializer_list的构造函数,也就⽀持任意多个值构成的 {x1,x2,x3...} 进⾏初始化。STL中的容器⽀持任意多个值构成的 {x1,x2,x3...} 进⾏初始化,就是通过std::initializer_list的构造函数⽀持的。

        我们就可以把这个initializer_list理解为一个数组就行,用一个initializer_list接收我们传过来的参数。

        我们可以运行一下这个代码。

        我们的两次结果都是输出16可以证明,我们的这个底层就是两个指针,下面的地址可以看出它就是和数组的区别不大。

        下面我们来看一下它的好处。

        

        我们可以看一下,上面的pair的意思就是调用了两个参数的构造函数,下面我们的{}填入了三个值,如果我们正常来写的话。我们要写有三个参数的构造函数,四个要写四个构造函数,几个就要写几个参数的构造函数,这是很麻烦的,但是有了我们的initializer_list之后,不管你插入几个,我先把你存在我的这个initializer_list里面,然后通过for循环调用你的插入函数插进去就可以了。

        这个initializer_list是非常好用的,基本每个内部类中都有一个initializer_list为参数的构造函数。

三.. 右值引⽤和移动语义

        

        C++98的C++语法中就有引⽤的语法,⽽C++11中新增了的右值引⽤语法特性,C++11之后我们之前学习的引⽤就叫做左值引⽤。⽆论左值引⽤还是右值引⽤,都是给对象取别名。

 3.1 左值和右值

        

左值是⼀个表⽰数据的表达式(如变量名或解引⽤的指针),⼀般是有持久状态,存储在内存中,我

们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const

修饰符后的左值,不能给他赋值,但是可以取它的地址。

右值也是⼀个表⽰数据的表达式,要么是字⾯值常量、要么是表达式求值过程中创建的临时对象

等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。

值得⼀提的是,左值的英⽂简写为lvalue,右值的英⽂简写为rvalue。传统认为它们分别是left

value、right value 的缩写。现代C++中,lvalue 被解释为loactor value的缩写,可意为存储在内

存中、有明确存储地址可以取地址的对象,⽽ rvalue 被解释为 read value,指的是那些可以提供

数据值,但是不可以寻址,例如:临时变量,字⾯量常量,存储于寄存器中的变量等,也就是说左

值和右值的核⼼区别就是能否取地址。

        下面我们来举一些左值和右值的例子。

        

        你像如图上面这些都是可以取地址的,都可以称为左值,下面我们来举一些右值的例子。

        

        你像下面这四个,都是无法取地址的,都是右值,你像我们的返回值,如果返回的单纯是值的话,那么它也是右值,因为它是临时变量,出了作用域就销毁了,如果返回的是一个数的引用,那么它就是左值,因为它是可以取地址的。

        x+y的本质也就是产生一个临时变量,也是无法取地址的,所以是右值。

        你像最后一个string,它是一个匿名对象,也是无法取地址的,也属于右值。

        

3.2 左值引⽤和右值引⽤

        

        Type& r1 = x; Type&& rr1 = y; 第⼀个语句就是左值引⽤,左值引⽤就是给左值取别名,第⼆个就是右值引⽤,同样的道理,右值引⽤就是给右值取别名。

左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值

右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)

template <class T> typename remove_reference<T>::type&& move (T&&arg);

move是库⾥⾯的⼀个函数模板,本质内部是进⾏强制类型转换,当然他还涉及⼀些引⽤折叠的知

识,这个我们后⾯会细讲。

        需要注意的是变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量变量表达式的属性是左值语法层⾯看,左值引⽤和右值引⽤都是取别名,不开空间。从汇编底层的⻆度看下⾯代码中r1和rr1 汇编层实现,底层都是⽤指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要然到⼀起去理解,互相佐证,这样反⽽是陷⼊迷途。

        下面我们用代码演示一下。

        

        这些代码是我们左值引用左值的,发现是没有问题的,我们的左值能不能引用我们的右值呢?

        当然是可以的,但是不能直接引用,因为我们的右值都是无法取地址的,特点都是值不能改变,所以我们只需要在左值引用前面加上const就能实现引用右值了。

        

        这是我们的右值引用。

        

        这是我们的左值引用右值的写法,有左值引用右值,那么是否存在右值引用左值呢?

        当然是存在的,但是也是无法直接引用的,需要强转,库中给我们写好了强转函数。

        

        这样就可以了。

        

template <class _Ty>
remove_reference_t<_Ty>&& move(_Ty&& _Arg)
{ // forward _Arg as movable
return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}
        这是我们强转函数的底层实现,就是强转一下就行了。
        

3.3 引⽤延⻓⽣命周期

     右值引⽤可⽤于为临时对象延⻓⽣命周期,const 的左值引⽤也能延⻓临时对象⽣存期,但这些对象⽆法被修改。

        

        我们看一下这个函数,我们通过调式可以知道,这个匿名对象过了他那一行就析构了,但是我们对它右值引用或者左值引用之后,它的生命周期就延长了,和引用一样出了作用域才销毁,右值引用的本质就是把右值变成了左值,可以先这样理解。

3.4 左值和右值的参数匹配

C++98中,我们实现⼀个const左值引⽤作为参数的函数,那么实参传递左值和右值都可以匹配。

C++11以后,分别重载左值引⽤、const左值引⽤、右值引⽤作为形参的f函数,那么实参是左值会

匹配f(左值引⽤),实参是const左值会匹配f(const 左值引⽤),实参是右值会匹配f(右值引⽤)。

右值引⽤变量在⽤于表达式时属性是左值,这个设计这⾥会感觉跟怪,下⼀⼩节我们讲右值引⽤的

使⽤场景时,就能体会这样设计的价值了

        我们来看一下这个例子,我们可以先看一下分别都调用哪个函数,重点看倒数第二个。

        

        可以看看和你想的是否一样。我们前几个肯定没问题,倒数第二个为什么调用左值引用呢?

        我们上面也说了,右值引用的本质你可以理解为把右值变成了左值。

        

3.5 右值引⽤和移动语义的使⽤场景

3.5.1 左值引⽤主要使⽤场景回顾

        左值引⽤主要使⽤场景是在函数中左值引⽤传参和左值引⽤传返回值时减少拷⻉,同时还可以修改实参和修改返回对象的价值。左值引⽤已经解决⼤多数场景的拷⻉效率问题,但是有些场景不能使⽤传左值引⽤返回,如addStrings和generate函数,C++98中的解决⽅案只能是被迫使⽤输出型参数解决。那么C++11以后这⾥可以使⽤右值引⽤做返回值解决吗?显然是不可能的,因为这⾥的本质是返回对象是⼀个局部对象,函数结束这个对象就析构销毁了,右值引⽤返回也⽆法概念对象已经析构销毁的事实。

        左值引用的只要作用就是两个,我们之前学习的返回值的时候,我们可以直接返回它的引用,这样可以减少拷贝,还有就是我们的swap函数也可以用左值引用作为参数,这样可以直接完成交换,不用再用指针了。

那么我们右值引用到底有什么用呢?

        我们需要先看一下下面的场景。

3.5.2 移动构造和移动赋值

        

移动构造函数是⼀种构造函数,类似拷⻉构造函数,移动构造函数要求第⼀个参数是该类类型的引

⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值。

移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函

数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤。

对于像string/vector这样的深拷⻉的类或者包含深拷⻉的成员变量的类,移动构造和移动赋值才有

意义,因为移动构造和移动赋值的第⼀个参数都是右值引⽤的类型,他的本质是要“窃取”引⽤的

右值对象的资源,⽽不是像拷⻉构造和拷⻉赋值那样去拷⻉资源,从提⾼效率。下⾯的bit::string

样例实现了移动构造和移动赋值,我们需要结合场景理解。

        我们下面举两个例子看一下。

        

        我们可以看一下这个例子,这里我们无法用左值引用传回去,因为str出了作用域就销毁了,你传别名过去也是一个销毁的对象,这显然不符合我们的要求,我们用右值返回呢,显然也是无法完成的,出了作用域就销毁了。我们的这个str传回去的时候需要先调用两次拷贝构造,第一次调用拷贝构造先生成一个临时对象,这个临时对象在传给接收方时,接受方也要调用拷贝构造接受它,这时候代价是有一点大的,可能有人会说了,不就是两次拷贝构造吗,有什么代价大的,那么如果时下面这个场景呢?

        

        这时候代价是不是就变得非常大了,你传入1000,那么你就要调用非常多次的拷贝构造了,此时代价就非常大了,那么我们的右值构造也无法完成这个需求,那么它有什么用啊,虽然它无法直接影响我们拷贝构造的次数,但是可以间接影响啊,我们下面来举个例子,让你了解一下移动构造。

        

        我们此时可以简单的看一下这个代码,我们原来的string类,我们的参数不管给左值还是右值它都调用的是我们的拷贝构造,因为我们的这个const左值引用既可以接收左值也可以接收右值,此时我们的传值传参的代价会很大,如果我们加一个右值引用类型的拷贝构造函数呢,是不是就会好很多呢?这个就称为我们的移动构造。

        下面我们就来看一下它的神奇之处吧。

        

        ,看完上面的代码之后你可以分析一下这个过程,首先我们需要先调用一次构造生成s1,其次我们就要调用拷贝构造生成s2,s3的右边需要先调用构造生成一个匿名对象,再调用移动构造生成s3,第四个我们就要调用移动构造了,因为最后两个的右边都是右值,此时就调用移动构造了,如果屏蔽掉移动构造,此时你最后两个还得调用拷贝构造,我们移动构造是直接把两个字符串交换了,此时我们的s3,s4,都是nullptr,都给了我们的这些右值,反正这些右值早晚销毁,我还不如直接交换呢,此时交换完之后,销毁了调用析构,此时这些右值都是空了,析构也没有代价了,此时平白无故少了两次拷贝构造。

        我们的编译器优化,我们执行不出来,我们用g++演示一下。

        

        我们发现和我们预期的一样。

        下面我们再来看一下我们上面没有解决的问题,来解决一下,就是那两个函数的问题。

        

        如果屏蔽掉移动构造,下面我们来分析一下这个,首先我们肯定是先调用一次构造生成一个临时对象,再调用拷贝构造传给第一个参数,第二个参数也是同理,进入addStrings函数之后,还会调用一次构造生成一个str对象,下面就来到了我们的重点了,原来我们传值返回的时候,是先调用拷贝构造生成一个临时对象,临时对象再调用拷贝构造给了我们接收方,两次拷贝构造。

        

        就得到了这个结果。

        我们不屏蔽移动构造再来运行一下。

        

        得到了这个结果。

        但是还有一个疑问,这里的str也是左值啊,为什么会调用移动构造呢?这里还是编译器还是有一点优化了,给它了一个move(str)的处理操作,使它直接调用了移动构造,代价更小。

        至此这就是我们其中一种场景的使用了,下面我们再来使用一下移动赋值。

        

        

        这样就没有问题了,这是其中一个使用场景。

3.5.3 编译器优化

        之前博客我们讲过这个问题,现在在拿出来说说,我们不仅维护c++的人在努力,编译器也在努力,下面我们来看看它的优化。

右值对象构造,只有拷⻉构造,没有移动构造的场景
图1展⽰了vs2019 debug环境下编译器对拷⻉的优化,左边为不优化的情况下,两次拷⻉构造,右
边为编译器优化的场景下连续步骤中的拷⻉合⼆为⼀变为⼀次拷⻉构造。
需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码优化为⾮常恐怖,会直接
将str对象的构造,str拷⻉构造临时对象,临时对象拷⻉构造ret对象,合三为⼀,变为直接构造。
变为直接构造。要理解这个优化要结合局部对象⽣命周期和栈帧的⻆度理解,如图3所⽰。
linux下可以将下⾯代码拷⻉到test.cpp⽂件,编译时⽤ g++ test.cpp -fno-elide
constructors 的⽅式关闭构造优化,运⾏结果可以看到图1左边没有优化的两次拷⻉。
        下面我们来看一下,此时只有拷贝构造没有移动构造,编译器首先第一次优化是合二为一,我们看看它是如何合二为一的。
        
        这个是合二为一的场景,左边没有优化,右边是优化版本一,首先第一次优化就是它把临时对象和str合二为一了,先创建临时对象,它认为str是临时对象的别名,直接通过str去拷贝构造,此时只拷贝构造了一次。
        最终版本就是它先创建了ret对象,它认为这个str是ret的别名,此时一次拷贝构造都不存在。
        此时一次拷贝构造都没有。
        但是我们的拷贝赋值还是存在的。
        
        此时拷贝赋值还是存在的。
        
右值对象构造,有拷⻉构造,也有移动构造的场景
图2展⽰了vs2019 debug环境下编译器对拷⻉的优化,左边为不优化的情况下,两次移动构造,右
边为编译器优化的场景下连续步骤中的拷⻉合⼆为⼀变为⼀次移动构造。
需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码优化为⾮常恐怖,会直接
将str对象的构造,str拷⻉构造临时对象,临时对象拷⻉构造ret对象,合三为⼀,变为直接构造。
要理解这个优化要结合局部对象⽣命周期和栈帧的⻆度理解,如图3所⽰。
linux下可以将下⾯代码拷⻉到test.cpp⽂件,编译时⽤ g++ test.cpp -fno-elide
constructors 的⽅式关闭构造优化,运⾏结果可以看到图1左边没有优化的两次移动。

    我们来看一下存在这两种的情况。

        

        此时vs2019的release直接就把拷贝构造和移动构造通过别名的方式给直接不使用了。

        但是移动赋值还是存在的。


3.5.4 右值引⽤和移动语义在传参中的提效

查看STL⽂档我们发现C++11以后容器的push和insert系列的接⼝否增加的右值引⽤版本

当实参是⼀个左值时,容器内部继续调⽤拷⻉构造进⾏拷⻉,将对象拷⻉到容器空间中的对象当实参是⼀个右值,容器内部则调⽤移动构造,右值对象的资源到容器空间的对象上把我们之前模拟实现的bit::list拷⻉过来,⽀持右值引⽤参数版本的push_back和insert。

其实这⾥还有⼀个emplace系列的接⼝,但是这个涉及可变参数模板,我们需要把可变参数模板讲

解以后再讲解emplace系列的接⼝。

下面我们来看一个代码来理解。

        

        我们看一下这个代码,它都调用了什么。

        

        这是我们运行的结果,和它们之间的关系,这是我们库中实现的,我们看一下这个push_back方法。

        

        我们发现它不仅存在左值版本,还存在右值版本,我们用一下我们之前实现的链表,来让我们更加清晰的认识一下这里。

        

        这是我们之前实现的链表,我们来使用一下。

        

        我们发现全是构造和拷贝构造,这使得代价很大,现在我们学着库中来增加一下右值引用的版本。

        

        此时我们增加了push_back和inset的右值版本,我们再来运行一下看看。

        

        我们发现为什么还是拷贝构造啊,我们通过调式可以看到,push_back确实进入的是右值版本,但是我们的insert确又进入了左值版本,这是为什么呢?

        我们前面讲过,右值引用的本质就是左值,可以修改,所以你的push_back进入了右值引用,但是此时你这个x的属性变成了左值了,此时肯定是要传到左值版本的insert中的,所以我们需要move一下再传。

        

        这样就会进入右值引用的insert了。

        

        我们发现还是拷贝构造,这是因为你的insert函数中进行new Node操作的时候还是用了x,此时这个x的属性是左值,当然就会进入左值引用的构造函数了,我们先添加一下右值引用的构造函数。

        ‘修改一下下面的问题。

        

        那么此时行不行呢?

        我们来试一下。

        

        还是不行的,因为我们的构造函数中的data又变成左值了,此时还是拷贝构造,我们最后把data加上move就可以了。

        此时就解决了这个问题了。

        

四.lambda

        4.1 lambda表达式语法

lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。

lambda 表达式语法使⽤层⽽⾔没有类型,所以我们⼀般是⽤auto或者模板参数定义的对象去接

lambda 对象。

lambda表达式的格式: [capture-list] (parameters)-> return type {function boby }

[capture-list] : 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据[]来

判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下⽂中的变量供 lambda 函数使

⽤,捕捉列表可以传值和传引⽤捕捉,具体细节7.2中我们再细讲。捕捉列表为空也不能省略。

(parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连

同()⼀起省略

->return type :返回值类型,⽤追踪返回类型形式声明函数的返回值类型,没有返回值时此

部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进⾏推导。

{function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以

使⽤其参数外,还可以使⽤所有捕获到的变量,函数体为空也不能省略。

        下面我们就来写几个看一下。

        

        我们看一下这几个例子,第一种就是如果你用->了,必须说明返回类型,就是第一种情况,第二种就是没有参数和自动推返回值的类型,第三种就是我们写的一个交换函数。

        可能有人觉得这些东西函数都可以实现,而且也没有简单到哪里去,下面我们就来看一下他的应用。

4.2 lambda的应⽤

        

        在学习 lambda 表达式之前,我们的使⽤的可调⽤对象只有函数指针和仿函数对象,函数指针的类型定义起来⽐较⿇烦,仿函数要定义⼀个类,相对会⽐较⿇烦。使⽤ lambda 去定义可调⽤对

象,既简单⼜⽅便。

        lambda 在很多其他地⽅⽤起来也很好⽤。⽐如线程中定义线程的执⾏函数逻辑,智能指针中定制删除器等, lambda 的应⽤还是很⼴泛的,以后我们会不断接触到。

下面我们来举个例子看一下。

        这是我们以前的写法,如果要写一个类根据什么排序,但是我们拥有lambda之后,就可以直接使用,不用再写仿函数了,我们来看一下。

        

        就是这样的,传给函数让它自己推类型。直接使用lambda。

        

4.3 捕捉列表

lambda 表达式中默认只能⽤ lambda 函数体和参数中的变量,如果想⽤外层作⽤域中的变量就

需要进⾏捕捉 .

第⼀种捕捉⽅式是在捕捉列表中显⽰的传值捕捉和传引⽤捕捉,捕捉的多个变量⽤逗号分割。[x,

y, &z] 表⽰x和y值捕捉,z引⽤捕捉。

第⼆种捕捉⽅式是在捕捉列表中隐式捕捉,我们在捕捉列表写⼀个=表⽰隐式值捕捉,在捕捉列表

写⼀个&表⽰隐式引⽤捕捉,这样我们 lambda 表达式中⽤了那些变量,编译器就会⾃动捕捉那些

变量。

第三种捕捉⽅式是在捕捉列表中混合使⽤隐式捕捉和显⽰捕捉。[=, &x]表⽰其他变量隐式值捕捉,

x引⽤捕捉;[&, x, y]表⽰其他变量引⽤捕捉,x和y值捕捉。当使⽤混合捕捉时,第⼀个元素必须是

&或=,并且&混合捕捉时,后⾯的捕捉变量必须是值捕捉,同理=混合捕捉时,后⾯的捕捉变量必

须是引⽤捕捉。

lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态

局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使

⽤。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。

默认情况下, lambda 捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改,

mutable加在参数列表的后⾯可以取消其常量性,也就说使⽤该修饰符后,传值捕捉的对象就可以

修改了,但是修改还是形参对象,不会影响实参。使⽤该修饰符后,参数列表不可省略(即使参数为空)。

        下面我们就来演示一下这几种情况。

        

        我们发现是无法直接使用的,那么我们怎么捕捉呢?

        

        就是要用哪个变量就在[]中填入就行。

        此时就可以使用a和b了。

        这是我们的传值捕捉,是无法修改值的,下面看一下传引用捕捉。

        

        我们可以发现a++是无法实现的,因为它是传值捕捉而不是传引用捕捉,b是可以修改的,修改也是影响外面的b的,这里要区别一下,这里的&b在其他地方是取地址在这里是引用,注意一下这个特别的地方。

        

        第一个是传值,给了一个等号,此时需要谁编译器会自己去找,使用的全是传值捕捉,第二个是传引用捕捉,此时可以修改这些变量的值。

        

        这种表示除了b其它的都是传引用捕捉,此时我们也发现b是无法修改的。

        

        这就是除了b是传引用,其他都是传值。

        

        我们定义了一个全局变量和一个静态变量,发现是可以直接使用的,不需要捕捉,也无法捕捉。

        

        我们发现默认是类似传引用捕捉的。

        我们的变量无法改变的原因就是编译器给我们传过去的类型加了一个const,我们的这个mutable就是去除const的,此时我们就可以修改了,也不会影响外面的值。

        

        如图所示。

        下面我们来看看它这么神奇是怎么实现的吧。

        4.4 lambda的原理

   

lambda 的原理和范围for很像,编译后从汇编指令层的⻆度看,压根就没有 lambda 和范围for

这样的东西。范围for底层是迭代器,⽽lambda底层是仿函数对象,也就说我们写了⼀个

lambda 以后,编译器会⽣成⼀个对应的仿函数的类。

仿函数的类名是编译按⼀定规则⽣成的,保证不同的 lambda ⽣成的类名不同,lambda参数/返

回类型/函数体就是仿函数operator()的参数/返回类型/函数体, lambda 的捕捉列表本质是⽣成

的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕

捉,编译器要看使⽤哪些就传那些对象。

上⾯的原理,我们可以透过汇编层了解⼀下,下⾯第⼆段汇编层代码印证了上⾯的原理。

        底层就是这么一个东西,就是你传入的一个参数,他就会在类中生成一个参数,简单了解一下就行,不作为重点。

        

这段代码展示了 C++ 中 函数对象(Functor) 和 Lambda 表达式 两种「可调用对象」的实现原理与使用方式 —— 核心都是让一个「对象 / 匿名对象」具备像函数一样的调用能力(通过 operator() 或编译器自动生成的调用运算符),下面分两部分拆解实现细节,再对比两者的关联。

一、先明确核心概念

「可调用对象」是 C++ 中能通过 对象(参数列表) 语法调用的实体,常见的有:函数指针、函数对象、Lambda 表达式。你的代码中:

  • Rate 类的实例 r1 是 函数对象(通过重载 operator() 实现);
  • auto r2 和 auto func1 是 Lambda 表达式(编译器自动生成匿名函数对象,本质也是函数对象)。

二、第一部分:函数对象(Rate 类)的实现原理

Rate 是一个「带状态的可调用对象」,核心是 重载 operator() 运算符,让对象能像函数一样被调用。

  1. 定义 Rate r1(rate) 时:调用 Rate 的构造函数,将外部的 rate=0.49 存入对象的私有成员 _rate(对象的「状态」被初始化);
  2. 调用 r1(10000, 2) 时:
    • 触发 operator() 重载函数;
    • 传入参数 money=10000year=2
    • 结合对象自身的状态 _rate=0.49,计算并返回 10000 * 0.49 * 2 = 9800

        

五. 类型分类

        这个是很简单的,记住这几个概念即可。

C++11以后,进⼀步对类型进⾏了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值

(expiring value,简称xvalue)。

纯右值是指那些字⾯值常量或求值结果相当于字⾯值或是⼀个不具名的临时对象。如: 42

truenullptr 或者类似 str.substr(1, 2)str1 + str2 传值返回函数调⽤,或者整

aba++a+b 等。纯右值和将亡值C++11中提出的,C++11中的纯右值概念划分等价于

C++98中的右值。

将亡值是指返回右值引⽤的函数的调⽤表达式和转换为右值引⽤的转换函数的调⽤表达,如

move(x)static_cast<X&&>(x)

泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值。

        就是右值包括我们的直接定义的右值叫做纯右值,我们move的右值叫做将亡值。

六.引用折叠

        

C++中不能直接定义引⽤的引⽤如 int& && r = i; ,这样写会直接报错,通过模板或 typedef

中的类型操作可以构成引⽤的引⽤。

通过模板或 typedef 中的类型操作可以构成引⽤的引⽤时,这时C++11给出了⼀个引⽤折叠的规

则:右值引⽤的右值引⽤折叠成右值引⽤,所有其他组合均折叠成左值引⽤。

下⾯的程序中很好的展⽰了模板和typedef时构成引⽤的引⽤时的引⽤折叠规则,⼤家需要⼀个⼀

个仔细理解⼀下。

像f2这样的函数模板中,T&& x参数看起来是右值引⽤参数,但是由于引⽤折叠的规则,他传递左

值时就是左值引⽤,传递右值时就是右值引⽤,有些地⽅也把这种函数模板的参数叫做万能引⽤

Function(T&& t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模

板参数T的推导int&,再结合引⽤折叠规则,就实现了实参是左值,实例化出左值引⽤版本形参的

Function,实参是右值,实例化出右值引⽤版本形参的Function。

下面我们来举一些例子来帮助我们更好的理解一下。

        

        我们来看一下这个例子。

        我们的f1给的参数是一个左值引用,我们给它别名的时候又使用了引用,此时就存在一个规则了,如果两个引用折叠除非两个折叠的引用都是右值引用,总体才能是右值引用,否则就是左值引用,我们上面也写了四个让猜类型。

        下面的也是,传入不同类型的参数来看类型,因为我们的参数是T&的形式,所以我们不管传入什么类型的值,最终还是左值引用的。

        通过报错也可以验证出来,不能传右值,除非加上const,本质上这样的函数还叫左值引用。

        

        可以看一下这一个,此时就是可以是左值引用,也可以是右值引用,根据我们的那个规则。

        也叫万能引用。

        下面我们再深入了解一下这个万能引用。

        

        可以看一下这个例子,自己推一下这个类型,第一个我们传入了一个右值,那么我们的类型是什么呢?是int,为什么呢,我们可以通过运行代码来看一下,为什么是int而不是int&&呢?

        如果T是int&&的话,此时我们的Tx=a就会报错了,因为右值引用不能直接引用左值,此时没有报错能运行,证明T就是int。

        第二个传入一个左值,此时T就是int&了,这个很简单,一会儿运行一下我们通过地址也是能看出来的,第三个传入一个将亡值,此时我们的T的类型是int,传入的是右值吗,只能是int或者int&&,上面也讲了为什么不是int&&了。

        第四个传入了一个const类型的,此时我们的T就是const int&类型了,很好理解这里不过多讲解了,最后一个是const int类型的。下面我们运行一下通过地址也是能看出来的。

        

        可以对照着看一下。

七.完美转发

        在学学习这个之前,我们先看一个例子。

        

        我们的list的push_back写了两个版本,我们在前面也讲过为什么,那么这两个版本的第二个属不属于我们上面说的万能引用呢?

        相信大家看见这个和我们上面的那个形式一样就可能说这个是万能引用,但是这是错误的,因为你的这个T是在你定义这个list<>的时候<>括号中填的类型,此时已经确定了,你是无法传入其他类型的,因此它不属于万能引用。

Function(T&& t)函数模板程序中,传左值实例化以后是左值引⽤的Function函数,传右值实例化

以后是右值引⽤的Function函数。

但是结合我们在5.2章节的讲解,变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定

后,右值引⽤变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传

递给下⼀层函数Fun,那么匹配的都是左值引⽤版本的Fun函数。这⾥我们想要保持t对象的属性,

就需要使⽤完美转发实现。

template <class T> T&& forward (typename remove_reference<T>::type&

arg);

template <class T> T&& forward (typename remove_reference<T>::type&& arg);

完美转发forward本质是⼀个函数模板,他主要还是通过引⽤折叠的⽅式实现,下⾯⽰例中传递给

Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引⽤返回;传递给

Function的实参是左值,T被推导为int&,引⽤折叠为左值引⽤,forward内部t被强转为左值引⽤

返回。

        

        此时我们这样修改一下它就是万能引用了,但是此时还存在一个问题,就是我只有右值的时候才move,左值我是不move的啊,这怎么办呢?

        我也无法区分x是左值还是右值啊,此时就要用到完美转发了。

        

        这就是我们完美转发的使用,下面我们再来看一个例子了解一下。

        

        我们看一下这个例子,运行一下。

        

        全是左值引用,这是为什么呢?因为我们上面提到过,右值引用的本质也是左值,所以我们的t一定是左值,此时我们无法判断我们传过来的是左值还是右值,此时怎么匹配相对应的函数呢?此时就要用到完美转发了。

        

        这样改一下。

        

        此时就达到我们想要的结果了。

        

template <class _Ty>
_Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept
{ // forward an lvalue as either an lvalue or an rvalue
return static_cast<_Ty&&>(_Arg);
}
        这就是底层的原理,就是实现一个强转。
        

八.可变参数模板

        下面我们来看一个东西。

        

        我们如图所示要使用下面这几种,如图所示我们需要写四个模板类型的Print函数,这样太麻烦了,c++有没有简单一点的办法呢?

        有的,就是我们的可变参数模板。

        

8.1 基本语法及原理

C++11⽀持可变参数模板,也就是说⽀持可变数量参数的函数模板和类模板,可变数⽬的参数被称

为参数包,存在两种参数包:模板参数包,表⽰零或多个模板参数;函数参数包:表⽰零或多个函

数参数。

template <class ...Args> void Func(Args... args) {}

template <class ...Args> void Func(Args&... args) {}

template <class ...Args> void Func(Args&&... args) {}

我们⽤省略号来指出⼀个模板参数或函数参数的表⽰⼀个包,在模板参数列表中,class...或

typename...指出接下来的参数表⽰零或多个类型列表;在函数参数列表中,类型名后⾯跟...指出

接下来表⽰零或多个形参对象列表;函数参数包可以⽤左值引⽤或右值引⽤表⽰,跟前⾯普通模板

⼀样,每个参数实例化时遵循引⽤折叠规则。

可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。

这⾥我们可以使⽤sizeof...运算符去计算参数包中参数的个数。

        下面我们看看可变参数模板是怎么实现这个操作的。

        

        这个效果和上面是一样的。

        我们要是想访问这个包中的元素该怎么办呢?

        是像下面这样吗?

        

        这个是一个严重的错误,它就不支持这样的语法。

        那么我们该如何进行访问呢?

        此时我们就要用到下面这个知识了。

8.2 包扩展

        

        对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个 包时,我们还要提供⽤于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元 素应⽤模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(...)来触发扩展操作。
C++还⽀持更复杂的包扩展,直接将参数包依次展开依次作为实参给⼀个函数去处理。

        

        如上图这样是一个访问的方式,我们讲一下原理:

参数包传参离不开这 3 个核心动作,全程由编译器主导:

  1. 打包:调用函数时,编译器自动把多个实参 “打包” 成一个参数包(比如Print(1, "a", 3.14) → 打包成(int, string, double)参数包);
  2. 推导:模板函数接收参数包时,编译器自动推导参数包的「类型列表」和「参数个数」(比如Args...推导为int, string, double,个数是 3);
  3. 展开:传参或使用时,通过...(展开符)把参数包 “拆成独立参数”,逐个传递(比如ShowList(args...) → 拆成ShowList(1, "a", 3.14))。

主要是分这三步,通过推导打包生成一个参数列表,然后一个一个的进行传参,传一个就拆一个,一直到没有参数。

        

        底层就是这样调用的,然后一个一个的进行拆除,拆一个打印一个,一直到没有参数就结束。

        

        有人可能会问不写空参那个写一个这个行不行,答案是当然不可以了,因为你的这个条件是运行的时候结束的,而我们需要的是编译时能结束的条件,显然你的这个是不行的。

        

1. 递归终止时机完全反了!

你的逻辑是:“如果剩余参数包 args 为空,就直接 return,不打印”—— 但实际递归流程中,args 为空时,x 是 最后一个参数(比如最后一轮调用 ShowList(2.2),此时 args 为空,x=2.2)。按你的代码,此时会直接 return,导致 最后一个参数永远打印不到(比如 2.2 会被跳过)。

2. 无法处理 “无参调用”,还会导致递归死循环?

原代码的 “无参版 ShowList ()” 有两个作用:① 处理 Print() 无参调用;② 作为递归的最终终止(当参数包拆到空时匹配)。而你删除了无参版,只保留了 “T x + Args... args” 的模板 —— 这个模板 必须接收至少 1 个参数(T x),无法匹配无参调用:

  • 当递归拆到最后一步(比如 ShowList(2.2) 之后,要调用 ShowList() 无参),编译器找不到匹配的无参函数,会报 链接错误(LNK2019 无法解析的外部符号)

  • 更糟的是:如果编译器尝试强制推导无参调用,会因模板要求 “至少 1 个参数 T x” 而推导失败,直接编译报错,而非死循环,但本质是 “缺少终止条件”。

        还有一种访问的方式我们来看一下。

        

         注意GetArg必须返回或者到的对象,这样才能组成参数包给Arguments,参数包每个参数传给GetArg处理,处理以后返回值作为实参组合参数包传递给Arguments函数。

        我们只需要打印出来值,所以我们返回什么都是可以的,这里就是直接把这个包给了GetArg函数,此时它就会一个一个的被打印出来。

        这里讲起来也是很抽象的,还是需要大家多去理解一下。

        

8.3 empalce系列接⼝

        

template <class... Args> void emplace_back (Args&&... args);

template <class... Args> iterator emplace (const_iterator position, Args&&... args);

C++11以后STL容器新增了empalce系列的接⼝,empalce系列的接⼝均为模板可变参数,功能上

兼容push和insert系列,但是empalce还⽀持新玩法,假设容器为container<T>,empalce还⽀持

直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。

emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列

第⼆个程序中我们模拟实现了list的emplace和emplace_back接⼝,这⾥把参数包不段往下传递,

最终在结点的构造中直接去匹配容器存储的数据类型T的构造,所以达到了前⾯说的empalce⽀持

直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。

传递参数包过程中,如果是 Args&&... args 的参数包,要⽤完美转发参数包,⽅式如下

std::forward<Args>(args)... ,否则编译时包扩展后右值引⽤变量表达式就变成了左值。

        下面还是看例子吧。

         empalce的效率是比push_back高的,下面我们通过例子看一下。

        

        这是我们c++库中的,push_back的实现是通过模板参数来生成的,而我们的emplace_back则是通过我们的可变参数模板形成的。

        

        

        先看一下这两个例子,我们传左值的时候,和push_back是一样的都是会走拷贝构造,当我们传右值的时候,它就会走移动构造,因为你传入的是一个string的右值对象,通过万能引用,它还是需要走移动构造转移资源的。

        

        但是这个就不同了,我们的push_back我们很好理解,需要先调用构造生成一个临时对象,然后再调用我们的移动构造,但是我们的emplace_back就不一样了,它是直接构造就行了,下面我们来分析一下为什么?

        

        我们上面也说了,这个push_back的参数是模板参数,在定义我们的list对象的时候确定的,比如list<string>我们的value_type就是string了,此时你就需要先隐士构造一个临时对象,然后调用移动构造转移资源,这样就完成了参数传递,但是我们的这个emplace_back的参数不是定义的时候确定的,是你传参的时候确定的,我们传入的是一个const char* 类型的字符串,然后它需要给string类型,此时就需要生成一个临时对象了,临时对象是右值,所以就调用移动构造了,但是我们的emplace_back传参是推类型的,通过这个字符串,推出类型是const char* 类型的,通过这个节点构造的时候就直接构造了。

        

        

        我们再来看一下多参数的,首先先调用了sting的构造构造一个string对象,你传入的是一个左值,此时emplace和push都需要调用拷贝构造,传入右值也是一样,都是调用移动构造。

        再看一下这个,这个就是需要注意两点,第一点就是写法问题,我们的push_back的参数在定义的时候就被确定为pair类型了,此时你就要用{}来构造一个这个类型,但是我们的emplace_back则是不能这样用,因为我们的类型没有确定,你用了{}反而会让编译器认为你是个initializer_list类型,但是这个类型也不支持传不同类型的值啊,所以就会报错,只能传入两个值,此时第一个的类型确定,需要生成一个pair的临时对象然后再传过去,而我们的这个可变参数则是通过函数包推演出两个类型为const char*,和int,然后不断的向下传递,最终通过我们的这两个参数直接构造一个pair对象,更加的高效。

        

九.新的类功能

      9.1 默认的移动构造和移动赋值

原来C++类中,有6个默认成员函数:构造函数/析构函数/拷⻉构造函数/拷⻉赋值重载/取地址重

载/const 取地址重载,最后重要的是前4个,后两个⽤处不⼤,默认成员函数就是我们不写编译器

会⽣成⼀个默认的。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。

如果你没有⾃⼰实现移动构造函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意⼀

个。那么编译器会⾃动⽣成⼀个默认移动构造。默认⽣成的移动构造函数,对于内置类型成员会执

⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调⽤

移动构造,没有实现就调⽤拷⻉构造。

如果你没有⾃⼰实现移动赋值重载函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意 ⼀个,那么编译器会⾃动⽣成⼀个默认移动赋值。默认⽣成的移动构造函数,对于内置类型成员会 执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调 ⽤移动赋值,没有实现就调⽤拷⻉赋值。(默认移动赋值跟上⾯移动构造完全类似)

如果你提供了移动构造或者移动赋值,编译器不会⾃动提供拷⻉构造和拷⻉赋值。

        我们来验证一下上面的东西。

        

        我们只写了一个构造没有写它说的其他的东西,下面我们验证一下它说的。

        

        看一下这个,我们的s1的资源就被转移走了,我们此时并没有写移动构造,但是资源却被转移走了,说明自动生成了移动构造。

        

        我们把析构加上了。

        

        此时就没有转移资源了。

        

        

9.2 成员变量声明时给缺省值

成员变量声明时给缺省值是给初始化列表⽤的,如果没有显⽰在初始化列表初始化,就会在初始化列表⽤这个却绳⼦初始化,这个我们在类和对象部分讲过了,忘了就去复习吧。

9.3 defult和delete

C++11可以让你更好的控制要使⽤的默认函数。假设你要使⽤某个默认的函数,但是因为⼀些原因

这个函数没有默认⽣成。⽐如:我们提供了拷⻉构造,就不会⽣成移动构造了,那么我们可以使⽤

default关键字显⽰指定移动构造⽣成。

如果能想要限制某些默认函数的⽣成,在C++98中,是该函数设置成private,并且只声明补丁已,

这样只要其他⼈想要调⽤就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指⽰编译器不⽣成对应函数的默认版本,称=delete修饰的函数为删除函数。

        看一下这个,我们写了拷贝构造理论上是不会自动生成我们的移动构造的,但是我们在移动构造的后面加上=deffault,它还是会生成移动构造的,我们在函数声名后面加上=delete此时我们就无法使用这个函数了。

9.4 final与override

这个我们在继承和多态章节已经进⾏了详细讲过了,忘了就去复习吧。

十. STL中⼀些变化

下图1圈起来的就是STL中的新容器,但是实际最有⽤的是unordered_map和unordered_set。这

两个我们前⾯已经进⾏了⾮常详细的讲解,其他的⼤家了解⼀下即可。

STL中容器的新接⼝也不少,最重要的就是右值引⽤和移动语义相关的push/insert/emplace系列

接⼝和移动构造和移动赋值,还有initializer_list版本的构造等,这些前⾯都讲过了,还有⼀些⽆关

痛痒的如cbegin/cend等需要时查查⽂档即可。

容器的范围for遍历,这个在容器部分也讲过了。

        其他的我们都了解的差不多了,我们看一下swap就行。

        

        我们看一下这些swap函数,这是c++98时候实现的,第一个是算法库中的,如果我们要是传几个vector类型的参数,这里就要进行三次深拷贝,要是再封装一下,调用次数很多,所以c++98的解决方案就是直接在我们的容器中实现自己的swap函数,并且当你定义这个类型的时候它会在全局中也生成一个swap函数,此时编译器会找最匹配的,你虽然用的是算法库中的swap实际调用的还是自己类中实现的,相对来说还是比较麻烦的,我们看看c++11是怎么改进的。

        

        它直接使用了move函数,此时就调用移动构造了,代价很小。还新增了一个两个数组的交换,很少用。

 十一. 包装器

        11.1 function

        std::function 是⼀个类模板,也是⼀个包装器。 std::function 的实例对象可以包装存

储其他的可以调⽤对象,包括函数指针、仿函数、 lambda bind 表达式等,存储的可调⽤对

象被称为 std::function ⽬标。若 std::function 不含⽬标,则称它为。  

        函数指针、仿函数、 lambda 等可调⽤对象的类型各不相同, std::function 的优势就是统

⼀类型,对他们都可以进⾏包装,这样在很多地⽅就⽅便声明可调⽤对象的类型,下⾯的第⼆个代

码样例展⽰了 std::function 作为map的参数,实现字符串和可调⽤对象的映射表功能。

        简单来说就是我们从最开始的函数指针到仿函数再到lambda,函数指针太麻烦,lambda我们无法取到类型,所以出了一个包装器,就是把他们统一一下。

        我们下面直接看例子看看它是怎么使用的吧。

        

        基本就是这样用的,相当于给它们一个名字。

        我们再来看一下类中的方法如何访问。

        

        我们看一下这个类和f4,f5,当我们访问静态函数的时候我们要知道静态函数是不属于类中的,它是存在于方法区的,所以这里只需要传两个参数就行,不用传this指针,静态函数不加&也行,但是我们的非静态就不行了,如图中f5,此时报错了,原因就是我们没有传this指针。

        

        我们就这样调用一下就行,不传指针也行,如下图。

        

        这样也是可以的,因为我们传入的ps不是直接作为this指针使用的,底层还是很复杂的。

        

        我们一般用右值引用,通过匿名对象的操作来完成。

        下面这种情况是不行的。

        

        这样是不行的,因为底层要对我们传入的这个参数进行一些操作,给了const我们的传入的东西就不能随意使用了,所以会报错。

        我们下面来看一道题。

        

        这个题我们先来看一下传统的写法。

        

        这是我们传统的写法。

        我们下面来试试用我们学的方法来完成这个代码。

        

        我们这里使用count的作用就是相当于我们的查找,只不过count返回的是0或者1是我们需要的,我们的find返回的是迭代器,没有这个效果好。

        这个stoi函数的作用就是把我们的字符串转换成我们的int数字。

        我们的思路就是在map中传两个参数,一个是我们需要的类型,string来存储我们的符号,一个是不同符号对应的函数,通过遍历这个vector然后找到符号就找它前面的两位,然后相加压入栈中,不是符号就变成int类型的数字。

        

11.2 bind

        

bind 是⼀个函数模板,它也是⼀个可调⽤对象的包装器,可以把他看做⼀个函数适配器,对接收

的fn可调⽤对象进⾏处理后返回⼀个可调⽤对象。 bind 可以⽤来调整参数个数和参数顺序。

bind 也在<functional>这个头⽂件中。

调⽤bind的⼀般形式: auto newCallable = bind(callable,arg_list); 其中

newCallable本⾝是⼀个可调⽤对象,arg_list是⼀个逗号分隔的参数列表,对应给定的callable的

参数。当我们调⽤newCallable时,newCallable会调⽤callable,并传给它arg_list中的参数。

arg_list中的参数可能包含形如_n的名字,其中n是⼀个整数,这些参数是占位符,表⽰

newCallable的参数,它们占据了传递给newCallable的参数的位置。数值n表⽰⽣成的可调⽤对象

中参数的位置:_1为newCallable的第⼀个参数,_2为第⼆个参数,以此类推。_1/_2/_3....这些占

位符放到placeholders的⼀个命名空间中。

        概念过于枯燥,我们直接来看例子理解吧。

        

        这是我们使用的前提条件,有几个参数就定义几个这个using这种东西。

        下面我们直接来看例子吧。

        

        我们写了几个函数,下面我们来操作一下,看看是怎么使用的。

        

        先来看一下这个例子,就是调用这个bind函数,第一个参数传入我们的函数名字,第二个参数传入我们这个sub1传入我们Sub函数的参数顺序,第一个例子是_1,_2说明第一个参数就传我们Sub函数的第一个参数,第二个参数传入我们的第二个参数,而第二个例子表示我们的sub2表示先传第二个参数再传第一个,也就是此时a就是5,b就是10了,和上面相对应着传,我们运行一下看看结果。

        

        我们发现结果和我们说的也是一样的,这是它的第一个功能,第二个就是绑定参数。

        

        来看一下这几个例子,sub3表示的就是我们绑定了第一个参数,我们的a就是100了,此时只需要传b就行。sub4表示b被绑定了,b永远是100,只需要传入a即可。

        后面的sub5,sub6,sub7都是这样的,很好理解我们的f7就表示我们的第一个参数就绑定了就是Plus。

        这是我们的运行结果,我们下面来看个例子,我们银行计算利息。

        

        这是我们银行存钱利率的计算,我们存入不同的年份,利率和year都是不一样的,此时我们就可以使用绑定很好的表示出来。

        

        这些就能很好的表示我们存入年份对应的利率了,只需要输入本金即可。

        至此我们的c++1已经基本讲完了,还有一个智能指针,单独一个博客讲解一下。

十二.结束语

        感谢读到这里的每一位朋友!技术之路漫长,每一次代码的调试、每一个知识点的梳理,都因你的驻足而更有意义。如果文章对你有帮助,欢迎点赞收藏,也期待在评论区和你交流更多技术细节~本期的技术分享就到这里啦!感谢你的耐心观看。文中若有疏漏或更好的优化方案,欢迎随时指出,一起在技术的世界里共同进步!

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐