1. C++11简介
2. 列表初始化
3. 变量类型推导
4. 默认成员函数控制
5. 右值引用6. 新的类功能
7. lambda表达式
8.包装器9.可变参数列表
1. C++11简介
在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于TC1主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语
言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。
2. 列表初始化
2.1 C++98中{}的初始化问题
在C++98中,标准允许使用花括号{}对数组元素进行统一的列表初始值设定。比如:
int array1[] = {1,2,3,4,5}; int array2[5] = {0};
对于一些自定义的类型,却无法使用这样的初始化。比如:
vector<int> v{1,2,3,4,5};
就无法通过编译,导致每次定义vector时,都需要先把vector定义出来,然后使用循环对其赋初始值,非常不方便。C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
2.2 内置类型的列表初始化
int main() { // 内置类型变量 int x1 = { 10 }; int x2{ 10 }; int x3 = 1 + 2; int x4 = { 1 + 2 }; int x5{ 1 + 2 }; // 数组 int arr1[5]{ 1,2,3,4,5 }; int arr2[]{ 1,2,3,4,5 }; // 动态数组,在C++98中不支持 int* arr3 = new int[5]{ 1,2,3,4,5 }; // 标准容器 vector<int> v{ 1,2,3,4,5 }; map<int, int> m{ {1,1}, {2,2},{3,3},{4,4} }; return 0; }
注意:列表初始化可以在{}之前使用等号,其效果与不使用=没有什么区别。
2.3 自定义类型的列表初始化
1. 标准库支持单个对象的列表初始化
class Point { public: Point(int x = 0, int y = 0) : _x(x), _y(y) {} private: int _x; int _y; }; int main() { Point p{ 1, 2 }; return 0; }
2. 多个对象的列表初始化
多个对象想要支持列表初始化,需给该类(模板类)添加一个带有initializer_list类型参数的构造函数即可。注意:initializer_list是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器以及获取区间中元素个数的方法size()。#include<initializer_list> namespace Bernard { template<class T> class vector { public: typedef T* iterator; vector(initializer_list<T> l) { _start = new T[l.size()]; _finish = _start + l.size(); _endofstorage = _start + l.size(); iterator vit = _start; for (auto e : l) *vit++ = e; } vector<T>& operator=(initializer_list<T> l) { vector<T> tmp(l); std::swap(_start, tmp._start); std::swap(_finish, tmp._finish); std::swap(_endofstorage, tmp._endofstorage); return *this; } private: iterator _start; iterator _finish; iterator _endofstorage; }; } int main() { Bernard::vector<int> b = { 1, 2, 3, 4, 5 }; b = { 10, 20, 30 }; return 0; }
3. 变量类型推导
3.1 为什么需要类型推导
在定义变量时,必须先给出变量的实际类型,编译器才允许定义,但有些情况下可能不知道需要实际类型怎么给,或者类型写起来特别复杂,比如:
#include <map> #include <string> int main() { short a = 32670; short b = 32670; // c如果给成short,会造成数据丢失, //如果能够让编译器根据a+b的结果推导c的实际类型,就不会存在问题 short c = a + b; std::map<std::string, std::string> m{ {"apple", "苹果"}, {"banana","香蕉"} }; // 使用迭代器遍历容器, 迭代器类型太繁琐 std::map<std::string, std::string>::iterator it = m.begin(); while (it != m.end()) { cout << it->first << " " << it->second << endl; ++it; } return 0; }
C++11中,可以使用auto来根据变量初始化表达式类型推导变量的实际类型,可以给程序的书写提供许多方便。将程序中c与it的类型换成auto,程序可以通过编译,而且更加简洁。
3.2 decltype类型推导
3.2.1 为什么需要decltype
auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型。但有时候可能需要根据表达式运行完成之后结果的类型进行推导,因为编译期间,代码不会运行,此时auto也就无能为力。
template<class T1, class T2> T1 Add(const T1& left, const T2& right) { return left + right; }
如果能用加完之后结果的实际类型作为函数的返回值类型就不会出错,但这需要程序运行完才能知道结果的实际类型,即RTTI(Run-Time Type Identification 运行时类型识别)
C++98中已经支持RTTI:
·typeid只能查看类型不能用其结果类定义类型
·dynamic_cast只能应用于含有虚函数的继承体系中
运行时类型识别的缺陷是降低程序运行的效率。
3.2.2 decltype
decltype是根据表达式的实际类型推演出定义变量时所用的类型,比如:
1. 推演表达式类型作为变量的定义类型int main() { int a = 10; int b = 20; // 用decltype推演a+b的实际类型,作为定义c的类型 decltype(a + b) c; cout << typeid(c).name() << endl; const int f = 1; double d = 2.3; decltype(f*d) g; cout << typeid(g).name() << endl; return 0; }
2. 推演函数返回值的类型
void* GetMemory(size_t size) { return malloc(size); } int main() { // 如果没有带参数,推导函数的类型 cout << typeid(decltype(GetMemory)).name() << endl;//void * __cdecl(unsigned int) // 如果带参数列表,推导的是函数返回值的类型,注意:此处只是推演,不会执行函数 cout << typeid(decltype(GetMemory(0))).name() << endl;//void* return 0; }
4. 默认成员函数控制
在C++中对于空类编译器会生成一些默认的成员函数,比如:构造函数、拷贝构造函数、运算符重载、析构函数和&和const&的重载、移动构造、移动拷贝构造等函数。如果在类中显式定义了,编译器将不会重新生成默认版本。有时候这样的规则可能被忘记,最常见的是声明了带参数的构造函数,必要时则需要定义不带参数的版本以实例化无参的对象。而且有时编译器会生成,有时又不生成,容易造成混乱,于是C++11让程序员可以控制是否需要编译器生成。
4.1 显式缺省函数
在C++11中,可以在默认函数定义或者声明时加上=default,从而显式的指示编译器生成该函数的默认版本,用=default修饰的函数称为显式缺省函数。
class A { public: A(int a) : _a(a) {} // 显式缺省构造函数,由编译器生成 A() = default; // 在类中声明,在类外定义时让编译器生成默认赋值运算符重载 A& operator=(const A& a); private: int _a; }; A& A::operator=(const A& a) = default; int main() { A a1(10); A a2; a2 = a1; return 0; }
4.2 删除默认函数
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且不给定义,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
class A { public: A(int a) : _a(a) {} // 禁止编译器生成默认的拷贝构造函数以及赋值运算符重载 A(const A&) = delete; A& operator=(const A&) = delete; private: int _a; }; int main() { A a1(10); // 编译失败,因为该类没有拷贝构造函数 //A a2(a1); A a3(20); //a3 = a2;// 编译失败,因为该类没有赋值运算符重载 return 0; }
注意:避免删除函数和explicit一起使
5 右值引用
5.1 右值引用概念
C++98中提出了引用的概念,引用即别名,引用变量与其引用实体公共同一块内存空间,而引用的底层是通过指针来实现的,因此使用引用,可以提高程序的可读性。
void Swap(int& left, int& right) { int temp = left; left = right; right = temp; } int main() { int a = 10; int b = 20; Swap(a, b); }
为了提高程序运行效率,C++11中引入了右值引用,右值引用也是别名,但其只能对右值引用。
int Add(int a, int b) { return a + b; } int main() { double x = 1.1, y = 2.2; // 以下几个都是常见的右值 10; x + y; fmin(x, y); // 以下几个都是对右值的右值引用 int&& rr1 = 10; double&& rr2 = x + y; double&& rr3 = Add(x, y);// 引用函数返回值,返回值是一个临时变量,为右值 cout << &rr1 << endl; rr1 = 20; cout << &rr1 << endl; return 0; }
为了与C++98中的引用进行区分,C++11将该种方式称之为右值引用。
5.2 左值与右值
左值与右值是C语言中的概念,但C标准并没有给出严格的区分方式,一般认为:能够取地址的,一般情况下可以修改(const修饰的不能修改)的称为左值;不能取地址的,不能修改,并且不能出现在等号左边的称为右值.
int main() { int a = 10; int b = 20; // a和b都是左值,b既可以在=的左侧,也可在右侧, // 说明:左值既可放在=的左侧,也可放在=的右侧 a = b; b = a; const int c = 30; // 编译失败,c为const常量,只读不允许被修改 //c = a; // 因为可以对c取地址,因此c严格来说不算是左值 cout << &c << endl; // 编译失败:因为b+1的结果是一个临时变量,没有具体名称,也不能取地址,因此为右值 //b + 1 = 20; return 0; }
因此关于左值与右值的区分不是很好区分,一般认为:
1. 普通类型的变量,因为有名字,可以取地址,都认为是左值。
2. const修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址(如果只是const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间),C++11认为其是左值。
3. 如果表达式的运行结果是一个临时变量或者对象,认为是右值。
4. 如果表达式运行结果或单个变量是一个引用则认为是左值。
总结:
1. 不能简单地通过能否放在=左侧右侧或者取地址来判断左值或者右值,要根据表达式结果或变量的性质判断,比如上述:c常量
2. 能得到引用的表达式一定能够作为引用,否则就用常引用。
C++11对右值进行了严格的区分:
·C语言中的纯右值,比如:a+b, 100
·将亡值。比如:表达式的中间结果、函数按照值的方式进行返回。
5.3 左值引用与右值引用比较
左值引用总结:
1.左值引用只能引用左值,不能引用右值2.但是const左值引用既可以引用左值也可以引用右值
int main() { // 左值引用只能引用左值,不能引用右值。 int a = 10; int& ra1 = a; // ra为a的别名 // int& ra2 = 10; // 编译失败,因为10是右值 //const 左值引用既可以引用左值,又可以引用右值 const int& ra2 = 10; const int& ra3 = 10 + 20; return 0; }
注意: 普通引用只能引用左值,不能引用右值,const引用既可引用左值,也可引用右值。
右值引用总结:
1.右值引用只能引用右值,不能引用左值2.右值引用可以引用move以后的左值
int main() { // 右值引用只能右值,不能引用左值。 int&& r1 = 10; // error C2440: “初始化”: 无法从“int”转换为“int &&” // message : 无法将左值绑定到右值引用 int a = 10; //int&& r2 = a; // 右值引用可以引用move以后的左值 int&& r3 = std::move(a); return 0; }
5.4 左值引用和右值引用的使用场景
下面来看一下之前string类的内部模拟实现:
namespace Bernard { class string { public: typedef char* iterator; iterator begin() { return _str; } iterator end() { return _str + _size; } string(const char* str = "") :_size(strlen(str)) , _capacity(_size) { //cout << "string(char* str)" << endl; _str = new char[_capacity + 1]; strcpy(_str, str); } // s1.swap(s2) void swap(string& s) { ::swap(_str, s._str); ::swap(_size, s._size); ::swap(_capacity, s._capacity); } // 拷贝构造 string(const string& s) :_str(nullptr) { cout << "string(const string& s) -- 深拷贝" << endl; string tmp(s._str); swap(tmp); } // 赋值重载 string& operator=(const string& s) { cout << "string& operator=(string s) -- 深拷贝" << endl; string tmp(s); swap(tmp); return *this; } ~string() { delete[] _str; _str = nullptr; } char& operator[](size_t pos) { return _str[pos]; } void reserve(size_t n) { if (n > _capacity) { char* tmp = new char[n + 1]; strcpy(tmp, _str); delete[] _str; _str = tmp; _capacity = n; } } void push_back(char ch) { if (_size >= _capacity) { size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2; reserve(newcapacity); } _str[_size] = ch; ++_size; _str[_size] = '\0'; } //string operator+=(char ch) string& operator+=(char ch) { push_back(ch); return *this; } const char* c_str() const { return _str; } private: char* _str; size_t _size; size_t _capacity; // 不包含最后做标识的\0 }; Bernard::string to_string(int value) { bool flag = true; if (value < 0) { flag = false; value = 0 - value; } Bernard::string str; while (value > 0) { int x = value % 10; value /= 10; str += ('0' + x); } if (flag == false) { str += '-'; } std::reverse(str.begin(), str.end()); return str; } } // 左值引用的使用场景 // 1、做参数 void func1(Bernard::string s) {} void func2(const Bernard::string& s) {} int main() { Bernard::string s1("hello world"); // func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值 //func1(s1); func2(s1); // operator+=可以使用传左值引用返回 s1 += 'A'; // to_string 不能用左值引用返回,这个就是左值引用短板 // 如果函数返回对象出了函数作用域就不在了,就不能使用做引用返回,就会存在拷贝 //Bernard::string ret1 = Bernard::to_string(1234); return 0; }
上面的问题如何解决呢?首先我们分析一下to_string这个函数
右值引用使用场景一(主要是对深拷贝的类,如vector,list,string等):
C++11之后通过引入移动构造将此次拷贝构造转变为资源的转移,而这种资源的转移实际不需要用到拷贝操作,而只是采用了封装一个函数采用swap的方式把出作用域就会销毁的资源通过swap的方式转移了保护了起来,提高了内存的允许效率和资源的利用效率
// 移动构造 string(string&& s) :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(string&& s) -- 移动构造" << endl; this->swap(s); }
再来分析一下下面这种情况:
int main() { Bernard::string ret1; ret1 = Bernard::to_string(1234); return 0; }
通过上述的分析我们需要在string类的模拟实现中再增加上移动构造和移动赋值函数
// 移动构造 string(string&& s) :_str(nullptr) , _size(0) , _capacity(0) { cout << "string(string&& s) -- 移动构造" << endl; this->swap(s); } // 移动赋值 string& operator=(string&& s) { cout << "string& operator=(string&& s) -- 移动赋值" << endl; this->swap(s); return *this; }
右值使用场景二(使用在容器插入函数接口,如果实参是右值,就可以转移资源减少拷贝):
template<class T> struct ListNode { ListNode* _next = nullptr; ListNode* _prev = nullptr; T _data; }; template<class T> class List { typedef ListNode<T> Node; public: List() { //_head = new Node; _head = (Node*)malloc(sizeof(Node)); _head->_next = _head; _head->_prev = _head; } void PushBack(const T& x) { //Insert(_head, x); Insert(_head, x); } void PushBack(T&& x) { //cout << &x << endl; // 这里x属性退化为左值,其他对象再来引用x,x会识别为左值 //Insert(_head, x); // 这里就要用完美转发,让x保持他的右值引属性 Insert(_head, std::forward<T>(x)); } void PushFront(T&& x) { //Insert(_head->_next, x); Insert(_head->_next, std::forward<T>(x)); } void Insert(Node* pos, T&& x) { Node* prev = pos->_prev; //Node* newnode = new Node; //newnode->_data = std::forward<T>(x); // 关键位置 Node* newnode = (Node*)malloc(sizeof(Node)); new(&newnode->_data)T(std::forward<T>(x)); // prev newnode pos prev->_next = newnode; newnode->_prev = prev; newnode->_next = pos; pos->_prev = newnode; } void Insert(Node* pos, const T& x) { Node* prev = pos->_prev; //Node* newnode = new Node; //newnode->_data = x; // 关键位置 Node* newnode = (Node*)malloc(sizeof(Node)); new(&newnode->_data)T(x); // prev newnode pos prev->_next = newnode; newnode->_prev = prev; newnode->_next = pos; pos->_prev = newnode; } private: Node* _head; }; int main() { list<std::string> lt; std::string s1("1111"); // 这里调用的是拷贝构造 lt.push_back(s1); // 下面调用都是移动构造 lt.push_back("2222"); lt.push_back(std::string("2222")); lt.push_back(std::move(s1)); return 0; }
5.5 完美转发
完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。
看看下面这段代码:
void Func(int x) { // ...... } template<typename T> void PerfectForward(T t) { Fun(t); }
PerfectForward为转发的模板函数,Func为实际目标函数,但是上述转发还不算完美,完美转发是目标函数总希望将参数按照传递给转发函数的实际类型转给目标函数,而不产生额外的开销,就好像转发者不存在一样。
所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)。
C++11通过forward函数来实现完美转发, 比如:void Fun(int &x) { cout << "左值引用" << endl; } void Fun(const int &x) { cout << "const 左值引用" << endl; } void Fun(int &&x) { cout << "右值引用" << endl; } void Fun(const int &&x) { cout << "const 右值引用" << endl; } // 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。 // 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力, // 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,但是使用完美转发可以保持它的右值属性 // 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要完美转发来达到我们的目的 template<typename T> void PerfectForward(T&& t) { Fun(std::forward<T>(t)); } int main() { PerfectForward(10); // 右值 int a; PerfectForward(a); // 左值 PerfectForward(std::move(a)); // 右值 const int b = 8; PerfectForward(b); // const 左值 PerfectForward(std::move(b)); // const 右值 return 0; }
6.新的类功能
原来C++类中,有6个默认成员函数:
1.构造函数 2.析构函数
3.拷贝构造函数 4.拷贝赋值重载
5.取地址重载 6.const取地址重载
第5,6个用处不大,默认成员函数就是我们不写编译器会生成一个默认的。
C++11新增了两个:移动构造函数和移动赋值运算符重载针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
1.如果自己没有实现移动构造函数,且析构函数,拷贝构造,拷贝赋值都没有实现,那么编译器会自动生成一个默认移动构造,默认生成的移动构造,对内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,如果没有实现就调用拷贝构造。
2.如果自己没有实现移动赋值拷贝函数,且析构函数,拷贝构造,拷贝赋值都没有实现,那么编译器会自动生成一个默认移动赋值,默认生成的移动赋值,对内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,如果没有实现就调用拷贝构造。
3.如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值
7 lambda表达式
7.1 C++98中的一个例子
在C++98中,如果待排序元素为自定义类型,需要用户定义排序时的比较规则(仿函数):
struct Goods { string _name; double _price; int _num; // ... }; 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; } }; struct CompareNumLess { bool operator()(const Goods& gl, const Goods& gr) { return gl._num < gr._num; } }; struct CompareNumGreater { bool operator()(const Goods& gl, const Goods& gr) { return gl._num > gr._num; } }; int main() { vector<Goods> v = { { "苹果", 2.1, 300 }, { "香蕉", 3.3, 100}, { "橙子", 2.2 , 1000}, { "菠萝", 1.5, 1} }; // 要求分别按名字、价格、数量进行排序,升序或降序 sort(v.begin(), v.end(), ComparePriceLess()); sort(v.begin(), v.end(), ComparePriceGreater()); sort(v.begin(), v.end(), CompareNumLess()); sort(v.begin(), v.end(), CompareNumGreater()); return 0; }
随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法, 都要重新去写一个仿函数,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式。
7.2 lambda表达式
int main() { vector<Goods> v = { { "苹果", 2.1, 300 }, { "香蕉", 3.3, 100 }, { "橙子", 2.2, 1000 }, { "菠萝", 1.5, 1 } }; // 要求分别按名字、价格、数量进行排序,升序或降序 auto f1 = [](const Goods& g1, const Goods& g2) { return g1._price > g2._price; }; sort(v.begin(), v.end(), f1); 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._num > g2._num; }); sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._num < g2._num; }); return 0; }
上述代码就是使用C++11中的lambda表达式来解决,可以看出lambda表达式实际是一个匿名函数。
7.3 lambda表达式语法
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
1. lambda表达式各部分说明:
·[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
·(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
·mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
·->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
·{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意: 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。
因此C++11中最简单的lambda函数为:[ ]{ }; 该lambda函数不能做任何事情。int main() { // 最简单的lambda表达式, 该lambda表达式没有任何意义 []{}; // 省略参数列表和返回值类型,返回值类型由编译器推导为int int a = 3, b = 4; [=] {return a + 3; }; // 省略了返回值类型,无返回值类型 auto fun1 = [&](int c) {b = a + c; }; fun1(10); cout << a << " " << b << endl; // 各部分都很完善的lambda函数 auto fun2 = [=, &b](int c)->int {return b += a + c; }; cout << fun2(10) << endl; //------------------------------------------------------------ int x = 1, y = 2; // 实现add的lambda auto add1 = [](int x, int y)->int{return x + y; }; cout << add1(x, y) << endl; // 在捕捉列表,捕捉a、b, 没有参数可以省略参数列表,返回值可以通过推演,也可以省略 //auto add2 = [x, y]{}->int{return x + y + 10; }; auto add2 = [x, y]{return x + y + 10; }; cout << add2() << endl; return 0; }
注:通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。
2. 捕获列表说明
捕捉列表描述了上下文中哪些数据可以被lambda使用,以及使用的方式传值还是传引用。
·[var]:表示值传递方式捕捉变量var
·[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
·[&var]:表示引用传递捕捉变量var
·[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
·[this]:表示值传递方式捕捉当前的this指针
注意:
a. 父作用域指包含lambda函数的语句块
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量;[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。
比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
d. 在块作用域以外的lambda函数捕捉列表必须为空。
e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
f. lambda表达式之间不能相互赋值,即使看起来类型相同int main() { int a = 0, b = 1; // 标准写法1 auto swap1 = [](int& x, int& y)->void { int tmp = x; x = y; y = tmp; }; swap1(a, b); // 尝试利用捕捉列表,捕捉当前局部域的变量, // 这样就不用传参或者减少传参,省略参数和返回值 // 这里传值方式捕捉,拷贝外面的a和b给lambda里面的a、b // lambda里面的a、b的改变不会影响外面 auto swap2 = [a, b]()mutable { int tmp = a; a = b; b = tmp; }; swap2(); auto swap3 = [&a, &b] { int tmp = a; a = b; b = tmp; }; swap3(); auto swap4 = [&] { int tmp = a; a = b; b = tmp; }; swap4(); return 0; }
7.4 函数对象与lambda表达式
函数对象,又称为仿函数,即可以像函数一样使用的对象,就是在类中重载了operator()运算符的类对象。
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); // lamber auto r2 = [=](double monty, int year)->double{return monty*rate*year; }; r2(10000, 2); return 0; }
从使用方式上来看,函数对象与lambda表达式完全一样。
函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()
8.包装器
先来看看下面这段代码:
// ret = func(x); // 上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能是lamber表达式对象? //所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!为什么呢?我们继续往下看 // 函数模板会被实例化多次 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 func(double i) { return i / 2; } struct Functor { double operator()(double d) { return d / 3; } }; //int main() //{ // // 函数名 // cout << useF(func, 11.11) << endl; // // // 函数对象 // cout << useF(Functor(), 11.11) << endl; // // // lamber表达式 // cout << useF([](double d)->double{ return d / 4; }, 11.11) << endl; // // return 0; //}
代码执行结果:
![]()
上面这段代码在我们调用useF时,函数执行跳转到useF时会实例化多份,因为useF第一个参数类型是模板,三次传参的类型都是不同的,会实例化三份出来,实例化出来的地址都是不同的
#include<functional> //使用包装器以后,useF看到的都是包装器,只会实例化一份 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 func(double i) { return i / 2; } struct Functor { double operator()(double d) { return d / 3; } }; int main() { // 函数名 std::function<double(double)> f1 = func; cout << useF(f1, 11.11) << endl; // 函数对象 std::function<double(double)> f2 = Functor(); cout << useF(f2, 11.11) << endl; // lamber表达式 std::function<double(double)> f3 = [](double d)->double{ return d / 4; }; cout << useF(f3, 11.11) << endl; return 0; }
上面代码执行结果:
上面的代码只会实例化一份,useF函数看到的都是包装器.
包装器使用的几种场景:
#include<functional> int f1(int a, int b) { return a + b; } struct Functor1 { 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)> ff1 = f1; cout << ff1(1, 2) << endl; // 包装仿函数 std::function<int(int, int)> ff2 = Functor1(); cout << ff2(1, 2) << endl; // 包装成员函数 std::function<int(int, int)> ff3 = Plus::plusi; cout << ff3(1, 2) << endl; //std::function<double(Plus, double, double)> ff4 = &Plus::plusd; //cout << ff4(Plus(),1.1, 2.2) << endl; // 包装lambda表达式 auto f5 = [](int a, int b){return a + b; }; std::function<int(int, int)> ff5 = f5; cout << ff5(1, 2) << endl; }
std::bind调整可调用类型参数
可以将bind函数看作是一个通用的函数适配器,他接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表
#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() { //placeholder表示占位参数 std::function<int(int, int)> f1 = bind(Plus, placeholders::_1, placeholders::_2); cout << f1(1, 2) << endl; // 想把plus绑定成一个值+10 std::function<int(int)> f2 = bind(Plus, 10, placeholders::_1); cout << f2(5) << endl; // 绑定固定的可调用对象 std::function<int(int, int)> f3 = bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2); cout << f3(1, 2) << endl; std::function<int(int, int)> f4 = bind(&Sub::sub, Sub(), placeholders::_2, placeholders::_1); cout << f4(1, 2) << endl; return 0; }
运行结果:
9.可变参数列表
在C++11之前,类模板和函数模板只能包含固定数量的模板参数。
在C++11之中,类模板和函数模板允许模板定义中包含0到任意个模板参数。
声明可变参数模板时需要在typename或class后面带上省略号"...",省略号的作用:
1、声明一个参数包,包含0到任意个参数
2、在模板定义的右边,可以将参数包展开成一个个独立的参数
可变参数模板函数定义如下:template<class... T> void f(T... args) { cout<<sizeof...(args)<<endl;//打印变参的个数 }
展开参数包的方法有两种:
1、通过递归的模板函数将参数包展开
2、逗号表达式和初始化列表方式展开参数包
通过递归函数展开参数包,需要提供一个参数包展开的函数和一个递归终止函数。
#include<string> void ShowListArg() { cout << endl; } // 展开函数 template <class T, class ...Args> void ShowListArg(T value, Args... args) { cout << value << " "; ShowListArg(args...); } template <class ...Args> void ShowList(Args... args) { ShowListArg(args...); } int main() { ShowList(); std::string s("111"); ShowList(1, 2, 'A', "ssss", s); ShowList(23, 11.11, "121313"); ShowList(1, 2, 3); ShowList(1, 2, 3, 4, 5); return 0; }
代码执行结果:
逗号表达式展开参数包方式一:
template <class T> void PrintArg(T t) { cout << t << " "; } template <class ...Args> void ShowList(Args... args) { // 列表初始化 // {(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... ) int arr[] = { (PrintArg(args), 0)... }; cout << endl; }
逗号表达式展开参数包方式二:
template <class T> int PrintArg(T t) { cout << t << " "; return 0; } // 这里得&&是万能引用,实参是左值,参数包的这个形参就是左值引用 // 实参是右值,参数包的这个形参就是右值引用 template <class ...Args> void ShowList(Args&&... args) { // 列表初始化 int arr[] = { PrintArg(args)... }; cout << endl; }