C++11核心特性深度解析:从列表初始化到lambda与包装器

一、C++11

1.1 列表初始化

C++98 传统的{}一般数组和结构体可以用{}进行初始化

C++11中的{}

C++11以后想统一初始化方式,试图实现一切对象皆可用{}初始化,{}初始化也叫做列表初始化

内置类型支持,自定义类型也支持自定义类型本质是类型转换,中间会产生临时对象(依赖于构造函数),最后优化了以后变成直接构造

{}初始化的过程中,可以省略掉=

C++11中的 std::initializer_list

上面的初始化已经很方便了,但是对象容器初始化还是不太方便,比如一个 vector 对象,我想用N个值去构造初始化,那么我们得实现很多个构造函数才能支持。所以这是很不方便的

C++11库中提出了一个 std::initializer_list 的类这个类的 本质是底层开一个数组,将数据拷贝过来,std::initializer_list 内部有两个指针分别指向数组的开始和结束

std::initializer_list 支持迭代器遍历

容器支持一个 std::initializer_list 的构造函数也就支持任意多个值构成的{x1,x2,x3...}进行初始化。STL中的容器支持任意多个值构成的{x1,x2,x3...}进行初始化就是通过 std::initializer_list 的构造函数支持的

1.2 右值引用和移动语义

C++98的C++语法中就有引用的语法而C++11中新增了 右值引用语法的特性我们之前学习的引用就叫做 左值引用无论是左值引用还是右值引用,都是给对象取别名

那什么是右值引用呢?左值引用和右值引用有什么区别呢

左值是一个表示数据的表达式(如变量名或解引用的指针),一般是有持久状态,存储在内存中,我们可以获取它的地址

左值可以出现在赋值符号的左边,也可以出现在赋值符号的右边定义时 const 修饰符后的左值,不能给它赋值,但是可以取它的地址

右值也是一个表示数据的表达式要么是字面值常量,要么是表达式求值过程中创建的临时对象等右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边右值不能取地址

总结左值和右值的核心区别在于能否取地址

接下来,我们来看一下哪些是左值,哪些是右值。

. 左值引用不能直接引用右值,但是 const 左值引用可以引用右值

. 右值引用不能直接引用左值,但是右值引用可以引用 move(左值)

move 是库里面的一个函数模板本质内部是进行强制类型转换,还涉及到了引用折叠

注意变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值

. 引用延长生命周期

右值引用可用于为临时对象延长生命周期, const 的左值引用也能延长临时对象生命周期,但这些对象无法被修改

. 左值和右值的参数匹配

C++98中,我们实现一个 const 左值引用作为参数的函数,那么实参传递左值和右值都可以匹配

C++11以后,分别 重载左值引用,const 左值引用,右值引用作为形参的函数,那么实参是左值会匹配左值引用,实参是 const 左值会匹配 const 左值引用,实参是右值会匹配右值引用

. 右值引用和移动语义的场景

左值引用场景回顾:

左值引用主要使用场景在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值

左值引用主要解决的是拷贝效率的问题(因为引用是取别名,不会开辟新的空间)

但是有些场景下不能传左值引用返回。比如在调用了某个函数之后,该函数返回的是一个局部对象,此时就不能传引用返回了,否则就是野引用问题了

那左值引用不能解决这个问题,右值引用可以吗?

也不能因为右值引用也不能否认该局部对象在函数调用结束之后被销毁的事实

. 移动构造和移动赋值

移动构造函数也是一种构造函数,类似拷贝构造函数移动构造函数要求第一个参数是该类类型的引用,而且是右值引用如果还有其它参数额外的参数必须有缺省值

移动赋值是一个赋值运算符的重载,它跟拷贝赋值构成函数重载,类似拷贝赋值函数移动赋值函数要求第一个参数是该类类型的引用,这个参数也必须是右值引用

对于像 string/vector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义因为移动构造和移动赋值的第一个参数都是右值引用的类型,它的本质就是要"窃取"引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从而提高效率

接下来看看移动构造和移动赋值如何使用

1.3 类型分类

C++11以后,进一步对类型进行了划分,右值被划分为纯右值和将亡值

