详解c++---c++11(下)

目录标题

  • default关键字
  • delete关键字
  • lambda表达式
    • 为什么会有lambda表达式
    • lambda的用法
    • 多线程和lambda
    • lambda的底层
  • 可变参数模板
  • emplace
  • 包装器
    • 为什么会有包装器
    • 包装器的使用
  • bind

default关键字

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

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:YCF::string _name;int _age;
};
int main()
{Person s1;Person s2 = s1;Person s3 = std::move(s1);return 0;
}

这里使用的是我们自己创建的string类对象,在这个对象里面我们给拷贝构造做了标记所以当使用string类的拷贝构造函数的时候会自动的答应出相关的信息,那么上面的代码运行的结果就如下:
在这里插入图片描述
可以看到我们这里没有自己写Person类的移动构造并且我们还自己实现了左值引用版本的拷贝构造,所以根据规则来看编译器这里是不会自己生成对应的移动构造的,但是当我们在移动构造的声明语句后面添加一个default的话编译器在这里会强制生成对应的默认成员函数,那么这就是default关键字的用法,当然这里大家要注意的一点就是这里的自动生成只针对那几个默认成员函数你随便给的函数是不会生成的,那么这就是default关键字的作用,

delete关键字

如何让一个类无法被拷贝呢?答案有很多个其中一个就是将拷贝构造私有比如说下面这样的代码:

class A
{
public:A(){}~A(){delete[] p;}
private:A(const A& aa):p(aa.p){}
private:int* p = new int[10];
};
int main()
{A aa1;A aa2(aa1);return 0;
}

代码的运行结果如下:
在这里插入图片描述
可以看到这里报错了,并且报错的原因是私有的成员函数不能被访问,但是这么写只能保证在类外面无法被拷贝,但是在里面依然还是可以使用拷贝构造的,比如说下面的代码:

class A
{
public:void func(){A tmp(*this);}A(){}~A(){delete[] p;}
private:A(const A& aa):p(aa.p){}
private:int* p = new int[10];
};
int main()
{A aa1;aa1.func();return 0;
}

并且将这段代码运行一下还会发现这里报错了,因为这里的拷贝是浅拷贝导致了同一块空间被析构了两次:
在这里插入图片描述
所以就会报错来终止这里的程序运行,第二个种方式就是只声明不实现这样的话调用拷贝构造函数就无法正常的链接从而阻止拷贝,那么这种方法就是c++98采用的方法,这里的代码如下:

class A
{
public:void func(){A tmp(*this);}A(){}~A(){delete[] p;}
private:A(const A& aa);
private:int* p = new int[10];
};

代码运行一下就会出现这样的结果:
在这里插入图片描述
第三种方式就是delete,被delete修饰的函数就称为被删除的函数无法被调用,编译器不生成对应函数的默认版本比如说下面 的代码:

class A
{
public:void func(){A tmp(*this);}A(){}~A(){delete[] p;}// C++11 A(const A& aa) = delete;
private:int* p = new int[10];
};

将上面的代码运行一下就可以看到这样的结果:
在这里插入图片描述
编译器告诉我们这个函数已经被删除了无法正常调用,那么这就是delete关键字的作用。

lambda表达式

为什么会有lambda表达式

在学习c语言的时候我们学习过一个东西叫做函数指针,这个东西可以让我们更加方便的使用函数传递函数,但是这个东西在遇见一些比较复杂的情况时会变得很难理解,比如说下面的代码:

void(*func(void (*f)()))()

大家知道这是一个什么东西吗?是不是很复杂对吧!上面代码表示的意思就是一个函数的参数是一个函数指针这个函数不需要参数并且没有返回类型,并且这个函数的返回值也是一个函数指针并且这个函数不需要参数也没有返回值,听起来很简单但是写起来就很复杂那么这就是c语言函数指针的缺点,那么为了解决这个缺点c++就提出来一个东西叫做仿函数,它可以大大的降低函数指针的可读性,比如说下面的代码:

#include <algorithm>
#include <functional>
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;}
};
int main()
{vector<Goods> v = { { "苹果", 2.1, 5 },{ "香蕉", 3, 4 },{ "橙子", 2.2, 3 },{ "菠萝", 1.5, 4 } };sort(v.begin(), v.end(), ComparePriceLess());return 0;
}	

这里我们将创建了一个可以比较物品价格的仿函数,有了这个仿函数之后就可以联合sort函数来对数组里面的数据进行价格上的排序,通过调试便可以看到容器里面的内容如下:
在这里插入图片描述
如果我想让容器里面的数据按照价格排序完之后接着按照价格的降序进行排序的话这里就又得创建一个不一样的仿函数来实现差不多的内容,比如说下面的代码:

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(v.begin(), v.end(), ComparePriceGreater());return 0;
}

