本期我们来学习C++11
目录
C++11简介
在 2003 年 C++ 标准委员会曾经提交了一份技术勘误表 ( 简称 TC1) ,使得 C++03 这个名字已经取代了C++98称为 C++11 之前的最新 C++ 标准名称。不过由于 C++03(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++11新增的语法特性非常多,我们只讲解部分常用的小故事:1998 年是 C++ 标准委员会成立的第一年,本来计划以后每 5 年视实际需要更新一次标准, C++ 国际标准委员会在研究C++ 03 的下一个版本的时候,一开始计划是 2007 年发布,所以最初这个标准叫C++ 07。但是到 06 年的时候,官方觉得 2007 年肯定完不成 C++ 07 ,而且官方觉得 2008 年可能也完不成。最后干脆叫C++ 0x 。 x 的意思是不知道到底能在 07 还是 08 还是 09 年完成。结果 2010 年的时候也没完成,最后在2011 年终于完成了 C++ 标准。所以最终定名为 C++11 。
统一的列表初始化
{}初始化
struct Point
{
Point(int x,int y)
:_x(x)
,_y(y)
{
cout << "Point(int x,int y)" << endl;
}
int _x;
int _y;
};
int main()
{
int array1[] = { 1, 2, 3, 4, 5 };
int array2[5] = { 0 };
Point p = { 1, 2 };
return 0;
}
std::initializer_list
了解了上面的特性,我们来看一个问题
这两个是不是一样的?
答案是不一样
vector后面是可以不断加值的,而point不行
point是多参数构造支持隐式类型转换,而vector是直接的构造
这里可以直接调用构造的原因是C++新增了一个类型,叫做initializer_list
我们来看看什么是initializer_list
也就是说,只要是一个{ } 括起来的列表,就可以识别成initializer_list,而{ }里有几个值,它不关心
它还有size,begin,end等等,大家猜一猜它是怎么实现的
{10,20,30} 是一个常量数组,存在常量区里
我们这里sizeof一下,是8,如果是64位下就是16
它的就是使用了两个指针,一个指向数组的起始位置,一个指向结尾的下一个位置,可以帮助我们读取数组,注意,只可以读,不可以写
也就是说,给了我们一个常量数组,我们去调用这个函数的构造,然后让指针指向位置
这里的ptr1是不支持的,因为会有冲突
我们可以直接写,这里就是直接调用initializer_list的构造函数
我们再回到原理的问题,vector是如何支持的?
是因为它直接写了一个支持initializer_list的构造函数
这也是为什么之前point的{ } 里不可以增加值,而vector可以,point的构造函数参数需要一一对应,而initializer_list可没说{ } 里要写多少个值
这里的实现也非常简单,使用reserve,然后用范围for即可,我们可以把我们之前的代码拿出来,加上这个,也可以支持initializer_list
这是我们自己的vector
vector(initializer_list<T> lt)
{
reserve(lt.size());
for (auto e : lt)
{
push_back(e);
}
}
我们加一个构造即可
此时就成功了
map不可以这样写
因为map这里的T是一个pair
要这样写才行 ,这里是一个双重含义,外边的括号是initializer_list,里面的是类似之前point的
另外不止是构造,有时候赋值也是支持的
声明
auto
在 C++98 中 auto 是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto 就没什么价值了。 C++11 中废弃 auto 原来的用法,将 其用于实现自动类型腿断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初 始化值的类型。
我们之前也经常使用auto,这里就不在讲解
decltype
关键字decltype将变量的类型声明为表达式指定的类型
假设我们这里想定义一个和pf相同类型的变量,我们该怎么办呢?typeid只能看却不能用,我们的一种办法是使用auto,pf1 = pf,但是这里是初始化了,如果我们不想初始化呢?
我们就可以使用decltype
我们来看一个使用场景
我们这里创建bb时,就可以使用decltype,就方便了很多
typeid推出的类型是一个字符串,只能看不能用,decltype可以把对象的实际类型推出,这个类型可以用来再定义变量,或者作为模板的实参
他还可以推导表达式,在一些特殊的情况下我们是会用到的
nullptr
由于 C++ 中 NULL 被定义成字面量 0 ,这样就可能回带来一些问题,因为 0 既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11 中新增了 nullptr ,用于表示空指针
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
范围for循环
范围for也是我们经常使用的,这里也就不再介绍
STL中一些变化
首先就是新容器,其中unordered我们之前已经进行了详细讲解,这里就不再多说
array就是静态数组,类型上不一样,物理上一样
对于普通的静态数组,越界是可能检查不出来的
而array是可以的,a1[15]会转换为指针的解引用,而a2[15]是operator[ ]函数调用,是内部检查
array非常的鸡肋,他的初衷是想替代静态数组
但是我们为什么不直接用vector呢?还可以初始化(C++11里还增加了不少类似array一样鸡肋的东西,所以一直在被骂)
forward也是非常鸡肋,是一个单链表
他只支持头插头删,不支持尾插尾删,他的insert是在当前位置之后插入
他的唯一优点可能就是每一个节点少了一个指针。。。
新容器看完了我们来看新接口
他加了一堆cbegin,cend等等,这是const对象的,这也是被吐槽的一个地方
还增加了支持initializer_list,所有容器均支持{ }列表初始化的构造函数,这个还是很不错的
还有一个重大更新,这个算一个黑科技
所有容器均新增了emplace系列,这里涉及右值引用和模板可变参数,我们下面会讲
可以使性能提升,有效地方是相当大的
push_back这些也改了,增加了一个重载的右值引用版本
所有的容器都增加了移动构造和移动赋值,这可以使我们不用再担心深拷贝,使深拷贝的性能提高了百分之90
这些一切的矛头都指向了右值引用和移动语义,下面我们就来看看他们到底是什么吧
右值引用和移动语义
传统的 C++ 语法中就有引用的语法,而 C++11 中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名 。什么是左值?什么是左值引用?左值是一个表示数据的表达式 ( 如变量名或解引用的指针 ) , 我们可以获取它的地址,一般 可以对它赋 值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边 。定义时 const 修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。什么是右值?什么是右值引用?右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值 ( 这个不能是左值引用返回) 等等, 右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能 取地址 。右值引用就是对右值的引用,给右值取别名。
左值和右值的区别就是是否可以取地址,左值可以取地址,右值不行
看这里的报错我们也可以看出来
我们看这个常量字符串,也是左值,只是不能被修改而已
引用是取别名,左值引用就是给左指取别名,右值引用就是给右值取别名
右值引用是使用两个&&
这里的r6如果用一个&就编译不过了
那么左值引用能否可以给右值取别名,右值引用能否给左指取别名呢?
这里是不行的
但我们加上const就可以了,没有左值引用之前有些地方也得引用右值
我们在之前的函数在不修改的时候都会告诉大家尽量加上const,加上const的好处就是既可以引用左值也可以引用右值
这里的r3同样不行
我们再加上const就可以了
所以const左值引用是可以给右值取别名
直接引用也是不行的
但我们move一下就可以了,这个move可能会带来其他影响(这里没有)
所以右值引用可以引用move以后的左值
左值引用的使用场景和价值是什么?
使用场景:1.做参数,2.做返回值,价值是减少拷贝
左值引用在哪些场景下解决问题不到位呢?
局部对象的返回不能用左值引用,这是C++没有处理好的场景
比如这里
即使加上const也无法解决问题
这里的关键在于str的生命周期结束了,传值返回是返回拷贝,拷贝就有代价
这里用左值引用也可以,不需要const的,因为str本身就是左值,但是str本身已经销毁了,引用那块空间又如何?
这是第一个问题,栈帧销毁后,还有一些其他问题,str指向的空间也是被销毁的(调用了析构函数)
所以我们以前解决这种问题时是传值返回
str在返回时会先拷贝临时对象,然后再把临时对象拷贝给ret,但是这里连续拷贝两次,如果str很大的话,比如100w字节,那么代价就太大了,所以编译器在这里进行了优化,只拷贝了一次
但即使编译器优化了,代价还是太大了,而且有些场景甚至无法优化
比如这里
下面我们利用一份简洁的string来验证一下
namespace bai
{
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)
{
assert(pos < _size);
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
};
}
我们看结果 ,这里第一个深拷贝是ret1的,其他3个是ret2的,不过ret2这里其实只调用了两次深拷贝,这里的赋值,他的拷贝是利用拷贝构造完成的,我们用的是现代写法
我们接着往下看
首先,这里的两个func是构成函数重载的,左值会匹配上面的,右值会匹配下面的
那此时呢?上面的加了const,调用是否存在歧义?现在都能接收右值
首先是构成重载的,这里编译并不会报错
并且没有调用歧义,编译器会自动寻找最合适的
我们把内置类型的右值叫做纯右值,自定义类型的右值叫做将亡值
我们看自定义类型,比如 s1+s2,这里就是运算符重载,我们再看to_string,这里就是函数调用
我们看s1+s2,是一个表达式,但本质还是函数调用(调用operator+),他的返回值是一个临时对象,就是一个右值,是一个将亡值
我们看这里,str是会拷贝临时对象,然后临时对象再拷贝给ret2,func()这个表达式的返回值就是一个右值,一个将亡值,为什么叫将亡值呢?因为他的生命周期只在这一行,下一行就会析构
ret2 = func(),这里是一个赋值,如果右边的是左值,这里只能老老实实去拷贝,是深拷贝,如果是右值将亡值,这里就可以用移动拷贝,就可以直接把资源拿过来,还会把自己不要的资源扔掉
我们运行一下
这里代价就小了很多 ,拷贝构造就变成了移动构造,移动构造是直接转移资源,代价要小很多
右值引用可以在某些场景下极大的提高效率
我们再来看一个场景
我们先看这段代码
对于拷贝构造,也有移动拷贝
我们知道,这里编译的优化指的是合二为一,连续的构造/拷贝构造都会合二为一
那么这里合二为一后是拷贝构造还是移动构造呢?按我们的理解应该是拷贝构造,因为str是一个左值,但是这样的场景太多了,str符合将亡值的特征,出了作用域就没了,所以编译器会把str识别成右值-将亡值
所以这里我们的运行结果是移动拷贝,这样就降低了代价,提高了效率,如果这里返回的是map,那么效率的提高是非常客观的
这里是一样的,把str识别成了将亡值
直接转移资源,效率得到了提升
大家要记住,左值引用的核心是减少拷贝,提高效率,右值引用的核心价值是进一步减少拷贝,弥补左值引用没有解决的场景,如传值返回
这句话里,右值引用的重点是弥补左值引用没有解决的场景,场景1:自定义类型中深拷贝的类,必须传值返回的场景
浅拷贝的类,移动构造不需要实现,传值返回拷贝代价不是很大,也就是说,右值引用是专门用来解决自定义类型中深拷贝的类
另外大家要注意move
我们看调试的监视窗口,copy1和2没什么问题,包括move(ret2)后再构造copy2,但是我们看copy3,这里就是直接把ret2的资源给copy3了,这里说明返回的ret2的右值,move的底层实现是有些复杂的,所以大家把一个值move后再拷贝构造,就赋予了可以抢走资源的权力,这里大家要注意一下,move的意义就是我们想把一个值的资源交给另一个,就可以使用move,move不会改变属性,而move返回的值的属性会被改变
我们再看下一个场景
我们之前会认为这两段代码没什么区别,但是我们看看结果
首先这里是list
我们尾插了一个节点,里面存的是string
这是以前的push_back(c++98),s1传给了val,val是s1的别名,s2传给val,val是s2的别名
如果是库里面,要进行如下操作,Node* newnode = new Node(val),这里会调用Node的构造函数
所以这里的s1构造时其实是有点麻烦的
一直到构造函数这里,才会进行拷贝构造
这里要开一个和s1一样大的空间
但如果这里是一个右值呢?C++11之后我们可以使用右值引用
我们看下面的函数
这里就会经历和s1一样的情况,一直到构造函数,不过到了构造函数时,这里会将资源直接转移过去,不过我们日常的push_back都不会这样写
而是这样写的,这样写也是移动构造,因为这里传参时不能直接传给val,而是会先构造临时对象,是一个右值,资源就会直接转移过去,也就是说,这样写比以前的方式要少拷贝一次
如果我们把移动构造的代码屏蔽掉,这里的结果就是三个深拷贝,也就是说三个push全是构造+拷贝构造,这里为什么右值也会出现深拷贝呢?
因为我们是const引用
那么右值引用出现的意义是什么呢?我们就可以区分左值和右值,如果是左值,我们就深拷贝,是右值的话我们就转移资源
这里总结一下,在容器的插入接口,如果插入对象是右值,可以利用移动构造转移资源给数据结构中的对象,也可以减少拷贝,所以,所有的容器在C++11时都增加了右值引用版本
完美转发
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(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;
}
我们看上面的代码,PerfectForward函数是一个模板函数,使用了右值引用,我们下面传入了一个左值a,是否可以呢?
按照我们前面学习的知识是不行的
但是这里是可以的,模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
如果这里是一个具体的类型,那他是写死的,就是一个右值引用,但如果是模板,那就是万能引用,如果实参是左值,就是左值引用(也叫做引用折叠)
如果是一个左值,这里推模板时
相当于int&&折叠成int&
实参是右值,就是一个右值引用,这里就没有什么折叠了
我们再看看运行的结果,非常奇怪,怎么都是左值引用呢?难道都折叠了吗?
我们修改成这样的代码,这里的结果呢?
这里大家要知道一个知识
r 和 rr 都是左值,地址是一样的
我们知道右值不能取地址,右值也不能修改
但右值引用是需要支持修改的
这里str被识别为右值,需要把str的资源转移给ret,这里是移动构造
这里是需要修改s的,如果s的属性是右值,那就出问题了
我们这样理解,你是右值,不能修改,我是你的引用,会开一块空间,把你储存起来
右值引用变量的属性会被编译器识别成左值,否则是移动构造的场景下,无法完成资源转移,必须要修改
此时我们回过头来看,虽然这里是右值引用,但t的属性是左值,所以才会有那样的结果
如果我们就是想让t保持原有的属性呢?这里就要用到完美转发
完美转发是库里面的一个函数,如果t是左值引用,保持左值属性,是右值引用就保持右值属性
下面我们来修改一下我们以前的链表
我们先把上面拷贝构造这里的代码屏蔽一点
我们看这里的运行结果,都是构造+深拷贝,但是我们的string是写了移动构造的
这里换成std的就可以调用移动构造
另外我们仔细对比,会发现我们自己的s1这里调用了两次深拷贝?其实不是,这里是list的哨兵位节点,我们使用的是new
就是这样导致的,new会先调用构造,库里面使用的内存池,哨兵位的只开空间,而不调用new,所以没有
按照我们以前写的,不管我们传入左值还是右值,最后都会匹配到左值引用上
所以我们需要加一些东西
首先我们先加一个来区分左值和右值,下一步他会调用insert,所以我们要给insert也加一个
我们还要继续加
在最开始的位置也要加一个区分,此时运行会报错
我们需要先修改成这样
还要在empty这里加一个匿名对象
此时我们再看结果,怎么还是这样呢?
这里大家调试一下就会发现,和之前的原因一样,右值引用的属性会被编译器识别为左值
所以我们需要用到完美转发
这里也需要修改,只要我们想传到下一层,并且保持属性不变,就需要完美转发
此时运行还是有问题,我们接着调试
最后发现这里也是需要修改的,遇到问题我们就调试一下
此时我们再看结果,就解决了
下面我们讨论一些别的问题
这里的构造我们可以删掉一个吗?
答案是不行的,显示这里有问题
这里可能会有疑问,这不是模板吗?
但注意,这是类的模板,万能引用有个前提,这个T并不是实例化,而是推出来的
而这里的T并不是推出来的,是在list_node那里就实例化了
那如果我们就是想要删掉这个,有什么办法吗?
那就要写出这样
lambda表达式
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,
3 }, { "菠萝", 1.5, 4 } };
sort(v.begin(), v.end());
}
我们有一个商品,需要对他进行排序,这里使用默认的sort是会报错的
我们重载一个operator小于或者大于可以解决吗?可以
那如果我们第一次排序需要对价格进行排序,第二次排序需要对评价进行排序,那又该怎么办呢?
这里就不可以了,因为operator我们只能重载一个,这里只有仿函数才能解决问题
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 CompareEvaluateGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._evaluate > gr._evaluate;
}
};
命名也是一个问题,我们的命名是比较规范的,外一有人写了个compare1,compare2等等,那怎么办呢?下面我们就来看看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 函数不能做任何事情
写出来就是这样的,我们目前不用管捕捉列表,那么怎么使用呢?
这里使用auto就非常舒服
我们的排序就可以这样写了
另外这个返回值是可以省略的
我们可以这样使用,不用auto推出了再传进去
此时就可以有各种排序,这样看是不是就比仿函数好用呢?
lambda是一个可调用对象,在C++里,还有函数指针,仿函数也都是可调用对象
函数指针建议能不用就不用,写起来难受,看起来也难受
仿函数就是一个类,重载operator(),对象可以像函数一样使用,一般在模板使用,但是有些地方使用不合适,所以就有了lambda
lambda是一个匿名函数对象,写出来后一般传给auto,或者模板使用,一般在函数内部直接定义使用
大家要记住lambda的语法,其中->int 因为会自动推导,所以很多时候就忽略了
里面的函数体也可以写多行,比如这里我们写了一个swap,大家可以调试看看他是怎么走的
我们在函数体里调用其他函数是什么结果?我们来看一看
这里报错了,下面我们调用一个全局的看看
这里是可以调用的,那我们想要调用局部的该怎么办呢?
我们先看看捕捉列表的使用,这里就是捕捉了rate,可以直接使用,并且我们的add2没写返回值类型,他可以自动推出来
捕获列表说明:捕捉列表描述了上下文中那些数据可以被 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 表达式之间不能相互赋值 ,即使看起来类型相同
第一种就是传值捕捉,对应的还有传引用捕捉
首先,我们捕捉了x和y后是不能修改x和y的
传值捕捉是拷贝过去的,也就是说我们int x,和swap1 = [ x , y] 中两个x不是同一个x,函数体里调用时还是会建立一个栈帧的,我们可以认为是一个函数调用,并且是const的,不能修改
但是我们加上mutable就可以修改的,mutable的意思是可变的
不过即使里面的x和y交换了,外边的x和y也没有交换,和我们最初学的函数传值调用是一样的
如果我们想要让外面的x和y改变,这里就要用引用捕捉
这里看着像取地址,但其实是引用
假设外面有很多变量,我们一个一个写太麻烦了,怎么办呢?
这时候就可以这样写,这里我们还省略了参数列表
我们还可以组合起来使用
这里有一个const变量e,也可以捕捉
但是e是不能修改的
我们可以看到e确实被捕捉了
这两个e的地址还是一样的,所以引用捕捉是很灵活的,普通变量就是普通捕捉,const变量就const捕捉
回到最初的话题,想要使用add,捕捉一下就行
这里f2也不能赋值给f1
我们把他们的类型打印出来,非常的长,还不一样,是不同的类
这里就有疑问了,他们不是匿名对象吗?怎么是类?
lambda和范围for其实有点像,我们没学习范围for前看着很神奇,但底层就是迭代器而已,lambda的底层是仿函数,也就是说,我们定义了f1和f2,编译器生成了两个类
后面的一长串是uuid,每次生成的都不一样,有兴趣的大家可以百度一下,这样做可以让每个lambda都不一样,而f1和f2就是仿函数的对象
箭头这里就是调用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,但他们是一样的
lambda底层就是一个仿函数,底层就是把lambda替换成仿函数,生成一个仿函数类,类的名字为了不冲突叫做lambda+uuid,这样不同的lambda就是不同的类
新的功能
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。 ( 默认移动赋值跟上面移动构造完全类似 )如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
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:
bai::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
/*Person s3 = std::move(s1);
Person s4;
s4 = std::move(s2);*/
return 0;
}
这里我们屏蔽析构,拷贝构造,赋值,此时编译器就会默认生成移动构造
对于自定义类型string _name,会调用他的移动构造或拷贝构造
我们的string是实现移动构造的,怎么回事呢?
原因是s2 = s1是调用拷贝构造,Person类中没有拷贝构造,而且是左值,左值怎么会调用移动构造呢?
此时就是默认生成的移动构造
我们把析构函数放出来,又变成了这样
原因就是不能实现他们三中的任意一个
大家再仔细想想,如果一个类需要我们显示的写析构,那么这个类就是深拷贝的类,比如string,vector,list都要写析构,都是深拷贝的类,析构,拷贝构造,拷贝赋值是三位一体的,再想想迭代器,不需要写析构,不需要拷贝构造,所以编译器生成的非常香的
强制生成默认函数的关键字default:
C++11 可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
我们可以看到,此时即使我们写了析构函数,也会生成默认成员函数
这里也是一样的
禁止生成默认函数的关键字delete:
如果能想要限制某些默认函数的生成,在 C++98 中,是该函数设置成 private ,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11 中更简单,只需在该函数声明加上 =delete 即可,该语法指示编译器不生成对应函数的默认版本,称=delete 修饰的函数为删除函数。
我们可以看到有了这行代码后,,s2和s3就报错了
可变模板参数
我们之前提过一次,printf和scanf都是可变模板参数,参数中的三个点就是可变参数
就像这样
底层有一个数组,把实参存起来,访问时会依次取出来
模板参数和函数参数是类似的,模板参数传递的是类型,函数参数传递的是对象
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
模板参数的Args是我们取的,一般都会选这个名字
我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数
如果传递的时候只有一个参数,那么参数包里就有一个类型,(Args... args)里用这个类型定义了一个形参,如果有两个类型,就用这两个类型定义两个对象,上面的Args是参数的类型,下面的(Args... args)是参数的包
就像这样,我们再来看看传了几个参数
这里使用sizeof查看,因为是可变参数,所以要加...,还要加在括号外面,非常奇怪,要特殊记一下,这里我们的第一个数字都传给value,剩下的传给参数包
我们也可以把他的底层认为是一个数组,和函数可变参数一样,但其实并不是这样,我们只是可以这样想
所以会有人想到这样使用参数包,认为底层是数组,直接取出来用,但是是不行的,那我们怎么把参数包的内容取出来呢?
第一种办法是加一个T,然后这样写,非常的奇怪
原理是递归函数方式展开参数包,上面的ShowList是递归终止函数,下面的是展开函数
如果只有一个参数,比如我们只传了一个1,那么就直接去终止函数,如果是多个参数,比如剩下的三个,就要去展开函数,第一个值传给val,推出T的类型是int,剩下的参数传给参数包,然后里面再次调用ShowList,继续传参数,此时参数就少了一个,比如我们传递了1,2,3.14,此时就只有2和3.14了,2匹配val,而3.14继续往下传,此时就调用终止函数,是一个编译递归
非常奇怪,不过我们日常中基本不会使用,简单了解就行
emplace_back只有参数包,那底层是怎么走的呢?
template <class T>
void _ShowList(T val)
{
cout << val << " ";
cout << endl;
}
template <class T,class ...Args>
void _ShowList(T val, Args... args)
{
cout << val << " ";
_ShowList(args...);
}
template <class ...Args>
void ShowList(Args... args)
{
_ShowList(args...);
}
int main()
{
ShowList(1);
ShowList(1,2);
ShowList(1,2,"xxx");
ShowList(1,2,3.14);
return 0;
}
那就是再多一层 ,将之前的两个ShowList变为子函数,原理还是一样的
那如果是无参的呢?
我们将终止函数修改成这样即可
我们将他改名为CppPrint,就实现了一个类似C语言的print函数,这些了解即可
上面说这是第一种方式,下面我们来看第二种,更加抽象
template<class T>
void PrintArg(T t)
{
cout << t << " ";
}
void CppPrint()//重载0个参数版本
{
cout << endl;
}
//args代表0-N的参数包
template <class ...Args>
void CppPrint(Args... args)
{
int a[] = { (PrintArg(args),0)... };
cout << endl;
}
他的意思是将参数包的第一个值,传给PrintArg,剩下的参数包就是int a[ ]里的...,编译器在这里会进行推导,编译器知道有多少个参数,比如这里有三个参数
那么就会变成这样,相当于调用三次 ,数组是根据{ }里的值,来判断数组要开多大,才会展开,但是不能显示的去表示,所以就写成...,这里的(PrintArg(args),0)是逗号表达式,逗号表达式取的是后面的值,用0初始化数组,非常抽象,各位了解即可
我们可以给他简化一下,不用逗号表达式
我们再来看点别的东西
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
,_day(day)
{}
private:
int _year;
int _month;
int _day;
};
template <class ...Args>
Date* Create(Args... args)
{
Date* ret = new Date(args...);
return ret;
};
int main()
{
Date* p1 = Create(2021, 7,23);
return 0;
}
这段代码我们通过可变参数来调用构造函数
如果我们少传一个参数,是不行的,因为没有默认参数,那如果加上的话就很多样化了
我们把默认参数先全设置为1
此时我们可以传递0到3个参数
这里p5也可以运行,p5调用的是拷贝构造
因为有了参数包,我们传递的参数就非常灵活,可以传递0到n个
下面我们来看看emplace系列,我们看到他们都使用了&&,但这里不是右值引用,带有模板参数的是万能引用,当你是左值时这里就是左值引用,是右值时是右值引用
我们再看这段代码,我们在创建pair时用了make_pair
调用的是右值引用的版本
emplace_back也可以这样传
emplace_back的底层就和我们上面的Date类似,先传给new Node(args..),参数包一直往下传,直接构造,就像图里的10和20,而30使用了make_pair,调用的是拷贝构造,emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
这里他俩并没有什么区别
int main()
{
std::list< std::pair<int, bai::string> > mylist;
mylist.emplace_back(10, "sort");
mylist.emplace_back(make_pair(20, "sort"));
mylist.push_back(make_pair(30, "sort"));
mylist.push_back({ 40, "sort" });
return 0;
}
这个场景下他们会有区别,当pair的一个参数是string时
我们看第一个,是直接通过string构造的,因为pair的参数int和sort作为参数包一直往下传,最后直接初始化pair即可,直接初始化相当于在节点里用这个参数构造string
再看第二个,这里是编译器进行了强制优化,老一点的编译器可能是先构造再拷贝构造
再看push_back,他们就是先构造再拷贝构造,不过我们写了右值,所以这里是移动构造,因为push_back不是参数包,必须是pair,必须先创建pair对象,再移动构造
这里意味着,emplace_back可以不断往下传参数包,直接构造,而push_back只能先构造,再拷贝构造(或者移动构造)
所以有些人会说emplace_back更高效一点,其实如果我们使用移动构造的话,并不会差多少,移动构造直接转移资源,如果没有移动构造,比左值引用,那emplace_back就会高效一点,右值引用就大差不差了
我们可以认为emplace_back是一个更强大的push_back,适应性更高,更加灵活push_back只能传日期类对象,而emplace_back可以传对象,也可以传对象的参数包
包装器
function
C++中有非常多的可调用类型,比如函数指针,仿函数,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;
}
我们在调用函数useF,可以通过函数指针,仿函数和lambda,那么函数模板会被实例化为3份
我们可以看地址,或者静态变量count是否变化来得知,可以被实例化为3份,接下来我们来看看包装器,把他们变为1份,并且这其实还不是核心,有时候我们需要把可调用对象存到容器里,比如存到vector<>,那这里怎么写呢?lambda我们甚至写不出来
function 包装器function 包装器 也叫作适配器。 C++ 中的 function 本质是一个类模板,也是一个包装器。
#include <functional>
// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
//模板参数说明:
//Ret: 被调用函数的返回类型
//Args…:被调用函数的形参
需要一个头文件,包装器的本质可以理解为适配器模式,他可以包装出我们想要的东西
这里看到,包括仿函数对象,都可以包装,上面返回值和参数都是double,所以我们这样写,语法大家要记一下
有了包装器,我们就可以这样玩,把对象存到vector里,这里取出了vector中的对象,对象是一个包装器,包装器包装了函数指针,lambda和仿函数对象
还可以更简便一点,不用先初始化,包装器解决的是可调用对象的类型问题,在此之前,比如我们想要把可调用对象存到容器里,我们写函数指针就只能存函数指针,写仿函数对象就只能存仿函数对象,lambda我们都没办法写,但是有了包装器我们就都可以存
bind
我们这里有一个减法,假设我们输入的是10,5,那么就是10-5,在不改变这个函数的情况下,如果我们想要让参数位置换一下,变成5-10该怎么办?于是就有了bind(绑定)
std::bind 函数定义在头文件中, 是一个函数模板,它就像一个函数包装器 ( 适配器 ) , 接受一个可 调用对象( callable object ),生成一个新的可调用对象来 “ 适应 ” 原对象的参数列表 。一般而言,我们用它可以把一个原本接收N 个参数的函数 fn ,通过绑定一些参数,返回一个接收 M 个( M可以大于N ,但这么做没什么意义)参数的新函数。同时,使用 std::bind 函数还可以实现参数顺序调整等操作。
// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
可以将 bind 函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“ 适应 ” 原对象的参数列表。调用 bind 的一般形式: auto newCallable = bind(callable,arg_list);其中, newCallable 本身是一个可调用对象, arg_list 是一个逗号分隔的参数列表,对应给定的callable的参数。 当我们调用 newCallable 时, newCallable 会调用 callable, 并传给它 arg_list 中 的参数 。arg_list 中的参数可能包含形如 _n 的名字,其中 n 是一个整数,这些参数是 “ 占位符 ” ,表示newCallable 的参数,它们占据了传递给 newCallable 的参数的 “ 位置 ” 。数值 n 表示生成的可调用对象中参数的位置:_1 为 newCallable 的第一个参数, _2 为第二个参数,以此类推
我们先来看这段代码
这是绑定的语法,placeholders是一个命名空间,_1代表第一个参数,_2代表第二个参数,以此类推(如果我们把命名空间展开可以直接写_1和_2,不过一般我们不展开)
我们可以看到有各种数字,就代表了参数,我们继续看上面的代码
如图,我们输入的10传给了_1,然后_1传给a,5传给了_2 ,_2传给b
知道了这些,我们就会调整参数顺序了,就是把_1和_2位置换一下
此时10还是传递给_1,但是_1会传递给b,5传递给_2,_2传递给a
bind的一个作用是,当我们看到有些函数的参数顺序很别扭时,我们可以包装一下他
比如有人写了第二个函数,但是我们自己写一般是第一种,所以看着就很别扭,就需要调整一下
我们再看一个例子
我们有一个Plus函数,我们使用时要传递三个参数,但是如果我就是不想传递第三个参数怎么办呢?有人可能会想到缺省参数,但是缺省是写死的,如果想第三个参数可以变化怎么办?
我们就可以使用bind来完成,直接传第三个参数,绑定后就可以只传递两个参数,这里相当于函数简化
我们又有了一个Plus2
首先4.0这些rate应该写在前面,然后大家认为后面的参数应该是_2和_3还是_1和_2呢?
答案是_1和_2
这里我们可以把绑定的rate认为是一个缺省参数,我们只需传两个参数,所以是_1和_2更合理一点
如果是_2和_3,那这种情况该怎么传?传_1和_3吗?是不合适的
这里大家认为固定的参数不参与排序即可
我们再看这个,我们有一个类Sub,有一个static的sub函数,而我们此时bind是找不到的,因为类是一个域,编译时默认只会在全局去找,如果没有指定或者展开,是不会去类域里找的
所以我们得指定类域
那当我们有一个非静态的ssub呢?
此时就报错了
首先我们需要加上&符号, 非静态的成员函数取地址前面要加&符号,这是规定
静态的可以不加,也可以加,不过推荐加上,这样不容易混淆
此时Sub2还是报错,为什么呢?非静态的实际是几个参数?看起来是三个,实际上是4个
我们需要一个Sub对象,然后传一下,此时我们绑定了两个参数,一个是对象st的地址,另一个是rate,这是一种写法,我们再来看另一种
传递一个对象过去,Sub2也是一样,传递指针可以,传递对象也可以
这里的指针和对象不是传递给this指针,是传递给operator( )调用,是指针就指针调用,是对象就对象调用
绑定的底层和lambda类似,生成了一个仿函数,仿函数调用函数有多种方式
这里我们typeid的话是一个function,因为我们上面写的就是一个function
如果我们使用auto,就可以看到这样一个,底层是一个叫做Binder的类,最终还是重载的还是operator(),这里我们了解即可
这里对象像函数一样调用,那什么样的对象可以像函数一样调用呢?就是仿函数
以上即为本期全部内容,希望大家可以有所收
C++11中的内容还差智能指针和线程库,这两个后续会单独开一篇来讲
如有错误,还请指正