当前位置: 首页 > news >正文

【c++】c++11(一)列表初始化,右值引用和移动语义

hello~ 很高兴见到大家! 这次带来的是C++中关于C++11这部分的一些知识点,如果对你有所帮助的话,可否留下你宝贵的三连呢?
个 人 主 页: 默|笙

在这里插入图片描述

文章目录

  • 一、C++11
  • 二、列表初始化
    • 1. c++98和c++11里的{}
    • 2. 与C++11里的std::initializer_list区分
  • 三、右值引用和移动语义
    • 3.1 左值和右值
    • 3.2 左值引用和右值引用
    • 3.3 延长生命周期
    • 3.4 左值和右值的参数匹配
    • 3.5 右值引用和移动语义的使用场景
      • 1. 左值引用使用场景
      • 2. 右值引用使用场景---移动构造和移动赋值
    • 3.6 类型分类
    • 3.7 引用折叠
    • 3.8 完美转发

一、C++11

  1. c++11是c++发展以来的第二个主要版本,是从c++98开始的最重要的更新。
  2. 之前的博客接触到的都是c++98最开始的版本所涉及的内容,接下来会讲解C++11里面用的最多也是最重要的语法。

在这里插入图片描述

二、列表初始化

1. c++98和c++11里的{}

  1. C++98里传统的{}一般只能用于数组和结构体的构造,而到了C++11,它几乎能实现一切对象的初始化。{}初始化也被称为列表初始化。
  2. 内置类型支持,自定义类型同样支持,它的本质是类型转换(构造 + 拷贝构造),中间会产生临时对象,之后编译器优化变成了直接构造。
  3. {}初始化的过程中,可以直接省略掉=,不过习惯上还是加上=。
  4. 对于容器的insert/push操作就会变得很方便,不需要构造对象或匿名对象传递,可以之间用{}初始化之后直接传递。
struct Point
{int _x;int _y;
};class Date
{
public:Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){cout << "Date(int year, int month, int day)" << 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;
};int main()
{//c++98int a[] = { 1, 2, 3, 4, 5 };int a2[5] = { 1, 2, 3, 4, 5 };//数组Point p = { 1, 2 };//结构体//c++11int x1 = { 2 };int x2{ 2 };//可以不带等号Date d1 = { 2025, 1, 1 };const Date& d2 = { 2025, 1, 2 };//引用的是{2025,1,2}构造的临时对象Date d3 = { 2025 };Date d4 = 2025;//C++98支持单参数隐式转换,可以不用{},这个是不能省略=号的vector<Date> v;v.push_back(d1);v.push_back({ 2025, 1, 1 });//可以直接用{}初始化后传参return 0;
}

2. 与C++11里的std::initializer_list区分

  1. 可能会存在initializer_list构造函数和{}列表初始化分不清的情况,其实很好区分,它们有一个本质的区别。{}列表初始化是一种语法,而initializer_list是一种语义。

在这里插入图片描述

  1. initializer_list构造(必须要有initializer_list构造函数)是根据构造函数的参数传递参数,它所传递的实参跟普通构造函数的形参是一一对应的关系(initializer_list构造函数只有一个形参,但这个形参可以包含任意数量的值,这个值要和普通构造函数一一对应)。

  2. 而{}列表初始化则与构造函数类型无关,它能够调用任意普通构造函数,优先会考虑initializer_list构造,如果没有,那么就会去找更加匹配的构造函数。

  3. initializer_list底层的实现是两个指针,一个指向存储数据的数组(一个临时数组)的开始,一个指向数组的结尾

在这里插入图片描述

auto il = { 1,2,3,4,5,6,7,8,8 };
cout << sizeof(il) << endl;
cout << typeid(il).name() << endl;

在这里插入图片描述

  1. 64位系统里指针的大小是8个字节,两个就是16个字节大小。

  2. 很多时候我们都是initializer_list构造和{}列表初始化调用其他构造函数混合构造。比如:这是因为pair类型没有initializer_list构造,而STL容器都有initializer_list构造。