代码的运行结果如下:
在这里插入图片描述
目前为止上面的代码理解起来写起来都还没有什么明显的问题,而且不看仿函数的内容只根据函数名也能够理解仿函数的作用可是如果Goods类里面的内容变多了呢?如果我们要求对类中的每个成员变量都得有其对应的比较仿函数呢?如果这个仿函数的名字取得不好无法根据函数名来推断出函数的功能呢?比如说对价格的升序叫做func1降序则叫func2呢?你觉得面仿函数在这里还方便吗?是不是就有点麻烦而且写起来内容很多但是价值却很少对吧,就好比我们花了很大的功夫给弹夹装子弹但是弹夹打空之后却只有几颗子弹打中了敌人,那么为了解决这个问题我们就有了lambda表达式,这个东西就可以帮助我们解决上面的问题,那么使用lambda表达式之后上面的代码就变成下面这个样子:

#include <algorithm>
#include <functional>
struct Goods
{string _name;  // 名字double _price; // 价格int _evaluate; // 评价// ...Goods(const char* str, double price, int evaluate):_name(str), _price(price), _evaluate(evaluate){}
};
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; });return 0;
}	

是不是看起来代码简洁了很多对不对并且运行的结果也是一样的先是价格升序:
在这里插入图片描述
再是价格降序:
在这里插入图片描述
明白了lambda的作用之后紧接着我们就来看看lambda的使用。

lambda的用法

lambda表达式的书写格式:[capture-list](parameters) mutable ->return-type{statement}
[capature-list]:捕捉列表,该列表总是出现在lambda函数开始的位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量提供的lambda函数使用。必须写
2.parameters:参数列表,与普通函数的参数列表一致,如果没有参数传递则可以连同()一起省略。有参数就写没有参数就可以不写
3.mutable:默认情况下lambda函数总是一个const函数,而添加了mutable便可以取消其常量属性。使用该修饰符时,即使参数列表为空这里也依然不能省略,那么这个东西一般可以不写。
4.->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可以省略。返回值类型明确的情况下也可以省略,因为编译器会自动的对返回类型进行推导。
5.{statement}:函数体。在函数体内除了可以使用其参数外,还可以使用所有捕获的变量。这个必须要写的。

那么知道lambda的各个部位的作用之后我们来看看如何写出一个用于比较两个数大小的lambda表达式,首先捕捉列表必须得写,因为这里没有什么变量需要我们进行捕捉所以方括号里面就为空,紧接着就是参数列表该lambda表达式的作用就是比较两个数的大小,所以参数列表里面就得有两个整型的参数,那么这里的代码如下:

[](int x,int y)

然后就是mutable这个功能比较简单也不需要去掉常量属性,所以这里不需要添加mutable那么我们就可以将其去掉,接着就是返回值,该函数的作用是比较两个参数的大小所以返回值的类型就是bool类型,但是编译器可以自行的推断返回值类型所以这里可以写也可以不写,最后就是函数体,在函数体里面我们需要判断两个参数的大小,所以函数体里面直接返回参数的比较结果就行,那么这里的代码就如下:

[](int x, int y){ return x>y ;};//没有添加返回值的形式
[](int x, int y)->bool{ return x>y ;};//添加了返回值的形式

那么这里就存在一个问题如何使用这个lambda表达式呢?答案是先创建一个lambda对象来接收这个表达式然后在用这个对象来使用这个表达式,但是这里有个问题lambda对象的类型是什么呢?答案是我们不知道但是编译器知道,所以我们得使用auto来创建对象并像函数一样使用这个对象来实现一些功能,比如说下面的代码:

int main()
{auto compare= [](int x, int y)->bool { return x > y; };if (compare(1, 2)){cout << "第一个参数大" << endl;}else{cout << "第二个参数大" << endl;}return 0;
}

代码的运行结果如下:
在这里插入图片描述
那么看到这里大家心里肯定会存在一个疑问上面的表达式中没有创建对象为什么可以将lambda直接传递的使用呢?答案是这里的lambda表达式实际上是一个对象,上面的直接传递就是创建了一个匿名对象将匿名对象进行传递,下面则是创建一个匿名对象将匿名对象赋值给compare,然后再使用compare进行比较。lambda的函数体是一个单独的作用域,无法使用外部的数据,比如说下面的代码:

int main()
{int a = 1;int b = 2;auto add1 = [](int x, int y) {return x + y; };cout << add1(a, b) << endl;auto add2 = [](int x) {return x + b;/*直接使用了外部变量b*/ };cout << add2(a) << endl;return 0;
}

