【C++】C++11(3):类&&lambda

前文回顾:【C++】C++11(2):右值引用补充&&可变参数模板


目录

一 新的类功能

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

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

3 defult和delete

4 final与override

5 委托构造函数

6  继承构造函数

二 STL中的一些变化

三lambda

1 lambda语法表达式

2 捕捉列表

3 lambda的应用

4 lambda的原理

四 包装器

1 function

2 bind


一 新的类功能

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

原来C++类中,有6个默认成员函数:构造函数/析构函数/拷贝构造函数/拷贝赋值重载/取地址重
载/const取地址重载,最后重要的是前4个,后两个用处不大,默认成员函数就是我们不写编译器
会生成⼀个默认的。C++11新增了两个默认成员函数,移动构造函数和移动赋值运算符重载

 如果你没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成⼀个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。

对于内置类型完成值拷贝,对于自定义对象实现它的拷贝构造

默认生成的移动构造,对于内置类型实现值拷贝,对于自定义类型有移动构造调用移动构造,没有移动构造调用拷贝构造 

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

如果自身是需要深拷贝的类型,需要自己实现移动构造

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;
}

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

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

3 defult和delete

• C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为⼀些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。

class Person
{
public:
 Person(const char* name = "", int age = 0)
 :_name(name)
 , _age(age)
 {}
 Person(const Person& p)
 ,_age(p._age)
 {}
 Person(Person&& p) = default;
 
 //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;
}


• 如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他⼈想要调⽤就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不⽣成对应函数的默认版本,称=delete修饰的函数为删除函数。

4 final与override

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

5 委托构造函数

6  继承构造函数

- 继承构造函数是 C++11 引入的一项特性,它允许派生类直接继承基类的构造函数,而不需要手动重新定义它们。这一特性显著简化了派生类的编写,特别是在基类有多个构造函数的情况下。

- 派生类继承基类的普通构造函数,特殊的拷贝构造函数/移动构造函数不继承

- 继承构造函数中派生类自己的成员变量如果有缺省值会使用缺省值初始化,如果没有缺省值那么跟之前类似,内置类型成员不确定,自定义类型成员使用默认构造初始化。

派生类有自己的成员时,一般不适合用继承构造


二 STL中的一些变化

• 下图1圈起来的就是STL中的新容器,但是实际最有用的是unordered_map和unordered_set。这 两个我们前⾯已经进行了非常详细的讲解,其他的⼤家了解⼀下即可。

• STL中容器的新接⼝也不少,最重要的就是右值引用和移动语义相关的push/insert/emplace系列接口(1:插入数据系列接口)和移动构造和移动赋值(2),还有initializer_list版本的构造(3)等,这些前⾯都讲过了,还有⼀些⽆关 痛痒的如cbegin/cend等需要时查查文档即可。

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


三lambda

1 lambda语法表达式

lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。
lambda 表达式语法使⽤层⽽⾔没有类型,所以我们⼀般是⽤auto或者模板参数定义的对象去接
收 lambda 对象。

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

返回类型称为尾置返回类型,我们之前的返回类型都称之为前置返回类型

表达式拆分:

底层会帮你生成一个类,这个类的名称也是编译器自己定的,例如lambda1,lambda2,这个类里有一个operator(),上面的参数就是operator()的参数,返回值就是operator()的返回值,函数体就是operator()的函数体

可以省略的部分:

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

• ->return type :返回值类型,⽤追踪返回类型形式声明函数的返回值类型,没有返回值时此
部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进⾏推导。

不能省略的部分:
[capture-list] :捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据[ ]来 判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下⽂中的变量供 lambda 函数使用,捕捉列表可以传值和传引⽤捕捉,具体细节3.2中我们再细讲。捕捉列表为空也不能省略。

• {function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以 使⽤其参数外,还可以使⽤所有捕获到的变量,函数体为空也不能省略。

2 捕捉列表

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

第⼀种捕捉方式是在捕捉列表中显示的传值捕捉传引用捕捉捕捉的多个变量⽤逗号分割。[x,y,&z]表示x和y值捕捉,z引用捕捉。

默认情况下, lambda捕捉列表值捕捉是被const修饰的,也就是说传值捕捉的过来的对象不能修改,mutable加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为空)。