map<int, int> m = { {1, 1}, {2, 2} };
//pair类型:{}列表调用其他的构造函数初始化pair类型,再通过initializer_list(外面的这个{})整个构造。

三、右值引用和移动语义

C++11增加了右值引用语法特性,在这之前学到过的引用叫做左值引用。无论是左值引用还是右值引用,它们的作用都是给对象起别名。

3.1 左值和右值

  1. 左值是一个有明确内存地址,通常有变量名,声明周期超出当前语句的表达式,也就是有持久状态,不会马上销毁。我们可以用&获取它的地址,定义时const修饰后的左值虽然不能给它赋值,但是可以取它的地址。左值可以在赋值符号的左边,也可以在右边。
  2. 右值也是一个表达式,但它不是一个有持久内存地址的表达式,而是一个临时性的,声明周期短暂的表达式,通常也没有变量名与其绑定。比如字面量常量10,2;比如表达式求值过程中产生的临时变量。右值可以出现在赋值符号的右边,但不能出现在左边,右值不能取地址
  3. 左值和右值最本质的区别就是能否取地址
  4. 左值的英文简写为lvalue,右值的英文简写为rvalue。传统认为它们分别是leftvalue、rightvalue的缩写。在现代C++中,lvalue被解释为loactor value的缩写,可意为存储在内存中,有明确地址可以取地址的对象,而rvalue被解释为 read value,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量,字面量常量,存储于寄存器中的变量等,也就是说左值和右值的核心区别就是能否取地址。
int* p = new int(0);//指针变量p
cout << &p << endl;
int b = 1;//普通变量b
cout << &b << endl;
const int c = b;//const变量c
cout << &c << endl;
*p = 10;//指针解引用
cout << &*p << endl;
string s("123");//string类对象s
cout << &s << endl;
s[0] = 'x';//类成员访问(另类指针解引用)
cout << (void*) & s[0] << endl;
//&s[0]是char*类型,编译器会对其特殊处理,将其视为c风格字符串进行打印
//所以要强转成其他类型如void*

在这里插入图片描述

10;//字面量常量
x + y;//运算产生临时变量
fmin(x, y);//函数返回值
string("111");//匿名对象

在这里插入图片描述

  1. 这些都是右值,因为它们都无法取到地址。string(“111”)也是不行的。

3.2 左值引用和右值引用

  1. 左值引用只有一个&,右值引用则有两个&,为&&,如:Type& l1 = x, Type&& r1 = y。第一个为左值引用,也就是给左值x取别名,第二个是右值引用,也就是给右值y起别名。无论是左值引用还是右值引用它们的作用都是起别名
  2. 左值引用不能够直接引用右值,但const左值引用可以引用右值
  3. 右值引用不能够直接引用左值,得用move强转一下(类似于强转为Type&&),也就是可以引用move(左值)
  4. move它是库里面的一个函数模板,它的功能就是实现强制转换,不过它还涉及一些引用折叠的知识。
  5. 需要注意的是,变量表达式全部都是左值属性,也就意味着一个右值被右值引用绑定之后,这个右值引用变量表达式的属性是左值属性
  6. 左值引用和右值引用都是取别名,它们的底层也都是通过指针实现的。
double x = 1.1, y = 4.4;int&& rr1 = 10;//右值引用右值
int& r1 = 10;//左值引用不能够直接引用右值
const int& cr1 = 10;//const左值引用能够引用右值double&& rr0 = x;//右值不能引用左值
double&& rr00 = move(x);//可以将x用move处理一下,就能被右值引用取别名了
double&& rr000 = (double&&)x;//其实move和这里的强转操作目前可以认为是等价的double&& rr2 = x + y;
double& r2= x + y;
const double& cr2 = x + y;double&& rr3 = fmin(x, y);
double& r3 = fmin(x, y);
const double& cr3 = fmin(x, y);string&& rr4 = string("111");
string& r4 = string("111");
const string& cr4 = string("111");

在这里插入图片描述

