【C++】C++11的那些新特性
本篇博客,让我们一起来看看C++11的那些新特性!
所使用的编译器:VS2019
本篇博客所有的测试源码都可以在我的GITEE仓库找到
[TOC]
1.前言
C++11是C++的标准委员会在2011
年更新的C++新特性。说白了就是一个升级包。和JAVA\PYTHON
这种更新比较频繁的语言相比,C++更新的就没有那么顺风顺水了,而且每一次更新虽然修复了一些问题,但也带来了更多的“没太大必要”的更新
比如没啥用的
array
容器,和int arr[10]
这种内置方式的区别主要在于越界检查
不过咱们这种小菜鸡,只有学习的权力,哪有啥资格评定C++标准呢?我听大佬说,现在最关注的C++更新便是网络库
的上线了,不过那个貌似得等到C++23
去了
话不多说,让我们来康康一些C++11的新功能吧!
2.列表{}
初始化
C++11更新了初始化方式,不管是什么类型的数据,我们都可以用花括号的方式进行初始化
1 | struct TestA{ |
之前我们已经习惯于用这张方式来初始化数组或者结构体,这在C++98
中已经支持
而C++11
则在这种玩法之上,又增添了一部分新操作,那就是直接用花括号初始化,你甚至可以把=
给省略了
1 | int arr2[]{ 1,2,3,4,5 }; |
不过上面这种写法没有什么意义,还增加了代码理解的难度,不如直接用原本的写法。
更多时候,我们是在new
初始化多个数据的时候使用这种方式。
2.1 new初始化多个数据
在动态内存管理那一章节,我们学习了new的两种使用方式,也提到了()/[]
这两个括号的区别
1 | int *p1 = new int(3);//开辟一个int的空间,并初始化为3赋值给p1 |
在C++11中,我们可以直接用花括号,对new开拼出来的数组进行批量初始化。打印的时候,可以看到p2
中的数据都是没有进行初始化(vs也报了警告)而p3
中的数据都完成了初始化
对于结构体数据而言,我们可以用花括号直接调用其构造函数
其中t5
是发生了隐式类型转换+调用构造函数进行的初始化
1 | TestA t5 = { 1, 3 };//对类来说,会进行隐式类型转换+调用构造函数 |
通过调试+打断点可以看到其调用了构造函数进行初始化
同样的,调用new的时候,我们可以用多个花括号的方式进行批量初始化。这在new
一个对象数组的时候非常方便
2.2 initializer_list
2.2.1 STL容器初始化
不光是我们自己写的类、内置类型可以使用这种方式进行初始化,stl
库里面的容器也可以使用相同方式进行初始化
1 | vector<int> v1 = { 1,2,3,4 }; |
同样的,我们还可以用类似的方式初始化vector
内部的对象
作为容器的一份子,map
也有一个利用il
进行初始化的构造函数
使用方式和之前提到的没啥区别,这里就不多讲啦
1 | map<int, int> m1 = { {10, 20},{ 30,40},{50,60} }; |
你可能会好奇,这种初始化的底层是怎么实现的?那么就要提到c++11
新增的一个容器了
2.2.2 initializer_list 容器
如果你把vector
等容器的版本设定为C++11
,看文档的时候便会发现C++11新增了一个用initializer_list
进行初始化的操作
再来看看initializer_list
,这不就是我们刚刚用的花括号嘛?
这个容器的成员函数很少,只有两个迭代器以及size()
它最重要的特性,便是当我们使用auto
自动推到参数类型的时候,{1,2,3,4}
这种类型会被推到成initializer_list
1 | auto il = { 10, 20, 30 }; |
因为他有迭代器,所以我们也可以使用范围for进行打印操作
个人理解,当其他容器使用initializer_list
进行初始化的时候,本质上调用的接口和利用迭代器进行初始化是一样的
2.3.1 插入il
部分容器的insert
函数也添加了使用il
进行插入连续数据的操作
1 | set<int> s2 = { 1,2,3,4 }; |
在vector
中使用il
插入的时候,需要指定pos位置这个和vector
的其他几个插入函数是一样的
2.4 模拟实现il构造函数
那么,如何让我们自己模拟实现的vector
也能支持这个功能呢?【模拟实现vector源码】
和STL
库里面的代码一样,添加上initializer_list
的构造函数即可
因为il
容器已经有了它自己的迭代器,我们完全可以复用迭代器构造的操作,直接进行遍历然后push_back
即可!
1 | //initializer_list构造 |
试试自定义类型,也可以很好的支持!
这里还有一个优化的地方,那便是开始构造的时候,直接给vector开对应il size()
的空间大小,避免后续push_back
的时候需要多次扩容
1 | //initializer_list构造 |
通过调试能发现达成了我们的需求(因为调试步骤太多,截图不方便,这里就不演示了。感兴趣的老哥可以去我的gitee仓库下源码自己试试)
3.变量声明
C++11提供了多种简化的声明/定义
方式,比如我们熟悉的auto
3.1 auto
C++98
中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局 部的变量默认就是自动存储类型,所以auto就没什么价值了。
C++11给auto上了实现自动类型推断的全新功能,方便我们在定义一个变量的时候直接用auto
进行类型推导
1 | auto num = 1;//int |
需要注意的是,如果想用auto
推到,则必须初始化。只给一个auto i
是不行的!
auto
还可以用于范围for等其他操作,这些都在之前讲解这个关键字的博客中有提到,这里就不多说了!
3.2 decltype
关键字decltype
可以把变量声明成我们想要的目标类型;可以理解为和typedef
的作用有一定的类似,但是它是可以自动推导的!
1 | int a = 10, b = 20; |
怎么样,是不是觉得很神奇?很方便?
需要注意的是,使用decltype
关键字来指定函数指针的时候,函数名和函数参数之间需要加上&
,否则无法正确推导类型
1 | decltype(add) &(int, int) // 正确 |
那让我们来看看另外一个语言声明变量的方式吧!😂 压根不需要变量类型捏(因为python是弱类型的语言)
1 | ## Python |
要是C++
也有这么方便就好了😭
3.3 typeid
这个关键字上面已经用过好几次了,就是用来打印变量的类型的
1 | int a = 10; |
4.左/右值引用
在之前我们学习了引用的基本操作,但那个是左值引用
。当时我们尚未引入左指引用和右值引用的区别。本篇博客里面将会详细讲解~
4.1 左值/右值区别
要解答这两种引用的区别,首先我们需要直到左值/右值
分别代指什么
左值:
- 可以存在于
=
左边或右边 - 可以取地址
- 可以对它赋值(const除外)
右值:
- 右值是一个表示数据的表达式,比如:函数返回值
[不能是左值引用返回的]
、表达式返回值A+B
、字面常量10
- 右指只能出现在
=
的右边,不能出现在左边(俺可没见过A+B=C
的代码语法) - 右指不能取地址
左值和右值最大的区别便是:左值可以取地址,右值不能取地址。
4.1.1 将亡值(概念)
右值还分为两种情况:
1 | // 纯右值 |
在右值引用的介绍中将用上将亡值的概念。
4.2 左值引用
左值引用就是对左值的引用。我们之前学习的就是这一类型
1 | // a、b、c、*a都是左值 |
之前也提到了,其作用主要是在针对出了作用域不会销毁的变量进行引用返回,以节省拷贝的代价。亦或者是引用传参,减少形参拷贝代价。
4.3 右值引用
来看看几个比较常见的右值吧,其中Add
是一个简单的相加函数
1 | double x = 1.1, y = 2.2; |
虽然我们不能直接对右值进行取地址/赋值
操作,但是在右值引用过后,便可以对引用值进行取地址/赋值
操作(右值引用后的变量是一个左值,或者说右值引用本身是个左值)
这是因为右值引用的时候,会把当前引用的数据放入一个位置存起来。存放位置:普通变量在
栈
,全局变量/静态变量在静态区
(没有验证过,可能不对)。
1 | int&& rr1 = 10; |
如果你不喜欢右值引用被修改,则可以使用const
进行修饰。
4.4 两个引用的区别
用一个表格来总结二者的区别
左值引用 | 右值引用 |
---|---|
只能引用左值 | 只能引用右值 |
const左值引用可同时引用左值/右值 | 可引用move 后的左值 |
4.4.1 move
move可以把左值换成右值,但不能把右值转左值
谨慎使用
move
,如果当前对象在后续还需要使用,则不能move
将其改为右值,否则可能资源被掠夺导致该对象失效!
以简单的A对象为例,这个对象中有一个公有成员int _a
;
1 | class A |
测试代码如下
1 | A a = 10; |
当我们move了a对象后,因为没有其他人来对a做移动操作,所以a对应的空间都还是存在的,此时访问a不会有问题。
1 | A a = 10; |
此时我们用move将a赋值给了b,但依旧可以访问a中的成员。因为这个成员是一个内置类型,虽然被移动给了b,但是a对象本身并不会被move销毁。执行这个代码可以看到,构造函数只在构造a的时候被调用了一次,b对象并没有调用构造函数(而是调用了编译器默认生成的拷贝构造)
1 | ❯ g++ test.cpp -o test1 && ./test1 |
这个测试的结论是:move只是将a对象标记为“将亡值”,并不会对这个对象本身做任何操作。被move的对象在后续仍然可以使用,但这并不是推荐的做法,最好不要这么做!
比如当你将一个智能指针unique_ptr<int> sp1
通过move移动给了sp2对象,此时sp1就完全不能使用了,解引用sp1会发生解引用空指针的段错误!
4.4.2 右值引用函数传值
这道题是在大厂的笔试选择题中遇到的。
下面的例子可以体现出右值引用的性质:右值引用本身是一个左值!
1 | void test_func(int& a) |
见main函数里面的代码,我们将a使用move函数改为右值后传入test函数,再调用test_func,最终调用的是int&
这个函数!这是因为test函数中的int&& a
作为右值引用,本身是一个左值,自然会调用左值引用的函数
1 | ❯ g++ test.cpp -o test && ./test |
如果不存在int&
的函数,则会调用const int&
的函数。如果没有左值引用的函数重载,则会报错。
1 | test.cpp: In function ‘void test(int&&)’: |
4.5 右值引用使用场景
右值引用可以提高移动构造/移动赋值
等深拷贝场景的效率
什么场景可以使用左值引用提高效率?
- 操作符重载:前置++
- 操作符重载:
+=
- 出了作用域后不会销毁的变量,如输出型参数(即传入函数进行处理的参数)
而有一些场景是左值引用无法处理的:
- 操作符重载:后置++(需要返回一个全新变量)
- 操作符重载:
+
(需要返回一个全新变量) - 模拟实现string中的
to_string
函数
这些场景大多有一个特性,那就是会生成一个全新的变量(对象)其对象生命周期出了函数作用域便会销毁(将亡值)
如果使用左值引用返回,就会出现访问已经销毁了的对象的错误。
假设我们有一个
vector<vector<int>>
,若内部的vector
很大的时候,拷贝构造的代价是很大的!
4.5.1 输出型参数
如果在C++98
的情况下,我们只能用输出型参数来解决这个问题
1 | vector<vector<int>>& test(vector<vector<int>>&v1,int val) |
4.5.2 右值引用 移动构造
在C++11
中,我们可以使用右值引用的拷贝构造来解决这个问题
下方就是一个具体示例
1 | muxue::string to_string(int val) |
在默认情况下,如果想使用这个to_string
函数,就需要进行深拷贝进行传值返回。这是无可避免的代价
如果使用左值引用返回,这里就会有bug。因为出了函数作用域后,临时对象str
会被销毁。而如果我们使用左值引用取别名,在进行赋值的时候,便会出现利用str的别名进行拷贝构造,而str是一个已经销毁的对象的问题
而如果我们使用右值引用返回,则不会出现这种问题。前提是我们自己实现了右值引用的构造函数和赋值重载
一般我们把右值引用的构造函数/赋值重载称作
移动构造/移动赋值
为什么叫移动呢?因为右值引用是会直接拿取对象的资源
std::string
我们可以先用库里面的string
观察一下,当我们使用move之后的右值进行构造的时候,会直接拿掉对象的资源!
1 | string s1 = "1234134"; |
而在使用右值进行返回的时候,编译器会进行一波优化,直接使用移动构造拿取资源,避免多次拷贝构造造成的空间和时间损失
在处理这种问题的时候,就比输出型参数好太多了。
my::string
不过库里面的string
涉及到了buf
之类的高级操作,也不适合我们调试查看调用的具体情况。所以这里我们再使用自己写的string来演示一下
这里我还发现了之前模拟实现string的一个bug,在
push_back
操作的时候,没有给末尾加上\0
,导致析构的时候报错了
- 模拟实现string代码见我的gitee仓库【传送门】
在演示之前,我们先要实现自己的移动构造/移动赋值
1 | //移动赋值 |
这里我直接复用了之前已经写好的一个swap
函数,实现了一个“现代写法”的构造,直接交换了二者的资源。避免深拷贝带来的副作用
接下来用下面的几个来测试一下拷贝构造的操作
1 | muxue::string s1 = "1234"; |
通过在构造函数中添加打印,可以看出这几个分别调用了什么构造函数
- s2调用了深拷贝构造,因为s1是一个左值
- s3调用了移动构造,因为
to_string
函数中return
的是一个将亡值 - s4先是在
运算符+重载
中调用深拷贝构建了一个string
的临时对象,在使用移动构造进行return
运算符+重载
的代码如下,和to_string
一样,都是return
了一个将亡值
1 | //相加重载 |
将一个对象move成为右值之后,便可以使用移动赋值
移动构造直接移动资源
这时候如果调用拷贝构造,就很是浪费:
- 本来tmp的资源就要销毁了,你还得先把他的资源复制一份给自己,再销毁tmp
- 那为何不把tmp的资源直接拿给自己呢?省去了复制的消耗!
这便是移动构造的优势之处!
调试体现出来的,便是深拷贝中两个对象_a
的地址完全不同
而移动构造是直接把s1的_a
资源拿了过来!
其最明显的特征,便是s3的_a
地址就是s1的!
STL的更新
如果我们把自己模拟实现的移动构造删除,那么所有的return都会去调用深拷贝,代价就很大了。对象很大的时候,来一次深拷贝有可能可以把整个系统干废😂
所有STL
的容器,在C++11之后,都支持了右值引用的插入、移动构造和移动赋值。
C++11
的swap
也提供了一个直接使用右值进行资源替换的版本,效率更高
4.6 编译器优化
在之前有关构造函数的博客里面有提到过,当我们return
一个对象的时候,编译器会把两次拷贝构造优化成一次
和拷贝构造一样,执行移动构造的时候,编译器也有一定的优化
不过这个优化就取决于编译器的处理了。不排除有些编译器没有做此等处理哦!
4.7 优化插入效率
有了右值引用,只要我们实现一个右值引用方式的插入,也可以优化插入时的效率
1 | muxue::list<muxue::string> t; |
5.完美转发(万能引用)
c++11
提供了一个万能引用,既可以引用左值,也可以引用右值
1 | void Fun(int& x) { |
通过测试上面的代码我们会发现,不管是传入一个左值还是传入一个右值,其都会调用左值引用。
这是因为右值引用之后,形参t
就是一个左值,所以调用了左值的函数。(右值引用本身是个左值)
我们也不能粗暴的使用
move
来解决这里的问题,因为有时候一些左值对象在后续还是需要使用的,move
之后变成右值,资源被拿走了咋办!
1
2
3
4
5 >template<typename T>
>void PerfectForward(T&& t)
>{
Fun(std::move(t));
>}
而完美转发的存在就是为了将右值保持其右值属性,依旧调用右值对应的函数,其语法如下,使用forward
函数进行完美转发
1 | template<typename T> |
这时候第二种情况就正确掉用了对应的右值引用函数,也没有改变左值的属性
再把函数改成我们自己写的string,也能看出完美转发的作用
1 | template<typename T> |
5.1 使用场景
有些场景下,我们需要对一个函数传入不同类型的参数,这时候就需要用万能引用+完美转发
来进行不同的处理
比较典型的便是很多STL容器都提供了一个新的尾插函数emplace_back
1 | template <class... Args> |
这里便使用了万能引用,以及可变模板参数(后面会提到关于可变模板参数的内容)
利用我们自己写的string进行构造中的打印,即可看出二者的区别
在上面的场景中
emplace_back
直接调用了构造函数;push_back
构造+移动构造(如果不使用万能转发,就会变成构造+拷贝构造)
在传参中的常量字符串会被先构造一个临时对象,再被移动构造到目标区域,移动构造的效率是很高的,所以这两种方式的差距并不算很大。如果emplace_back没有使用完美转发,那么传入的临时对象将会被视作左值,调用muxue::string
的拷贝构造,此时就会出现拷贝的额外耗时了。
不过差距肯定是有的,如果为了兼容性,使用push_back
肯定更好,因为emplace
是C++11
新增的操作。
5.2 将可变模板参数转为C语言的可变参数列表
在我尝试实现一个简单的日志类的时候,遇到了这个问题。最终成功解决
我想通过debug,info,warning这些函数来打印对应等级的日志,它们最终调用的都是_logging
函数,并在_logging
函数中统一进行vsnprintf
,我要怎么才能把可变参数列表传过去?还是说不支持这样的操作?
1 | public: |
如果不这么做,就得把使用vsnprintf的几行代码重复写到每一个独立函数中,有点代码重复
1
2
3
4 va_list ap;
va_start(ap, format);
vsnprintf((char *)_log_info.c_str(), _log_size - 1, format, ap);
va_end(ap);
目前百度到如下办法,无作用
1 | void warning(const char *def_name, const char *format, ...) |
直接调用logging函数的能正常打印,调用warning函数无法正常打印。如下,第一行是直接调用warning的,没有打印出消息内容
1 | [23-04-27 19:59:35] WARN | 1682596775 | test | |
大佬回答中,提到了可以用可变模板参数+完美转发,直接将可变参数列表传过去就行了
1 | // c++ 中我们一般使用 variadic templates,除非你要和 c 库兼容 |
感谢大佬的回答,在这里贴上完整可用的代码【也可以去我的github看】
1 | public: |
测试
1 | void LogTest() |
1 | INFO | test | this in info |
6.新增的默认成员函数
在初识类和对象的时候,我便在博客中提到了C++的几个默认成员函数
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值重载
- 取地址重载
- const 取地址重载
在C++11
中也多了两个成员函数,那便是前文所讲述的移动构造/移动赋值
但是想让编译器默认生成移动构造可没那么容易:只有你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,编译器才会帮你整一个移动构造出来
编译器默认生成的移动构造:对于内置类型会执行逐成员按字节拷贝;对自定义类型成员,则需要看这个成员是否实现移动构造, 如果实现了就调用移动构造,没有实现就调用拷贝构造。
同样的,移动赋值也需要满足上面的条件,编译器才会帮你生成。
1 | class TestB{ |
通过测试可以看出来,编译器默认生成了移动拷贝和移动赋值重载。并调用了自定义类型的移动拷贝/移动赋值
6.1 关键字default
这个关键字的作用之前好像记录过? 不记得了
default
关键字的作用是让编译器强制生成一个指定的成员函数
还是上面的TestB
类的代码,如果我们自己写一个拷贝构造,编译器就不再会生成默认的移动构造/移动赋值
,而是会去调用string里面的拷贝构造、拷贝赋值
这时候我们太懒了,不想自己写移动版本了,于是就用default
强制让编译器干活
现在就正确调用了对应的移动构造和移动赋值了!
7.可变模板参数
在5.1
提到的emplace_back
函数中,便出现了下面这种语法
1 | template <class... Args> |
这就是一个可变的模板参数,允许一个函数有多个参数,且不要求是相同类型
使用sizeof
即可查看参数的个数
7.1 递归解参数包
而如果你想查看参数的类型并使用它,则需要进行递归取出参数来
1 | template <class T> |
其中void ShowArgs(const T& val)
函数的作用,是当参数包中只有一个参数的时候,调用对应的单参函数,而不会报错
另外一种办法便是提供一个无参的同名函数,用作参数包递归的结尾
错误解法
可能有人想使用这样的方法来解包,当参数包里的函数只有一个的时候,结束递归
但是这样是不行的!
- 递归推参数包是一个
编译时逻辑
- 通过
sizeof
判断是一个运行时逻辑
在编译这个函数的时候,已经开始找对应的函数进行调用了。当参数包里面的参数只有1个或者0个的时候,编译器编译的时候发现找不到对应函数,就直接报错了。
7.2 数组解包
除了上面的递归解包,这里还可以使用数组的方式直接来解包
1 | template <class... Args> |
可以看到arr
数组里面解包出了传入的参数
但是这种方法不通用,只适用于所有参数都是相同类型的情况,如果是不同类型则会报错
通用办法是使用一个逗号表达式,来获取一共有多少个参数以及解包
1 | template <class T> |
一共有多少个参数,那么数组里面就会有多少个0
7.3 emplace_back
std容器的库函数中emplace_back
的参数包还使用了万能引用,这就让它的使用更加灵活
1 | template <class... Args> |
可以直接传入两个参数,他会自动解包参数,创建一个键值对
1 | std::list<std::pair<int, muxue::string>> t; |
而push_back
则不支持这么干
8.lambda表达式
在之前,我们使用sort的时候,如果是内置类型,默认会返回一个升序序列。如果我们需要返回降序,则需要改变比较规则,传入一个仿函数来使用自定义的比较对比
1 |
|
因为int是内置类型,库中自带的greater/less
仿函数即可满足我们的需求。而如果我们排序的是自定义类型,则需要自己实现一个对应的仿函数
1 | //价格降序 |
8.1 情景描述
但是如果需要处理的对象有很多不同的成员变量的时候(比如京东淘宝上商品不同的筛选方式)我们就需要实现非常非常多的仿函数
这样一来,程序的代码行数就会变多
在VS编译器下,这种问题还算好解决,我们可以快速跳转道函数定义。但如果我们没有这个功能可用,在处理大文本代码的时候,怎么很快的找到对应的仿函数呢?
特别是在项目合作的时候,万一有个家伙编程命名规范很差劲,我们无法从函数名推断函数功能,再加上不能直接跳转定义,那麻烦事可多了。
8.2 lambda出场
这时候就可以试试用lambda表达式拉,以下是lambda表达式的书写格式
1 | [capture-list](parameters)mutable -> return-type{statement} |
说明一下各个位置分别写的是啥玩意
[capture-list]
捕捉列表,用于编译器判断为lambda表达式,同时捕捉该表达式所在域的变量以供函数使用(parameters)
参数,和函数的参数一致。如果不需要传参则可连带()
一起省略mutable
默认情况下捕捉列表捕捉的参数是const
修饰的,该关键字的作用是取消const使其可修改-> return-type
函数返回值类型{statement}
函数体,和普通函数一样。除了可以使用传入的参数,还可以使用捕捉列表获取的参数
8.3 基本使用
先来写一个最简单的lambda
表达式试试水吧
1 | auto Add = [](int a, int b) {return a + b; }; |
可以看到,这个表达式的使用方法和函数完全一致,也成功提供了结果
因为我们返回值的类型是明确的,所以这里可以省略类型,让编译器自己来推断。当然也可以显示指定类型,这样可以更精确的控制
lambda表达式还支持复制给相同类型的函数指针,但是一般都不要这么用!
1
2
3
4 //f2是一个lambda
void(*PF)();
PF = f2;
PF();
8.4 捕捉列表和mutable
学会了基本使用,我们再来看看捕捉列表是怎么玩的
1 | void TestLambda1() |
这里我们捕捉了函数作用域里面的局部变量a/b
,直接在lambda
表达式内部使用👍
因为不需要传入参数,所以我们可以直接把参数()
和返回值一并省略掉
mutable
默认情况下,我们捕捉到的参数是带const
的,我们并不能对其进行修改。
这时候就需要使用前面提到的mutable
关键字来修饰
注意:这个关键字使用的时候必须带上函数参数的
()
1 | auto func5 = [a, b]()mutable { |
8.5 捕获的几种方式
注意,当我们在对象里面以值传递方式捕获参数的时候,还需要捕获this指针来调用类内部的函数
1 | [val]:表示值传递方式捕捉变量val |
其中第一个就是我们上面演式的[a,b]
这样最直接的值捕获
而最后一个的this指针主要用用于类内部;需要注意,this指针是不能被引用捕获的!因为函数里面的this指针本来也只是个传值参数而已,对于这个指针本身来说,引用捕获的意义其实并不是很大。
1 | clang++ test.cpp -o test -std=c++17 |
在C++17中新增了一个对*this
的传值捕获,这部分可以去看C++17的博客。
8.5.1 全捕获=
当一个作用域里面的变量很多,而我们又不想一个一个写的时候,可以使用=
捕捉全部变量
1 | int a = 10, b = 20; |
8.5.2 引用全捕或
除了基本的全捕或,我们还可以用一个&
以引用的方式捕获全部参数。
1 | int a = 10, b = 20; |
引用了过后,我们也可以修改参数了
8.5.3 全捕获+单独操作
如果只是仅仅的全捕或还不够,我们还想单独修改某一个参数的时候,可以以不同的方式进行捕获操作
1 | auto func8 = [=,&e] { |
这样一来就方便多了
8.6 最终呈现
这样,当我们sort
的时候,就不再需要用仿函数了,而是可以直接用lambda
表达式来完成相同的操作,大大增加代码可读性!
这是因为排序所用的方法直接就在sort这里用lambda
的形式给出了,看代码的时候,也不需要去找定义,更不用担心函数命名规则的问题了。
1 | vector<Goods> v1 = { {"牛奶",20,100},{"杯子",10,200},{"饼干",15,50} }; |
8.7 lambda底层:仿函数
实际上,lambda的底层就是把自己转成了一个仿函数供我们调用。这也是为何sort可以以lambda
来作为排序方法的原因——底层都是仿函数嘛!
8.8 lambda递归
如果需要用lambda写递归函数,那么lambda必须用fuction明确指定类型,而不能用auto!
比如下方是一个最简单的斐波那契的递归
1 | // 这里必须要引用捕获自身 |
这个时候编译就会出现如下的报错,因为auto是要这个lambda表达式的定义完全结束了才能进行类型推断,而我们又在lambda的函数体内使用了fib本身,那么就出现了声明前使用的问题!
1 | test.cpp: In function ‘int main()’: |
正确的写法如下,用function明确指明这个lambda表达式的类型!而且还需要引用捕捉自身!
1 | // 这里必须要引用捕获自身,[&fib],否则他不知道fib是啥玩意 |
这时候才能通过编译!
9.包装器function
function包装器,也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。
那么这个东西是用来干啥的呢?
- 把所有的可调用对象封装成统一的格式
什么是可调用对象?
- 函数
- 函数指针
- 仿函数对象
- lambda表达式
9.1 基本使用
我们可以用function
来包装这些不同的可调用对象,说白了就是产生了另外一个相同的可调用对象。类似于“引用”了这个函数
1 | class AddClass{ |
引用类中非static成员
需要注意的是,当我们使用静态成员函数的时候,必须要带上一个this
指针才能很好的访问。所以我们需要穿入一个AddClass()
的匿名对象来适配包装器
同时,非静态的成员函数还必须要进行&
取地址操作。静态的则可以不加
为了统一,可以给成员函数都加上取地址以防忘记
9.2 特殊场景的作用
这个东西呢,看起来好像没啥用,但是在一些地方可以帮大忙
比如模板函数,假设我们知道在函数B里面需要调用一个模板函数A多次,而且每次调用都是相同类型的(或者说就只有已知的几个特定类型),那么就可以先用fuction
对这个模板函数进行指定的实例化,避免每一次调用的时候,后台都需要单独去实例化一个函数,减小模板的性能损耗!
9.3 改造逆波兰表达式OJ
leetcode逆波兰表达式:https://leetcode.cn/problems/evaluate-reverse-polish-notation/
之前写这个OJ的时候,我用的是栈和switch/case
语句
1 | class Solution { |
现在我们就不需要这么麻烦了,可以使用包装器来改造这个OJ题的答案
1 | class Solution { |
这里我们还用到了前面提到过的{}
初始化构造。现在我们只需要从funcmap
里面取出封装器封装的lambda
表达式进行操作就可以了!
代码一下就简洁了许多,但是这也只有学习过C++11
的人才看得懂,属于一个进阶用法。这种键值对的方式也更方便后续代码的扩展。想添加其他的运算符,就只需要给map里面新增运算符字符串和对应的lambda表达式就可以了。
如果用switch/case
的老办法,就还得自己再写一个判断分支,会产生一定的代码冗余和重复。
测试的时候发现出现了一些问题,int溢出了。这大概率是因为leetcode修改了测试用例。
把所有的int都改成long long即可。反正逻辑性代码没有bug就OK啦。
10.bind绑定
在上面我们用fuction
包装一个对象内部的成员函数时,需要利用匿名对象传入一个this
指针。这样就很不方便了,明明是两个参数的函数,非要传入第三个参数。
要是我们再用9.3中map
的方式来封装一个可调用的表,那带this
指针的函数就没办法一起包装了
1 | class AddClass |
如图,最后addclass
中的非静态函数,就没有办法一同包装到map中
这里不能使用可变参数包,因为是实例化操作
这时候我们就可以使用bind
来进行参数绑定
10.1 使用
bind可以调整参数的顺序,绑定固有参数;最后形成一个新的可调用对象。
bind的第一个参数用于指定需要绑定的函数,后面就是绑定的参数,和需要自己传入的参数
1 | function<int(int, int)> func7 = bind(&AddClass::Addii,AddClass(), placeholders::_1, placeholders::_2); |
这时候我们就不需要传入this指针,因为当我们用bind
绑定的时候,已经默认传入了第一个参数了!
10.2 占位符placeholders
placeholders
是用来占位的,代表这里的参数需要用户手动传入,而_1
代表传入的第一个参数,_2
就是传入的第二个参数,以此类推
1 | function<int(int, int)> func7 = bind(&AddClass::Addii,AddClass(), placeholders::_1, placeholders::_2); |
因为有不同的后缀,所以我们还可以调整绑定的参数顺序!
1 | //Minii的作用是a-b |
我们调整了顺序之后,也得到了不同的结果!
10.3 异常
以下为cplusplus网站上对bind函数可能出现异常的描述
1 | Exception safety |
std::bind
只有在传入的函数的参数构造时抛异常,他才会抛出异常。比如在拷贝构造中抛出异常的类。
注意,如果传入的函数内部会抛出异常,和bind是没有关系的,这类异常会在函数运行时抛出,而不是bind的时候抛出。
另外,如果bind的目标函数参数和传入的参数数量/类型不一致,会引发编译时错误,而不是运行时异常。
比如下放的示例代码中,myexpclass的拷贝构造会检测成员变量是否为负数,如果为负数不允许拷贝,会抛出一个异常。在main函数中,我们尝试引发这个异常,就能观测到std::bind
在函数传参进行拷贝构造时抛出这个异常。(因为这里的参数没有使用引用,所以函数传参需要拷贝)
1 |
|
测试结果如下,test2参数进行bind的时候确抛出了该异常。
1 | > ./test |
11.static_assert
static_assert
是 C++11 引入的一个关键字,用于在编译时进行断言检查。它允许你在代码中添加一些条件,如果这些条件在编译时不满足,编译将会失败并显示错误消息。
static_assert
的主要作用是在编译时验证一些常量表达式的真假情况,从而帮助开发人员捕获一些潜在的问题,例如常量值是否符合预期、模板参数是否满足要求等。
示例:
1 |
|
在上面的示例中,Array
类使用 static_assert
来检查数组大小是否大于零。如果在实例化 Array
类时数组大小不满足要求,编译时将会失败,并显示指定的错误消息。
- static_assert在编译时进行合法性检查
- assert在运行时进行检查
12.constexpr
constexpr
是 C++11 引入的关键字,用于声明在编译时可以求值的常量表达式。它可以用于变量、函数、构造函数等上下文,用来告诉编译器在编译时计算表达式的值,从而将其作为常量使用。
constexpr
的主要作用包括:
- 编译时计算: 使用
constexpr
声明的变量或函数可以在编译时计算,而不需要在运行时进行计算。这有助于提高代码的性能,因为在编译时计算的结果可以直接嵌入到生成的机器码中。 - 常量表达式: 通过使用
constexpr
,你可以声明常量表达式,这些表达式可以用作编译时的常量值,例如数组大小、模板参数等。 - 模板元编程:
constexpr
可以与模板一起使用,用于在编译时执行一些复杂的操作,从而实现元编程技术。
示例:
1 | constexpr int factorial(int n) { |
在上面的示例中,factorial
函数被声明为 constexpr
,这意味着它可以在编译时计算。我们使用 constexpr
函数计算了阶乘,并在 main
函数中使用了 static_assert
进行编译时断言,确保计算的结果是正确的。
在具体运行到这里的时候,就不再需要实时计算,而是直接沿用了编译过程中生成的结果;提高了代码运行效率,但增加了编译时间。
总之,constexpr
关键字允许在编译时求值的常量表达式,有助于优化代码并支持一些元编程技术。它在 C++ 中为编译时计算提供了强大的工具。
13.remove_extent
1 |
|
若 T
是某类型 X
的数组,则提供等于 X
的成员 typedef type
,否则 type
为 T
。
注意若 T 是多维数组,则只移除第一维。添加 std::remove_extent
的特化的程序行为未定义。
1 | // C++14中多了一个辅助类型来使用该类 |
说人话的,这个东西的作用是获取到数组的成员的类型;如果传入多维数组,则只会接触第一维;示例:
1 | void test_extend() |
在linux下打印如下,这里就能看出文档里面说的对于多维数组只移除第一维
的含义
1 | $ ./test |
14.[[nodiscard]]
这个关键字的作用,是让编译器警告用户,不要忽略函数的返回值
1 | [[nodiscard]] int calculateSum(int a, int b) { |
即这个函数的返回值必须被接受且使用,否则就会被警告
1 | $ g++ test.cpp -o test -std=c++11 |
只要你用一个变量接受了这个返回值,就不会收到这个警告了
1 | [[nodiscard]] int calculateSum(int a, int b) { |
使用函数返回值作为判断条件,也算是使用了这个返回值,同样不会发出警告
1 | [[nodiscard]] int calculateSum(int a, int b) { |
15.std::begin/end迭代器
这里说的迭代器并不是容器内部的,std为了一致性,在C++11引入了std::begin/std::end
来进行对原生数组的迭代;
这两个函数除了可以构造一个原生数组的迭代器,还能获取到vector容器的迭代器;
1 |
|
如下是cplusplus网站上的示例代码,看了就会好吧;<iterator>
头文件会在<vector>
头文件中被包含,所以这里不需要再包含一次了。
1 | // std::begin / std::end example |
而用typeid获取变量名字
1 | int main() |
得到的结果如下,可见std::begin
本质上只是对int*
指针的一个封装。而如果用begin函数获取vector的迭代器,那么就会去取到vector之中封装好的迭代器,和直接使用arrv.begin()
的效果相同
1 | class std::_Vector_iterator<class std::_Vector_val<struct std::_Simple_types<int> > > |
16.std::any_of
类似的函数还有 std::for_each
函数定义
1 |
|
这个函数的作用是,根据给定的迭代器区间,遍历执行callback函数;只要callback函数返回了一次true,整个函数就会退出并返回true。否则返回false。
相当于只有迭代器区间中的所有值都不满足回调函数中的判断条件的时候,才会返回false;
使用示例
上示例
1 |
|
在上面的代码中,我们通过any_of
和lambda表达式来判断即将要进行批量相除操作的map中是否包含不满足相除条件的参数。可以看到,这之中有一对的除数是0,这是不符合除法的规则的。而判断条件就是判断第二个除数是否为0,所以any_of理应报错;
运行结果如下,符合预期
1 | args error |
如果将出错的哪一行注释掉,那么就不会报错
1 | all good to div |
17.std::atomic::compare_exchange_strong
1.说明
之前只接触过 std::atomic
,它是C++内置提供的一个模板类,帮我们封装了访问的原子性。
一般情况下,我们只会在数字变量上使用 std::atomic
,这时候对数据的修改操作就不需要我们自己加锁解锁了,std底层会自动封装锁的操作。
在 std::atomic
中还有两个函数,分别是 compare_exchange_strong
和 compare_exchange_weak
;
这两个函数的入参和功能都相同,我们主要关注的是前两个入参
- 和第一个入参except进行比较,如果原子量和该参数相同,则将原子量改成第二个参数,并返回true
- 如果原子量和第一个参数不同,则将第一个参数改为当前原子量的值,返回false
第一个入参是引用传参,是一个输入输出参数。在原子量和第一个参数不同的时候,这个参数会被修改。
1 | if(!x.compare_exchange_strong(0,10)){ |
为了避免上述情况中的线程安全问题,compare_exchange
函数会在原子量和第一个参数不一致的时候,通过第一个参数输出当前的原子量。这样就能保证线程安全。所以如果你需要获取到原子量的当前值,请不要给第一个参数传入右值,传入可修改的左值;
2.compare_exchange_strong的使用示例
1 | State cur_state = State::Ready; |
3.weak和strong的区别
C++ 中 std::atomic 类型的 compare_exchange 应该选择哪个版本? - 知乎 (zhihu.com)
C++11:原子交换函数compare_exchange_weak和compare_exchange_strong-CSDN博客
weak版本的CAS允许偶然出乎意料的返回(比如在字段值和期待值一样的时候却返回了false),不过在一些循环算法中,这是可以接受的。通常它比起strong有更高的性能。
而strong的返回值能保证它是绝对符合预期的。
结语
本篇超长的博客到这里就结束辣!
其实C++11还有其他的新特性,但是那些我会单开一篇文章来写~