C++进阶(6)C++11(2)

8.新的类功能

8.1默认成员函数

原来C++类中,有6个默认成员函数:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造
  4. 赋值重载
  5. 取地址重载
  6. const 取地址重载

最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。


C++11 新增了两个默认成员函数------不显式实现,编译器会自动生成

  • 移动构造;
  • 移动赋值;

针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:

8.1.1默认移动构造的生成条件、作用功能

编译器自动生成默认移动构造的条件比其他默认成员函数更苛刻:

  • 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器才会自动生成一个默认移动构造。

默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造:

  • 实现了移动构造:就调用移动构造;(编译器生成 or 用户显式实现)
  • 没有实现移动构造:但有拷贝构造,那就调用拷贝构造;

8.1.3默认移动赋值重载的生成条件、作用功能

编译器自动生成默认移动赋值的条件:

  • 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器才会自动生成一个默认赋值重载。

默认生成的移动赋值函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值:

  • 如果实现了,就调用移动赋值;(编译器生成 or 用户显式实现)
  • 没有实现,就调用拷贝赋值。

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


【测试】

cpp 复制代码
// 以下代码在vs2013中不能体现,在vs2019下才能演示体现上面的特性。
class Person
{
public:
    Person(const char* name = "", int age = 0)
        :_name(name)
        , _age(age)
    {}
    /*Person(const Person& p)    
        :_name(p._name)
        ,_age(p._age)
    {}*/
    
    /*Person& operator=(const Person& p)
    {
        if(this != &p)
        {
            _name = p._name;
            _age = p._age;
        }
    return *this;
    }*/
 
    /*~Person()
    {}*/

private:
    bit::string _name;
    int _age;
};

int main()
{
    Person s1;
    Person s2 = s1;
    Person s3 = std::move(s1);
    Person s4;
    s4 = std::move(s2);
 
    return 0;
}

  • s3调移动构造,结果是s1被偷家了。(s1变成空,或有效但未指定的状态)
  • s4调移动赋值,结果是s2被偷家了。(但它们的内容是一样的)

注意,有缺省参数的时候,声明缺省值就没有用了。


写一个析构,编译器就不会生成移动构造、移动赋值了:


这样设计的原因:

  • 用户显式实现了析构,一般也就意味着用户同时显式实现了:
    • 拷贝构造、赋值重载、移动拷贝、移动赋值重载。

编译器自动生成移动构造的条件看似苛刻,其实反而是更合理的。

这5个默认成员函数是一体化的------需要写的话都需要写,不需要写的话都不用写。

编译器自动生成的移动构造,对于:

  • 有自定义类型成员;
  • 而且自定义类型成员有自己的移动构造;
  • 而且没有指针成员管理资源;

这时候是有意义的。(自身没有资源需要管理,但自己的自定义类型成员有资源需要管理)

虽然person没有资源需要管理,但它的成员string有资源需要管理。

此时person自动生成的移动构造有意义了------要是没有它,就会调person的拷贝构造,而person的拷贝构造再去调用string的拷贝构造。

但是string明明有移动构造。

【注意】一个类不需要析构时,不要随便写析构。


8.2类成员变量初始化

C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化,这个我们在类和对象默认就讲了,这里就不再细讲了。

8.3强制生成默认函数的关键字default:

C++11可以让你更好的控制要使用的默认函数。

假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显式指定编译器生成移动构造

【应用场景】

  • 场景1:显式实现了构造或拷贝构造,但是没有默认构造。
  • 场景2:显式实现了拷贝构造,需要移动构造,但是不想自己写,编译器生成的就够用了。
cpp 复制代码
class Person
{
public:
    //默认构造
    Person(const char* name = "", int age = 0)
        :_name(name)
        , _age(age)
    {}
 
    //拷贝构造
    Person(const Person& p)
        :_name(p._name)
        ,_age(p._age)
    {}

    //指定编译器生成------移动构造
    Person(Person&& p) = default;

private:
    bit::string _name;
    int _age;
};

int main()
{
    Person s1;
    Person s2 = s1;
    Person s3 = std::move(s1);

    return 0;
}

【测试】

有了析构就不会生成移动构造:

可以指定生成移动构造,但是必须保证显式实现了移动构造、赋值重载。

你做了什么 编译器生成的 被删除的
什么都不写 默认构造、析构、拷贝构造、赋值重载、 移动构造、移动赋值
= default 移动构造 默认构造、析构、移动构造 拷贝构造、拷贝赋值
= default 移动赋值 默认构造、析构、移动赋值 拷贝构造、拷贝赋值
自定义移动构造 默认构造、析构 拷贝构造、拷贝赋值、移动赋值
自定义移动赋值 默认构造、析构 拷贝构造、拷贝赋值、移动构造

