
🎬 个人主页 :MSTcheng · CSDN
🌱 代码仓库 :MSTcheng · Gitee
🔥 精选专栏 : 《C语言》
《数据结构》
《算法学习》
《C++由浅入深》
💬座右铭: 路虽远行则将至,事虽难做则必成!
前言:在上一篇文章中我们向大家介绍了,类型转换,完美转发,以及可变参数模板,本篇文章就来着重介绍一下类的新共功能,lambda表达式,以及包装器。
文章目录
一、类的新功能
1.1移动构造和移动赋值
在之气C++类和对象的时候我们就向大家介绍过,C++的类有6个默认成员函数分别是:构造函数/析构函数/拷贝构造函数/拷贝赋值重载 /取地址重载/const 取地址重载,最后重要的是前4个,后两个用处不大,默认成员函数就是我们不写编译器会生成⼀个默认的。而C++11之后类就新增了两个成员函数 :移动构造和移动赋值运算符重载。
- 如果你没有自己实现移动构造函数 ,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个 。那么编译器会自动生成⼀个默认移动构造。默认生成的移动构造函数 ,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。(移动赋值重载也一样,显示写了就调用没有显示写就默认生成)
cpp
#include<iostream>
#include<string>
using namespace std;
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:
std::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);//当除构造之外的成员函数都没实现的时候,编译器为Person默认生成移动构造
//对一个有资源的对象进行了move了以后就不要写析构了否则会对空对象进行析构出问题
Person s4;
s4 = std::move(s2);//当除构造之外的成员函数都没实现的时候,编译器为Person默认生成移动赋值
return 0;
}
以上面这段代码为例,对于
Person类没有实现析构,拷贝构造,拷贝赋值等成员函数时,编译器会默认生成移动构造,对于内置类型_age就完成逐字节的拷贝,对于自定义类型_name则去调用string的移动构造,如果string实现了就调用移动构造,如果没有实现就调用拷贝构造! 如果有资源那我们就要显示写移动构造,不用编译器默认生成的移动构造。
1.2defult和delete
- C++11可以让你更好的控制要使用的默认函数。 假设你要使用某个默认的函数,但是因为⼀些原因这个函数没有默认⽣成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用
default关键字显⽰指定移动构造生成。 - 如果能想要限制某些默认函数的生成 ,在C++98中,是该函数设置成
private,并且只声明补丁已,这样只要其他⼈想要调⽤就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不⽣成对应函数的默认版本,称=delete修饰的函数为删除函数。
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;//使用default强制默认生成移动构造 即使已经写了拷贝构造等成员函数
//Person(const Person& p) = delete;//强制不生成拷贝构造 如果显示写了就不能再delete因为
//同一成员函数不能同时被删除和定义
private:
my_string::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
二、lambda表达式
2.1lambda的基本语法
-
lambda表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。lambda表达式语法使用层而言没有类型,所以我们一般是⽤auto或者模板参数定义的对象去接收lambda对象,下面来看一个简单的lambda表达式:cppint main() { // ⼀个简单的lambda表达式 auto add1 = [](int x, int y)->int {return x + y; }; cout << add1(1, 2) << endl; return 0; }
首先我们看到lambada表达式的整体结构:lambda表达式的格式:
cpp
[capture-list] (parameters)-> return type) { function boby}
auto用来接收lambda对象,我们不明确它的类型所以使用auto来接收。[capture-list] :捕捉列表 ,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下⽂中的变量供 lambda 函数使⽤,捕捉列表可以传值和传引⽤捕捉 。另外捕捉列表为空时也不能省略。(parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同()⼀起省略->return type :返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。{function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。

下面就来看看几种变化后的情况:
cpp
int main()
{
//======================================
// 1、捕捉为空也不能省略
// 2、参数为空可以省略
// 3、返回值可以省略,可以通过返回对象⾃动推导
// 4、函数题不能省略
//======================================
auto func1 = [] //省略了参数列表()以及返回值
{
cout << "hello" << endl;
return 0;
};
func1();//可以像函数一样调用
int a = 0, b = 1;
auto swap1 = [](int& x, int& y) //有参数时参数列表不能省略 返回值可以省略
{
int tmp = x;
x = y;
y = tmp;
};
swap1(a, b);
cout << a << ":" << b << endl;
return 0;
}
那学了这个lambda表达式到底有什么用呢?下面就来看看它的应用场景:
2.2lambda的应用
在学习
lambda表达式之前,我们使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦 ,而定义仿函数又要定义⼀个类 ,相对会比较麻烦。 但是使lambda可以直接定义在函数内部,不需要再去额外的定义类,且定义之后直接在函数内部调用lambda对象,简单又方便!
下面来看代码:
cpp
#include<vector>
#include<algorithm>
//描述商品的类
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
// ...
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{
}
};
//仿函数1
struct Compare1
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price < gr._price;
}
};
//仿函数2
struct Compare2
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price > gr._price;
}
};
//仿函数3
struct Compare3
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._evaluate > gr._evaluate;
}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3}, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end(), Compare1());//按照价格的升序去排序
sort(v.begin(), v.end(), Compare2());//按照价格的降序去排序
sort(v.begin(), v.end(), Compare3());//按照评价的高低去排序
//以上均是调用仿函数来实现不同方式比较的排序
//lambda对象priveLess 按照价格升序比较
auto priceLess = [](const Goods& gl, const Goods& gr)
{
return gl._price < gr._price;
};
sort(v.begin(), v.end(), priceLess);//可以传lambda对象直接调用
cout << "按照价格升序比较:";
for (auto& e : v)
{
cout << e._price<<" ";
}
cout << endl;
//也可以将lambda表达式直接放在第三个参数传参 因为lambda是一个表达式 直接放在参数位置没有任何问题
//这里调用的就是匿名的lambda表达式对象 按照价格降序排序
sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr){
return gl._price > gr._price;
});
cout << "按照价格降序排序:";
for (auto& e : v)
{
cout << e._price<<" ";
}
cout << endl;
//匿名lambda表达式对象 按照评价的降序排序
sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr) {
return gl._evaluate > gr._evaluate;
});
cout << "按照评价的降序排序:";
for (auto& e : v)
{
cout << e._evaluate<<" ";
}
cout << endl;
//匿名lambda表达式对象 按照评价的升序去排序
sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr) {
return gl._evaluate < gr._evaluate;
});
cout << "按照评价的升序去排序:";
for (auto& e : v)
{
cout << e._evaluate<<" ";
}
cout << endl;
}