传值捕捉是拷贝

如果非要修改传值捕捉,可以使用mutable,需要增加列表,注意:加了之后也只是里面能修改,外面的值不会改变。但是一般不会考虑去加mutable

我们上面学的捕捉都是显式捕捉,但是还有隐式捕捉

第⼆种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写⼀个=表示隐式值捕捉,在捕捉列表 写⼀个&表示隐式引用捕捉,这样我们 lambda 表达式中用了那些变量,编译器就会⾃动捕捉那些变量。

 // 隐式值捕捉
    // 用了哪些变量就捕捉哪些变量
    auto func2 = [=]
    {
        int ret = a + b + c;
        return ret;
    };
    cout << func2() << endl;

第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。[=, &x]表示其他变量隐式值捕捉,x引用捕捉;[&, x, y]表示其他变量引用捕捉,x和y值捕捉当使用混合捕捉时,第⼀个元素必须是&或=,并且&混合捕捉时,后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后面的捕捉变量必须是引用捕捉。

 // 混合捕捉1
    auto func4 = [&, a, b]
    {
        //a++;
        //b++;
        c++;
        d++;
        return a + b + c + d;
    };

捕捉全局变量:
捕捉列表必须为空,因为全局变量不⽤捕捉就可以⽤,没有可被捕捉的变量

int x = 0;
// 捕捉列表必须为空,因为全局变量不⽤捕捉就可以⽤,没有可被捕捉的变量 
auto func1 = []()
{
 x++;
};

int main()
{
//.....
}

捕捉局部的静态变量:

局部的静态和全局变量不能捕捉,也不需要捕捉

static int m = 0;
 auto func6 = []
 {
 int ret = x + m;
 return ret;
 };

lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使用。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。

捕捉this指针:

3 lambda的应用

在学习 lambda 表达式之前,我们使用的可调用对象只有函数指针和仿函数对象。函数指针的类型定义起来比较麻烦,仿函数需要定义一个类,相对也比较繁琐。而使用 lambda 来定义可调用对象,既简单又方便。

lambda 在很多其他场景下也很好用。比如在多线程中定义线程的执行函数逻辑,或者在智能指针中定制删除器等。lambda 的应用范围还是很广泛的,以后我们会在更多地方接触到它。

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 } };
 // 类似这样的场景,我们实现仿函数对象或者函数指针⽀持商品中 
 // 不同项的⽐较,相对还是⽐较⿇烦的,那么这⾥lambda就很好⽤了 
 sort(v.begin(), v.end(), ComparePriceLess());
 sort(v.begin(), v.end(), ComparePriceGreater());
 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;
 });
 return 0;
}

4 lambda的原理

lambda 的原理和范围for很像,编译后从汇编指令层的角度看,压根就没有lambda 和范围for
这样的东西。范围for底层是迭代器,而lambda底层是仿函数对象,也就说我们写了⼀个
lambda 以后,编译器会生成⼀个对应的仿函数的类。

仿函数的类名是编译按⼀定规则⽣成的,保证不同的 lambda 生成的类名不同,lambda参数/返
回类型/函数体就是仿函数operator()的参数/返回类型/函数体, lambda 的捕捉列表本质是生成
的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕
捉,编译器要看使用哪些就传那些对象。

C++ Lambda 表达式底层转化为仿函数类的示例


四 包装器

1 function

function是一个类模板,它用来封装可调用对象。底层可以理解为是一个仿函数,只是这个函数内部存储的可调用对象时,可以存储函数指针,仿函数,lambda,可以封装各种各样的可调用对象

template <class T>
class function; // undefined

template <class Ret, class... Args>
class function<Ret(Args...)>;