这些函数是一体化的,显式实现了一个,编译器就认为其他的都你自己显式实现了。

目的在于让编程的人不要瞎搞,写了这样又不写那个,很容易出错,要按规范来。

但是显式实现析构,编译器还是会生成默认拷贝构造、赋值重载。

8.4禁止生成默认函数的关键字delete

如果想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明不实现,这样只要其他人想要调用就会报错。

只声明不实现就够了?

并不是,还需要设置成私有。

否则只声明不实现的公有,使用的人可以在包了头文件之后,可以在类外定义。


在C++11中更简单,只需在该函数声明加上=delete即可------该语法指示编译器不生成对应函数的默认版本。

cpp 复制代码
class Person
{
public:
    Person(const char* name = "", int age = 0)
        :_name(name)
        , _age(age)
    {}
    
    Person(const Person& p) = delete;    //防拷贝

private:
    bit::string _name;
    int _age;
};

int main()
{
    Person s1;
    Person s2 = s1;
    Person s3 = std::move(s1);
 
    return 0;
}

称=delete修饰的函数为**"删除函数"**。

【应用场景】有时候不希望一个类被拷贝,就可以限制一下拷贝构造、赋值重载。

io流就是不想被拷贝的类,因为io流涉及缓冲区的概念,拷贝的话需要考虑缓冲区的内容拷贝不。

移动构造是受保护的,只能内部用,或者子类用,外部用不了。

8.5继承和多态中的final与override关键字

这个我们在继承和多态章节已经进行了详细讲解这里就不再细讲,需要的话去复习继承和多台章节吧。

  • final:修饰一个类,不能被继承;修饰一个虚函数,不能被重写。
  • overide:加在派生类的虚函数后面,检查是否完成重写。

9. 模版的可变参数

C++11的新特性------可变参数模板,能够支持创建可以接受可变参数的函数模板和类模板。

相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。

然而由于可变参数模版比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段呢,我们掌握一些基础的可变参数模板特性就够我们用了,所以这里我们点到为止,以后大家如果有需要,再可以深入学习。

写"可变·模版参数"的场景很少,一般写库才会用到。可变·模版参数属于了解内容。


C语言的函数的可变参数的标志是三个点"...",底层类似于用一个动态数组去接收,然后去数组里面依次把对应的参数解析出来。

C++的模版的可变参数也是用的三个点"...",下面就是一个基本可变参数的函数模板:

【...位置1】
cpp 复制代码
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。

template <class ...Args>        //三点在Args前
void ShowList(Args... args)     //三点在Args后
{}

//arguments:n.参数

**参数包:**上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为"参数包",它里面包含了0到N(N>=0)个模版参数。

  • 模版参数包:代表多个类型;

  • 形参参数包:代表多个参数;
    总的来说,参数包代表了多个类型和多个参数。

  • ++可以传任意多个参数,它们可以是任意类型。++


可变模版参数的调用方式多种多样。

之前的函数模版是参数类型可变,但个数固定。

cpp 复制代码
//固定参数个数
template <class A>
void ShowList(A&& args)
{

}

可变模版参数------参数类型可变、参数个数可变。

本质还是调用了以后,编译器去实例化,生成了各个版本的ShowList。

  • 无参的ShowList;
  • 一个int参数的ShowList;
  • 一个int参数和一个const char*的ShowList;
  • ......

我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。

C语言的printf的可变参数是用类似的方式解析的。是在运行时去取可变参数。

  • 而模版是在编译时就要解析出每个参数的类型。
  • for循环是运行时逻辑,是在运行获取和解析,所以不支持这样用。

由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来逐个获取参数包的值。

9.0 打印参数包内容

【...位置2】

【使用sizeof打印参数包的大小(参数的个数)】...紧跟在sizeof后

C++11开始,很多新增的语法写出来的方式很怪异,没有老语法好学。

9.1 递归函数方式展开参数包

【...位置3】

编译时·递归推导·解析参数:

【传递参数包】...紧跟在args后

递归需要结束条件,只能用参数匹配的方式。(编译时推导)

零个参数的参数包会优先匹配到无参的重载函数。

cpp 复制代码
// 递归终止函数
template <class T>
void ShowList(const T& t)
{
    cout << t << endl;
}

// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
    cout << value <<" ";
    ShowList(args...);        //递归
}

int main()
{
    ShowList(1);
    ShowList(1, 'A');
    ShowList(1, 'A', std::string("sort"));
 
    return 0;
}

【说明】

把x,y,z传给print,打印完x之后,把y,z向下传递,然后打印y,然后把z向下传递。

最后调用无参print。

程序员方便了,底层的编译器要做的事多了------根据调用情况生成不同的函数。

C++11出来了以后,C++编译器的开发难度大大提高。


为什么要一层一层地调用,每层只打印一个数据,而不在顶层就把三个数据打印完。

就是为了实现可变参数模版------一个模版就能顶无数个模版,帮程序员节省了很多个要写的模版

像ShowList(1, "1111", 1.1, 2, "2222")这样的就不用再写一个模版。

**【可变参数模版是模版的模版】**通过可变参数模版,可以实例化出一个参数的函数模版、两个参数的函数模版、三个参数的函数模版、......

9.2 逗号表达式展开参数包

【...位置4】

这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。

  • 这种就地展开参数包的方式实现的关键是逗号表达式。
  • 逗号表达式会按顺序执行逗号前面的表达式,逗号表达式的值是最后一个式子的值。

expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性------初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... )。

最终会创建一个元素值都为0的数组int arr[sizeof... (Args)]。

由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。

cpp 复制代码
template <class T>
void PrintArg(T t)
{
    cout << t << " ";
}

//展开函数
template <class ...Args>
void ShowList(Args... args)
{
    int arr[] = { (PrintArg(args), 0)... };
    cout << endl;
}

int main()
{
    ShowList(1);
    ShowList(1, 'A');
    ShowList(1, 'A', std::string("sort"));
     
    return 0;
}

【测试】

数组的大小取决于大括号{ }初始化列表里面的值的个数。

不用逗号表达式也可以,不过需要设置返回值:

编译的时候数组开多大,取决于初始化列表里面有多少个值,那就会去执行PrintArg(args)...,解析args里面有多少个参数。

数组的作用就是让编译器去展开参数包。

cpp 复制代码
// 编译推演生成下面的函数
void ShowList(int x, char y, std::string z)
{
	int arr[] = { PrintArg(x),PrintArg(y),PrintArg(z) };	//数组开3个int的空间
	cout << "现成" << endl;
}

也可以直接cout输出。


ostream arr[] = {cout << (args)...}是非法的,因为ostream没有拷贝构造。

使用auto也是推导成ostream,而且数组不支持auto。

ostream&也是一样,数组不支持引用类型。

bool arr也不行,虽然ostream支持隐式类型转换成bool。


以上都是尝试解析出参数包的每一个参数,实践当中并不一定需要解析出每一个参数。

9.3 STL容器中的empalce相关接口函数

http://www.cplusplus.com/reference/vector/vector/emplace_back/

http://www.cplusplus.com/reference/list/list/emplace_back/

cpp 复制代码
template <class... Args>
void emplace_back (Args&&... args);

首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。


9.3.1 empalce用法

emplace_back的功能在一起情况下,和push_back一致:

打印结果:


emplace的可变模版参数不支持插入多个值。

emplace的可变参数是针对一个结点内是一个自定义构造的情况:

这里是:"构造" + "拷贝构造(移动构造)"

  • 构造:pair的first成员;
  • 拷贝构造(移动构造):将pair给到list的结点的data上。

emplace_back插入的时候,可以单独写出pair的first成员和second成员,而不必传一个pair。

这里是:"构造"

  • 直接把构造pair的参数包往下传;
  • 底层直接用pair的参数包构造pair;
  • 省了一个拷贝构造(移动构造);

如果是push_back,那参数类型确定是string,就必须先构造一个string,给到形参。

这里是emplace_back,是模版,推导出来是cahr*------正好是用于构造string对象的参数包。


而构造string的参数包可以有多个参数:

添加n个ch的构造函数:

测试结果:


存储string的lt,只要是构造string的参数包(有对应的构造),都可以给emplace_back。

参数包往下传,最后用来构造string。


存储pair的lt1,只要是构造pair的参数包(有对应的构造),都可以给emplace_back。

参数包往下传,最后用来构造pair。


emplace_back不需要解析参数包,只需要把参数包传递到底层,用来构造对应的成员。

  • 不解析参数包;
  • 只是搬运一下参数包;

9.3.2 empalce实现

这里两个函数都只是搬运一下参数包。