乍一看
lambda表达式是不是比函数指针,仿函数要更加方便,定义仿函数时我们还可能因为仿函数名定义不规范而不知道仿函数的功能是什么,而lambda表达式显而易见,我们既可以写有名对象像priceLess对象一样,还可以写匿名对象直接在调用出写该表达式即可。
2.3捕捉列表
-
再回过头来看看捕捉列表,捕捉列表说的是:
lambda表达式中默认只能用lambda函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉。 下面举个例子你就明白了:cpp#include<iostream> using namespace std; //全局变量 int x = 0; int main() { //当前lambda所在函数局部域的变量 int a = 0, b = 1, c = 2, d = 3; //如果lambda里面想使用外面的变量 比如a,b,c,x 这时候就需要捕捉 auto func1 = [a, &b]() { //传值值捕捉的变量不能修改,引用捕捉的变量可以修改 //a++; b++; x++; //全部变量不用捕捉也能直接使用 并且在lambda表达式里面修改外面也会跟着改 int ret = a + b; return ret; }; cout << func1() << endl; func1(); cout << x << endl; cout << b << endl; }
lambda表达式捕捉的方式有三种:
-
第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉 ,捕捉的多个变量用逗号分割。
[x,y, &z]表⽰x和y传值捕捉,z传引用捕捉。注意:传值捕捉可以认为是加了const修饰是不能被修改的,传引用的捕捉以及全局变量可以修改,且里面修改影响外面。 -
第二种捕捉方式是在捕捉列表中隐式捕捉 ,我们在捕捉列表写一个
=表示隐式值捕捉,在捕捉列表写⼀个&表示隐式引用捕捉,这样我们lambda表达式中用了那些变量,编译器就会自动捕捉那些变量。cpp#include<iostream> using namespace std; int main() { //当前lambda所在函数局部域的变量 int a = 0, b = 1, c = 2, d = 3; // 隐式值捕捉 // 用了哪些变量就捕捉哪些变量 auto func2 = [=] { int ret = a + b + c; return ret; }; cout << func2() << endl; // 隐式引用捕捉 // 用了哪些变量就捕捉哪些变量 auto func3 = [&] { a++; c++; d++; }; func3(); cout << a << " " << b << " " << c << " " << d << endl; return 0; }3.第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。
[=, &x]表⽰其他变量隐式值捕捉,x引用捕捉;[&, x, y]表示其他变量引用捕捉,x和y值捕捉。当使用混合捕捉时,第⼀个元素必须是&或=,并且&混合捕捉时,后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后面的捕捉变量必须是引用捕捉。 注意:局部的静态和全局变量不能捕捉,也不需要捕捉cpp#include<iostream> using namespace std; int main() { //当前lambda所在函数局部域的变量 int a = 0, b = 1, c = 2, d = 3; // 混合捕捉 除了a和b是传值捕捉在main函数内部的其他变量都是传引用捕捉 auto func4 = [&, a, b] { //a++;//a和b是传值捕捉所以不能被修改 //b++; c++; d++; return a + b + c + d; }; func4(); cout << a << " " << b << " " << c << " " << d << endl; // 混合捕捉 除了a和b是传引用捕捉在main函数内部的其他变量都是传值捕捉 auto func5 = [=, &a, &b] { a++; b++; /*c++;//c和d是传值捕捉所以不能被修改 d++;*/ return a + b + c + d; }; func5(); cout << a << " " << b << " " << c << " " << d << endl; // 局部的静态和全局变量不能捕捉,也不需要捕捉 static int m = 0; auto func6 = [] { int ret = x + m; return ret; }; }
注意:实际上传值捕捉也可以修改加上一个mutable关键字就可以了:
cpp
#include<iostream>
using namespace std;
int main()
{
//当前lambda所在函数局部域的变量
int a = 0, b = 1, c = 2, d = 3;
// 传值捕捉本质是⼀种拷⻉,并且被const修饰了
// mutable相当于去掉const属性,可以修改了
// 但是修改了不会影响外⾯被捕捉的值,因为是⼀种拷⻉
auto func7 = [=]()mutable
{
a++;
b++;
c++;
d++;
return a + b + c + d;
};
cout << func7() << endl;
cout << a << " " << b << " " << c << " " << d << endl;
return 0;
}
注意两个点:
lambda表达式如果在函数局部域中,他可以捕捉lambda位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉,lambda表达式中可以直接使用。这也意味着lambda表达式如果定义在全局位置,捕捉列表必须为空。- 默认情况下,
lambda捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改,mutable加在参数列表的后⾯可以取消其常量性,也就说使⽤该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使⽤该修饰符后,参数列表不可省略(即使参数为空)。
2.4lambda的原理
lambda其实和范围for很像,范围for的底层会转换为迭代器,而lambda的底层其实会转化成仿函数 ,也就是说我们再写了一个lambda以后,编译器会生成一个对应的仿函数类 。而仿函数的类名是编译按照一定的规则生成的,为了保证不同的lambda生成的类名不同,lambda参数/返回类型/函数体 ------>仿函数operator()的参数/返回类型/函数体 , 而lambda的捕捉列表 本质质上是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是lambda类构造的实参,当然对于隐式捕捉,编译器要看使用了哪些对象就传那些对象!
下面我们给出一段代码并查看这段代码的汇编代码就能一探究竟:
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;
// lambda
auto r2 = [rate](double money, int year) {
return money * rate * year;
};
// 函数对象
Rate r1(rate);
r1(10000, 2);
r2(10000, 2);
}