std::function是⼀个类模板,也是⼀个包装器。 std::function 的实例对象可以包装存储其他的可调用对象,包括函数指针,仿函数、lambda 、bind 表达式等,存储的可调用对象被称为std::function 的目标。若std::function 不含目标,则称它为空。调用空 std::function 的目标导致抛出std::bad_function_call异常。

以上是 function 的原型,他被定义<functional>头⽂件中。std::function - cppreference.com
是function的官方文件链接。

函数指针、仿函数、 lambda 等可调用对象的类型各不相同, std::function 的优势就是统
⼀类型,对他们都可以进行包装,这样在很多地方就方便声明可调用对象的类型

非静态成员函数的调用也和其他的函数不一样:

包装普通成员函数: 
普通成员函数还有⼀个隐含的this指针参数,所以绑定时传对象或者对象的指针过去都可以

this指针不能显式调用

对象调用成员函数指针:

通过OJ题展示了 std::function 作为map的参数,实现字符串和可调用对象的映射表功能:

我们之前在学习vector的时候讲解过这道题,之前讲解的做法是这样的:

但是在学习了function之后,就有更简单的方法:

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的⼀个命名空间中。

调整参数顺序:

调整参数个数:

分别绑死第123个参数

实践中的用法:

成员函数对象进行绑死,就不需要每次都传递了

绑定的使用示例:  
复利通俗来说就是「利滚利」—— 不仅本金会产生利息,之前累计的利息也会像本金一样,继续产生新的利息,收益会随着时间呈指数级增长(而非单利的线性增长)

```cpp
#include <iostream>
#include <functional>  // 必需头文件:支持 std::function 和 std::bind
using namespace std;

int main() {
    // 复利纯收益计算核心 Lambda 表达式
    auto func1 = [](double rate, double money, int year)->double {
        double ret = money;  // 初始化本息和为原始本金
        for (int i = 0; i < year; i++)
        {
            ret += ret * rate;  // 每年本息和 = 上一年本息和 × (1 + 年利率),实现利滚利
        }
        return ret - money;  // 返回纯收益(扣除本金)
    };

    // 绑定逻辑:std::bind 固定 func1 的 rate=0.015(1.5%)和 year 参数,_1 为本金占位符
    function<double(double)> func_r1_5_y3 = bind(func1, 0.015, _1, 3);  // 1.5%利率 + 3年期限
    function<double(double)> func_r1_5_y5 = bind(func1, 0.015, _1, 5);  // 1.5%利率 + 5年期限
    function<double(double)> func_r1_5_y20 = bind(func1, 0.015, _1, 20); // 1.5%利率 + 20年期限

    // 输出10万本金在1.5%利率下不同期限的复利纯收益
    cout << func_r1_5_y3(100000) << endl;    // 10万本金 + 1.5%利率 + 3年 → 预期约4567.84
    cout << func_r1_5_y5(100000) << endl;    // 10万本金 + 1.5%利率 + 5年 → 预期约7728.40
    cout << func_r1_5_y20(100000) << endl;   // 10万本金 + 1.5%利率 + 20年 → 预期约34685.50

    // -------------- 固定利率10%,不同期限的函数绑定 --------------
    // 绑定逻辑:std::bind 固定 func1 的 rate=0.1(10%)和 year 参数,_1 为本金占位符
    function<double(double)> func_r10_y3 = bind(func1, 0.1, _1, 3);  // 10%利率 + 3年期限
    function<double(double)> func_r10_y5 = bind(func1, 0.1, _1, 5);  // 10%利率 + 5年期限
    function<double(double)> func_r10_y20 = bind(func1, 0.1, _1, 20); // 10%利率 + 20年期限

    // 输出10万本金在10%利率下不同期限的复利纯收益
    cout << func_r10_y3(100000) << endl;    // 10万本金 + 10%利率 + 3年 → 预期约33100.00
    cout << func_r10_y5(100000) << endl;    // 10万本金 + 10%利率 + 5年 → 预期约61051.00
    cout << func_r10_y20(100000) << endl;   // 10万本金 + 10%利率 + 20年 → 预期约572749.99

    return 0;
}
```

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值