前言:本篇文章将分享一些C++11版本所产生的一些新的技术以及对老版本的优化。
目录
六.function包装器
一.C++11简介
C++11是C++委员会自C++03起,经历了近10年的时间所进行的又一次更新。相比于C++98/03,C++11带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。
相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个重点去学习。
本文主要分享一些实际中比较实用的语法。
二.统一的列表初始化
1.{}初始化
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如:
struct Point { int _x; int _y; }; int main() { Point p = { 1, 2 }; return 0; }
C++11扩大了用花括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加:
class Date { public: Date(int year, int month, int day) :_year(year) ,_month(month) ,_day(day) {} private: int _year; int _month; int _day; }; int main() { Date d1(2022, 1, 1); // old style // C++11支持的列表初始化,这里会调用构造函数初始化 Date d2{ 2022, 1, 2 }; Date d3 = { 2022, 1, 3 }; return 0; }
2.std::initializer_list
std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值:
vector(initializer_list<T> l) { resize(l.size()); for (auto& e : l) { push_back(e); } }
initializer_list包含两个指针,一个指向常量数组的开始,一个指向常量数组的结尾的下一个位置。
构造函数的本质就是遍历l,将其数据一个一个尾插进容器。
int main() { vector<int> v = { 1,2,3,4 }; // 这里{"sort", "排序"}会先初始化构造一个pair对象 map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} }; // 使用大括号对容器赋值 v = {10, 20, 30}; return 0; }
三.右值引用和移动语义
1.左值引用和右值引用
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,也可以出现在赋值符号的右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
// 以下的p、b、c都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
对左值引用,我们使用单&符号:
// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
而对右值引用,我们则需使用双&&符号:
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
2.两者的比较
(1)左值引用
- 左值引用只能引用左值,不能引用右值。
- 但是const左值引用既可引用左值,也可引用右值。
- 左值引用是给原值另起一个别名,两者共用一个地址。
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a; // ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
(2)右值引用
- 右值引用只能右值,不能引用左值。
- 但是右值引用可以move以后的左值。
- 右值引用本身是左值。
- 右值引用是直接抢夺资源。
int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error : “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);//(move后边会分享)
return 0;
}
如上例, r3右值引用a之后,r3得到a的数据和地址,而a本身则不再具有资源和地址。
3.移动语义
在我们之前所分享的各种容器中,都拥有构造函数和赋值函数,在C++11之前,这两个函数都是通过左值引用来作为参数,但是C++11之后,我们可以用右值引用作为参数,而两种函数名也变为移动构造和移动赋值:
// 移动构造 string(string&& s) :_str(nullptr) ,_size(0) ,_capacity(0) { cout << "string(string&& s) -- 移动语义" << endl; swap(s); } // 移动赋值 string& operator=(string&& s) { cout << "string& operator=(string&& s) -- 移动语义" << endl; swap(s); return *this; }
移动构造和移动赋值,相较于传统的构造函数和赋值函数,因为右值引用可以直接夺舍资源,所以就避免了需要创建一个新的对象来进行资源交换这些繁琐的过程,大大提高了效率。
4.万能引用
template<typename T>
void fun(T&& t)
{}
函数模版里的右值引用,称为万能引用,可以传左值,右值,const左值及const右值,均会自动识别。
但是这里存在一个问题,因为右值引用本身再下一次传递时会被识别为左值,所以我们需要添加东西来保持右值的本身属性,而执行该步骤的是完美转发:
std::forward<T>(参数t)
完美转发可以保证在传参过程中参数会保持其原生类型属性。
四.lambda表达式
1.基础使用
在C++11到来之前,我们想对一个数据集合进行排序,可以通过使用sort函数,而遇到自定义类型的数据时,如果想要进行排序,我们分享过使用仿函数来进行排序。
但是仿函数的写法未免有些复杂和繁琐,所以C++11中就增加了对自定义类型的数据进行排序的lambda表达式,其本质为匿名函数对象。
其结构为:
[捕捉列表] (参数) mutable -> 返回值类型 { 函数体 }
默认情况下lambda表达式具有常性,如果要取消其常性,则需添加上述表达式的mutable。
当我们仅使用该表达式可以省略捕捉列表和返回值类型。该表达式的返回值,可以使用auto类型的数据进行接收:
auto fun1 = [](int a,int b)->{return a > b;};
auto fun1 = [](int a,int b){return a > b;};
上述表达式中的->也是可以省略的。
2.捕捉列表
int a = 1,b = 2;
auto swap = [a,b]()
{}
捕捉常量可以使lambda表达式捕捉到a,b的一份拷贝,但是默认为const类型,无法对其进行交换,如果需要,则需添加multable取消其常性。
但是就算添加了multable可以进行交换,但依然只是交换了拷贝,并不影响a,b本身。如果想要交换a,b本身,就需要传引用:
int a = 1,b = 2;
auto swap = [&a,&b]()
{}
此种捕捉方式,只能是我们传什么,就捕捉什么。除此之外,我们还可以一次性捕捉父作用域中的全部对象:
int a = 1,b = 2,c = 3,d = 4, e = 5;
auto swap = [=]()
{}
传“=”可以一次性捕捉所有对象,而传“&”则是一次性捕捉所有引用:
int a = 1,b = 2,c = 3,d = 4, e = 5;
auto swap = [&]()
{}
另外,我们还可以混合捕捉:
int a = 1,b = 2,c = 3,d = 4, e = 5;
auto swap = [&a,&b,c,e]()
{}
五.可变模板参数
前边我们分享过什么是可变参数,即函数的参数数量并不固定,可以随心所欲的添加参数,例如最常用的scanf和printf两个函数。在C++11中,大佬们为模版也设计了可变参数,具体结构如下:
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。
六.function包装器
包装器function是一种类模板,使用包装器需要添加头文件include<functional>,其结构为:
function<返回值类型(参数类型...)> 包装器名字
包装器可以使用的场景针对于可调用对象,包括函数指针,函数对象,lambda表达式,以及类的成员函数。
所谓包装器,就是给上述举例的这些包装一个相同的外壳,使它们调用起来更加方便统一:
#include <functional> int f(int a, int b) { return a + b; } struct Functor { public: 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)> func1 = f; cout << func1(1, 2) << endl; // 函数对象 std::function<int(int, int)> func2 = Functor(); cout << func2(1, 2) << endl; // lamber表达式 std::function<int(int, int)> func3 = [](const int a, const int b) {return a + b; }; cout << func3(1, 2) << endl; // 类的成员函数 std::function<int(int, int)> func4 = &Plus::plusi; cout << func4(1, 2) << endl; std::function<double(Plus, double, double)> func5 = &Plus::plusd; cout << func5(Plus(), 1.1, 2.2) << endl; return 0; }
结语
关于C++11就分享这么多,喜欢本篇文章记得一键三连,我们下期再见!