C++11的官网:C++11 - cppreference.com
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中,标准允许使用大括号{ }对数组或者结构体元素进行同一的列表初始值设定。比如:
#include<iostream>
using namespace std;
struct Point
{int _x;int _y;
};
int main()
{//对数组int arr1[] = {1,2,3,4,5};//对结构体Point p = {1,2};return 0;
}
C++11扩大了用大括号扩起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可以不添加。
#include<iostream>
using namespace std;
struct Point
{int _x;int _y;Point(int x,int y):_x(x), _y(y){}};
int main()
{//对于内置类型int a{ 1 };int a1 = { 10 };//对于数组int arr[]{1, 2, 3, 4, 5};int arr1[]={1, 2, 3, 4, 5};int brr[5]{1, 2, 3, 4, 0};int brr1[5]={1, 2, 3, 4, 0};//对于自定义类型Point p{ 1, 2 };//也会去调用构造函数Point p1 = { 1, 2 }; //多参数隐式类型转换//主要还是应用到这种场景int* p2 = new int[5];//这种初始化不太好初始化。int* p3 = new int[5]{1, 2, 3}; //直接初始化为1,2,3,0,0Point* p4 = new Point[2]{{ 1, 2 }, { 2, 3 }}; //需要提供构造函数return 0;
}
这里存在一个多参数的隐式类型转换
#include<iostream>
#include<string>
#include<vector>
using namespace std;
struct Point
{int _x;int _y;Point(int x, int y):_x(x), _y(y){}};
class A
{
public:A(int a=0):_a(a){}
private:int _a;
};int main()
{//C++98//仅支持构造函数单参数的隐式类型转换A a();//调用构造函数A a1 = 10; //这里存在隐式类型转换,先使用10构造一个A临时对象,在使用临时都对象拷贝构造a1,编译器合二为一,只有构造函数string s1("hello");//调用构造函数string s = "hello";//这里也是隐式类型转换//C++11//支持多参数构造函数隐式类型转换Point p1(1, 2);//调用构造函数//多隐式类型转换,可以使用explicit进行实验,加上后,就报错了Point p2{ 1, 2 };Point p3 = { 1, 2 };Point* ptr = new Point[2]{{ 1, 2 }, { 2, 3 }};//这里也是隐式类型转换return 0;
}
2.2 initializer_list
#include<iostream>
#include<string>
#include<vector>
using namespace std;int main()
{//C++98vector<int> v1;v1.push_back(1);v1.push_back(2);v1.push_back(3);//C++11vector<int> v2 = {1,2,3,4,5};return 0;
}
这里C++11使用了这样的构造函数:
支持迭代器
底层相当于是一个数组,用来储存数据。
把{ }里的数据放入 initializer_list,再使用initializer_list,初始化vector。
list和map也是可以的:
#include<iostream>
#include<string>
#include<vector>
#include<list>
#include<map>
using namespace std;int main()
{//C++98vector<int> v1;v1.push_back(1);v1.push_back(2);v1.push_back(3);//C++11 initializer_list类模板initializer_list<int> it1= { 1, 2, 3, 4, 5 };//也是一个类模板vector<int> v2 = {1,2,3,4,5};list<int> l1 = { 1, 2, 3, 4, 5 };//这里存在隐式类型转换,先构造一个pair的临时变量,再去拷贝构造initializer_list(数组)里面的pair元素,编译器会优化为一次构造。//再使用initializer_list 去初始化vectormap<string, string> dict = { { "sort", "排序" }, { "left", "左" }, {"right","右"} };pair<string, string> kv("sort", "排序");//调用构造函数pair<string, string> kv1("right", "右");//这里就是拷贝构造了,使用kv,kv1分别拷贝构造initializer_list(底层相当于一个数组)的元素//再用initializer_list初始化map.map<string, string> dict1 = { kv, kv1 };return 0;
}
自己模拟实现的list实现列表初始化,其他容器实现方法相似的。
list(initializer_list<T> it1){_head = new Node;_head->_next = _head;_head->_prev = _head;//1.直接迭代器遍历typename initializer_list<T>::iterator it = it1.begin();while (it!=it1.end()){push_back(*it);++it;}//2.范围for// for (auto e : it1)// {// push_back(e);// }//3.使用迭代器区间// list tmp(it1.begin(), it1.end());// std::swap(_head,tmp._head);}
#include"list.h"
int main()
{Ls::list<int> lt = {1,2,3,4,5};auto it = lt.begin();while (it!=lt.end()){cout << *it << " ";++it;}return 0;
}
还有一个赋值操作:先clear() 一下vector,然后插入数据。
#include<vector>
#include<iostream>
using namespace std;
int main()
{vector<int> v ;v.push_back(1);v.push_back(2);v.push_back(3);v = { 4,5,6 }; //思路也是一样的,for (auto e : v){cout << e << " ";}cout << endl;return 0;
}
3.声明
3.1auto
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没有什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型推导。这样要求必须进行显示初始化,让编译器将定义对象设置为初始化值的类型。
auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型
3.2 decltype
decltype是根据表达式的实际类型推演出定义变量时所用的类型。
1.推演表达式类型作为变量的定义类型
#include<vector>
#include<iostream>
using namespace std;
int main()
{const int x = 1;int ret = 0;double y = 2.2;//typeid只能查看类型不能用其结果来定义cout << typeid(x).name() << endl;//看变量的类型,这里推导出是int实际是const int //decltype 是根据表达式的实际类型推演出定义变量时所用的类型decltype (x) z = 10;//推演表达式类型作为变量的定义类型,z是const int decltype(x*y) ret; //ret的类型是doubledecltype(&x) p; //p的类型是int*return 0;
}
#include<vector>
#include<map>
#include<iostream>
using namespace std;int func()
{}int main()
{int(* pf)() = func;auto pfunc2 = func;decltype(pfunc2) func3 = func;decltype(&func) pfun4 = func;map<string, string> dict = { { "sort", "排序" }, { "left", "左" } };auto it = dict.begin();decltype(it) copyit = it;//可以这样用vector<decltype(it)> v; //这种方式是其他东西替代不了。v.push_back(it);return 0;
}
4.STL中的一些变化
1.新增一些容器
2.已有容器,增加一些好用或者提高效率的接口。比如:列表初始化,右值引用等。
array: 很鸡肋
#include<array>
#include<iostream>
using namespace std;int main()
{int a[10];array<int, 10> a1;//越界cout<<a[20]<<endl;//数组,编译器是抽查,有可能不会检查出来cout<<a1[20]<<endl; //a1.operator[](20) 必检查//array检查更加严格,直接报错//array也支持迭代器,更好兼容STL容器玩法return 0;
}
forward_list:单链表,与list相比就是少了一个指针。价值不是很大。很鸡肋。
新增的方法:
一开始的迭代器:
增加了cbegin等,觉得以前的写法不规范,把begin 和const begin 分开。也很鸡肋
5.右值引用和移动语义*
5.1左值引用和右值引用
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号的左边。定义时const修饰后的左值,不能给它赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
#include<iostream>
using namespace std;int main()
{int a = 10;//a是左值int&rp = a;//左值引用int* p = &a;//p是左值int& rp1 = *p;//rp1是左值引用const int b = 10;//b是左值,可以取地址const int& rp2 = b;//左值引用return 0;
}
什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,传值返回函数的返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
#include<iostream>
using namespace std;int main()
{double x = 1.1, y = 2.2;//以下几个都是常见的右值10;x + y;fmin(x, y);//传值返回//右值引用int&& rr1 = 10;double&& rr2 = x + y;double&&rr3 = fmin(x, y);return 0;
}
左值和右值的区分不是很好区分,一般认为:
1.普通类型的变量,因为有名字,可以取地址,都认为是左值
2.const修饰的常量,不可修改,只读类型的理论应该按照右值对待,但因为其可以取地址(如果只是const类型常量的定义,编译器不给取开辟空间,如果对该常量取地址时,编译器才为其开辟空间),C++认为时左值
3.如果表达式运行结果时一个临时变量或者对象,认为是右值。
4.如果表达式运行结果或单个变量是一个引用则认为是左值。
总结:
1.不能简单地通过能否放在=左侧右侧或者取地址来判断左值或者右值,要根据表达式结果或变量地性质判断.
右值引用能否引用右值?右值引用能否引用左值?
#include<iostream>
using namespace std;int main()
{double x = 1.1, y = 2.2;//以下几个都是常见的右值10;x + y;fmin(x, y);//传值返回//以下地p,b,c,*p都是左值int*p = new int(0);int b = 1;const int c = 2;//左值引用能否引用右值,不能直接引用,但是可以const引用const int&r1 = 10;const double& r2 = x + y;const double& r3 = fmin(x, y);//右值引用能否引用左值,也是不能直接引用,但是右值引用可以引用move以后左值。int*&& rr1 =move(p);int&&rr2 = move(*p);int&& rr3 = move(b);const int&& rr4 = move(c);return 0;
}
需要注意:右值是不能取地址的。但是给右值取别名后(即右值引用后,右值就变成了左值),会导致右值被存储到特定位置,且可以取到该位置的地址。
#include<iostream>
using namespace std;int main()
{double x = 1.1, y = 2.2;//以下几个都是常见的右值10;x + y;fmin(x, y);//传值返回int&& rr1 = 10;double&& rr2 = x + y;const double&&rr3 = x + y;cout << &rr1 << endl;cout << &rr2 << endl;rr1 = 20;rr2 = 30.1;//rr3 = 1.1; //const 不可修改return 0;
}
总结:
左值引用总结:
左值引用只能引用左值,不能引用右值
但是const左值引用既可以引用左值,也可以引用右值
右值引用总结:
右值引用只能右值,不能引用左值
但是右值引用可以引用move以后的左值
5.2右值引用的使用场景
首先右值引用是为了弥补左值的不足。
1.右值引用的第一个场景:拷贝构造
左值引用的使用场景(自己模拟实现string实验):
1.第一个场景:左值引用做参数
#include<iostream>
using namespace std;
#include"my_string.h"
void fun1(str::string s)
{}void fun2(str::string& s)
{}int main()
{str::string s("hello");fun1(s); //值传参会深拷贝,造成空间浪费fun2(s); //左值引用,可以解决这个问题return 0;
}
2.第二个场景:做返回值,出了作用域还在,只能解决部分问题
string& operator+=(char ch){push_back(ch);return *this;}
这种场景,tmp出了作用域就销毁了,无法使用左值引用
string operator+(char ch){string tmp(*this);push_back(ch);return tmp;//返回的时候会去调用深拷贝}
此时右值引用就发挥了作用:那么右值引用如何解决的?
C++对右值进行了严格地区分:
C语言中的纯右值,比如:a+b,100
将亡值。比如:表达式地中间结果、函数按照值地方式进行返回出了函数就销毁了。
使用string来进行讲解:9.string模拟实现_萌新一份的博客-CSDN博客
看下面分析:
又要拷贝构造,还要销毁tmp:
这里的tmp就是将亡值,tmp资源析构了很可惜,不如交给移动构造。
移动构造:其实这里和现代写法拷贝构造很像。
//移动构造:移动资源string(string&& s):_str(nullptr), _size(0), _capacity(0){cout << "移动构造(资源转移)" << endl;swap(s); //只需要交换指针和内置类型即可}
只需要交换这几个值就可以。
void swap(string&s){//swap最好应用于内置类型的交换std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}
tmp是将亡值,编译器去匹配最合适的函数,找到了移动构造:提高了效率
最简单的案例:
在没有移动构造的情况下:
#include<iostream>
using namespace std;
#include"my_string.h"
str::string fun()
{str::string s("hello");return s;
}int main()
{fun();return 0;
}
去调用深拷贝:
有移动构造的情况下:去调用移动构造
1.当返回值没有变量来接收时
1.当没有移动构造只有拷贝构造的时候 :这个时候编译器无法优化,因为返回值必须使用这个临时对象,因此没有优化空间
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
#include"my_string.h"str::string to_string(int value)
{str::string str;while (value){int val = value % 10;str += ('0' + val);value /= 10;}reverse(str.begin(), str.end());return str;
}int main()
{cout<< to_string(1234)<<endl;//没有变量接受返回值return 0;
}
str不能作为返回值,因为出了函数栈帧就销毁了,这里只能使用临时对象作为返回值,临时对象是在main函数栈帧里面的。
当有移动构造+拷贝构造的时候:有优化的空间,会直接把str当作右值进行处理,直接去调用移动构造,把资源转移给临时对象,临时对象作为返回值。
2.当返回值有变量来接收时
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
#include"my_string.h"str::string to_string(int value)
{str::string str;while (value){int val = value % 10;str += ('0' + val);value /= 10;}reverse(str.begin(), str.end());return str;
}int main()
{str::string ret = to_string(1234);//使用ret来接受返回值return 0;
}
当没有移动构造只有拷贝构造的时候:
编译器没有优化的时候:会调用2次拷贝构造。这个临时对象是放在main的函数栈帧里。
当编译器有优化的时候:只调用一次拷贝构造
当有移动构造+拷贝构造的时候:
编译器没有优化的时候:调用一次拷贝构造+一次移动构造,这里的临时对象是右值
当编译器优化的时候:只调用一次移动构造,str充当的是临时对象的位置,把str当成右值。
2.右值引用的第二个场景:移动赋值
string中使用的赋值运算:
//赋值运算符重载现代写法string& operator=(const string &s){if (this != &s){string tmp(s);//调用拷贝构造函数swap(tmp);}return *this;}
不管是move 还是正常的赋值都是调用赋值现代写法:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
#include"my_string.h"int main()
{str::string s1("hello world");str::string s2;s2 = s1;s2 = move(s1);return 0;
}
当有移动赋值后:
//移动赋值string& operator=(string &&s){cout << "转移资源" << endl;swap(s);return *this;}
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
#include"my_string.h"int main()
{str::string s1("hello world");str::string s2;s2 = s1; //调用赋值重载s2 = move(s1);//调用移动赋值return 0;
}
1.当没有移动构造和移动赋值时
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
#include"my_string.h"
str::string to_string(int value)
{str::string str;while (value){int val = value % 10;str += ('0' + val);value /= 10;}reverse(str.begin(), str.end());return str;
}
int main()
{str::string ret;ret = to_string(1234);return 0;
}
2次深拷贝,赋值现代写法需要深拷贝。
当有了移动赋值和移动构造后:
2次资源转移:
在C++11中,许多容器增加了右值引用的接口:
当string只有拷贝构造的时候:push_back会去匹配const左值引用
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
#include<list>
#include"my_string.h"str::string to_string(int value)
{str::string str;while (value){int val = value % 10;str += ('0' + val);value /= 10;}reverse(str.begin(), str.end());return str;
}int main()
{list<str::string> lt;str::string s("1111");lt.push_back(s);cout << endl;//一般都是这样写lt.push_back("22222");//这里先去调用string的构造函数,构造一个临时对象,然后使用临时对象push_back cout << endl;lt.push_back(to_string(333333));//return str 时,会拷贝构造临时对象,作为返回值,然后再push_backcout << endl;return 0;
}
当string 提供移动构造的时候:push_back会去匹配右值引用
所以在匹配右值引用的时候,string必须也要提供移动构造。
需要谨慎使用move:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
#include<list>
#include"my_string.h"int main()
{list<str::string> lt;str::string s("1111");lt.push_back(move(s));//把s的资源转移走了,所以需要谨慎使用movecout << endl;return 0;
}
把s的资源转移走了:
5.2完美转发
万能引用:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;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; }
//模板中的&&不代表右值引用,而是万能引用,其既能接受左值又能接受右值
//模板的万能引用只是提供了一个能够同时接受左值引用和右值引用的能力
template<class T>
void PerfectForward(T &&t) //模板(T)自动推导类型,&& 既能接收左值也能接收右值
{ Fun(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;
}
运行起来后,发现为啥都匹配了左值引用,不太对吧,不应该是:你是右值引用,我就去匹配右值的Fun,你是左值就去匹配左值的Fun
其实这里是右值引用后,会开辟一个特定空间位置,来存储右值,导致右值变为左值(引用类型的唯一作用是限制了接收的类型,后续使用中都退化成了左值),为了使右值还是右值,左值还是左值,需要使用完美转发,C++使用forward函数来实现完美转发:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;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; }
//模板中的&&不代表右值引用,而是万能引用,其既能接受左值又能接受右值
//模板的万能引用只是提供了一个能够同时接受左值引用和右值引用的能力
template<class 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;
}
使用完美转发,最终匹配上了:
使用场景:list是库里的,知道就行了解下
下面通过简单的链表和自己实现的string调试观察:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include"my_string.h"
#include<assert.h>
using namespace std;namespace Ls
{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->_next = _head;_head->_prev = _head;}void PushBack(T&& x){// 只要右值引用,再传递其他函数调用,要保持右值属性,必须用完美转发//Insert(_head, 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); // 关键位置,也需要完美转发,去调用string的移动赋值// prev newnode posprev->_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; // 关键位置// prev newnode posprev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}private:Node* _head;};
}int main()
{Ls::List<str:: string> lt;lt.PushBack("1111");return 0;
}
执行过程:
拷贝构造也是可以演示的,因为在库中使用的是内存池,内存池和malloc唯一的区别就是内存池更加的高效,但都是只申请空间,不初始化,这里使用定位new来模仿库中的,因为库中的都是移动构造,而不是上面的移动赋值:
void Insert(Node* pos, T&& x){Node* prev = pos->_prev;//模仿内存池Node* newnode = (Node*)malloc(sizeof(Node)); //malloc只申请空间,不初始化//new(&newnode->_data)T(x); //定位new,调用拷贝构造new(&newnode->_data)T(std::forward<T>(x));//定位new,完美转发调用移动构造// prev newnode posprev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}
6.lambda表达式
在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort的方法。
需要传可调用对象:函数指针qsort,仿函数
#include <algorithm>
#include <functional>
int main()
{int array[] = {4,1,8,5,3,7,0,9,2,6};// 默认按照小于比较,排出来结果是升序std::sort(array, array+sizeof(array)/sizeof(array[0]));// 如果需要降序,需要改变元素的比较规则std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());return 0; }
在C++98中,如果待排序元素为自定义类型,需要用户定义排序时的比较原则:提供仿函数
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
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;}
};struct ComparrPriceGreater//按照价格降序排序
{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,2 }, { "菠萝", 1.5 ,1} };sort(v.begin(),v.end(), ComparePriceLess()); //传仿函数的对象sort(v.begin(), v.end(), ComparrPriceGreater()); //传仿函数的对象return 0;
}
随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑都不一样,还要去实现多个类,特别时相同的命名,这些都给编程者带来了极大的不方便,因此,在C++11语法中出现了lambda表达式。
lambda表达式
lambda表达式的格式:
1.lambda表达式各部分说明
[capture-list]:捕捉列表,该列表总是出现在lambda函数的开始为,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中(作用域里的)的变量供lambda函数使用。
(prameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)
->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意:在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++中最简单的lambda函数为:[ ]{};该lambda函数不做任何事情。
实现一个两个数相加的lambda:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
int main()
{//实现一个两个数相加的lambdaauto add1=[](int a, int b)->int{ return a + b; };auto add3 = [](int a, int b){ return a + b; };//返回值类型可以省略//定义一样的变量//auto add2 = add1;//decltype(add1) add3 = add1;//不传递参数,参数列表也可以省略auto func1=[]{cout<< "hello world" << endl; };cout << add1(1, 2) << endl;func1();
}
捕捉列表是干什么的?
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
int main()
{int a = 1, b = 2;auto swap1 = [](int x, int y){ //传值不行int z = x;x=y;y = z;};swap1(a,b);cout << a << " " << b << endl;auto swap2 = [](int& x, int& y){ //需要传引用int z = x;x = y;y = z;};swap2(a, b);cout << a << " " << b << endl;//捕捉列表捕捉上下问文,传值捕捉还是不能改变,要想改变必须mutable,且参数列表不能省略auto swap3 = [a, b]()mutable{int z = a;a = b;b = z;};swap3();cout << a << " " << b << endl;//引用捕捉auto swap4 = [&a, &b](){int z = a;a = b;b = z;};swap4();cout << a << " " << b << endl;return 0;
}
捕捉列表说明:
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕捉所有父作用域中的变量,包括this
[&var]:表示引用传递捕捉捕捉var
[&]:表示引用传递捕捉所有作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针
注意:
a.父作用域指包含lambda函数的语句块
b.语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:[=,&a,&b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量[&,a,this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
c.捕捉列表不允许变量重复传递,否则就会导致编译错误。比如:[=,a]:=已经以值传递方式捕捉了所有变量,捕捉a重复,或[a,a]也是重复
d.在块作用域外的lambda函数捕捉列表必须为空。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
int a = 10, b = 20;
auto add1 = [](){return a + b; };
//auto add2 = [a,b](){return a + b; }; //在全局外不能捕捉对象
int main()
{return 0;
}
e.在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。
f.lambda表达式之间不能相互赋值,即使看起来类型相同。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;int main()
{auto f1 = []{cout << "hello world" << endl; };auto f2 = []{cout << "hello world" << endl; };// 此处先不解释原因,等lambda表达式底层实现原理看完后,大家就清楚了//f1 = f2; // 编译失败--->提示找不到operator=()return 0;
}
然后对上面sort进行改进,使用lambda表达式:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
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, 2 }, { "菠萝", 1.5, 1 } };auto priceLess = [](const Goods& g1, const Goods& g2){return g1._price < g2._price; };sort(v.begin(), v.end(), priceLess); //直接写,相当于匿名对象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的类型是什么?
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;int main()
{int a = 1, b = 2;auto swap1 = [](int&x, int&y){int z = x;x = y;y = z;};cout << typeid(swap1).name() << endl;return 0;
}
这个字符串是,uuid保证不会重复
看以下代码:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;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);// lambda表达式auto r2 = [=](double monty, int year)->double{return monty*rate*year; };r2(10000, 2);return 0;
}
反汇编:
实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类(类使用uuid实现唯一的),在该类中重载了operator(). lambda表达式:lambda+uuid的仿函数类
7.类中新增的功能
1.default
C++11中可以让你更好的控制要使用的默认函数,假设你要使用某个默认的函数,但是因为一些原因这个函数没有生成。比如:我们提供了拷贝构造,就不会生成移动构造,那么我们可以使default关键字显示指定移动构造生成:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;class A {
public:A(int a) : _a(a){}// 显式缺省构造函数,由编译器生成A() = default;//A& operator=(const 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;
}
2.删除默认函数
如果能想要限制某些默认函数的生成,在C++98中,该函数设置成private(但是在类中是可以使用的),并且不给定义,这样只要调用就会报错。在C++11中更简单,只需要在该函数声明上加上=delete即可,该语法只是编译器不生成对应函数的默认版本,成=delete修饰的函数为删除函数。
在C++98中,防止拷贝构造:
class A {
public:A() = default;//1.防止拷贝,只声明不定义,为了防止在类外有定义,可以设置成私有,但是在类里还是可以使用的//A(const A& a);
private://2.设置成私有的,但是在类里面可以使用,可以使用一个子函数来调用A(const A& a){}int _a=0;
};
在C++11中:不需要关心又是私有,又是定义的等等
class A {
public:A() = default;A(const A& a) = delete; //这个函数不能被用,类里外都不能用A& operator=(const A&) = delete;//防止赋值
private:int _a=0;
};
C++11中新增了两个成员函数:移动构造函数和移动赋值。当自己不写的时候会默认生成。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点是如下:
1.如果你没有自己实现移动构造函数,且没有实现析构函数、拷贝构造,拷贝赋值,那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有就调用拷贝构造。
使用自己写的string,没有实现析构函数、拷贝构造、拷贝赋值:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include"my_string.h"
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:str::string _name;int _age = 0;
};int main()
{//当没有实现拷贝构造,拷贝赋值,析构函数进行分析Person s1;Person s3 = std::move(s1);return 0;
}
结果发现:在有移动构造的时候,为什么是深拷贝,不应该是移动构造吗,其实这里是VS2013的一个BUG。在19中是去调用移动构造的。
当实现了3个的其中一个,就不会默认生成移动构造。自定义类型就去调用拷贝构造了。
2.如果你没有实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载,那么编译器就会自动生成一个默认的移动赋值。对内置类型完成值拷贝,对自定义类型完成,如果实现了移动赋值就去调用,没有实现就调用拷贝赋值。(和上面玩法一样)
同样需要在VS2019中观察:自定义类型调用移动赋值
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include"my_string.h"
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:str::string _name;int _age = 0;
};int main()
{Person s1;//当没有实现拷贝构造,、拷贝构造,拷贝赋值,进行分析Person s4;s4 = std::move(s1);return 0;
}
8.模板的可变参数
在C++98中,printf就是可变参数。
在C++11的新特性可变参数模板能够让你创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模板和函数模板中只能包含固定数量的模板参数,可变模板参数无疑是一个巨大的改进。
//Args 是一个模板参数包,args是一个函数形参参数包
//声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数
template<class...Args>
void ShowList(Args...args)
{}
上面的参数args前面有省略号,所以它就是一个可变模板参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模板参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模板参数的一个主要特点,也是最大的难点,即如何展开可变模板参数。由于语法不支持使用args[i]这样的方式获取可变参数,所以我们需要用一些奇招来一一获取参数包的值。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
//Args 是一个模板参数包,args是一个函数形参参数包
//声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数
template<class...Args>
void ShowList(Args...args)
{cout << sizeof...(Args) << endl; //算模板参数个数cout << sizeof...(args) << endl;//算函数形参参数包参数个数cout << endl;//这样不对/*for (size_t i = 0; i < sizeof..(Args); i++){cout << args[i] << "-";}*/
}int main()
{ShowList(1);ShowList(1,'A');ShowList(1, 'A', std::string("sort"));return 0;
}
递归函数方式展开参数包
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;//递归终止函数
//只有一个参数后,进行匹配
template<class T>
void ShowList(const T& t) //最后只有一个参数,来匹配这里
{cout << t << endl;
}//解析并打印参数包中每个参数的类型及值
template<class T,class...Args>//增加一个模板参数
void ShowList(T val,Args...args)
{cout << typeid(val).name() << ":" << val << endl;ShowList(args...);//递归解析
}int main()
{//第一个参数匹配TShowList(1, 'A', std::string("sort"));return 0;
}
逗号表达式展开参数包:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;template<class T>
void PrintfArg(T val)
{T copy(val);cout << typeid(val).name() << endl;
}template<class...Args>
void ShowList(Args...args)
{//逗号表达式,取最右边的,0为了初始化数组,...是一次向后传递{(PrintfArg(args), 0),(PrintfArg(args), 0),(PrintfArg(args), 0)}int arr[] = { (PrintfArg(args), 0)... };
}
int main()
{//第一个参数匹配TShowList(1, 'A', std::string("sort"));return 0;
}
也可以不使用逗号表达式,需要函数有返回值:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;template<class T>
int PrintfArg(T val)
{T copy(val);cout << typeid(val).name() << endl;return 0; //给返回值
}template<class...Args>
void ShowList(Args...args)
{//逗号表达式,取最右边的,0为了初始化数组,...是依次向后传递{(PrintfArg(args), 0),(PrintfArg(args), 0),(PrintfArg(args), 0)}int arr[] = { PrintfArg(args)... };
}
int main()
{//第一个参数匹配TShowList(1, 'A', std::string("sort"));return 0;
}
结果都是一样的:
STL容器中emplace_back:
分析push_back,它能够提高效率了,那emplace_back是干什么的。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include"my_string.h"
#include<vector>
using namespace std;int main()
{vector<str::string> v;str::string s("1111");v.push_back(s);//内存池:内部就是定位new+去调用拷贝构造cout << endl;//这里调用了2此移动构造,不知道什么原因,不用关心,知道就行v.push_back(move(s));//内存池:定位new+完美转发去调用移动构造转移资源,效率高return 0;
}
与push_back相比emplace_back作用:就是更加灵活一些
#include<iostream>
#include<vector>
#include<list>
using namespace std;int main()
{std::list<std::pair<int, char>> mylist;//这样写没什么区别mylist.push_back(make_pair(1,'A'));mylist.emplace_back(make_pair(1, 'A'));//但是emplace_back可以支持可变参数mylist.emplace_back(1,'A');for (auto e : mylist){cout << e.first << ":" << e.second << endl;}return 0;
}
分析以下代码:
#include<iostream>
#include<list>
#include"my_string.h"
using namespace std;int main()
{std::list<std::pair<int, str::string>> mylist;mylist.emplace_back(10, "sort");mylist.emplace_back(make_pair(20, "sort"));cout << endl << endl;mylist.push_back(make_pair(30, "sort"));mylist.push_back({40,"sort"});//先构造一个pair临时对象return 0;
}
emplace是直接构造到空间上,push是先构造临时对象,在调用移动构造:效率差不多
9.包装器
1.function
function包装器也叫做适配器。C++中的function本质是一个类模板,也是一个包装器。
#include<iostream>
using namespace std;//可调用对象类型//1、函数指针 void(*p)();//2、仿函数/函数对象//3、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 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;
}
count一直都是1,所以useF函数模板实例化出来了3份。
那能不能解决这个问题呢?使用包装器
模板参数说明:
Ret:被调用函数的返回类型
Args...:被调用函数的形参
使用方法如下:
#include<iostream>
#include<functional>
using namespace std;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 + 1;}double plusd(double a, double b){return a + b;}
};int main()
{//包装函数//函数指针//int(int, int) 返回值类型+参数类型function<int(int, int)> f1 = f;cout << f1(1, 2) << endl;//包装仿函数//匿名对象function<int(int, int)> f2 = Functor();cout << f2(1, 2) << endl;//包装成员函数function<int(int, int)> f3 = Plus::plusi;//&Plus::plusi;静态的可以不加&cout << f3(1, 2) << endl;//非静态,需要对象去调用,因此多了一个参数Plus,非静态必须加&function<double(Plus, double, double)> f4 = &Plus::plusd;cout << f4(Plus(), 1.1, 2.2) << endl;return 0;
}
对上面实例化3份的useF函数进行改进,只让它实例化出来一份:
#include<iostream>
#include<functional>
using namespace std;//可调用对象类型
//1、函数指针 void(*p)();
//2、仿函数/函数对象
//3、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 d){return d / 3;}
};int main()
{//增加一个包装器// 函数名(函数指针)function<double(double)> fun1 = f;cout << useF(fun1, 11.11) << endl;// 函数对象,匿名对象function<double(double)> fun2 = Functor();cout << useF(fun2, 11.11) << endl;// lamber表达式function<double(double)> fun3 = [](double d)->double{ return d / 4; };cout << useF(fun3, 11.11) << endl;return 0;
}
只实例化出来了一份:
使用场景:
逆波兰表达式:
力扣https://leetcode.cn/problems/evaluate-reverse-polish-notation/submissions/
class Solution {
public:int evalRPN(vector<string>& tokens) {map<string,function <int(int,int)>> countmap{ //包装器在这里通吃,函数指针等什么的都可以{"+",[](int a,int b)->int{return a+b;}},{"-",[](int a,int b)->int{return a-b;}},{"*",[](int a,int b)->int{return a*b;}},{"/",[](int a,int b)->int{return a/b;}},};stack<int> st;for(size_t i=0;i<tokens.size();i++){string& str=tokens[i];if(countmap.find(str)==countmap.end()) //是操作数{st.push(stoi(str));}else //是操作符{int right=st.top();st.pop();int left=st.top();st.pop();st.push(countmap[str](left,right)); }}return st.top();}
};
2.bind
std::bind函数定义在头文件<functional>中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来"使用"元对象的参数列表。
#include<iostream>
#include<functional>
using namespace std;int SubFunc(int a, int b)
{return a - b;
}class Sub
{
public:int sub(int a, int b){return a - b;}
};int main()
{function<int(int, int)> fun1 = SubFunc;cout<<fun1(10,1)<<endl;//不调整参数顺序,和fun1一样function<int(int, int)> fun2 = bind(SubFunc, placeholders::_1, placeholders::_2);cout<<fun2(10, 1)<<endl;//调整参数顺序function<int(int, int)> fun3 = bind(SubFunc, placeholders::_2, placeholders::_1);cout << fun3(10, 1) << endl;//bind主要用于调整参数个数function<int(Sub, int, int)> fun4 = &Sub::sub;cout << fun4(Sub(), 10, 1) << endl;//通过bind绑定参数,就可以不用每次调用都要写Subfunction<int(int, int)> fun5 = bind(&Sub::sub,Sub(),placeholders::_1, placeholders::_2);cout << fun5(10,1)<<endl;//也可以这样,绑定好参数就不用传了function<int(int)> fun6 = bind(&Sub::sub, Sub(),100, placeholders::_1);cout << fun6(3) << endl;//尽量不用auto//auto fun6 =bind(&Sub::sub, Sub(),100, placeholders::_1);return 0;
}
10.线程库
线程后面整理:6.23 6.24 6.25 6.28
线程库:
#include<iostream>
#include<list>
#include<thread>
using namespace std;
void Print(int n)
{for (int i = 0; i < n; i++){cout << i << endl;}
}
int main()
{thread t1;thread t2(Print,1000);t2.join();return 0;
}