参数包最后到底层的结点构造函数里面去匹配:


【测试】

lt.emplace_back("111111111111")一直没有解析参数包,直到结点构造函数_data("11111")调用string的const cahr*构造。

参数包传到底层,然后直接去匹配对应的构造。

本质:编译器生成了对应的emplace_back(const char* ch)、insert(const char* ch)、ListNode(const char* ch)。

用不同的参数包(构造),编译器就会生成多份emplace_back(...)、insert(...)、ListNode(...)。


问题:插入右值,但是调用的是string的深拷贝。

原因:右值往下传的时候,会退化成左值。

解决方法:forward完美转发。

以及最底层也要加上forward,少任一个都不行:


测试:

【emplace可变模版参数】用的时候不用解析,只需要不断往下传,在底层匹配对应的构造函数。


9.3.1 empalce功能比较

那么相对insert和push_back系列,emplace接口的优势到底在哪里呢?

cpp 复制代码
int main()
{
    std::list< std::pair<int, char> > mylist;
    // emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
    // 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别
    mylist.emplace_back(10, 'a');
    mylist.emplace_back(20, 'b');
    mylist.emplace_back(make_pair(30, 'c'));
    mylist.push_back(make_pair(40, 'd'));
    mylist.push_back({ 50, 'e' });

    for (auto e : mylist)
        cout << e.first << ":" << e.second << endl;
    
    return 0;
}

int main()
{    
    // 下面我们试一下带有拷贝构造和移动构造的bit::string,再试试呢    
    // 我们会发现其实差别也不到,emplace_back是直接构造了,push_back    
    // 是先构造,再移动构造,其实也还好。
    std::list< std::pair<int, bit::string> > mylist;
    mylist.emplace_back(10, "sort");
    mylist.emplace_back(make_pair(20, "sort"));
    mylist.push_back(make_pair(30, "sort"));
    mylist.push_back({ 40, "sort"});
    
    return 0;
}

emplace_back可以直接传待插入的对象。

也可以直接传·用于构造传待插入的对象的参数包------这会更高效。


插入右值就是"构造"+"移动构造",相比于直接构造,效率其实差不多。

对于左值,使用emplace插入会高效很多。

emplace_back总体而言是更高效,推荐使用。

emplace除了传成品,还可以直接传原材料。

10. lambda表达式

10.1C++98中的一个例子

在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法。

cpp 复制代码
#include <algorithm>
#include <functional>
int main()
{
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>());
return 0;
}

实践当中对整型数组的排序意义不大。

更多的是对结构体·数组的排序,按结构体的某一个成员来进行排序。


如果待排序元素为自定义类型,需要用户提供排序时的比较规则:

cpp 复制代码
struct Goods
{
    string _name;  // 名字
    double _price; // 价格
    int _evaluate; // 评价
 
    Goods(const char* str, double price, int evaluate)
        :_name(str)
        , _price(price)
        , _evaluate(evaluate)
    {}
};

struct ComparePriceLess
{
    bool operator()(const Goods& gl, const Goods& gr)
    {
        return gl._price < gr._price;    
    }
};

struct ComparePriceGreater
{
    bool operator()(const Goods& gl, const Goods& gr)
    {
        return gl._price > gr._price;
    }
};

int main()
{
    vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
    sort(v.begin(), v.end(), ComparePriceLess());       //sort是函数,需要匿名对象 
    sort(v.begin(), v.end(), ComparePriceGreater());
}

小于比较排升序。

大于比较排降序。


随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,都要重新去写一个类(仿函数)。

如果每次比较的逻辑不一样------按价格比较(升、降)、按评价......,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。

因此,在C++11语法中出现了Lambda表达式。

10.2lambda表达式

cpp 复制代码
int main()
{
    vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
        return g1._price < g2._price; });
 
    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
        return g1._price > g2._price; });
 
    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
        return g1._evaluate < g2._evaluate; });
 
    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
        return g1._evaluate > g2._evaluate; });
}

上述代码就是使用C++11中的lambda表达式来解决,可以看出lambda表达式实际是一个匿名函数。


【课堂演示】



lambda表达式:其他函数体内部,定义了一个局部匿名函数对象,这个对象是一个函数。

不一定是局部,定义在全局也是可以的。


返回值类型和参数可省略:

有返回值的时候,返回值的类型可以省略,可以自动推导。

没有返回值的时候,就要注意不要去获取返回值。

还是推荐把返回值写上,函数的逻辑更好懂,可读性更好。

