在我们C++的发展历程中,拥有两个很关键的版本,一个是C++98,一个是C++11,在这一章中我们一起就来了解下C++11给我们增加了哪些重要的新特性吧
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 能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅 功能更强大,而且能提升程序员的开发效率
列表初始化
C++98中{}的初始化问题
vector<int> v1{ 1, 2, 3, 4, 5 };//加不加=都是可以的,不过我们认识这种方式就好了,实际用的时候还是加上,更易看懂
vector<int> v2 = { 1, 2, 3, 4, 5 };
list<int> l1{ 1, 2, 3, 4, 5 };
list<int> l2 = { 1, 2, 3, 4, 5 };
map<string, int> m1{ { "熊大", 1 }, { "熊二", 2 }, { "光头强", 3 } };
map<string, int> m2 = { { "熊大", 1 }, { "熊二", 2 }, { "光头强", 3 } };
这些初始化方式都是在C++11后才合法的
内置类型的列表初始化
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<string, int> m{{"熊大",1}, {""熊二,2,},{"光头强",3},{"翠花",4}};
return 0;
}
自定义类型的列表初始化
class Point
{
public:
Point(int x = 0, int y = 0) : _x(x), _y(y)
{}
private:
int _x;
int _y;
};
int main()
{
Point p1(1, 2);
Point p2{ 1, 2 };//C++增加了下面两种初始化方式
Point p3 = { 1, 2 };
system("pause");
return 0;
}
那么我们在进行探究一下,容器时如何支持这种花括号的列表初始化呢?
vector(initializer_list<T> l) : _capacity(l.size()), _size(0)
{
_array = new T[_capacity];
for (auto e : l)
_array[_size++] = e;
}
vector<T>& operator=(initializer_list<T> l) {
delete[] _array;
size_t i = 0;
for (auto e : l)
_array[i++] = e;
return *this;
}
我们这里是以vector为例子,可以看到,容器支持花括号列表初始化,本质就是增加了一个initializer的构造函数,在内部进行赋值从而支持的
变量类型推导
为什么需要类型推导
在定义变量时,必须先给出变量的实际类型,编译器才允许定义,但有些情况下可能不知道需要实际类型怎么给,或者类型写起来特别复杂,比如:
#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();
auto it = dict.begin();
while (it != m.end())
{
cout << it->first << " " << it->second << endl;
++it;
}
return 0;
}
我们通过上面的例子其实不难发现,我们有些数据的类型在我们完成计算之前是没办法确定的,但是编译器需要确定类型才能定义,这时我们就不得不需要对其估算,自定类型,而这里auto的出现解决了这个问题,可以去根据前面数据类型自动推算,很是方便
同样auto可以解决类型名称复杂的问题,比如我们的迭代器遍历容器,类型名称很是复杂,可以使用auto来使其自动推导,简化写法,很是方便
我们来看这一段代码,我们原先并没有定义c的数据类型,但是编译器根据int与double的运算规则,推得c为double型,然后我们定义了一个string的字符串,打印出了它的类型,其实我们可以发现,他是一个类,C++中其实是没有string这个数据类型的,string是经过typedef的,原来的为basic_string
C++11 中,可以使用 auto 来根据变量初始化表达式类型推导变量的实际类型,可以给程序的书写提供许多方便。将程序中c 与 it 的类型换成 auto ,程序可以通过编译,而且更加简洁
decltype类型推导
为什么需要decltype
auto 使用的前提是:必须要对 auto 声明的类型进行初始化,否则编译器无法推导出 auto 的实际类型 。但有时候可能需要根据表达式运行完成之后结果的类型进行推导,因为编译期间,代码不会运行,此时auto 也就无能为力。如果能用 加完之后结果的实际类型作为函数的返回值类型就不会出错 ,但这需要程序运行完才能知道结果的实际类型,即RTTI(Run-Time Type Identifification 运行时类型识别 ) 。C++98 中确实已经支持 RTTI :typeid 只能查看类型不能用其结果类定义类型dynamic_cast 只能应用于含有虚函数的继承体系中运行时类型识别的缺陷是降低程序运行的效率
decltype
decltype是根据表达式的实际类型推演出定义变量时所用的类型
. 推演表达式类型作为变量的定义类型
推演函数返回值的类型
补充:auto无法做形参,无法做返回值
auto关键字
std::map<std::string, std::string> dict = { { "insert", "插入" }, { "sort", "排序" } };
std::map<std::string, std::string>::iterator it1 = dict.begin();
auto it2 = dict.begin();
// 这里it1和it2可以认为是完全一个类型的对象
// 唯一差别是it2的类型是编译器自动推导出来
// auto的优势就是可以把在类型比较复杂地方,简化代码的写法
// 这里要注意当容器存对象比较大,或者这个对象要做深拷贝,如string
// 最好给&和const,可以减少拷贝提高效率
// 容器支持范围for原理:范围for会被编译器替换成迭代器,也就是意味着支持迭代器就支持范围for
//for (auto e : dict)
for (const auto& e : dict)
{
cout << e.first << ":" << e.second << endl;
}
for (const std::pair<const std::string, std::string>& e : dict)//与上面的auto是一样的
{
cout << e.first << ":" << e.second << endl;
}
cout << endl;
范围for循环
这个我们在之前就已经有所了解了,范围for实际上遍历方式仍为迭代器遍历,我们出国STL容器使用范围for,数组也是可以的,原生指针可以认为是天然迭代器,vector与string就是天然指针
final与override
final修饰类,这个类就变成了最终类,不能被继承
final还可以修饰虚函数,这个虚函数就不能被重写了
override是子类重写虚函数,检查是否完成重写用的,不满足重写的条件,则会报错
智能指针
我们在后面的章节会对其进行详细了解
新增容器
对于容器而言
C++98容器:string/vector/list/deque/map/set/bitset+stack/queue/priority_queue
C++11新容器:
array(定长数组):实际用的很少,因为其长度固定,存储空间在栈上,空间本来就不大
forword_list(单链表):实际中用的也很少,不支持反向遍历,也不支持尾插尾删,插入只能在一个结点的后面插入
unordered_map/unordered_set:推荐使用,效率高于map与set,底层是哈希表
默认成员函数控制
我们可以看到,在这段代码中,我们对A类书写了拷贝构造,并没有写构造函数,但是这里我们的构造函数却是无法使用的,原因是拷贝构造也是构造的一种,这在设计上是有一定缺陷的
此时我们加上一句default,指定其需要显示的去生成,系统便自动地调用默认构造函数了
右值引用
右值引用概念
C++98 中提出了引用的概念,引用即别名,引用变量与其引用实体公共同一块内存空间,而引用的底层是通过指针来实现的,因此使用引用,可以提高程序的可读性。不过这里提出的主要是是左值引用,而在我们C++中主要提出的是右值引用
//不管是左值引用还是右值引用都是给对象取别名,左值引用主要是给左值取别名,右值引用则是主要给右值取别名
//左值就是左边的值?右值就是右边的?这个其实是不一定的,与左移右移一样,这里的左右不是方向,左边的不一定是左值,右边的值也不一定是右值
//int x1 = 10;int x2 = x1;这里x1是左值,10是右值,x2是左值
//我们可以认为,可以修改的值就是左值,左值通常是变量
//右值通常是常量,表达式或者函数返回值(临时对象)
int main()
{
int x = 1, y = 2;
//左值引用
int a = 0;
int& b = a;
//左值引用不能引用右值,const左值引用可以
/*int& e = 10;
int& f = x + y;*/
const int& e = 10;
const int& f = x + y;
//右值引用
int&& c = 10;
int&& d = x + y;
//右值引用也不能引用左值,但是可以引用move后的左值
//int&& c = a;
int&& m = move(a);
system("pause");
return 0;
}
我们用最简单的概念来说明下左值引用与右值引用
左右值的一些使用
我们可以看到,系统会自动进行匹配传入的值,传入的是右值,则会匹配右值函数,传入的是左值,则会匹配左值函数
C++11又将右值区分为:纯右值和将亡值
纯右值:基本类型的常量或者临时对象
将亡值:自定义类型的临时对象
因此关于左值与右值的区分不是很好区分,一般认为:1. 普通类型的变量,因为有名字,可以取地址,都认为是左值。2. const 修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址 ( 如果只是const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间 ) ,C++11认为其是左值。3. 如果表达式的运行结果是一个临时变量或者对象,认为是右值。4. 如果表达式运行结果或单个变量是一个引用则认为是左值。总结:1. 不能简单地通过能否放在 = 左侧右侧或者取地址来判断左值或者右值,要根据表达式结果或变量的性质判断,比如上述:c 常量2. 能得到引用的表达式一定能够作为引用,否则就用常引用。C++11 对右值进行了严格的区分:C语言中的纯右值,比如: a+b, 100将亡值。比如:表达式的中间结果、函数按照值的方式进行返回。
值的形式返回对象的缺陷
我们知道,在拷贝构造s3(s1)与=赋值拷贝中,编译器会对s1进行深拷贝,会开辟一块大小相同的空间赋进去,但是这里我们如果使用的是自定义类型,要去拷贝构造一个自定义类型,但是自定义类型在调用过后又会被销毁,此时如果再去拷贝构造,重新开辟空间深拷贝,会有些浪费,所以我们可以利用右值引用的移动构造,使其仅改变指针指向,将构造目标的指针指向将亡值,便完成了拷贝,省去了再次深拷贝的步骤,提高了效率
// 2、应用:右值引用的移动构造和移动赋值,可以减少拷贝
class String
{
public:
String(const char* str = "")//构造函数
{
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
// s2(s1)
String(const String& s)//拷贝构造,深拷贝//接收左值
{
cout<<"String(const String& s)-拷贝构造-效率低"<<endl;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
// s3(右值-将亡值)
String(String&& s)//接收右值,自定义类型
:_str(nullptr)//直接将s3指针初始化
{
// 传过来的是一个将亡值,反正你都要亡了,我的目的是跟你有一样大的空间,一样的值
// 不如把你的空间和值给我
cout << "String(String&& s)-移动构造-效率高" << endl;
swap(_str, s._str);//交换空指针与传入的指针,其实也就是将将亡值的指针直接换给s3,然后将亡值指针换成了nullptr,这里就避免了再次对将亡值进行深拷贝再传给s3,提高了效率
}
// s3 = s4
String& operator=(const String& s)//传统赋值拷贝,深拷贝
{
cout << "String& operator=(const String& s)-拷贝赋值-效率低" << endl;
if (this != &s)
{
char* newstr = new char[strlen(s._str) + 1];
strcpy(newstr, s._str);
delete[] _str;
_str = newstr;
}
return *this;
}
// s3 = 右值-将亡值
String& operator=(String&& s)//换为右值引用的深拷贝
{
cout << "String& operator=(String&& s)-移动赋值-效率高" << endl;
swap(_str, s._str);//直接将将亡值的指针换给this指针,将亡值出作用域自动析构
return *this;//返回被交换后指向将亡值的s3指针
}
~String()//析构函数
{
delete[] _str;
}
// s1 + s2//s1,s2不会改变,无法返回引用
String operator+(const String& s2)
{
String ret(*this);
//ret.append(s2);
return ret; // 返回的是右值
}
// s1 += s2//可以用引用返回
String& operator+=(const String& s2)//这里可以用引用返回其实是因为+=得出的结果还会赋回s1,改变了s1的值,这里返回的是s1
{
//this->append(s2);
return *this; // 返回是左值
}
private:
char* _str;
};
String f(const char* str)//传str指针的值,在调用完毕会销毁
{
String tmp(str);//拷贝构造str到tmp
return tmp; // 这里返回实际是拷贝tmp的临时对象
}
这里我们在调用f函数时,f函数内部也会进行拷贝构造
实际上通俗地去讲,我们添加右值引用去做移动拷贝,其实就是因为传统拷贝构造需要深拷贝,开空间,赋给对象,而现在我们用了右值引用,就不需要创建对象,直接可以将被拷贝对象的资源利用右值引用的指针交换给到对象,从而避免深拷贝,可以提高效率
// 3、应用:当传值返回值,返回是右值,结合前面学的移动构造和移动赋值,可以减少拷贝
class Solution1 {
public:
vector<string> letterCombinations(string digits) {
vector<string> v;
return v;
}
};
class Solution2 {
public:
// 核心思想:找出杨辉三角的规律,发现每一行头尾都是1,中间第[j]个数等于上一行[j-1]+[j]
vector<vector<int>> generate(int numRows) {
vector<vector<int>> vv;
// 先开辟杨辉三角的空间
vv.resize(numRows);
// ...
return vv;
}
};
int x1()
{
String s1("s1");
String s2("s2");
String s3 = s1 += s2; // 返回左值,拷贝构造
String s4 = s1 + s2; // 返回右值,移动构造
//现实中不可避免存在传值返回的场景,传值返回的拷贝返回对象的临时对象。
//如果vector只实现参数为const左值引用深拷贝,那么下面的代价就很大
//vector(const vector<T>& v)->深拷贝
//但是如果vector实现了参数右值引用的移动拷贝,那么这里效率就会很高
//vector(vector<T>&& v) ->移动拷贝
//结论:右值引用本身没太多意义,右值引用的实现了移动构造和移动赋值
//那么面对接收函数传值返回对象(右值)等等场景,可以提高效率
vector<string> v = Solution1().letterCombinations("abcd");
vector<vector<int>> vv = Solution2().generate(5);//如果进行深拷贝,代价过大
return 0;
}
其实我们的右值引用最大的作用就是完成了移动拷贝,帮助去除了拷贝构造途中将亡值还进行深拷贝而引起的冗余操作,直接将将亡值的资源给到了this指针,完成了移动拷贝,提高了效率
右值引用引用左值
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过 move 函数将左值转化为右 值 。 C++11 中, std::move() 函数 位于 头文件中,该函数名字具有迷惑性,它 并不搬移任何东西,唯一的功 能就是将一个左值强制转化为右值引用,然后实现移动语义
int main()
{
String s1("左值");
String s2(s1); // 参数是左值
String s3(f("右值-将亡值")); // 参数是右值-将亡值(传递给你用,用完我就析构了)
//String s4(move(s1));//此时用move是不合适的,因为给s1加上move之后就会使得原来s1中的资源被清空了,如果后面还需要调用s1就无法实现了
String s5("左值");
s5 = s1;
s5 = f("右值-将亡值");
return 0;
}
同样的,在我们很多容器中插入数据也基本都是两个重载实现的函数,一个左值引用,一个右值引用
我们来看,同样的插入一个数据,左值引用插入与右值引用插入会有什么区别呢?
其实我们就是,左值引用拷贝过去会进行深拷贝,开空间,调用拷贝构造,而右值引用调用的则是移动构造,直接将资源移动到后面的空间中,无需开空间
所以在我们的容器中会有emplace_back模板这样体现可变参数的方法
vector<pair<string, string>> vp;
vp.push_back(make_pair("右值", "右值"));
pair<string, string> kv("左值", "左值");
vp.push_back(kv);
vp.emplace_back(make_pair("右值", "右值"));
vp.emplace_back(kv);
vp.emplace_back("右值", "右值"); // 体现emplace_back模板可变参数特点的地方
对于push_back与emplace_back而言都是左值引用深拷贝,低效率,右值引用浅拷贝,高效率
总结
右值引用做参数和作返回值减少拷贝的本质是利用了移动构造和移动赋值
左值引用和右值引用本质的作用都是减少拷贝,右值引用本质可以认为是弥补左值引用不足的地方, 他们两相辅相成左值引用:解决的是传参过程中和返回值过程中的拷贝
做参数:void push(T x) -> void push(const T& x) 解决的是传参过程中减少拷贝
做返回值:T f2() -> T& f2() 解决的返回值过程中的拷贝
ps:但是要注意这里有限制,如果返回对象出了作用域不在了就不能用传引用, 这个左值引用无法解决,等待C++11右值引用解决右值引用:解决的是传参后,push/insert函数内部将对象移动到容器空间上的问题. + 传值返回接收返回值的拷贝
做参数: void push(T&& x) 解决的push内部不再使用拷贝构造x到容器空间上,而是移动构造过去
做返回值:T f2(); 解决的外面调用接收f2()返回对象的拷贝,T ret = f2(),这里就是右值引用的移动构造,减少了拷贝
完美转发
完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数
void Fun(int &x){ cout << "lvalue ref" << endl; }
void Fun(const int &x){ cout << "const lvalue ref" << endl; }
void Fun(int &&x){ cout << "rvalue ref" << endl; }
void Fun(const int&& x){ cout << "const rvalue ref" << endl; }
template<typename T>
void PerfectForward(T &&t)
{
// 右值引用会第二次之后的参数传递过程中右值属性丢失,下一层调用会全部识别为左值
// 完美转发解决
Fun(std::forward<T>(t)); //加上forward,即可标记为右值
}
int x3()
{
PerfectForward(10); // rvalue ref
int a;
PerfectForward(a); // lvalue ref
PerfectForward(std::move(a)); // rvalue ref
const int b = 8;
PerfectForward(b); // const lvalue ref
PerfectForward(std::move(b)); // const rvalue ref
}
lambda表达式
在我们了解lambda表达式之前,我们先来看这样一段代码
#include <algorithm>
#include <functional>
template<class T>
struct Greater
{
bool operator()(const T& x1, const T& x2)//重载括号
{
return x1 > x2;
}
};
bool g2(const int& x1, const int& x2)//g2函数
{
return x1 > x2;
}
int x4()
{
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>());
Greater<int> g1;
g1(1, 2); // g1是一个对象,这里调用的是他的operator()实现的比较
g2(1, 2); // g2是一个函数指针,这里是调用他指向的函数
// 他们是完全不同的对象但是他们用起来是一样的。
std::sort(array, array + sizeof(array) / sizeof(array[0]), g1);
std::sort(array, array + sizeof(array) / sizeof(array[0]), g2);
return 0;
}
struct Goods
{
string _name; // 名字
double _price; // 价格
int _num; // 数量
};
// 那么这里如果去重载Goods的operator>/operator<是不好的,因为你不知道需要按哪一项成员去比较
struct ComparePriceGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price > gr._price;
}
};
struct CompareNumGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._num > gr._num;
}
};
struct CompareNameGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._name > gr._name;
}
};
// 其实还有小于的,大于等于和小于等于,会发现我们要写很多个仿函数
// 其实直接写函数也可以,不过类似要写很多个函数
int main()
{
Goods gds[] = { { "苹果", 2.1 , 3}, { "相交", 3.0, 5}, { "橙子", 2.2, 9}, { "菠萝", 1.5, 10} };
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), ComparePriceGreater());
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), CompareNumGreater());
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), CompareNameGreater());
}
我们可以发现,我们如果需要去比较一个物品的不同属性,就需要分别去构建不同的重载函数,这其实是很复杂的,所以我们引入了lambda表达式
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 x6()
{
// 最简单的lambda表达式, 该lambda表达式没有任何意义
// 没有参数,没有返回值,就可以不写他们
[]{};
// 定义在函数中的匿名函数
int a = 3, b = 4;
// 实现a+b的lamber表达式
// 不捕捉
auto add1 = [](int x1, int x2)->int{return x1 + x2; };//直接将其当正常函数使用,并调用
add1(a, b);
// 直接捕捉a,b
//auto add2 = [a, b]()->int{return a+b; };
auto add2 = [=]()->int{return a+b; };//全部捕捉a,b
add2();
// 不捕捉
auto swap1 = [](int& x1, int& x2){ //当正常函数使用
int x = x1;
x1 = x2;
x2 = x;
};
swap1(a, b);
// 引用捕捉
/*auto swap2 = [&a, &b](){
int x = a;
a = b;
b = x;
};*/
auto swap2 = [&](){
int x = a;
a = b;
b = x;
};
swap2();
return 0;
}
通过上述例子可以看出, lambda 表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto 将其赋值给一个变量
这便是lambda表达式的基础用法,现在我们来看看它是如何解决我们上面出现的切换比较参数的问题的
Goods gds[] = { { "苹果", 2.1 , 3}, { "相交", 3.0, 5}, { "橙子", 2.2, 9}, { "菠萝", 1.5, 10} };
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), ComparePriceGreater());
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), CompareNumGreater());
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), CompareNameGreater());
// lamber
/*auto price_greater = [](const Goods& g1, const Goods& g2){//可以这么使用,但是不推荐
return g1._price > g2._price;
};
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), price_greater);*/
// 我们会发现使用lamber表达式在这些地方更方便一些,推荐这种lambda表达式直接做参数的方式
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2){return g1._price > g2._price;});
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2){return g1._price < g2._price; });
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2){return g1._name > g2._name; });
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2){return g1._name < g2._name; });
我们就可以发现,使用lambda表达式清晰了很多,并且方便了很多
捕获列表说明
捕捉列表描述了上下文中那些数据可以被 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 表达式之间不能相互赋值 ,即使看起来类型相同
lamber表达式写的格式
int add1(int a, int b)
{
return a + b;
}
int x7()
{
// [捕捉列表](参数)->返回值类型{函数体}
int a = 0, b = 1;
// 实现一个a+b的lamber表达式
auto add1 = [](int x1, int x2)->int{return x1 + x2; };
cout << add1(a, b) << endl;
// 捕捉列表就是捕捉跟我一个作用域的对象
// 传值捕捉 [a]捕捉a [a,b]捕捉a,b [=]捕捉同一作用域中的所有对象
// 传引用捕捉 [&a]捕捉a [&a,&b]捕捉a,b [&]捕捉同一作用域中的所有对象
// 传值补充的对象是不能被改变的。(加上mutable就可以改变了)
auto add2 = [a, b]()->int{return a + b; };
add2();
// 实现a和b交换
//auto swap = [](int& a, int& b){int c = a; a = b; b = c; };
auto swap = [](int& x, int& y){
int z = x;
x = y;
y = z;
};
swap(a, b);
/* 不正确的用法
auto swapab = [a, b]()mutable {//实际上就是传值传参
int c = a;
a = b;
b = c;
};
swapab();*/
auto swapab = [&a, &b](){//仅针对ab的交换函数
int c = a;
a = b;
b = c;
};
swapab();
return 0;
}
我们可以看到,其实lamber表达式的用法,主要注意捕捉列表与参数的使用,捕捉列表相当于传入实参,参数就是形参
lamber表达式的使用场景 (对比仿函数对象、函数指针)
int x8()
{
Goods gds[] = { { "苹果", 2.1, 3 }, { "相交", 3.0, 5 }, { "橙子", 2.2, 9 }, { "菠萝", 1.5, 10 } };
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), ComparePriceGreater());
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), CompareNumGreater());
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), CompareNameGreater());
// lamber优势就会让代码可读性更强
/*auto price_greater = [](const Goods& g1, const Goods& g2)->bool{return g1._price > g2._price; };
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), price_greater);
*/
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2)->bool{return g1._price > g2._price; });
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2)->bool{return g1._price < g2._price; });
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2)->bool{return g1._num > g2._num; });
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2)->bool{return g1._num < g2._num; });
return 0;
}
其实我们的lamber表达式就是去解决这种需要分类,分情况解决的问题的,作用和仿函数对象与函数指针相同,给lamber表达式就将需要比较的参数给到了我们手里,去方便操作
函数对象与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表达式的底层
int x9()
{
int a = 1, b = 2;
// 对象 = 对象(替换编译器生成的lamber_uuid仿函数的对象)
auto add = [](int x, int y)->int{return x + y; };
add(a, b); // call lamber_uuid仿函数的operator()
// 底层还是依靠仿函数来实现,也就是说你定义了一个lamber表达式,
// 实际上编译器会全局域生成一个叫lamber_uuid类,仿函数的operator()的参数和实现
// 就是我们写的labmber表达式的参数和实现
/*
00A5C8AC mov eax, dword ptr[b]
00A5C8AF push eax
00A5C8B0 mov ecx, dword ptr[a]
00A5C8B3 push ecx
00A5C8B4 lea ecx, [add]
00A5C8B7 call <lambda_afc2b2a8543babab622761003a6aa683>::operator() (0A5AEC0h)
*/
auto swapab = [&a, &b](){
int c = a;
a = b;
b = c;
};
swapab();
/*
0065DA4C lea ecx, [swapab]
0065DA4F call <lambda_574e874b35e37ce2b7269242f59eb074>::operator() (065ADC0h)
*/
return 0;
}
实际在底层编译器对于 lambda 表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda 表达式,编译器会自动生成一个类,在该类中重载了 operator() 。
通俗来讲,其实lambda表达式与范围for和迭代器的关系是有些像的,lambda表达式其实底层还是转为了仿函数的形式,只是拥有了一个lambda表达式使得操作起来更加方便了而已
线程库
thread类的简单介绍
在 C++11 之前,涉及到多线程问题,都是和平台相关的,比如 windows 和 linux 下各有自己的接口,这使得 代码的可移植性比较差 。 C++11 中最重要的特性就是对线程进行支持了,使得 C++ 在并行编程时不需要依赖 第三方库 ,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含 < thread > 头文件。
C++在之前想写多线程的程序,即可以在Windows下跑,也可以在Linux下跑,只能使用条件编译
#ifdef _WIN32
CreateThread(...)
#else
pthread_create(...)
#endif
而我们C++11来给我们引入了线程库
特点:跨平台、面向对象封装的类(每个线程是一个类对象)
实现原理:封装库时使用了条件编译,也就是说他的底层还是分别调用了不用平台的线程API扩展:C++缺点之一:就是更新有用的东西太慢了,比如线程库C++11(2011)才更新的,而且到现在也没有更新一个官方的封装好的靠谱网络库。其次一些不痛不痒的语法更新了一堆,增加学习成本。
我们来看这样一段代码
这是一个经典的多线程,我们定义了Add函数,可以对x进行++,我们将其并行执行,在我们不断增加数据量并执行时,发现百万以后加出来的结果并不对,这是因为当线程并行去执行时,对于++这个操作而言,需要先将x放入寄存器中,进行++后,再放回内存,三局语句并非原子,执行两个语句是在不同的cpu,在并行执行的过程中,当数据量足够多时,难免会出现两个cpu在一瞬间同时去拿到数进行++,此时拿到的数会相同,进行++再放回去也会相同,此时便出现了漏掉一个++的情况,这就是线程安全问题,但是++的总数不会大于所有执行次数,也就是两百万,因为只会漏加,不会多加
那么我们该如何解决这种问题呢?
第一种方式就是对其进行加锁,加上一个互斥锁
函数指针配和thread使用
此时无论我们运行多少次,其结果也都还是二百万
其实还有一种加锁方式
我们将锁加到内部,但是这种并行加锁并不好,速度跟串行加锁差的很多,虽然锁得粒度小,但是不断加锁,效率全都耗费在了不断切换的过程中了
还有一种方式叫做原子性操作,也就是我们C++11新引入的
这种方式是支持无锁编程的
仿函数配合thread使用
这种方式也是可以的,这里我们定义匿名对象防止其他人修改
还可以使用我们的lambda表达式
还有一种更加常用的方法
我们可以对4个线程同时进行分析
使用两个线程,一个线程打印奇数,一个线程打印偶数
我们可以看到,他们进行打印时,中间会有意外的发生,出现了竞争终端
未完待续。。