那什么是纯右值,什么是将亡值呢

纯右值指那些字面值常量或求值结果相当于字面值或是一个不具名的临时对象C++11中的纯右值概念等价于C++98中的右值

将亡值指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达如 move(x),static_cast<X&&>(x)

泛左值包括将亡值和左值

1.4 引用折叠

C++中不能直接定义引用的引用,如 int& && r = i,这样写会报错但是通过模板或 typedef 中的类型操作可以构成引用的引用

所以,这个时候,为了解决引用的引用的问题 ,就有了引用折叠

那引用折叠是怎么折叠的呢

右值引用的右值引用折叠成右值引用,其它所有组合均折叠成左值引用

1.5 完美转发

什么是完美转发?完美转发是用来干什么的

完美转发 forward 本质是一个函数模板,主要是通过引用折叠的方式来实现传递的实参是右值,T被推导成 int(以整型为例),没有折叠,forward 内部 t 被强转为右值引用返回,传递的实参是左值,T被推导成 int&,引用折叠为左值引用,forward内部 t 被强转为左值引用返回

cpp 复制代码
template <class T> 
T&& forward (typename remove_reference<T>::type&
arg);

完美转发可以保持原有对象的属性

1.6 可变参数模板

C++11支持可变参数模板也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包存在两种参数包:模板参数包,表示0或多个模板参数;函数参数包:表示0或多个函数参数

我们用 省略号来指出一个模板参数或函数参数表示的一个包在模板参数列表中class ... 或 typename ...指出接下来的参数表示0或多个类型列表在函数参数列表中,类型名后面跟... 指出接下来表示0或多个形参对象列表;函数参数包可以用左值引用或右值引用表示跟前面普通模板一样每个参数实例化时遵循引用折叠规则

可变参数模板的原理跟模板类似本质还是去实例化对应类型和个数的多个函数

我们可以用 sizeof... 运算符去计算参数包中参数的个数

cpp 复制代码
// 原理:编译本质这⾥会结合引⽤折叠规则实例化出以下四个函数
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);

. 包扩展

对于一个参数包,我们除了能计算它的参数个数,唯一能做的事情就是扩展它就是取出参数包里的数据

cpp 复制代码
// 本质可以理解为编译器编译时,包的扩展模式
// 将上⾯的函数模板扩展实例化为下⾯的函数
void Print(int x, string y, double z)
{
	Arguments(GetArg(x), GetArg(y), GetArg(z));
}

1.7 新的类功能

原来的C++类中,有6个默认成员函数C++11新增了两个默认成员函数移动构造函数和移动赋值运算符重载

如果没有显示的实现移动构造函数,且没有实现析构函数,拷贝构造,拷贝赋值重载中的任意一个那么编译器会自动生成一个默认移动构造

默认生成的移动构造函数对于内置类型成员会执行逐成员按字节拷贝自定义类型成员,需要看这个成员是否实现了移动构造如果实现了就调用移动构造,没有实现就调用拷贝构造

移动赋值运算符重载函数与移动构造类似对于自定义类型成员,看这个成员是否实现了移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值

如果显示的实现了移动构造和移动赋值,编译器不会自动提供拷贝构造和拷贝赋值

. default、delete

default 关键字显示指定函数生成,delete关键字显示指定函数删除

1.8 lambda表达式语法

lambda 表达式本质是一个匿名函数对象,跟普通函数不同的是它可以定义在函数内部

lambda 表达式语法使用层而言没有类型所以一般是用 auto或者模板参数定义的对象去接收 lambda 对象

cpp 复制代码
lambda 表达式的格式:[capture-list](parameters)->return type{
	function body
	}

[capture-list]:捕捉列表,该列表总是出现在 lambda 函数的开始位置编译器根据\[\]来判断接下来的代码是否为 lambda 函数捕捉列表能够捕捉上下文中的变量供 lambda 函数使用,捕捉列表可以传值和传引用捕捉。捕捉列表可以为空,但不能省略

(parameters):参数列表,跟普通函数的参数列表类似如果不需要参数传递,则参数列表可以省略不写