直接明了地写出return 0还好说,要是返回变量在定义的时候被定义成auto,就需要一层一层往下看,不写返回值的可读性就很差。

这种情况就最好把返回值写上,或者去把注释写清楚。


用仿函数的方式进行排序,缺点在于每种比较逻辑都要单独写一个仿函数,还要创建对应的对象。而且仿函数还有命名的问题。

不如使用lambda表达式:

测试:

缺点:不方便调试。

断点最好不要打到lambda函数体内:

否则按F10调试会在944行和945行之间来回跳------sort函数内部频繁调用这个lambda对象。

lambda函数、函数指针都是一种回调。


  • 传值捕捉本质是一种拷贝,并且const修饰了。
  • mutable相当于去掉const属性,可以修改了。------加mutable必须带(),哪怕没有参数

mutable只对值捕获有作用。


可以看到a和b的值只交换了一次:

  • mutable去掉常属性,修改了也不会影响外面被捕捉的值,因为是一种拷贝。

捕捉列表分为:传值捕捉、传引用捕捉。

在此之前,取地址和引用不会混淆:

  • int&:类型之后的&是引用;
  • &x:变量之前的&是取地址;

【各种捕获方式】
cpp 复制代码
int x = 0;

int main()
{
	// 只能用当前lambda局部域和捕捉的对象和全局对象

	int a = 0, b = 1, c = 2, d = 3;

	// 父域(能访问的)所有变量------传值捕捉
    // 所有值传值捕捉
	auto func1 = [=]
	{
		int ret = a + b + c + d + x;
		return ret;
	};
	
	// 父域(能访问的)所有变量------传引用捕捉
    // 所有值传引用捕捉
	auto func2 = [&]
	{
		a++;
		b++;
		c++;
		d++;
		int ret = a + b + c + d;
		return ret;
	};

	// 混合捕捉
	auto func3 = [&a, b]
	{
		a++;
		// b++;
		int ret = a + b;
		return ret;
	};

	// 混合捕捉
	// 所有值以引用方式捕捉,但是d用传值捕捉
	auto func4 = [&, d]
	{
		a++;
		b++;
		c++;
		//d++;

		int ret = a + b + c + d;
	};

    // 混合捕捉
	// 所有值以传值方式捕捉,但是d用传引用捕捉
	auto func5 = [=, &d]() mutable
	{
		a++;
		b++;
		c++;
		d++;
		int ret = a + b + c + d;
	}; 
    // 实际上只改变了外面的d

	return 0;
}
  • 默认可以访问:当前lambda局部域的对象、全局域的对象(例如:cout)
  • 父域的对象:需要依靠捕获列表捕获之后,才能使用。

lambda表达式一般都不加mutable,不想改变捕获变量的时候不用加,想改变捕获变量的时候传引用,加mutable允许在内部改变捕获变量的拷贝,不影响外部的捕获变量。

传值捕获加const修饰就是为了避免你认为里面改变了,外面跟着变。

mutable可以解扣,但是慎用。


10.3lambda表达式语法

lambda表达式书写格式:

cpp 复制代码
[capture-list] (parameters) mutable -> return-type { statement }

(parameters)、mutable、->return-type这三个部分可以省略。

最简单的lambda表达式只有:捕获列表、函数体。

  • 用lambda表达式来赋值,要在末尾"}"外面加上分号;
  • lambda表达式作参数,不用加分号;

10.3.1lambda表达式各部分说明

  • **[capture-list]-捕捉列表:**该列表总是出现在lambda函数的开始位置,编译器根据[]来 判断接下来的代码是否为lambda函数捕捉列表能够捕捉上下文中的变量供lambda 函数使用
  • (parameters)-参数列表:普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
  • ->returntype-返回值类型:追踪返回类型形式声明函数的返回值类型。
    • 没有返回值时此部分可省略。
    • 返回值类型明确情况下,也可省略,由编译器对返回类型进行推
  • **{statement}-函数体:**在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

有名函数就是:返回值、参数、函数体,外加函数名。

lambda表达式就可以理解为匿名函数。

【注意】

在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为

因此C++11中最简单的lambda函数为:

cpp 复制代码
[]{};

该lambda函数不能做任何事情。

