C++11特性
- 1.统一列表初始化{}
- 使用
- 原理
- 2. 声明
- 关键字 auto
- 关键字 decltype
- nullptr
- 3. 范围for
- 5. stl增加的容器
- 6. 右值引用
- 左值和右值的区分
- 左值和右值引用场景
- 移动构造
- 移动赋值
- 右值引用move()
- 完美转发forward()
- 模板中的万能引用
- forward作用
- 7. 新的类成员函数
- 移动构造
- 移动赋值
- 强制生成默认函数的关键字default
- 禁止生成默认函数的关键字delete
- 8. lambda表达式
- lambda语法
- 捕捉列表具体介绍[]
- 函数对象(仿函数)
- 9.可变参数模板Args
- 使用逗号表达式展开可变参数包
- 11.包装器
- 引入背景
- 包装器使用方法
- bind
1.统一列表初始化{}
使用
在C++98时期,使用{}只能初始化数组,结构体。如下:
struct XX
{int x;int y;
};
int main()
{int a[] = { 0,1,2,3,4 };int b[5] = { 0 };XX x = { 1,2 };return 0;
}
C++11为了方便我们统一列表初始化数据:
- 使用{}可以初始化所有的内置类型和用户自定义类型。并且=可以省略
struct XX
{int x;int y;
};
int main()
{//C++98int a[] = { 0,1,2,3,4 };int b[5] = { 0 };XX x = { 1,2 };//C++11int aa[]{ 0, 1, 2, 3, 4 };int bb[5]{ 0 };XX xx{ 1,2 };//C++11中初始化列表也可以适用于new表达式中// pa指向int数组的第一个元素,是整形的。//将三个数都初始化为1int* pa = new int[3]{ 1, 1, 1};return 0;
}
- 创建对象时也可以使用{}来调用构造函数
string str("hello world"); //c++98string str1 = { "hello world" };string str2{ "hello world" };map<int, string> m1 = { {1, "小明"}, {2, "小红"} };map<int, string> m2{ {1, "小明"}, {2, "小红"} };
原理
使用{}构造函数,其实是事先生成了一个initializer_list对象。
然后我们stl容器,都支持使用initializer_list对象调用构造:
因此我们实际上使用{}时,是先生成initializer_list对象,然后再构造。
2. 声明
关键字 auto
自动推断类型:非常方便
关键字 decltype
将变量的类型声明为指定类型:
- 可以使用该关键字声明变量
- 也可以使用该关键字创建指定类型的对象
const int a = 10;double b = 1.1;decltype(a * b) ret; //ret类型为 doubledecltype(&a) p; // p的类型为 const int*cout << typeid(ret).name() << endl;cout << typeid(p).name() << endl;vector<decltype(a* b)> v; //v的类型为doublecout << typeid(v).name() << endl;
nullptr
3. 范围for
可以使用该语法遍历容器或者数组
vector<int> v{ 1, 2, 3, 4, 5, 6, 7, 8, 9 };for (auto e : v){cout << e << " ";}cout << endl;
5. stl增加的容器
6. 右值引用
左值和右值的区分
左值:可以取地址,可以对它赋值,可以出现在赋值符号的左边。
右值:不可取地址,不能出现在赋值符号的左边。
注意:定义时const修饰的后的左值,不能给它赋值,但是可以取地址,因此它是左值。
如下左值引用和右值引用例子:
//下面都是左值int a = 10;int* p = &a;const int b = 10;//左值的引用int& ra = a;int*& rp = p;const int& rb = b;///int x = 10;int y = 20;int add(int, int);//下面都是右值20; //字面常量x + y; //表达式返回值add(x, y); //函数返回值//下面是右值引用int&& name1 = 20;int&& name2 = x + y;int&& name3 = add(x, y);
加上const可以实现,左值引用给右值取别名:
const int& cc = 10;cout << cc; // 10
使用move函数,可以实现右值引用给左值取别名:
int a = 10;int&& b = move(a); //使a暂时变为右值cout << b; // 10
左值和右值引用场景
看下面这段代码:分别是普通的左值引用构造,右值引用构造(注意看形参就可以区分)
然后主要介绍左值引用解决的什么情况,右值引用解决了什么情况。
//拷贝构造函数string(const string& str):_size(str._size), _capacity(str._capacity){_str = new char[_capacity + 1];strcpy(_str, str._str);}//移动构造string(string&& str):_str(nullptr),_size(0),_capacity(0){cout << "string(string&& str)--移动构造" << endl;swap(str);}//原赋值函数string& operator=(const string& s){if (this != &s){char* tem = new char[s._capacity + 1]; //先申请新空间strcpy(tem, s._str); //拷贝//没有抛异常,往下执行delete[] _str;//释放空间_str = tem; //赋回来_size = s._size;_capacity = s._capacity;}return *this;}//移动赋值string& operator=(string&& s){if (this != &s){cout << "string& operator=(string&& s)---移动赋值" << endl;swap(s);}return *this;}
移动构造
- 左值引用解决的问题:直接减少拷贝。1. 左值引用传参 2.传引用返回(函数内的局部对象不能用传引用返回,因为出作用域会销毁)
- 左值引用短板:如果是局部对象,就不能传引用返回,只能传值返回,传值返回(这样至少会产生一次拷贝构造)
- 右值引用本质上将参数右值的资源窃取过来,占为己有,(那么就不需要申请资源了),swap实现了这个功能。因为将亡值或者临时变量,本来就是要删除的数据,将这个数据直接返回,更高效。
- 右值引用可以解决上面的资源浪费的问题:增加了移动构造后,因为返回值是将亡值,所以会被识别成右值,调用移动构造。
移动赋值
当编译器识别出要赋值的参数为右值时,自动调用移动赋值,减少了拷贝,提高效率。
右值引用move()
将一个左值强制转化为右值
std::move()
下面例子将s1强制识别成右值,运行完之后,s1的数据被s2掠夺。
完美转发forward()
模板中的万能引用
模板中的&&不表示右值引用。它可以作推理使用:
当传进来左值时,折叠一个&,自动识别成左值引用。
当传进来右值时,识别成右值引用。
如下所示:
template<class T>
void Test(T&& t)
{Fun(t);
}
void Fun(int& x)
{cout << "左值引用" << endl;
}void Fun(int&& x)
{cout << "右值引用" << endl;
}
void Fun(const int& x)
{cout << "const 左值引用" << endl;
}void Fun(const int&& x)
{cout << "const 右值引用" << endl;
}
int main()
{int a = 10;Fun(a); //左值引用Fun(move(a)); //右值引用return 0;
}
forward作用
实际上,当模板识别成右值时,为了swap会自动把右值转换成左值(为了掠夺资源,变成左值),但是有时候我们可能需要保持它的右值属性(我们暂时不掠夺呢,把它传到应该掠夺的部分)。所以我们应该维持它的右值属性,继续往下传递。
使用forward可以保持右值属性不变,继续传下去。
按道理来说,上面小节的测试,应该都是左值引用,因为在Test里被识别成左值了。但是编译器应该做了一些处理,导致右值属性被保留。
工程中涉及到完美转发的情形就是利用forward一直保留变量的属性,直至进行资源掠夺阶段。
7. 新的类成员函数
移动构造和移动赋值的知识背景是右值引用部分。
移动构造
- 若自己没实现移动构造函数,并且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动构造,对内置类型成员会执行逐成员按字节拷贝。自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没实现就调用拷贝构造。
- 如果你提供了移动构造,编译器不会提供拷贝构造。
移动赋值
- 如果自己没实现移动赋值重载函数,并且没有实现析构函数、拷贝构造、宝贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动赋值函数,对于内置类型成员会逐字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没实现就调用拷贝赋值。
- 如果你提供了移动赋值,编译器不会提供拷贝赋值。
强制生成默认函数的关键字default
假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如: 我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
语法如下:
class string
{
//必须生成一个默认的移动构造string(string&& s) = default;
}
禁止生成默认函数的关键字delete
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
class string
{
//不再生成该移动构造函数string(string&& s) = delete;
}
8. lambda表达式
lambda表达式是C++借鉴其他语言所得到的产物。
其底层实现逻辑类似于仿函数的逻辑。
产生原因:
每次写仿函数都要实现一个类,很麻烦。
lambda语法
[capture-list] (parameters) mutable -> return-type {statement}
- [capture-list]:捕捉列表,编译器根据[]来判断下边的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
- (parameters):参数列表,与普通函数参数列表一致,如果不需要传递参数,可与()一同省略
- mutable:默认情况下,lambda函数总是const函数,mutable可以取消其常量性,使用该修饰符时,参数列表不可省略(即使参数为空)
- -> return-type:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导
- {statement}:函数体。可以使用捕获到的变量
注意:
在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
使用示例:
struct Stu
{string _name;int _age;
};
int main()
{vector<Stu> v{ {"zhangsan", 10}, {"lisi", 20}, {"wangwu", 3} };//lanmda表达式,按年龄排序sort(v.begin(), v.end(), [](Stu s1, Stu s2)->bool {return s1._age < s2._age; });//定义一个lambda表达式,按字符串排序auto com = [](Stu s1, Stu s2)->bool {return s1._name < s2._name; };sort(v.begin(), v.end(), com);return 0;
}
捕捉列表具体介绍[]
捕捉列表描述了上下文哪些东西可以被lambda使用,以及实用的方式是传值还是传引用。
- [var] :表示传递的方式是捕捉变量
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [&var]:表示引用的捕捉变量(不是取地址!!)
- [&=]:表示引用传递捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递的方式,捕获当前this指针
注意:
- 父作用域是包含lambda函数的语句块
- 捕捉列表可以灵活使用,以逗号分隔
比如:[=,&a,&b]:以引用的方式捕捉变量ab,剩下的按值传递。
[&, a, this]:a和this按值传递捕捉,其他变量按引用捕捉 - 捕捉列表不允许重复捕捉,否则会编译错误
比如:[=, a]就会重复捕捉a - 块作用域以外的lambda函数捕捉列表必须为空
- lambda表达式不能互相赋值
函数对象(仿函数)
函数对象,又称为仿函数,即可以像函数一样使用的对象,就是在类中重载了operator0运算符的类对象。
class Mutil
{
public:int operator()(const int& x, const int& y){return x * y;}
};int main()
{Mutil t1;cout << t1(5, 6) << endl;auto t2 = [](int x , int y) {return x * y; };cout << t2(5, 6) << endl;return 0;
}
- 首先仿函数和lambda使用方法完全类似,并且底层汇编i代码,调用也类似
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。
9.可变参数模板Args
首先看一下可变参数模板的示例:
//Args是一个模板参数包,args是一个函数形参参数包
//声明一个参数包Args...args,这个参数包中可以包含0~n任意个模板参数
template<class ...Args>
void ShowList(Args... args)
{//语法就是这样的cout << sizeof...(args) << endl;
}int main()
{ShowList();ShowList('a');ShowList('a','b');ShowList('a','b',1,2);return 0;
}
运行结果:
可知可变参数包可以接收任意个数的参数。
上面的args的前面有省略号,表示args一个可变的模板参数,我们把带有省略号的参数称为称为“参数包”,它里面包含了0~N个模板参数。
我们无法直接获取args的每个参数,只能通过展开参数包的方式来获取参数包中的每个参数。
下面举例,如何展开参数包里的参数:
int N = 0;
template<class T>
void ShowList(const T& val)
{//语法就是这样的,必须有一个单参数的,结束递归cout << "单参函数打印:" << val << endl;
}//Args是一个模板参数包,args是一个函数形参参数包
//声明一个参数包Args...args,这个参数包中可以包含0~n任意个模板参数
template<class T, class ...Args>
void ShowList(const T& val, Args... args)
{//语法就是这样的N++;cout << val << endl;ShowList(args...);
}int main()
{ShowList('a','b',1,2);cout << N;return 0;
}
代码解释:
首先main函数传进去四个参数,一个给T&模板,剩下三个给args函数包。以此类推,直到函数包只有一个参数2,最后一个参数传递,必须再写一个递归终止函数,否则会报错。
使用逗号表达式展开可变参数包
//template<class T>
//int PrintArg(T t)
//{
// cout << t << " ";
// return 0;
//}template<class T>
void PrintArg(T t)
{cout << t << " ";
}template<class ...Args>
void ShowList(Args... args)
{int arr[] = { (PrintArg(args),0)... };//int arr1[] = { PrintArg(args)... };cout << endl;
}//上面的showlist会推演成下面的表达式:template<class ...Args>
void ShowList(Args... args)
{int arr[] = { (PrintArg(1),0),(PrintArg('a'),0),(PrintArg(2),0),(PrintArg(string("123214")),0)};//int arr1[] = { PrintArg(args)... };cout << endl;
}int main()
{ShowList(1,'a', 3, string("123214"));return 0;
}
11.包装器
function包装器也叫适配器。C++中的function本质是一个类模板,也是一个包装器。
引入背景
ret = func(x)
上面这段代码func的具体类型是什么样的?func可能是函数?函数指针?函数对象(仿函数)?也有可能是lambda表达式对象?这些都是可调用的类型!
这些在一起会导致模板效率低下,如下:
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 y){return y / 3;}
};int main()
{//函数名cout << useF(f, 11.11)<<endl;//函数对象cout << useF(Functor(), 11.11) << endl;//lambda公式cout << useF([](double d)->double {return d / 4; }, 11.11);return 0;}
由上面的代码可知,count有三个,即useF实例化出了三份代码,分别是函数指针类型的、仿函数类型的、lambda表达式类型的。包装器可以很好解决上面的问题。
#include<functional>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 y){return y / 3;}
};int main()
{函数名//cout << useF(f, 11.11)<<endl;函数对象//cout << useF(Functor(), 11.11) << endl;lambda公式//cout << useF([](double d)->double {return d / 4; }, 11.11);//函数名function<double(double)> f1 = f;//仿函数function<double(double)> f2 = Functor();//lambda公式function<double(double)> f3 = [](double d)->double {return d / 4; };cout << useF(f1, 11.11) << endl;cout << useF(f2, 11.11) << endl;cout << useF(f3, 11.11) << endl;return 0;}
使用functional包装后,模板只实例化出一份。
包装器使用方法
记得要实例化成:返回值(参数)的格式。
下面是:普通函数,仿函数,lambda表达式,类成员函数,类静态成员函数的包装方法。
#include<functional>int f(int a, int b)
{return a + b;
}struct Functor
{int operator()(int a, int b){return a + b;}
};class Plus
{
public:static int plusi(int a, int b){return a + b;}double plusd(double a, double b){return a + b;}};int main()
{//函数名std::function<int(int, int)> f1 = f;cout << f1(1, 2) << endl;//函数对象std::function<int(int, int)> f2 = Functor();cout << f2(1, 2) << endl;//lambda表达式std::function<int(int, int)> f3 = [](int a, int b)->int {return a + b; };cout << f3(1, 2) << endl;//类的静态成员函数(无this指针)std::function<int(int, int)> f4 = &Plus::plusi;cout << f4(1, 2) << endl;//类的动态成员函数(得传入this指针)std::function<double(Plus, double, double)> f5 = &Plus::plusd;cout << f5(Plus(), 1.1, 2.0) << endl;return 0;
}
bind
bind是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
功能1:原本n个参数的函数->需要m个参数的函数
功能2:调整参数顺序
类的声明如下:
auto newFunc = bind(func, arg_list);
newFunc是一个新的可调用对象,arg_list是func的参数列表。当调用newFunc的时候,newFunc会调用func,并把arg_list中的参数传给func。
如下例子:
#include<functional>int Plus(int a, int b)
{return a - b;
}class Sub
{
public:int sub(int a, int b){return a - b;}
};int main()
{//表示绑定函数Plus, 其中原函数的第二个参数被放到了第一个位置//原函数的第一个参数被放到了第二个位置function<int(int, int)> func1 = bind(Plus, placeholders::_2, placeholders::_1);//auto func1 = bind(Plus, placeholders::_2, placeholders::_1); cout << func1(1, 2) << endl; //因为参数颠倒了,变为了 2-1//此函数是,绑定了第二个参数,第一个参数需要自己指定auto func2 = bind(Plus, placeholders::_1, 1);cout << func2(2) << endl;//此函数是,绑定了第1个参数,第二个参数需要自己指定auto func3 = bind(Plus, 1, placeholders::_1);cout << func3(2) << endl;//此函数绑定了成员函数,需要制定类域的函数地址,需要传入this指针auto func4 = bind(&Sub::sub, Sub(), 1, 2);cout << func4() << endl;//此函数传入了对象的地址Sub s;auto func5 = bind(&Sub::sub, &s, 1, 2);cout << func5() << endl;
}