return type:返回值类型没有返回值,这部分可省略返回值类型明确的情况下,也可省略,由编译器对返回类型进行推导

function body:函数体跟普通函数完全类似除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略

. 捕捉列表

lambda 表达式中默认只能用 lambda 函数体和参数中的变量如果想使用外层作用域中的变量就需要进行捕捉

捕捉方式分为两种值捕捉和引用捕捉

这两种捕捉方式有什么区别呢

区别在于值捕捉本质是一种拷贝,且被 const 修饰,不能被修改。而引用捕捉可以修改,会影响到变量本身的值

也可以混合捕捉,既值捕捉也可以引用捕捉但是对于同一个变量不能既值捕捉又引用捕捉

还有一种比较方便的使用方法=表示隐式的值捕捉,&表示隐式引用捕捉,这样 lambda 表达式中用了哪些变量,编译器就会自动捕捉哪些变量

当然了,隐式值捕捉和隐式引用捕捉也是可以混合使用的[=,&x]表示其它变量隐式值捕捉,x引用捕捉;[&,x,y]表示其它变量引用捕捉,x,y值捕捉。当使用混合捕捉时,第一个元素必须是 &或=

lambda 表达式如果在函数局部域中它可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉,lambda 表达式中可以直接使用

这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空

1.9 lambda 的原理

范围 for 的底层是迭代器,而 lambda 的底层是仿函数对象也就是说我们写了一个 lambda 以后,编译器会生成一个对应的仿函数的类

仿函数的类名是编译按一定规则生成的,保证不同的 lambda 生成的类名不同lambda 参数/返回类型/函数体就是仿函数 operator()的参数/返回类型/函数体,lambda 的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参

1.10 包装器

. function

cpp 复制代码
template <class T>
class function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;

std::function 是一个类模板,也是一个包装器。std::function 的实例对象可以包装存储其它的可调用对象,包括函数指针,仿函数,lambda,bind表达式等存储的可调用对象被称为 std::function 的目标若 std::function 不含目标,则称它为空

函数指针,仿函数,lambda 等可调用对象的类型各不相同std::function 的优势就是统一类型,对他们都可以进行包装,这样在很多地方就方便声明可调用对象的类型

. bind

cpp 复制代码
simple(1)
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 是一个函数模板,它也是一个可调用对象的包装器,可以把它看做一个函数适配器,对接收的 fn 可调用对象进行处理后返回一个可调用对象。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为第二个参数,以此类推。_1/_2/_3....这些占位符放到placeholders的一个命名空间中

相关推荐
JSMSEMI111 小时前
JSM12N60C 600V N沟道增强型功率MOSFET
开发语言·javascript·ecmascript
设计师小聂!1 小时前
Java异常处理
java·开发语言·后端·编辑器·idea
清水白石0081 小时前
从打印对象到高质量调试:彻底理解 Python 中 `__repr__` 和 `__str__` 的区别
开发语言·python
枕星而眠1 小时前
C++ 面向对象核心机制深度解析:多态性、虚函数、虚继承与 final 类
运维·开发语言·c++·后端
智者知已应修善业2 小时前
【51单片机8个LED,已经使用了D1D2,怎么样在不动D1D2的前提下实现D6~D8的流水灯】2024-1-19
c++·经验分享·笔记·算法·51单片机
Evand J2 小时前
【MATLAB例程】自适应渐消扩展卡尔曼滤波(AFEKF)三维雷达目标跟踪|效果已调优,附下载链接和运行结果,代码直接运行即可
开发语言·算法·matlab·目标跟踪·卡尔曼滤波·自适应滤波·代码定制
坚果派·白晓明2 小时前
鸿蒙PC适配实战:simdjson 三方库移植攻略与 AtomCode Skills 提效之道
c++·harmonyos·三方库·skills·atomcode·c/c++三方库·c/c++三方库适配
爱装代码的小瓶子2 小时前
3. 设计buffer模块
linux·服务器·开发语言·c++·php
郝学胜-神的一滴2 小时前
Qt 高级开发 027: QTabWidget自定义样式表美化实战
开发语言·c++·qt·程序人生·软件构建·用户界面