这段代码运行的结果如下:
在这里插入图片描述
可以看到在lambda函数体的内部是无法直接使用外部变量的,但是如果想用的话还是可以使用的,但是这里就得使用捕捉列表对数据进行捕捉,这样就可以在函数体里面使用外部的数据,比如说下面的代码:

int main()
{int a = 1;int b = 2;auto add1 = [](int x, int y) {return x + y; };cout << add1(a, b) << endl;auto add2 = [a,b]() {return a + b;};cout << add2() << endl;return 0;
}

代码的运行结果如下:
在这里插入图片描述

但是这里的捕捉默认情况下是传值捕捉,外部的a和内部的a不是同一个a并且内部的a还添加了一个const进行修饰,如果你想要去掉其常性的话就得添加mutable,比如说下面的代码:

int main()
{int a = 1;int b = 2;auto add2 = [a, b]() {a++; b++; };return 0;
}

在这里插入图片描述
如果想要能够进行修改的话还得添加mutable,那么 这里的代码如下:

int main()
{int a = 1;int b = 2;auto add2 = [a, b]() mutable {a++; b++; };cout << a << " " << b;return 0;
}

代码的运行结果如下:
在这里插入图片描述
可以看到虽然这里的代码运行结果没有报错但是普通捕捉里面修改捕捉变量的值是不会改变外部的被捕捉变量的,如果想要改变的话就得使用引用捕捉,那么这里的代码就如下:

int main()
{int a = 1;int b = 2;auto add2 = [&a, b]() mutable {a++; b++; };add2();cout << a << " " << b;return 0;
}

代码的运行结果如下:
在这里插入图片描述
可以看到因为变量a是引用捕捉而b是普通捕捉所以经历lambd啊表达式后a的值发生了改变,但是b的值却没有发生改变,除了这种争对单独变量的捕捉c++11还提供了一些特殊的捕捉方式:

[=]:表示值传递方式捕获所有父作用域中的变量(包括this),这里的父作用域指的是lambda外面一层的作用域
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针
[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量

那么这几个用法想必大家应该很好理解这里就不一一讲解了,但是这里有几点需要大家注意一下,第一点就是:捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复,第二点就是:捕捉列表在捕捉变量的时候只会向上捕捉,对于在父作用域但是位于捕捉列表下方的变量是捕捉不到的比如说下面的代码:

int main()
{int a = 1;int b = 2;auto add2 = [=]() {cout << c << endl; };int c = 3;add2();return 0;
}

代码的运行结果如下:
在这里插入图片描述
可以看到这里的代码是跑不过去的因为变量c是在捕捉列表的下面那么这点希望大家能够注意。

多线程和lambda

c++提供了一个线程库来帮助我们创建新的线程,比如说下面的图片:
在这里插入图片描述
使用thread函数再传递一个任务函数就就可以让编译器再创建一个线程并在另外一个线程里面执行任务函数的内容,并且thread函数的第一个参数表示要执行的函数,后面的可变参数就指的是函数需要的参数,那么接下来我们就可以写一段代码让其再创建一个线程用于打印数字1到100,那么这里的代码如下:

#include<thread>
void func(int x )
{for (; x <= 100; x++){cout << x << endl;}
}
int main()
{thread(func, 1);return 0;
}

代码的运行如下:
在这里插入图片描述
可以看到上面的代码发生了错误,原因就是我们上面创建了一个线程,但是之前的线程是不会等待新创建出来的线程的,所以我们得调用join函数来等待一下之前的进程:这里为了更好的观察结果将打印的范围减少,那么这里的代码如下:

#include<thread>
void func(int x )
{for (; x <= 10; x++){cout << x << endl;}
}	
int main()
{thread t1(func, 1);cout << "########################" << endl;t1.join();cout << "########################" << endl;return 0;
}

代码的运行结果如下:
在这里插入图片描述
可以看到这里正常的打印出来了数字1~10,那么同样的道理我们是不是可以再创建一个进程来执行另外一个函数的内容,那么这里的代码如下:

#include<thread>
void func1(int x )
{for (; x <= 10; x++){cout << x << endl;}
}
void func2(int x)
{for (; x <= 10; x++){cout << x << endl;}
}
int main()
{int i = 1;thread t1(func1, i);thread t2(func2, i);t1.join();t2.join();return 0;
}

代码的运行结果如下:
在这里插入图片描述
可以看到这里的运行结果十分的混乱,那么这就说明上面确实是多个线程在执行不同的任务,既然多个线程可以执行不同的任务,那么同样的道理我们可以使用多个线程来执行一个相同的任务,比如说让新创建出来的两个线程一起完成1到10的打印,那这里就可以对上面的代码进行一下改进就行,把两个函数的传值传参改成传引用传参就行,并且这里因为特殊原因在传递参数的时候得使用ref对参数进行修饰,那么这里的代码如下:
在这里插入图片描述
可以看到虽然这里打印的出来的结果很乱,但是确实是两个进程在一起合作完成相同的任务,因为thread是一个模板所以我们不仅可以使用函数来完成上面的功能,我们还可以使用lambda表达式来完成上述的功能,那么这里的代码就如下:

#include<thread>
int main()
{int i = 1;thread t1([&i]() {for (; i <= 10; i++){cout << i << "func2" << endl;}});thread t2([&i]() {for (; i <= 10; i++){cout << i << "func2" << endl;}});t1.join();t2.join();return 0;
}

代码的运行结果如下:
在这里插入图片描述

lambda的底层

看到这里想必大家是不是觉得lambda很神奇啊!那lambda的底层是如何来实现的呢?我们来看看下面这段代码:

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);// lamberauto r2 = [=](double monty, int year)->double {return monty * rate * year;};r2(10000, 2);return 0;
}