三、包装器
3.1function
cpp
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 异常(异常在后面的文章中介绍) 。总结一句话就是:包装器就是用来存储可调用对象,包括函数指针对象,仿函数对象,lambda对象,bind(后面介绍)对象,从而方便调用的一个工具
cpp
#include<functional> //包装器定义在functional这个头文件中记得包含
#include<vector>
int f(int a, int b)
{
return a + b;
}
struct Functor
{
public:
int operator() (int a, int b)
{
return a + b;
}
};
class Plus
{
public:
Plus(int n = 10)
:_n(n)
{}
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b) const
{
return (a + b) * _n;
}
private:
int _n;
};
int main()
{
// void(*pf)(int) = nullptr;
//
// 包装各种可调用对象
function<int(int, int)> f1 = f;
function<int(int, int)> f2 = Functor();
function<int(int, int)> f3 = [](int a, int b) {return a + b; };
cout << f1(1, 1) << endl;
cout << f2(1, 1) << endl;
cout << f3(1, 1) << endl;
//可以直接使用一个vector来存储function,每个function存这可调用对象
vector<function<int(int, int)>> vf = { f , Functor(),[](int a, int b) {return a + b; } };
for (auto& f : vf)
{
cout << f(1, 1) << endl;
}
// 包装静态成员函数
// 成员函数要指定类域并且前面加&才能获取地址
function<int(int, int)> f4 = &Plus::plusi;//拿到Plus类中plusd函数的地址
cout << f4(1, 1) << endl;
//=============================
//注意这里千万不要忘记了this指针 所以在定义f5时第一个参数的类型为Plus*
//然后再传参的时候第一个参数传 对象ps的地址!!!
//=============================
function<double(Plus*, double, double)> f5 = &Plus::plusd;
Plus ps;
cout << f5(&ps, 1.1, 1.1) << endl;
//function<double(Plus, double, double)> f6 = &Plus::plusd;
//============================
//注意如果我们在定义f6时给第一个参数的类型加上了const
//那么我们也要到Plus这个类中 把对应的函数也加上const
//不然会出现const this指针给非const指针赋值的情况导致报错!!!
//=============================
function<double(const Plus&, double, double)> f6 = &Plus::plusd;
cout << f6(Plus(), 1.1, 1.1) << endl;
cout << f6(ps, 1.1, 1.1) << endl;
//如果将第一参数类型设计成右值引用 那么在调用f7传值的时候注意传右值
//传匿名对象,或者move(左值)
function<double(Plus&&, double, double)> f7 = &Plus::plusd;
cout << f7(Plus(), 1.1, 1.1) << endl;
cout << f7(move(ps), 1.1, 1.1) << endl;
return 0;
}
注意:
函数指针、仿函数、
lambda等可调⽤对象的类型各不相同 ,std::function的优势就是统一类型,对他们都可以进行包装,这样在很多地方就方便声明可调用对象的类型 ,比如将他们的类型都转为function之后可以直接存入vector中,调用起来也非常方便。
3.2bind(绑定)
cpp
simple(1)
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
bind 是⼀个函数模板,它也是⼀个可调用对象的包装器 ,可以把他看做⼀个函数适配器,对接收的fn可调用对象进行处理后返回⼀个可调用对象。 bind 可以用来调整参数个数和参数顺序另外bind 也在<functional>这个头文件中。
调用bind的⼀般形式auto newCallable = bind(callable,arg_list);其中newCallable本身是⼀个可调用对象,arg_list是⼀个逗号分隔的参数列表,对应给定的callable的参数。当我们调⽤newCallable时,newCallable会调用callable,并传给它arg_list中的参数。 下上面的文字不理解没关系,下面直接上代码你就理解了:
cpp
#include<functional>
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
int Sub(int a, int b)
{
return (a - b) * 10;
}
int SubX(int a, int b, int c)
{
return (a - b - c) * 10;
}
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
cout << Sub(10, 5) << endl;
// bind 本质返回的一个仿函数对象
// 调整参数顺序(不常用)
// _1代表第一个实参
// _2代表第二个实参
// ...
//牢牢记住不管bind这个函数的参数顺序怎么变化
//_1永远代表newSub1的第一个参数 _2永远代表第二个参数以此类推
auto newSub1 = bind(Sub, _2, _1);
cout << newSub1(10, 5) << endl;
// 调整参数个数
auto newSub2 = bind(Sub, _1, 10);
cout << newSub2(20) << endl;
auto newSub3 = bind(Sub, 5, _1);
cout << newSub3(20) << endl;
auto newSub4 = bind(SubX, 5, _1, _2);
cout << newSub4(10, 20) << endl;
//无论怎么调整_1和_2的位置 _1永远是newSub5的第一个参数10
auto newSub5 = bind(SubX, _1, 5, _2);
cout << newSub5(10, 20) << endl;
auto newSub6 = bind(SubX, _1, _2, 5);
cout << newSub6(10, 20) << endl;
// 成员函数对象进行绑死,就不需要每次都传递了
function<double(Plus&&, double, double)> f6 = &Plus::plusd;
Plus pd;
cout << f6(move(pd), 1.1, 1.1) << endl;
cout << f6(Plus(), 1.1, 1.1) << endl;
// auto f7 = bind(&Plus::plusd, Plus(), _1, _2);
function<double(double, double)> f7 = bind(&Plus::plusd, Plus(), _1, _2);
cout << f7(1.1, 1.1) << endl;
return 0;
}
注意:
arg_list中的参数可能包含形如_n的名字,其中n是⼀个整数,这些参数是占位符,表示newCallable的参数,它们占据了传递给newCallable的参数的位置。数值n表生成的可调⽤对象中参数的位置:_1为newCallable的第⼀个参数,_2为第⼆个参数,以此类推。_1/_2/_3....这些占位符放到placeholders的⼀个命名空间中。
3.2.1bind的引用
计算一个复利的lambda:
cpp
#include<functional>
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
int main()
{
// 100w
// 计算复利的lambda
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;
};
//cout << func1(0.2, 1000000, 3) << endl;
//cout << func1(0.2, 1000000, 10) << endl;
//cout << func1(0.2, 1000000, 30) << endl;
// 绑死一些参数,实现出支持不同年华利率,不同金额和不同年份计算出复利的结算利息
function<double(double)> func3_1_5 = bind(func1, 0.015, _1, 3);
function<double(double)> func5_2_0 = bind(func1, 0.02, _1, 5);
function<double(double)> func10_2_5 = bind(func1, 0.025, _1, 10);
function<double(double)> func20_3_5 = bind(func1, 0.035, _1, 30);
cout << func3_1_5(1000000) << endl;
cout << func5_2_0(1000000) << endl;
cout << func10_2_5(1000000) << endl;
cout << func20_3_5(1000000) << endl;
}
此时我们绑死
func1,因为func1执行的就是计算功能,我们并不用去关系它是如何计算的,我们只关心我们输入的钱数以及存钱年数,以及最终的计算结果,所以我们就可以绑死func1每次只输入钱数和年数即可!
四、总结
以上就是C++11的新特性补充了,C++11还有很多的特性这里只列举了最常见的一些特性,还有一些特性后期再进行补充。
html
MSTcheng 始终坚持用直观图解 + 实战代码,把复杂技术拆解得明明白白!
👁️ 【关注】 看普通程序员如何用实用派思路搞定复杂需求
👍 【点赞】 给 "不搞虚的" 技术分享多份认可
🔖 【收藏】 把这些 "好用又好懂" 的干货技巧存进你的知识库
💬 【评论】 来唠唠 ------ 你踩过最 "离谱" 的技术坑是啥?
🔄 【转发】把实用技术干货分享给身边有需要的程序员伙伴
技术从无唯一解,让我们一起用最接地气的方式,写出最扎实的代码! 🚀💻
感谢能够看到这里的小伙伴,如果这篇文章有帮到您,还请给个三连!你们的持续支持是我更新最大的动力!谢谢!