3.3 延长生命周期

  1. 右值引用的作用之一就是延长右值的生命周期,可以将它们绑定,虽然左值const引用也可以延长这些右值的生命周期,但是左值const引用的右值是无法被修改的,而右值引用却可以被修改。
string s1("111");
const string& s2 = s1 + s1;
s2 += "2";string&& s3 = s1 + s1;
s3 += "2";

在这里插入图片描述

3.4 左值和右值的参数匹配

  1. 在C++98中,我们很多的函数参数类型都是一个const左值引用,**这样不仅能匹配左值,也能够匹配右值,只是不能通过改变形参来改变实参。
  2. 但是自C++11以后,如果分别重载左值引用,const左值引用,右值引用作为形参的func函数,那么如果传递的实参是左值的话,会匹配左值引用func函数,const 左值会匹配const左值引用func函数,右值会匹配右值引用func函数。
void func(int& x)
{std::cout << "左值引用重载 f(" << x << ")\n";
}
void func(const int& x)
{std::cout << "const的左值引用重载 f(" << x << ")\n";
}
void func(int&& x)
{std::cout << "右值引用重载 f(" << x << ")\n";
}int main()
{int x = 1;const int y = 2;func(x);func(y);func(10);func(move(x));return 0;
}

在这里插入图片描述

  1. 对于左值变量x,传递x调用的是形参类型为左值引用(int&)的函数;对于const左值变量y,传递y调用的是形参类型为const左值引用(const int&)的函数;对于右值字面常量10,传递10调用的是形参类型为右值引用(int&&)的函数,如果没有右值引用func函数,那么传递10和move(x)都会调用形参类型为const左值引用func函数。
  2. 总而言之,调用func函数会匹配形参与它传递的实参更合适的函数。

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

1. 左值引用使用场景

  1. 左值引用作为形参:减少拷贝,并且可以修改实参。
  2. 左值引用作为返回值:减少拷贝,并且可以返回对象本身(避免返回临时对象),同时允许对返回的对象进行修改。
  3. const左值引用作为形参:减少拷贝,并且可以接受左值和右值,但不能修改实参。在C++98中就已经支持,因此可以用于实现泛型参数传递(即既可以传递左值也可以传递右值)。
  4. 可以总结为三点:减少拷贝,能够通过形参影响实参,const左值引用的广泛匹配能力

2. 右值引用使用场景—移动构造和移动赋值

  1. 移动构造函数是一种构造函数,它类似于构造函数,移动构造函数要求第一个参数是该类类型的右值引用,它允许有除此之外的其他参数,不过必须要有缺省值
  2. 移动赋值是一个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的右值引用
  3. 移动构造和移动赋值只对像vector/string这样的实例化出来的对象含有资源,拷贝对象需要深拷贝的类才有意义。它与构造函数和普通赋值重载函数根据传递过来的实参对这个形参的空间进行深拷贝不同,它是直接将这个对应的右值实参的空间和资源抢夺过来,而不是去老实深拷贝,从而提高效率。

在这里插入图片描述

  1. 为什么它是直接抢夺这些右值的的空间和资源?这是因为右值的生命周期很短,抢夺之后不会对右值产生什么影响,右值本来就是马上会销毁掉的。而对于左值是不能这样干的,因为左值是能够长久存在,以后可能会用得上的表达式,如果左值的空间和资源被抢走,会对之后程序对这个左值的使用产生影响。

  2. 这里有一个实现了移动构造和移动赋值重载的string类(省略):

mosheng::string s1("xxxxxxx");
mosheng::string s2 = s1;
mosheng::string s3 = move(s1);

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

  1. 上面两幅监视窗口的图是关于move(s1)(强制转换为右值)移动构造的,可以看到,通过移动构造s3窃取了s1的所有资源,因为s3目前的地址与原来s1的地址一致。而s1则失去了全部的资源,这也意味着对于左值进行移动构造是不好的,因为如果程序后续还有调用s1的可能。
