C++11
- 前言
- 正式开始
- 统一的初始化列表
- { }初始化对象
- { }为容器初始化
- 赋值运算符重载也支持{}
- 声明
- auto
- decltype
- nullptr
- STL中一些变化
- array
- forward_list
- unordered_map 和 unordered_set
- 右值引用
- 表达式
- 左值和右值
- 左值
- 右值
- 右值引用的使用场景
- 移动构造和移动赋值重载
- 右值分类
- 移动构造
- 编译器无优化情况下
- 接收传值的返回值
- 移动赋值重载
- 完美转发
- 万能引用
- std::forward\<T\>
- 应用场景
- 默认成员函数
- 几个关键字
- 强制生成默认函数的关键字default:
- 禁止生成默认函数的关键字delete:
- 继承和多态中的final与override关键字
- 可变参数模板
- 递归解包
- 数组解包
- lambda表达式
- 引例
- 语法简介
- 使用示例
- 捕获列表
- lambda表达式的底层
- 包装器
- 引例
- 包装器介绍
- 绑定器
- bind调整参数顺序
- bind调整参数个数
前言
先说一个小故事:
1998年是C++标准委员会成立的第一年,本来计划以后每5年视实际需要更新一次标准,C++国际
标准委员会在研究C++ 03(03主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动)的下一个版本的时候,一开始计划是2007年发布,所以最初这个标准叫C++ 07。但是到06年的时候,官方觉得2007年肯定完不成C++ 07,而且官方觉得2008年可能也完不成。最后干脆叫C++ 0x。x的意思是不知道到底能在07还是08还是09年完成。结果2010年的时候也没完成,最后在2011年终于完成了C++标准。所以最终定名为C++11。
相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个重点去学习。
这里把c++的官方网站给出来:cppreference.com。
但是我平时查东西的话一般不去这个网站,一般去的是cplusplus,这个用起来排版比官方的网站好看一点。
废话不多说,开讲。
正式开始
统一的初始化列表
{ }初始化对象
在C++98中,标准允许使用花括号{ }对数组或者结构体元素进行统一的列表初始值设定。比如:
struct Point
{
int _x;
int _y;
};
int main()
{
int array1[] = { 1, 2, 3, 4, 5 };
int array2[5] = { 0 };
Point p = { 1, 2 };
return 0;
}
但C++11中,可以说一切都可用{ }初始化,使其可用于所有的内置类型和用户自定义的类型,而且还可以省略=。
比如说int:
再比如说我现在自己写一个自定义类型Date:
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
测试:
上面的{ }会调用其构造函数,但其实是先用2023,8,19构造出一个临时对象,然后再调用拷贝构造将临时对象拷贝给d2,但是编译器做了优化,让其直接调用构造。d3同理。但是可以看到后两个用着看起来非常别扭,我在这也建议各位不要这样用。
{ }为容器初始化
{ }的主要应用场景是给容器初始化对象,{ }括起来一堆同类型的数据后,这个花括号所包的东西有其对应的类型,叫做initializer_list,这也是库里的一个容器,C++11后STL库中的容器都提供了一个参数为initializer_list的构造函数,我这里就先用vector和list看看怎么用:
很简单,不过多叙述,我们看看STL中给的接口:
vector
map
等等接口就不演示了。反正就是能用{ }初始化。
再来看一下{ }的类型:
就是一个库中的容器。库中也可以找到:
其也提供了begin和end,所以也是支持迭代器遍历的:
上面用的是STL库中的vector等容器,其中提供了用{ }初始化的接口,但是我前面在模拟实现vector的博客中并未介绍这个接口,而且也没写这个接口,所以我自己模拟实现的vector是不能用{ }初始化的,我这里把简陋版的模拟实现给出来:
namespace FangZhang
{
template<class T>
class vector {
public:
typedef T* iterator;
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;
};
}
并没有实现{ }初始化,测试就通不过:
想要实现的话也很简单,自己写一个就好,但是这里简陋版本的没有push_back等接口来复用,所以要搞的麻烦一点:
此时就能跑过了:
但重点也不是这,而是给容器中存放自定义类型:
上面v1中存放的是d1,d2,d3三个自定义类型Date的对象,v1初始化的时候就是用的{ }初始化的。
.
v2中也是用{ }初始化的,但是其中三个{ }是先各自调用Date的构造函数构造出临时对象,然后再给v2进行初始化。
.
Date类型的对象就是多参数构造初始化的。
再比如说:
map中存放的是pair,这里的数据跟上面的一样,也实现调用构造创建出对象,然后再一个一个放到map中。pair类型也是多参数构造初始化的。
所以这个{ }初始化要用的话也是常用在多参数构造初始化对象。
赋值运算符重载也支持{}
operator=也能用{ }。
演示一下:
就不说那么多了。
总结一下:C++11后一切对象都可以用列表初始化,但建议普通对象还是用以前的方式初始化,容器若有需求可以用{ }初始化(容器一般是一上来就要给值的话才会用到,否则一般也用不上)。
声明
c++11提供了多种简化声明的方式,尤其是在使用模板时。
auto
这个早在我最初讲C++的博客中已经说过了,不懂的同学点传送门:【C++】从C语言入门C++的基础知识。
decltype
关键字decltype将变量的类型声明为表达式指定的类型。
看;
可用其指定后的类型再创建变量。
decltype指定完类型后就不能改变了。
看:
y1用的auto,识别出20.2为浮点数,所以y1也是浮点数。
但y2的类型是由decltype(x)决定的,也就是x决定的,那么y2的类型就是int。
nullptr
这个也是在刚刚auto的传送门中的。还有范围for也是。
STL中一些变化
新增了几个容器。
array和forward_list就是静态数组和单链表。可以说比较鸡肋。unordered_map和unordered_set还是非常有用的。解释一下为什么。
array
其实前面有一篇中也说过这个了,但是我具体想不起来哪一篇了,这里就再讲讲。
搞array的初衷是想让每个C++程序员放弃用数组。但是并没有产生这样的效果。
array比数组更安全一点。越界了一定是会报错的。因为array中重载的 [ ] 中有assert判断是否越界,所以不管是越界读还是越界写都是可以判断出来的。
但是数组就不行了,数组越界读是不会报错的,而且越界写还是抽查。看:
越界读(根本检查不到)
越界写(抽查):
数组后面的几位能检查到
数组只有有效数据后面的几位能够检查到,其检查的机制就是判断后几位的数据是否发生更改,更改了就说越界,没更改就没越界。但是往后多走几位就检查不到了:
这里程序没报错,但也没崩掉,而是正常运行了。
这就是数组比较bug的地方了。
但是array也并没有取缔数组,因为也没有太大的用途,程序员们已经习惯了数组的简便,用array比数组麻烦,又要传类型,又要传大小的:
而且array是静态的数组,如果N传大了有可能导致栈溢出,我前一篇博客中也演示了STL的位图,其中就是用的静态数组,个数定多了就给栈撑爆了。所以说用array还不如用vector,动态开辟总不会出现把栈撑爆的情况。
再说单链表。
forward_list
这个也是鸡肋。
期初设计单链表就是为了节省一点点空间,但是这一点点空间节省了也没有太大的用处,用list效率更高,所以要不要单链表其实也无所谓的。
unordered_map 和 unordered_set
这两个非常有用。我前面有专门博客详细讲解,不懂的同学下面这三个传送门:
【C++】模拟实现哈希(闭散列和开散列两种方式)
【C++】模拟实现unordered_map和unordered_set
【C++】位图和布隆过滤器
其实上面这三个应该是在一章中的,但是时间原因我写成了三篇,不懂哈希的同学挨着顺序看就行。
如果我们再细细去看会发现基本每个容器中都增加了一些C++11的方法,但是其实很多都是用得比较少的。比如提供了cbegin和cend方法返回const迭代器等等,但是实际意义不大,因为begin和end也是可以返回const迭代器的,这些都是属于锦上添花的操作。
其实cbegin和cend没必要整,但是可能是委员会觉得begin和end什么调用const迭代器可读性不强吧。所以就又搞出了cbegin等。
总结一下容器相关的知识:
- 都支持initializer_list构造,用来支持列表初始化
- 都提供了有一些用处不不大的接口,比如cbegin、cend。
- 移动构造和移动赋值(用来提高效率)
- 优质引用参数的插入(也是用来提高效率)
后两点前面没讲,可能很多同学听起来比较懵逼,下面就要讲这个C++11中相对重要的东西。
右值引用
先看这个(看不懂没关系,下面就要讲):
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们
之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
先讲一些概念。
表达式
这应该是初学C语言的时候就要学的东西。
那么表达式是什么呢?
表达式是一种有值的语法结构,它由运算符(变量、常量、函数调用返回值)结合而成,每个表达式一定有一个值。
这个清楚了再说左值和右值。
左值和右值
简单来说,能够取地址的表达式就是左值,不能取地址的表达式就是右值。
先说左值。
左值
如果详细来说就是:
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址,左值可以出现赋值符号的左边,也可出现在右边。定义时const修饰符后的左值,不能赋值,但是仍可以取它的地址。
看图:
这里的a、b、p、*p都是可以取地址的。都是左值。
我们可以对左值进行引用,给左值取别名。这就是左值引用:
再来说右值。
右值
就是不能取地址的表达式。
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这里指传值返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
看图:
上面的10,x+y,fmin的返回值。都是右值。
解释一下:
10
10取不了地址,这应该都懂。x + y
x + y会产生一个临时数据,也是int的,这个临时数据的值就是30。而临时数据具有常属性(值不能修改),如果我们能取地址,就能通过该地址来改变这个临时数据,那么这样就和常属性相悖了。所以不能取地址。fmin(x, y)
返回值也是会产生临时数据,相关内容在刚刚auto那里的传送门。还是临时数据具有常属性,不能更改,所以就不能取地址。
那么右值引用是什么呢?
就是给右值加了引用(说了跟没说一样),看图:
右值引用的语法就是两个&&,比左值多一个。
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可
以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地
址,也可以修改rr1。
如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇,这个了解一下就行,实际中右值引用的使用场景并不在于此,这个特性也不重要。
各位也先不要关心右值引用有什么用,等会自然会讲到的。
两个问题:
- 左值引用可以引用右值吗?
答案是加const就可以。
不加const:
.
.
加上const:
原因就是x+y后的值具有常属性。不能被修改,左值加了const之后也就不能修改了。
但是要记住加const的左值引用既能引用左值,又能引用右值。而左值引用只能引用左值。
- 右值引用可以引用左值吗?
答案是需要用move
move唯一的功能是将一个左值强制转化为右值,继而能让右值引用使用该值。
先看不加move的:
.
.
再看加move的:
也就是说右值引用只能引用右值,不能引用左值。但是右值引用可以引用move以后的左值。
上面也说到了,右值被引用之后,引用的变量变成了左值,比如说 int&& rr1 = 10, rr1就是一个左值,因为我们是可以对rr1取地址的:
rr1就会被存储到新空间中。但10仍然是一个右值。
上面讲的并不是右值引用的场景,下面就讲讲右值引用的真正场景。
右值引用的使用场景
我们先来想想左值引用解决了那些问题?
- 做参数。
a、减少拷贝,提高效率。
b、做输出型参数。 - 做返回值
a、同上。
b、引用返回,可修改返回对象,比如说有些容器中经常用到的operator 。
但是C++98的左值引用遇到下面的场景很难处理:
就是返回局部对象。如果是内置类型的还好说,比如说int、指针什么的,但是一旦遇到自定义类型,就会导致效率大大降低。因为返回的时候要调用拷贝构造,又是深拷贝什么的,如果这种局部对象传值返回多了,就会导致最终程序性能降低。
而且我们不能用左值引用作为返回值的,因为局部的对象生命周期就在那个函数中,出了函数对象就销毁了。这样完蛋了,引用了一块未开辟的空间,就出问题了。
如果是在C++11之前,请问作为程序员的你,如何应对这种问题呢?
是有方法的。
- 搞成全局对象,但是很危险,尽量不要用全局对象,会导致线程安全问题,但是我之前的博客中没有讲线程,这里也就不多说了,以后会有专门线程的博客的。
- 用new在堆上开空间来创建对象。但是如果这样的话,我们容易忘记用delete释放空间,这样就容易导致内存泄漏。也不是什么好办法。
- 用输出型参数,多给一个引用/指针就行,但这个可以说是最优解了,以前人也就是这么干的。但是不太符合我们使用习惯,正常应该是直接拿返回值就行了。像下面这样:
而C++右值引用的一个重要功能就可解决上述问题。
下面才是右值引用的真正场景:
移动构造和移动赋值重载
又添加了个新构造函数,移动构造,还多了一个移动赋值重载,也是一个复制重载。
只不过二者参数都是右值引用。
讲之前又得再说一下右值。
右值分类
右值可分为两类:
- 内置类型右值 ===》纯右值
- 自定义类型右值 ===》将亡值
也就是字面常亮、临时对象、匿名对象啥的了。
将亡值,听名字就是快要没了的值,也是挺形象的。
移动构造
我这里用一下我之前模拟实现string的代码(过一眼直到有啥函数接口就行):
namespace FangZhang
{
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(const string& s) -- 拷贝构造(深拷贝)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
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(s);
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
// 拷贝赋值
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)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
上面的构造中没有实现移动构造,所以说如果我们用一个常量字符串初始化一个对象的时候,就会先调用构造函数,然后再调用拷贝构造函数,但是版本高一点的编译器都会自动优化成直接调用构造函数。
编译器无优化情况下
vs下搞配置麻烦,我就直接用g++了(和上面同样的代码):
中间那个-fno什么的就是不让编译器做那个优化,这里就能看到,是先构造再拷贝构造的。构造函数是先创建临时变量,然后再将这个临时对象拷贝给s。
而没有加-fno的,会直接调用构造函数去初始化s。
但是如果我加上了移动构造:
// 移动构造
string(string&& s)//s右值引用,引用的就是临时对象中的数据
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 资源转移" << endl;
swap(s); // 直接将当前对象中的内容(也就是空指针、0、0)与将亡值s进行交换
// 因为s中的数据是临时对象的,而s也即将就被销毁了,在s销毁之前将其有效的数据
// 交给当前对象,然后当前对象将没用的数据给s,正好让s销毁。
}
加上移动构造后,还是先构造临时对象,然后再调用移动构造去将那个临时对象中的数据与s中的数据交换。
这样的话,效率就会大大提高。因为移动构造中,是将数据进行了简单的交换,而不是再开空间再拷贝了。
看一下图解:
再分情况:
上面的过程走完之后,tmp对象就销毁了。
对于拷贝构造中的tmp对象,拷贝构造函数并没用利用到tmp中的资源
可以说tmp对象就是移动构造中的参数s,可以直接利用到tmp中的资源,直接交换,从而提高了运行效率。因为一直用的是原空间,交换的时候只是交换了指针的数值而已,不会有深拷贝那么大的开销。
拷贝构造和移动构造,编译器会自动调用最匹配的,就像函数重载一样。
上面给的这个例子有点不太方便查看,我们换个例子:
代码:
FangZhang::string str1("hello");
FangZhang::string str2(str1);
FangZhang::string str3(move(str1));
测试如下:
没有移动构造
.
str1是左值,move后的str1是右值。
.
都调用的是拷贝构造,因为拷贝构造中的参数const string& s既可以接收左值,又可以接收右值。
有移动构造
.
这里move后的str1就是右值所以就会调用移动构造。
调用移动构造能将str1变为右值后的临时数据直接和str3交换,这样效率就会高很多。
这里就不画图解了。
接收传值的返回值
现在写一个字符串转换的函数:
namespace FangZhang
{
FangZhang::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
FangZhang::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
return str;
}
}
如果用这个函数的返回值去创建对象,会咋样?
就一段代码:
FangZhang::string str1 = FangZhang::to_string(-321);
在编译器没有优化的前提下:
如果有移动构造就会调用两次移动构造,一次为临时对象与返回值进行资源交换,一次为当前对象与临时对象进行资源交换。
但如果没有移动构造的话就会调用两次拷贝构造,也是一次为临时对象进行深拷贝,然后在通过临时对象为当前对象进行深拷贝。
但如果有编译器优化的话就会都变为一次直接给当前对象操作的。
下面我直接将构造函数中的那句打印去掉,不然影响观察。
演示一下g++下的(没有编译器优化):
有移动构造
两次移动构造。.
对应图解:
这里 str 、tmp 和 str1 交换前后指向的是同一块空间。
.
.
.没有移动构造
两次拷贝构造
.
.
对应图解:.
.
可以看到和上面的描述是一样的。
再演示一下vs下的(有编译器优化):
有移动构造:
.
对应图解:
这里 str 和 str1 交换前后指向的是同一块空间。
.
.
.没有移动构造:
.
对应图解:
.
移动赋值重载
先看一下没有移动赋值重载是啥样的:
拷构、拷赋、拷构。解释一下:
这里因为我拷贝赋值中调用了拷贝构造,所以又打印了一次拷贝构造。复用了一下代码,写起来更方便一点。但是本质上还是先给当前对象开空间再将值拷贝进去。
但是当我加上了移动赋值重载:
string& operator=(string&& s)
{
cout << "string& operator=(string s) -- 移动赋值(资源移动)" << endl;
swap(s);
return *this;
}
就不需要再开空间了,测试一下(这里我把移动构造屏蔽了):
可以看到,省去了那个拷贝构造的一步,根本不需要再开空间再拷贝了,直接将没有的数据和即将失效的有用数据进行交换,这样就能大大提高效率。
注意我上面的测试中把移动构造屏蔽了。所以在搞临时对象的时候调用了拷贝构造。
如果我加上移动构造:
就变成了都调用移动的。因为传值返回的时候编译器会识别出返回值str是一个将亡值,就会去调用移动构造来将str的资源交换给临时对象并释放str,然后又识别出临时对象是一个将亡值,再调用移动赋值来将临时对象的数据交换给当前对象并释放临时对象。
画一下图解:
纯调用拷贝的:
纯调用移动的:
可以看到,纯调用拷贝的,从头到尾开了三块空间,前两块都没利用上。但是纯调用移动的,从头到尾只开了一块空间,一直用的是那一块,交换的时候只交换指针就行了,这样效率就非常高。
移动赋值和移动构造一样,也是编译器会自动选择最匹配的去调用。
比如下面这样:
但是我这里的s3会将s1变为空:
这里是我自己写的会导致这样,但是有的不是那么新的编译器也可能会导致这种情况。
比如:
在vs2013下是上面那样,s1会被修改成空的,但是在19和g++下就不会改变成空的,还是abc。
所以说,move还是要慎用的。
右值引用的主要场景就是这里的移动构造、移动赋值。能够解决传值返回自定义类型对象效率慢的问题。非常的有用。
STL库中也是提供了移动构造的,比如说string、map等等容器中:
而且STL中容器的插入接口也增加了右值引用版本:
看:
中间vector在push_back的时候调用了两次拷贝构造,是库中的具体实现决定的,这里也就不看库中咋实现的了。
反正就是演示一下STL库中给了有右值引用的插入接口。
记住:上面的移动构造和移动赋值只有在成员深拷贝的时候才有作用。如果没有深拷贝的话,那就基本没什么用了。
完美转发
这个东西是为了保持传过来的参数的属性(左值/右值)。听不懂没关系。
要先讲一下万能引用。
万能引用
万能引用,也叫引用折叠。先看长啥样:
函数模版,其中参数t既可以接收左值,也可以接收右值,但注意只有在函数模版这里才行。
下面写几个函数:
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; }
在那个函数模版中调用一下:
测试:
这里全部调用的是左值引用。
引用折叠,就是将引用的对象的属性折叠起来了,都变为左值。
如果想要保持引用的对象属性,用到完美转发。也就是给 t 套上一个东西。
std::forward<T>
像下面这样:
测试:
应用场景
这里用一下我前面写的list模拟实现的代码,完整代码就不给了,我只把需要改成完美转发的地方改一下,新添加三个函数:
这个在list_node中:
这个在list中:
这个在list中:
简单画个图解:
上面打印拷贝构造是在初始化lt的时候调用的,不是push_back调用的,push_back中调用的是移动构造,是在new创建Node的时候调用的,更准确是_data(std::forward<T>(X))处调用的。
默认成员函数
前面博客中讲过,如果你写一个类,里面会默认生成6个成员函数,分别是构造、析构、拷构、拷赋、取地址重载、const取地址重载。
但C++11之后又变了,添加了两个,那就是刚刚讲的移动构造和移动赋值。
但是生成移动构造和移动赋值是有条件的:
.
.
移动构造:如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
.
.
默认移动赋值跟上面移动构造完全类似:如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。
析构、拷构、拷赋。这三个一般情况下只要实现了其中的一个就得要实现其他的两个,因为一个类如果实现了析构,意思就是该类需要释放空间,那么就是其内部需要开空间的,那么拷构、拷赋默认生成的就会导致浅拷贝问题,就得要自己实现。
【注】如果提供了移动构造或移动赋值,编译器就不自动生成拷构和拷赋。
写一个Person类:
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
// 这里会默认生成拷贝构造+移动构造
private:
FangZhang::string _name;
int _age;
};
测试一下:
然后我在Person类中写出析构:
这样就调用了拷贝构造。
再去掉Person的析构,将string的移动构造注释掉:
这里就是成员没有移动构造,就调用成员的拷贝构造。
再看一下移动赋值:
再加上Person的析构:
下面调用拷赋后又调用了拷构是因为拷赋中调用了拷构来创建对象。
总结一下,大多数情况下,都不会用默认的,都得自己写,但是像Person这样的类就不用写(类中的成员变量都自己有自己的构造、析构、拷构、拷赋、移构、移赋等)。
几个关键字
强制生成默认函数的关键字default:
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。
比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
如下:
禁止生成默认函数的关键字delete:
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁
已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上 =delete 即
可,该语法指示编译器不生成对应函数的默认版本,称 =delete 修饰的函数为删除函数。
比如不想让一个类拥有拷贝功能。此时就不能让类对象拷贝另一个类对象。如下:
出个题:要求用delete关键字实现一个类,使得该类只能在堆上创建对象。
我们可以直接让该类的析构函数后面用 =delete修饰,这样对象生命周期结束时会自动调用其析构函数,而这里的析构函数已经没了,就没法调用析构,此时就只能在堆上创建对象。
再改一下:
class HeapOnly
{
public:
HeapOnly()
{
_str = new char[10];
}
~HeapOnly() = delete;
private:
char* _str;
};
内部还要开空间。
此时在堆上定义已经没事了。
但是有一个问题,指针可不会调用析构函数,自定义类型才会。但是析构已经没了。
这里我没有讲堆上的对象释放,就会导致内存泄漏。如何解决呢?
如果直接delete指针ptr的话就会调用HeapOnly的析构,报错:
我们可以自己在类内写一个专门释放的函数:
但是这里还没解决问题,因为只是释放了对象内部的数据,对象在堆上的空间还没释放。
可以调用operator delete ( ),其实这个就相当于free。
而且有两种调用方式:
一种是调用完destroy后,ptr指向的空间还未释放,手动释放掉ptr指向的空间。
第二种是destroy中就释放ptr指向的空间:
但如果这么写的话就不需要在ptr后面再调用 operator delete (ptr) 了。
上面的就是解法。
如果题目中没有要求用delete的话,也可以将析构私有,然后再写一个和上面一样的Destroy就行。
继承和多态中的final与override关键字
这两个我在前面的博客中已经讲过了,这里就不过多赘述了,不懂的同学点传送门:
【C++】继承知识点详解
【C++】多态
可变参数模板
可变参数,C语言也接触过,比如说printf、fprintf等等。
就是不知道参数有多少个(0 ~ n个)。
看一下长啥样:
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。
先看几个:
都是能跑过的。
我们可以用sizeof…(args)来打印当前参数包中有几个参数:
但是我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持像使用数组那样args[i]的方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。
递归解包
在模版参数中增加一个模版参数T。
T能够取到当前参数包中的首个参数,打印后,在递归调用后面的参数包,当参数包中参数个数为0时就会走上面的ShowList:
上面就算是一种解包的方式,还有一种解包方式:
数组解包
这里数组arr中的 PrintArgs(args)… 会将参数包中的参数挨个展开,该值将会展开成{PrintArgs(arg1), PrintArgs(arg2), PrintArgs(arg3), etc … },最终会创建一个元素值都为0的数组int arr[sizeof…(Args)]。
通过初始化列表来初始化一个变长数组, {PrintArgs(args)…}在创建数组的过程中会先调用PrintArgs(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。
如果说看起来比较模糊 / 不理解的同学也不要担心,这个也不是什么重点内容。
说这个是为了讲一下STL中一些容器的emplace接口:
简单来说,emplace就是插入的功能,emplace_back就是尾插的功能。
可以看到上面的emplace类型的函数参数是可变的,而且还用的是万能引用。其本质上是直接用可变参数中的内容来直接实现当前对象中的内容。
有些地方会讲用emplace类型的函数要比之前insert、push_back等函数的效率要高一些。严谨点来说的话,需要区分情况。
看代码:
对于容器中只存放一个内置类型而言:
但对于 自定义类型 或者 多个类型 还是有点区别的
这里先写一个自定义类型Date方便打印构造的信息:
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year = 1, int month = 1, int day = 1)" << endl;
}
Date(const Date& d)
:_year(d._year)
, _month(d._month)
, _day(d._day)
{
cout << "Date(const Date& d)" << endl;
}
private:
int _year;
int _month;
int _day;
};
这里会将参数包中的数据挨个直接去初始化list中存放的Date类的成员。而不是先构造再拷贝构造。效率会比后者高一点。
再比如:
上面的两个emplace_back是直接调用构造函数了,所以啥都没打印。
但有的容器中效率是相等的,具体取决于底层实现,比如说vector:
其实差距也不是很大,但是各位以后能用emplace类型的函数插入的就尽量用吧。
C++11的新特性可变参数模板能够让你创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段呢,我们掌握一些基础的可变参数模板特性就够我们用了,所以这里我们点到为止,以后大家如果有需要,再可以深入学习。
lambda表达式
在C语言的时候,我们用qsort可以对数据进行排序,但是需要传一个我们自己写的比较函数的函数指针,有点麻烦。
然后为了比函数指针用起来方便一点,C++中搞了仿函数来替代函数指针。
再往后就是这里要将的lambda表达式了。
引例
先写出如下商品结构体:
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
测试:
vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,3 }, { "菠萝", 1.5, 4 } };
如果我现在想要按照评价、名字、价格来升序/降序打印一下,如果用库中的sort,我们还要写配套的仿函数,这样的话,就得写6个仿函数,听着就非常麻烦。
例如下面的两个仿函数:
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; // 价格降序
}
};
剩下的4个就不写了,可以看到很麻烦。而且如果命名的不够规范的话,每个仿函数都按照Compare1、Compare2……Compare6 这样的方式命名,那不就给人恶心死了。
所以就不好用。所以得学学lambda表达式。
下面我们先来看一下lambda表达式的语法:
语法简介
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type {statement}
翻译成中文就是 [捕捉列表] (参数列表) 易变的 -> 返回值 {函数体}
.
.
lambda表达式各部分说明[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[ ]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同( )一起省略
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。一般情况下返回值类型都会省略。
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量
注意:
在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[ ]{ }; 该lambda函数不能做任何事情
使用示例
先来几个看看:
两个树相加的lambda
不需要写到全局中,但也能充当函数的功能。
.
但是这个还用不了。上面包括{ } 和 { }中的内容的是一个对象,但是没有明确的类型,底层有,但是上层拿不到。我们可以用auto来接收:
然后,就可以用add来调用函数:
我们也可以省略返回值:
再写一个,交换变量的:
但是上面的函数体有多条语句,看起来不方便,我们可以写成和平时一样的函数体:
上面也把返回值去掉了。上面介绍语法的时候也说了,一般情况下都是不需要写返回值的。让它自动推导到就行。
下面来说说捕捉列表和mutable的意义。
还是上面的交换,把参数列表去掉,[ ]中写a,b。[ ]捕捉的是与当前lambda表达式在同一作用域的才行。
但是上面说a,b不可变。
.
.
这是因为默认捕捉来的对象不能修改,要加multable关键字:
但其实mutable也没用,因为这里的捕捉列表是传值捕捉,跟普通函数的传值一样,没法修改外部的变量:
如果想修改的话,得要在变量前加&,后面的()mutable可要可不要:
这样就成了。
见过猪跑了,下面就说说开始仿函数太多的问题:
直接用lambda表达式,可读性会更强。
排序后的结果:
降序:
升序
捕获列表
捕获列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针
上面的父作用域指包含lambda函数的语句块
几种方式可以混着用。
其实最后一条的this指针可以归类到第一条当中,因为this也是传值的。
下面就先用五个变量abcde简单演示一下 = 和 &:
用=,直接传值获得父作用域中的所有变量:
这里的父作用域就相当于是main函数。
用&的:
再来看一下混着用的一点要求:
- 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量- 捕捉列表不允许变量重复传递,否则就会导致编译错误。
比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复
这里a是不能改变的,因为传值的a没有加mutable关键字。
再看一下第二种情况:
父作用域,可理解为当前所在的函数(栈帧)中,但全局变量/静态变量的话也能调用。
.
.
.
全局变量和静态变量存放在静态区,任何地方都能调到:
.
.
.
其实捕捉列表就相当于是传参,有传值的,有传引用的。和普通的函数还是有点相似的地方的。
lambda表达式的底层
lambda表面看上去挺高大上的,其实底层就是仿函数。就像范围for一样,底层就是迭代器。
再说一下仿函数的概念:函数对象,又称为仿函数,即可以像函数一样使用的对象,就是在类中重载了operator()运算符的类对象。
编译器正式语法编译之前就会将lambda表达式实例化为一个仿函数。
我这里写一个lambda和一个仿函数:
我们来看一下汇编代码:
上面仿函数和lambda中都调用了operator()就能证实:lambda底层就是仿函数。
但是调用()的前面有一个<lambda_…>::这个东西。我们来写两个lambda看看是啥样:
都是lambda后面跟了一串数字,而且还不一样。这串数字叫做uuid,生成的uuid 99.99%都是不相同的,每个lambda生成对应的uuid,降低重复的概率。如果想要了解的话各位自行百度一下。
所以说,lambda地层是将lambda转成一个仿函数的类,类名称是lambda_uuid,就算定义多个表面上看起来完全相同的lambda,底层也是不一样的。
这也就导致了lambda之间不能相互复制,因为底层实现出来的uuid不一样。就算lambda一样,也只是
最熟悉的陌生人:
但还有一些地方要注意一下:
包装器
包装器也算是对lambda的延伸。这个作为了解的知识,还是有点用的。
function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。
那么我们来看看,我们为什么需要function呢?
现在我给出一个 ret = func(x),你是否能够判断出func是啥?
学到这里肯定是不能了,因为有可能是普通函数,有可能是仿函数,还有可能是lambda表达式。
那么能不能有一个东西能把这三种 “函数” 包装起来呢?
引例
给出如下函数模版:
然后写一个普通函数,一个仿函数,一个lambda:
运行结果:
可以看到,每个count的地址都是不一样的,所以count都为1,也就是说上面的三个 “函数” 在函数模版中会实例化出三分代码,各走各的。
包装器可以很好的解决上面的问题
包装器介绍
std::function在头文件 <functional> 中。
可以看到,一种全新的用法。
template <class Ret, class... Args> class function<Ret(Args...)>;
拆开看:
template <class Ret, class… Args>,这是模版参数,而且是变长的。第一个模版参数代表的是所要包装的函数的返回值,后面的就是所包装的函数的参数。
class function<Ret(Args…)>。实际用法跟这里的差不多。
先整几个看看:
现在包装这四个函数:
左后一个类的成员函数包装的时候需要给模版参数传一下类名,取成员函数的地址的时候需要加&,而且调用的时候需要传一个匿名对象。学得扎实的同学应该能看懂,这在我前面的博客中多少涉猎一点的。
我们这里就可以用包装器去实例化一下前面的函数模版:
介绍的差不多了,来个题练练,这也是前面做过的:
逆波兰表达式
就直接给答案了:
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<long long> st;
map<string, function<long long(long long, long long)>> react =
{
{"+", [](long long x, long long y){return x + y;}},
{"-", [](long long x, long long y){return x - y;}},
{"*", [](long long x, long long y){return x * y;}},
{"/", [](long long x, long long y){return x / y;}}
};
for(auto str : tokens)
{
if(react.count(str))
{ // str为 + - * /
long long right = st.top();
st.pop();
long long left = st.top();
st.pop();
st.push(react[str](left, right));
}
else
{ // str为数字
st.push(stoll(str));
}
}
return st.top();
}
};
我在里面就用到了包装器和lambda表达式,比之前的做法要简单一点,详细的就不讲了。简单讲一下思路,遇到数,入栈;遇到操作符出栈两次,将两个数按照操作符运算的结果再入栈。
关于map的博客我前面也有,不熟悉的同学翻一翻。
再来说绑定器。
绑定器
这个可以配合着包装器和lambda来用。
绑定可以认为是对参数的适配器,是用来调整参数的,可以改变参数的顺序和个数。
前面包装器中也是有一个问题的,就是与普通函数、仿函数、静态成员函数的返回值和参数都相同的成员函数,包装的时候需要在参数包中加一个类名,而且传参时参数还要多传一个对象。
这时想要将成员函数和其他函数放一块时就要用绑定(bind),通过bind来调整参数的顺序和个数。
先看下bind:
上面两个bind分为带返回值的和不带返回值的。但是重点不在这。重点在用法:
调用bind的一般形式:auto newCallable = bind(callable,arg_list);其中,newCallable本身是一个可调用对象,callable是需要绑定的函数,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。
arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推。
这几个占位的参数在命名空间placeholders中:
这些_1, _2什么的代表绑定对象的形参,1就是第一个形参,2就是第二个形参,n就是第n个的形参。
首先讲一个用处不太大的:调整参数顺序。
bind调整参数顺序
写个函数:
测试:
解释一下:
bindfunc1接收的是bind(Div, _1, _2),调用bindfunc1时传参2,4是按照第一个位置和第二个位置传的,也就是2传给了_1(对应到Div中就是a),4传给了_2(对应到Div中就是b)。所以结果就是2/4,返回值类型为int,得0。
bindfunc2接收的是bind(Div, _2, _1),调用bindfunc2时传参2,4是按照第二个位置和第一个位置传的,也就是2传给了_2(对应到Div中就是b),4传给了_1(对应到Div中就是a)。所以结果就是4/2,返回值类型为int,得2。
上面第二种接收方式就是用包装器接收的。
这就是对参数顺序的调整。比较鸡肋,因为可以直接调整一下2和4的顺序就行。
bind调整参数个数
这个有点用。
调整个数,可以解决刚刚包装成员函数的问题。
写两个类:
测试:
绑定sub的时候,直接让第一个参数固定死,一直是Sub(),这个匿名对象,就不用再传这个参数了。
如果不用auto的话就是这样:
再比如说:
最左边的x:
中间的b:
就是绑定死一个参数,上面本来要传三个,但把一个绑死,就只需要传两个了。
C++11就讲到这里,还有部分知识没有讲,比如说智能指针,后面会有专门的博客详谈。
到此结束。。。