通过反汇编可以看到下面这样的结果:

在这里插入图片描述
创建仿函数对象的时候是调用Rate类的构造函数来进行创建,在调用仿函数对象来执行内容的时候是调用的operator()重载来实现具体的内容,那么我们再来看看lambda表达式的反汇编图片:
在这里插入图片描述
这里跟仿函数的也是一样,首先调用lambda表达式的构造函数来创建对象,然后调用对象的时候使用的就是operator()重载函数,所以通过上面的例子我们不难发现lambda表达式的底层就是通过创建仿函数对象调用仿函数来实现的。

可变参数模板

在我们之前的学习中遇到过很多可变参数的函数,比如说c语言的printf函数和scanf函数,这两个就是很经典的可变参数函数:
在这里插入图片描述
在这里插入图片描述
在c语言里面使用三个点来表示这里的参数是可变的,那么c++也延续了这样的表示方法,比如说下面的代码:

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

Args是一个模板参数包,args是一个函数形参参数包,声明一个参数包Args…args,这个参数包中可以包含0到任意个类型的参数,并且在使用可变参数的时候还可以通过sizeofsizeof…(args)来得到可变参数的个数,比如说下面的代码:

template <class ...Args>
void ShowList(Args... args)
{//这里的sizeof计算的是参数包中的参数个数cout << sizeof...(args) << endl;//这里的三个点不能省略
}
int main()
{ShowList(1);ShowList(1, 1.1);ShowList(1,1.1,string("xxxxx"));return 0;
}

代码的运行结果如下:
在这里插入图片描述
打印可变参数包里面的内容有两种方法但是这两种方法都十分的难解,第一种就是通过递归的方式来打印参数包里面的内容,比如说下面的代码:

void ShowList()
{cout << endl;
}
template <class T,class ...Args>
void ShowList(T val,Args... args)
{cout << val << endl;ShowList(args...);
}
int main()
{ShowList(1);ShowList(1, 1.1);ShowList(1,1.1,string("xxxxx"));return 0;
}

这种写法需要一个类型参数和一个参数包,然后通过自身递归的方式来进行打印,当然这里的递归逻辑想必大家看的不是那么的懂,我们就来讲解一下三个参数的逻辑,当我们传递三个参数给这个函数时,val会接收第一个参数,然后其他的参数就会全部都给参数包args,那么对应在上面的代码就是1传递给value,1.1和string就会传递给args,然后递归调用ShowList的时候传递的是参数包,那么这个时候依然是本函数进行对接val就接收参数包的第一个参数也就是1.1,参数包则接收剩下的参数string,如果剩余的参数为空的话就会匹配空类型的参数,最终结束了递归,那么上面的代码运行的结果如下:
在这里插入图片描述
第二种写法就是通过数组来辅助我们实现,那么这里有了数组的辅助我们就不需要上面模板的第一个参数,那么这里的代码如下:

template<class T>
void PrintArg(T t)
{cout << t << " ";
}
template <class ...Args>
void ShowList( Args... args)
{int arr[] = { (PrintArg(args),0)... };
}
int main()
{ShowList(1);cout << endl;ShowList(1, 1.1);cout << endl;ShowList(1,1.1,string("xxxxx"));return 0;
}