// 移动构造
string(string&& s)
{cout << "string(string&& s) -- 移动构造" << endl;std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}
  1. 移动构造和移动赋值重载的底层实现是与右值的资源进行一个交换,能交换的原因:s虽然是所匹配的右值实参的别名,但是它的属性是左值,我们可以通过s来改变右值,也就可以将它的资源抢夺过来(进行一个交换)。
    在这里插入图片描述

  2. 这里的str会被编译器自动识别为右值(出,用ret去接收传回的str的拷贝,如果没有移动构造,这里会调用两次拷贝构造函数,一次拷贝到临时对象(右值),一次拷贝到ret;如果有移动构造,这里会调用两次移动构造,过程类似拷贝构造。编译器第一层优化:传值返回的时候会将两次移动构造优化为一次(vs2019debug),省略构造临时对象,第二层优化:编译器识别到str的地址和ret的地址是一样的,那么它会直接构造ret,省略移动构造。

在这里插入图片描述

  1. 这里更改一下代码,先构造一个ret,然后给ret赋值。str被编译器识别为右值,会调用移动构造构造出临时对象(右值),然后通过移动赋值赋值给ret。编译器可能会优化为一次移动赋值。

3.6 类型分类

  1. C++11以后,进一步对类型进行了划分,将右值划分为纯右值和将亡值。
  2. 将亡值指的是那些由左值通过强制转换得到的右值表达式,如move(x)、static_cast<X&&>(x)。将亡值具有标识符,也就是有名字
  3. 纯右值是指本身就是右值的表达式,包括字面量常量、匿名对象、函数按值返回的结果等。纯右值没有标识符,也就是没有名字
  4. 由于将亡值是由左值转换来的,所以将亡值和左值一起又被合称为泛左值。

在这里插入图片描述

3.7 引用折叠

  1. C++中不能直接定义的引用的引用比如int& &&r = i,会直接报错,而通过模板或者是typedef中的引用可以构成引用的引用,它们会进行引用折叠,折叠成左值引用或右值引用。

在这里插入图片描述

  1. 右值引用的右值引用折叠之后才会是右值引用,与它的左值引用折叠是左值引用。左值引用无论是与它的左值引用还是右值引用折叠最后都会是左值引用
  2. r2的类型是int&,r3是int&,r4是int&&,r5是int&,只有右值引用的右值引用折叠出来的才会是右值引用。
//由于引用折叠,左值引用无论是跟它的左值引用还是右值引用折叠,结果都是左值引用
template<class T>
void f1(T& x)
{}
//右值引用的右值用折叠是右值引用,跟它的左值引用折叠是左值引用,可以折叠出两种结果,也叫做万能引用
template<class T>
void f2(T&& x)
{}int main()
{int n = 0;//传递左值,T为int类型,实例化为void f1(int& x)f1<int>(n);//传递的右值,int& 无法匹配右值,编译报错//f1<int>(0);//传递左值,T为int&类型,折叠之后还是左值引用,实例化为void f1(int& x)f1<int&>(n);//传递右值,int& 无法匹配右值,编译报错//f1<int&>(0); //传递左值,T为int&&类型,折叠之后还是左值引用,实例化为void f1(int& x)f1<int&&>(n);//传递右值,int& 无法匹配右值,编译报错//f1<int&&>(0); //T为const int&类型,折叠之后为const int&,实例化为void f1(const int& x),左值和右值都可以匹配上f1<const int&>(n);f1<const int&>(0);//T为const int&&类型,折叠之后为const int&,实例化为void f1(const int& x),左值和右值都可以匹配上f1<const int&&>(n);f1<const int&&>(0);//传递左值,T为int类型,实例化为void f2(int&& x),int&& 无法匹配左值,编译报错//f2<int>(n); f2<int>(0);//T为int&类型,左值引用的右值引用折叠后为左值引用,实例化为void f2(int& x)。左值能匹配,右值报错f2<int&>(n);//f2<int&>(0); //T为int&&类型,右值引用的右值引用折叠后为左值引用,实例化为void f2(int&& x)。//f2<int&&>(n);f2<int&&>(0);return 0;
}
  1. 在实际使用过程中,就是模板的自动推导类型,这里是显式给定的类型。
  2. 像f2这样的模板,为了能够使左值和右值都可以使用这个函数,实现泛用,所以传左值和左值引用T会推导为左值引用类型,折叠成左值引用,传右值T推导为非引用类型,没有折叠,就是右值引用。有些地方也把这种函数模板的参数叫做万能引用。
  3. 切记,万能引用折叠的前提一定得是函数模板,它必须得在传递实参的时候进行类型推导,类模板是行不通的,因为在定义对象的时候,类型就已经被推导出来了

3.8 完美转发

  1. 对于上面的f2函数,传左值实例化以后是左值引用的f2函数,传右值实例化以后是右值引用的f2函数。
  2. 而变量表达式都是左值属性,这也就意味着我们传过去的实参无论是左值还是右值都会变成右值属性的形参。如果我们我们用这个形参去调用其他的万能引用函数,最后函数实例化出来的都是左值引用的函数。如果move一下,那么函数实例化出来的都是右值引用的函数。这两种都不符合我们的期望。
  3. 我们期望如果是右值那么继续保持右值的属性,左值继续保持左值的属性。这样就不会影响之后的调用。于是,完美转发forward应运而生。

在这里插入图片描述

  1. std::forward 本质上是一个函数模板,它通过引用折叠机制实现完美转发。其模板参数 T 来自于外层函数模板(如下面的Function函数模板参数T)的类型推导结果。forward 利用这个推导出的 T 类型来判断原始实参的值类别,并通过返回类型的引用折叠来保持原始值类别。

在这里插入图片描述

  1. 倘若不使用forward,t会是左值属性,调用的Fun将会一直是匹配左值的函数。
    在这里插入图片描述

在这里插入图片描述

  1. 若使用forward,左值和右值都将保持它们的属性,传递右值10,T被推导为int,没有折叠,forward内部t被强转为右值引用返回;传递左值i,T被推导为int&,引用折叠为左值引用,forward内部被强转为左值引用返回。

今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~
让我们共同努力, 一起走下去!

http://www.dtcms.com/a/526479.html

相关文章:

  • 自己买主机可以做网站吗杭州seo网站哪家好
  • 娄底公司网站建设现在c 做网站用什么软件
  • IT疑难杂症诊疗室技术文章大纲
  • 3347. 执行操作后元素的最高频率 II
  • 查网站流量的网址网站开发实用案例教程
  • 中山网络公司网站猪八戒wordpress
  • 电子商务网站开发 ppt呼市网站设计
  • 南联网站建设吉林市网站建设
  • 福州网上商城网站建设公司装修孕妇怎么办
  • 网站二级域名怎么做群晖wordpress 外网
  • 基础设施建设网站个人网站论文设计内容简介
  • 建设网站要买空间吗跨境电商平台开发
  • 深圳建设网站公司排名泉州网红打卡地
  • 安徽省公共资源交易中心网站小制作小发明做法视频
  • 网站备案拍照背景湖南seo优化公司
  • 宣城网站seo易云自助建站
  • 社交网络平台系统建模设计
  • 网站建设上网站建设会议记录
  • 网站跟网页的区别是什么做网站那些好
  • 建设部职业资格注册网站大气企业网站织梦模板
  • 苏州企业管理咨询服务惠州seo代理商
  • 参数曲线切向量与叉乘向量的精确计算与分析
  • 怎么做电商卖东西网站seo外包服务
  • 社团网站模板简述网站建设及维护全过程
  • 马鞍山的网站建设公司制作海报
  • 电商平台网站定制网络规划设计师教程读后感
  • 合肥做网站的公司百度广告公司名字 三个字
  • 婚恋网站建设项目创业计划书中国建设银行官网站纪念币预约
  • 合肥专业网站建设公司哪家好竞价推广运营
  • 东风地区网站建设价格低wordpress 博客多人