cpp 复制代码
int main()
{
    // 最简单的lambda表达式, 该lambda表达式没有任何意义
   []{}; 
    
    // 省略参数列表和返回值类型,返回值类型由编译器推导为int
    int a = 3, b = 4;
   [=]{return a + 3; }; 
    
    // 省略了返回值类型,无返回值类型
    auto fun1 = [&](int c){b = a + c; }; 
    fun1(10)
    cout<<a<<" "<<b<<endl;
    
    // 各部分都很完善的lambda函数
    auto fun2 = [=, &b](int c)->int{return b += a+ c; }; 
    cout<<fun2(10)<<endl;
    
    // 复制捕捉x
    int x = 10;
    auto add_x = [x](int a) mutable { x *= 2; return a + x; }; 
    cout << add_x(10) << endl; 
    return 0;
}

通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调用。

如果想要直接调用,可借助auto将其赋值给一个变量。

10.3.2 捕获列表说明

捕捉列表描述了上下文中那些数据可以被lambda使用 ,以及使用的方式传值还是传引用

  • **[var]:**表示值传递方式捕捉变量var
  • **[=]:**表示值传递方式捕获所有父作用域中的变量(包括this)
  • **[&var]:**表示引用传递捕捉变量var
  • **[&]:**表示引用传递捕捉所有父作用域中的变量(包括this)
  • **[this]:**表示值传递方式捕捉当前的this指针

【注意】

  • a. 父作用域指包含lambda函数的语句块;
  • b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:
    • =, \&a, \&b\]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量。

  • c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。比如:
    • =, a\]:=已经以值传递方式捕捉了所有变量,捕捉a重复

  • **e.**在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
  • f. lambda表达式之间不能相互赋值,即使看起来类型相同。
cpp 复制代码
void (*PF)();
int main()
{
    auto f1 = []{cout << "hello world" << endl; };
    auto f2 = []{cout << "hello world" << endl; };
    // 此处先不解释原因,等lambda表达式底层实现原理看完后,大家就清楚了
    //f1 = f2;   // 编译失败--->提示找不到operator=()
    // 允许使用一个lambda表达式拷贝构造一个新的副本
    auto f3(f2);
    f3();
    // 可以将lambda表达式赋值给相同类型的函数指针
    PF = f2;
    PF();
 
    return 0;
}

10.4函数对象与lambda表达式

函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的类对象。

cpp 复制代码
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);  
  
    // lamber
    auto r2 = [=](double monty, int year)->double
        {
            return monty*rate*year; 
        };
    r2(10000, 2);
 
    return 0;
}

从使用方式上来看,函数对象与lambda表达式完全一样。

函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。

实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。


【课堂演示】

lambda和仿函数都是函数对象,调用方式也是一样的。


从汇编底层来看:

和引用类似,引用是一个语法上的新概念,但是底层没有引用的概念,引用的底层是指针。

lambda表达式也是一个语法上的新概念,底层没有lambda表达式,lambda被编译之后,会生成对应的仿函数,仿函数的类型名是"lambda_ + 一串字符串(UUID)"。

对我们来说它是一个匿名函数(匿名类),只能用auto接收,但是对编译器它不是匿名的,它有名字,这个名字是编译器自己生成的。


  • 捕捉列表中的对象rate,是作为构造的参数传给lambda,等价于成员变量存在lambda类对象中。
  • 捕捉列表中的变量是以成员变量的形式存储的。
  • 捕捉的本质是构造函数的初始化参数。

lambda表达式的调用,本质还是去调用operator()。


这里传参都是传地址(防止参数过大)。

以传值方式捕获所有父域变量,并不会真的传递所有,编译器只会传递用到的变量。


对编译器而言不是匿名的,它有名字,这个名字是编译器自己生成的------lambda_uuid。

UUIDUniversally Unique Identifier(通用唯一识别码)的缩写。

相关推荐
@atweiwei5 小时前
用 Rust 构建agent的 LLM 应用的高性能框架
开发语言·后端·rust·langchain·eclipse·llm·agent
skilllite作者5 小时前
Spec + Task 作为「开发协议层」:Rust 大模型辅助的标准化、harness 化与可回滚
开发语言·人工智能·后端·安全·架构·rust·rust沙箱
Dxy12393102165 小时前
Python序列标注模型上下文纠错详解
开发语言·python
孙鹏宇.5 小时前
左值右值.
java·开发语言
风吹迎面入袖凉5 小时前
【Redis】Redisson分布式锁原理
java·服务器·开发语言
A.A呐5 小时前
【QT第五章】系统相关
开发语言·qt
Mapleay5 小时前
CLion IDE 使用
c++
李白你好5 小时前
Java GUI-未授权漏洞检测工具
java·开发语言
兵哥工控5 小时前
MFC用高精度计时器实现五段时序控制的改进
c++·mfc