可以看到数组里面的内容为一个逗号表达式,逗号表达式的取值为最后一个表达式的取值,数组在初始化的时候得推断数组中有多少个元素,数组里面添加了三个点,这个就表示数组在推断的时候需要把这个参数包进行展开,展开的空间为多大,这个数组的大小就是多大,但是展开的过程中需要将参数传递给函数PrintArg,然后在函数里面就接着打印传递过来的值,那么这就是打印参数的大致逻辑,代码的运行结果如下:
在这里插入图片描述
当然这么写不是很好,我们可以将代码进行一下改造,不添加逗号表达式并让上面的函数返回一个0,那么这里的代码如下:

template<class T>
int PrintArg(T t)
{cout << t << " ";return 0;
}
template <class ...Args>
void ShowList( Args... args)
{int arr[] = { PrintArg(args)... };
}
int main()
{ShowList(1);cout << endl;ShowList(1, 1.1);cout << endl;ShowList(1,1.1,string("xxxxx"));return 0;
}

代码的运行结果如下:
在这里插入图片描述
那么这就是打印可变参数的两种方式。

emplace

在之前的使用过程中对于容器我们一般传递的都是左值,但是在学习c++11之后我们发现容器不仅可以传递左值还可以传递右值,并且传递右值的效率会比单独传递左值的效率高很多,但是不管你是传递左值还是传递右值之前学习的插入方式一次都只能传递一个值,而emplace版本的插入函数它可以一次传递0~N个参数,我们来看看list容器的emplace版本的push_back函数
在这里插入图片描述
emplace版本的insert函数
在这里插入图片描述
首先对于内置内省emplace版本的插入函数和左值右值版本的插入函数没有任何区别,并且当emplace版本的函数不传递参数的时候编译器会自动使用该类型的默认构造函数来进行插入,比如说下面的代码:

int main()
{std::list<int> list1;list1.push_back(1);list1.emplace_back(2);list1.emplace_back();for (auto ch : list1){cout << ch << " ";}
}

代码的运行结果如下:
在这里插入图片描述
但是emplace可以接收多个参数但是并不代表他能一次性插入多个值,比如说下面的代码就是错误的:

int main()
{std::list<int> list1;list1.push_back(1);list1.emplace_back(2,3,4,5);for (auto ch : list1){cout << ch << " ";}
}

在这里插入图片描述
虽然对于内置类型这里没有多大的差别,但是对于内置类型这里却有了很大的不同,比如说使用list存储pair类型的数据,之前的写法就是传递数据给make_pair函数,然后这个函数创建出来pair类型的对象,然后再把对象传递过去进行插入,但是有了emplace_back之后我们就可以这样来进行插入,比如说下面的代码:

int main()
{std:list<pair<int, char>> mylist;mylist.emplace_back(make_pair(1,'a'));//构造+拷贝构造mylist.emplace_back(2, 'b');//构造
}

emplace_back会直接拿参数来构造函数需要的对象,而push_back则是先创建一个对象然后再拷贝构造函数需要的对象,那么为了大家能够更好的观察到这里的现象我们可以再用下面的代码来进行测试:

int main()
{pair<int, YCF::string> kv(20, "sort");std::list< std::pair<int, YCF::string> > mylist;mylist.emplace_back(kv); // 左值mylist.emplace_back(make_pair(20, "sort")); // 右值mylist.emplace_back(10, "sort"); // 构造pair参数包cout << endl;mylist.push_back(kv); // 左值mylist.push_back(make_pair(30, "sort")); // 右值mylist.push_back({ 40, "sort" }); // 右值return 0;
}

代码的运行结果如下:
在这里插入图片描述
可以看到这里有了移动拷贝之后插入数据的代价还不是很大,那如果将移动拷贝去掉了呢?这里是不是就全部变成深拷贝了,比如说下面的图片:
在这里插入图片描述
这一下差别是不是就很大了对吧,所以大家经常会听到这么一句话,如果没有实现移动构造这里就使用emplace系列的接口,如果实现了右值版本的接口这里就不需要使用emplace系列的接口因为差别不是很大,那么这就是emplace系列的介绍。

包装器

为什么会有包装器

首先大家来看看下面这行代码:

ret = func(x);

上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!为什么呢?我们来看看下面的代码:

template<class T>
void func(T t)
{static int x = 0;cout << "x的值为:" << x++ << endl;cout << "x的地址为:" << &x << endl;
}
int main()
{func(1);func(2);func(3);return 0;
}

代码的运行结果如下:
在这里插入图片描述
因为我们这里只传递了一个整型的参数所以模板就实例化出来了一个类型,而我们知道static修饰的变量是存放在静态区的它不会随着函数的结束而结束,所以我们每次调用函数的时候都可以看到它的值在不断的累加并且地址也是一样的,但是如果我们传递另外一个类型的参数呢?比如说下面的代码:

int main()
{func(1);func(2);func(3);func(1.1);func(2.2);func(3.3);return 0;
}

代码的运行结果如下:
在这里插入图片描述
可以看到这里的x就有了两个,并且通过之前的学习我们知道静态变量是在函数第一次运行的时候进行创建,而这里有两个静态成员变量,那这是不是就说明了模板实例化出来两个函数呢?好看到这里我们知道了静态变量可以用来观察模板实例化出来了多少个模板对象,那么我们再来看看下面的代码:

template<class F, class T>
T useF(F f, T x)
{static int count = 0;cout << "count:" << ++count << endl;cout << "count:" << &count << endl;return f(x);
}
double f(double i)
{return i / 2;
}
struct Functor
{double operator()(double d){return d / 3;}
};
int main()
{// 函数名cout << useF(f, 11.11) << endl;// 函数对象cout << useF(Functor(), 11.11) << endl;// lamber表达式cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;return 0;
}

代码的运行结果如下:
在这里插入图片描述
可以看到这里传递三个不同的对象过去,并且函数模板也实例化出来了三个不同的函数对象,但是这是不是有点浪费啊,我们能不能将这三个类型的对象合成一个呢?这样传递给模板函数的时候就只用实例化出来一个函数就可以了,是不是就节省了很多资源,那么为了实现这个功能c++11就提供了一个东西叫做包装器,包装器包含在头文件functional里面,那么接下来我们就来看看function的使用。

包装器的使用

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

可以看到function是一个模板类,Ret:表示被调用函数的返回类型,Args…:被调用函数的形参,那么在定义包装器的时候就是在尖括号里面首先写返回值类型然后再加一个括号并且在括号里面添加被调用函数的参数,比如说我们创建了一个函数,这个函数的功能就是计算两个整型参数的返回值,并且返回类型也是整型,那么这里的代码如下:

int f(int x, int y)
{return x + y;
}

然后我们就可以创建一个包装器来接收这个函数,因为这个函数的返回值是int参数是两个int,所以function的尖括号里面的内容就是这样:function<int(int,int)>,function是一个类我们使用这个类来出创建对象,初始化的时候可以使用对应函数来初始化,也可以在创建之后使用赋值符号来进行初始化,比如说下面的代码:

#include<functional>
int f(int x, int y)
{return x + y;
}
int main()
{function<int(int, int)> f1(f);function<int(int, int)> f2;f2 = f;return 0;
}

包装器中含有其他函数之后就可以用包装器来调用里面的函数,比如说下面的代码:

#include<functional>
int f(int x, int y)
{return x + y;
}
int main()
{function<int(int, int)> f1(f);f1(2, 3);function<int(int, int)> f2;f2 = f;f2(3, 4);return 0;
}

代码的运行的结果如下:

int main()
{function<int(int, int)> f1(f);function<int(int, int)> f2;f2 = f;cout<<f1(2, 3)<<endl;cout<<f2(3, 4)<<endl;return 0;
}

代码的运行结果如下:
在这里插入图片描述
包装器不仅能够接收我们写的函数还可以接收仿函数对象,比如说下面的代码:

struct Functor
{
public:int operator() (int a, int b){return a + b;}
};int main()
{Functor ft;function<int(int, int)> f1(ft);function<int(int, int)> f2 = Functor();cout<<f1(2, 3)<<endl;cout << f2(3, 4) << endl;return 0;
}

代码的运行结果如下:
在这里插入图片描述
可以看到包装器也可以包装仿函数对象,那么对于lambda也是可以的比如说下面的代码:

int main()
{function<int(int, int)> func3 = [](const int a, const int b){return a + b; };cout << func3(1, 2) << endl;return 0;
}

代码的运行结果如下:
在这里插入图片描述
除了这些以外包装器还可以包装一些类中的成员函数,但是这里大家得注意一下,在包类中函数的时候得指定作用域,并且如果包装的是非静态的成员函数得在作用域的前面加上&,如果是静态的话则可以加也可以不加,比如说下面的代码:

class Plus
{
public:static int plusi(int a, int b){return a + b;}double plusd(double a, double b){return a + b;}
};
int main()
{function<int(int, int)> func4 = &Plus::plusi;cout << func4(1, 2) << endl;function<double(double, double)> func5 = &Plus::plusd;cout << func5( 1.1, 2.2) << endl;return 0;
}

代码的运行结果如下:
在这里插入图片描述
可以看到这里运行出错了,原因就是对于非静态的成员变量,我们看到的是两个函数,但实际上是三个参数因为编译器帮我们省略了一个this指针,所以对于类中的成员函数在创建包装器显式实例化的时候得加上类名表示this指针,在使用包装器传递参数时也得传递this指针过去,那么这里的this指针就是类所创建的匿名对象,那么上面的代码修改之后就变成下面这样:

int main()
{function<int(int, int)> func4 = &Plus::plusi;cout << func4(1, 2) << endl;function<double(Plus,double, double)> func5 = &Plus::plusd;cout << func5(Plus(), 1.1, 2.2) << endl;return 0;
}

运行的结果也符合我们的预期:
在这里插入图片描述
但是这么写会让人感觉有点奇怪,所以我们可以创建一个该类型的对象,然后通过lambda表达式来捕捉这个对象最后调用里面的函数来实现同样的功能,那么这里的代码就如下:

int main()
{Plus plus;function<int(int, int)> func6 = [&plus](double x, double y)->double {return plus.plusd(x, y); };cout<<func6(1.1, 2.2)<<endl;return 0;
}

代码的运行结果如下:
在这里插入图片描述
可以看到这里也可以正常的运行。那么大家知道包装器如何使用之后我们就来看看之前所遇到问题,使用包装器依次包装并调用该函数之后,再来看看运行结果就可以发现这里只生成了一个模板函数:
在这里插入图片描述
那么这就是包装器的功能希望大家能够理解。

bind

std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作,我们来看看bind的模型:

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

fn表示调用的对象,Args表示的就是调用对象的参数列表,我们直接通过下面的代码来了解bind

#include<functional>
int Plus(int a, int b)
{return a + b;
}
int sub(int a, int b)
{return a - b;
}
int main()
{//表示绑定函数plus 参数分别由调用 func1 的第一,二个参数指定std::function<int(int, int)> func1 = std::bind(Plus, placeholders::_1,placeholders::_2);return 0;
}

bind一般和function结合在一起进行使用,function我们知道是什么意思,后面的bind我们知道第一个参数Plus表示的是上面的函数Plus,那后面的placeholders::_1和placeholders::_2又代表的是什么意思呢?这里我们可以查一下官方的文档
在这里插入图片描述
可以看到placeholders是一个命名空间,上面的bind表示的就是第一个位置传递给第一个参数,第二个位置则传递给第二个参数,也就相当于什么事情都没有干,但是我们将代码稍微修改一些变成下面这样:

int main()
{//表示绑定函数plus 参数分别由调用 func1 的第一,二个参数指定//std::function<int(int, int)> func1 = std::bind(Plus, placeholders::_1,//	placeholders::_2);std::function<int(int, int)> func1 = std::bind(sub, placeholders::_2,placeholders::_1);return 0;
}

修改成为这样的话就表示第一个位置传递给第二个参数,第二个位置的值传递给第一个参数,那么就可以用下面的代码来进行一下测试:

int main()
{//表示绑定函数plus 参数分别由调用 func1 的第一,二个参数指定//std::function<int(int, int)> func1 = std::bind(Plus, placeholders::_1,//	placeholders::_2);std::function<int(int, int)> func1 = std::bind(sub, placeholders::_2,placeholders::_1);cout<<func1(2, 1)<<endl;cout<<func1(1,2)<<endl;return 0;
}

代码的运行结果如下:
在这里插入图片描述
是不是感觉顺序被换了对吧,那么除了可以交换参数的顺序bind还可以固定参数,比如说function要是包装了类里面的函数,那么每次调用的时候都得传递匿名类对象,比如说下面的代码:

class Sub
{
public:int sub(int a, int b){return a - b;}
};
int main()
{function<int(Sub, int, int)> func4 = &Sub::sub;cout << func4(Sub(), 10, 100) << endl;cout << func4(Sub(), 20, 200) << endl;return 0;
}

代码的运行结果如下:
在这里插入图片描述
那么有了bind之后我们就可以这么来写,在固定位置前面添加固定的参数,这样以后在调用的时候就不需要再传递之前邦迪的参数了,那么这里的代码如下:

int main()
{function<int(Sub, int, int)> func4 = &Sub::sub;cout << func4(Sub(), 10, 100) << endl;cout << func4(Sub(), 20, 200) << endl;function<int(int, int)> func5 = bind(&Sub::sub, Sub(), placeholders::_1,placeholders::_2);cout<<func5(10, 100)<<endl;cout<<func5(20, 200)<<endl;return 0;
}

代码的运行结果如下:
在这里插入图片描述
那么这就是本篇文章的全部内容希望大家能够理解。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/22114.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java中字符串相关的类

目录 String类 StringBuffer类 StringBuilder类 String类 String类&#xff1a;代表字符串。Java 程序中的所有字符串字面值&#xff08;如 "abc" &#xff09;都作为此类的实例实现。 String是一个final类&#xff0c;代表不可变的字符序列。 字符串是常量&…

webpack相关

在 webpack < 4 的版本中&#xff0c;通常将 vendor 作为一个单独的入口起点添加到 entry 选项中&#xff0c;以将其编译为一个单独的文件&#xff08;与 CommonsChunkPlugin 结合使用&#xff09;。而在 webpack 4 中不鼓励这样做。而是使用 optimization.splitChunks 选项…

Vim使用tips ---用鼠标定位代码

在vim界面里我们想用鼠标操作定位代码是不可以的&#xff01;&#xff01; 但是我们可以更改设置&#xff1a; 按下 Esc 键返回到普通模式shift&#xff1a; 输人命令 set mouse a回车 这时候我们就可以定位到代码啦&#xff01;&#xff01; 还可以批量选中复制和删除&…

【Docker】Docker高级网络(NetWork)

【Docker】Docker高级网络(NetWork) 文章目录 【Docker】Docker高级网络(NetWork)1. 概述2. 网络2.1 网桥类型2.2 创建网络自定义桥2.3 查看所有网络2.4 查看特定网络的细节2.5 删除特定网络2.6 多个容器使用指定网络 参考文档&#xff1a;高级网络配置 Docker – 从入门到实践…

[论文分享]MR-MAE:重构前的模拟:用特征模拟增强屏蔽自动编码器

论文题目&#xff1a;Mimic before Reconstruct: Enhancing Masked Autoencoders with Feature Mimicking 论文地址&#xff1a;https://arxiv.org/abs/2303.05475 代码地址&#xff1a;https://github.com/Alpha-VL/ConvMAE&#xff08;好像并未更新为MR-MAE模型&#xff09; …

nginx部署vue项目

nginx部署vue项目 一. vue配置讲解二. Nginx配置讲解三. vue项目部署到nginx 一. vue配置讲解 在vue.config.js文件中&#xff0c;module.exports是配置路由转发的&#xff0c;几个重要的属性讲解 ① publicPath publicPath: /ulane/, 此属性配置上&#xff0c;如果访问的路…

耳夹式骨传导耳机哪个牌子好?耳夹骨传导耳机盘点!

随着科技的日新月异&#xff0c;耳夹式骨传导耳机也在不断更新换代。市场上涌现出许多品牌&#xff0c;这使得消费者在购买时感到困惑。别担心&#xff01;我们为你整理了一些市场上口碑和销量优秀的品牌和耳夹式骨传导耳机产品&#xff0c;希望能帮到你。 一&#xff1a;南卡…

苹果iOS App Store上架操作流程详解:从开发者账号到应用发布

很多开发者在开发完iOS APP、进行内测后&#xff0c;下一步就面临上架App Store&#xff0c;不过也有很多同学对APP上架App Store的流程不太了解&#xff0c;下面我们来说一下iOS APP上架App Store的具体流程&#xff0c;如有未涉及到的部分&#xff0c;大家可以及时咨询&#…

​python接口自动化(二十八)--html测试 报告——下(详解) ​

简介 五一小长假已经结束了&#xff0c;想必大家都吃饱喝足玩好了&#xff0c;那就继续学习吧。一天不学习&#xff0c;自己知道&#xff1b;两天不学习&#xff0c;对手知道&#xff1b;三天不学习&#xff0c;大家知道&#xff1b;一周不学习&#xff0c;智商输给猪。好了开个…

【架构设计】高并发架构实战:从需求分析到系统设计

写在前面 很多软件工程师的职业规划是成为架构师&#xff0c;但是要成为架构师很多时候要求先有架构设计经验&#xff0c;而不做架构师又怎么会有架构设计经验呢&#xff1f;那么要如何获得架构设计经验呢&#xff1f; 1 高并发是什么 高并发是指系统在同一时间内处理的请求量…

js判断两个数组是增加还是删除

JS判断两个数组的数据&#xff0c;增加的数据以及删除的数据。 // 第一个参数是新数组&#xff0c;第二个参数是旧数 const compareArrays function(arr1, arr2 ) {let remove []let add []// 旧数据循环for (let i 0; i < arr2.length; i) {let item arr2[i];if (arr…

数字 IC 设计职位经典笔/面试题(二)

共100道经典笔试、面试题目&#xff08;文末可全领&#xff09; FPGA 中可以综合实现为 RAM/ROM/CAM 的三种资源及其注意事项&#xff1f; 三种资源&#xff1a;BLOCK RAM&#xff0c;触发器&#xff08;FF&#xff09;&#xff0c;查找表&#xff08;LUT&#xff09;&#xf…