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

C++进阶--C++11

文章目录

  • C++进阶--C++11
    • 列表初始化
      • C++98的传统{}
      • C++11的{}
      • C++11中的initializer_list
    • 右值引用和移动语义
      • 左值和右值
      • 左值引用和右值引用
    • 左值和右值的参数匹配
    • 右值引用和移动语义的应用场景
      • 左值引用的使用场景
      • 移动构造和移动赋值
    • 结语

很高兴和大家见面,给生活加点impetus!!开启今天的编程之路!!
在这里插入图片描述
今天我们学习C++11的内容,进一步学习C++的重要变革
作者:٩( ‘ω’ )و260
我的专栏:C++进阶,C++初阶,数据结构初阶,题海探骊,c语言
欢迎点赞,关注!!

C++进阶–C++11

列表初始化

C++98的传统{}

在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++98中,{}其实还可以用来使用隐式类型转换,在c++11中,也能够进行隐式类型转换,这里我们放到后面来讲。

C++11的{}

C++11以后想统⼀初始化方式,试图实现一切对象皆可用{}初始化,{}初始化也叫做列表初始化
内置类型支持,自定义类型也支持,自定义类型本质是类型转换,中间会产生临时对象,最后优化了以后变成直接构造
{}初始化的过程中,可以省略掉=
C++11列表初始化的本意是想实现⼀个大统⼀的初始化方式,其次他在有些场景下带来的不少便利,如容器push/inset多参数构造的对象时,{}初始化会很方便

我们直接来看代码实现:

// C++98⽀持的
int a1[] = { 1, 2, 3, 4, 5 };
int a2[5] = { 0 };
Point p = { 1, 2 };
//c++11支持的
//内置类型支持,可以将=去掉
Point p = { 1, 2 };
Point p1 { 1, 2 };int a={10};
int a{10};class Date{
public:Date(int year=1,int month=1,int day=1):_year(year),_month(month),_day(day){}
private:int _year;int _month;int _day;
}
//自定义类型支持,可以将=去掉
//本质上是使用{2025,5,20}来构造一个临时对象,d1来拷贝构造这个临时对象,优化后直接构造
Date d1 = {2025,5,20};//多参数
Date d2{2025,5,20};
// 需要注意的是C++98⽀持单参数时类型转换,也可以不⽤{}
Date d3 = { 2025};//单参数
Date d4 = 2025;
//不能写成Date d4  2025;必须使用{}来初始化的时候,才能将=给去掉
// 这里d10引用的是{2025,5,20}构造的临时对象
const Date& d10 = {2025,5,20};//隐式类型转换为Date,会创建一个临时对象,引用的是一个临时对象,所以需要添加constvector<Date> v;
v.push_back(d1);
v.push_back(Date(2025, 1, 1));
// ⽐起有名对象和匿名对象传参,这⾥{}更有性价⽐
v.push_back({ 2025, 1, 1 });

注意:只有当是使用{}来初始化类型的时候,=才可以去掉

C++11中的initializer_list

学了上面的知识点之后,我们来观察一下:

Date d1 = {2025,5,20};
Date d2 = {2025};vector<int> ={1,2,3,4,5,6,7,8,9};//可以写无数个

这三句代码,前面的d1和d2可以使用0到3个实参来初始化成员变量,因为进入初始化列表的时候我传递了缺省值。
但是我使用{}来初始化vector的时候是可以写无数个整形的,难道说这里我需要传递无数个缺省参数吗?

注意:虽然我们这里都是写的{}来初始化变量,但是底层不同。
前者我调用隐式类型转换使用实参来构造一个Date,中间会生成一个临时对象,d1拷贝构造这个临时对象。
后者我使用initializer_list,本质上是使用initializer_list来初始化vector

c++11之后,每一个容器都能够使用initializer_list来构造,而且,c++11中新增了initializer_list这个类
另外,c++11中容器的赋值也支持initializer_list的版本

在这里插入图片描述

所以这里的vector的初始化的过程是怎样的呢?
首先,编译器会将{…}识别为一个initializer_list,随后将这一坨隐式类型转换为一个initializer_list,使用这个initializer_list来初始化这个vector。

initializer_list的底层是什么?
initializer_list底层其实是两个指针,在vs上,会现将{…}中的数据拷贝到同等规模的数组之中,在msvc编译器上,这个数组是在栈中的,initializer_list类中的成员变量是first和end指针(地址连续的数据结构下,指针等价于迭代器),指向栈中数组的开始和结束位置的下一个位置

我们在编译器上验证一下:
在这里插入图片描述
接下来写一段代码验证是在栈上的:

int main()
{auto il1 = { 10, 20, 30 };initializer_list<int> il2 = { 10, 20, 30,5,5,5,5,5,5,5, };cout << il1. begin() << ":" << il1.end() << endl;int x[3] = { 1,1,1 };cout << x << endl;const char* str = "xxxxxxx";cout << (void*)str << endl;return 0;
}

在这里插入图片描述
这里的begin()和end()的位置和栈上的位置距离较近,离常量区(代码段)距离较远,所以肯定是在栈上。

接下来我们来看几个示例:

// {}列表中可以有任意多个值
// 这两个写法语义上还是有差别的,第⼀个v1是直接构造,
// 第⼆个v2是构造临时对象+临时对象拷⻉v2+优化为直接构造
//第三个引用的是临时对象,所以需要添加上const
vector<int> v1({ 1,2,3,4,5 });
vector<int> v2 = { 1,2,3,4,5 };
const vector<int>& v3 = { 1,2,3,4,5 };//隐式类型转换构造一个vector再来进行赋值
//若要实现,肯定函数头是这个(为了能够连续赋值,肯定需要返回这个类)
vector& operator= (initializer_list<value_type> il);
map& operator= (initializer_list<value_type> il);
v1 = {9,9,9,9,9};//在类中还可以这样
//构造一个vector,使用范围for来进行插入
vector(initializer_list<T> l)
{for (auto e : l)push_back(e)
}

右值引用和移动语义

c++98我们就学了引用,c++11中这个其实叫左值引用,我们还会学习右值引用。只要是引用,都是给对象取别名

左值和右值

我们需要理清常见的左值和右值有哪些?以及左值和右值的概念是什么?

左值和右值都是一个表示数据的表达式,左值可以在等号左边或者右边,被const修饰的左值只能够在等号右边,右值只能够在等号的右边。

左值:核心是左值能够取地址,一般就是存储在栈堆等位置上
右值:核心是右值不能够取地址,有些存储在寄存器上,寄存器就是没有地址的

哪些东西是左值或是右值呢?
右值:字面量常量,临时变量(传值传参,传值返回,表达式计算,隐式类型转换),匿名对象,注:字面量常量只有内置类型才有!!
左值;变量,对象

这里我们来举例几个代码来看一下:

//右值
x+y;//临时对象
10;//字面量常量
stirng("1111");//匿名对象
int test(int a)
{return a;
}//左值
int a=10;
string s;
//或者是自定义类型的对象

左值引用和右值引用

1:Type& r1 = x; Type&& rr1 = y; 第一个语句就是左值引用,左值引用就是给左值取别名,第二个就是右值引用,同样的道理,右值引用就是给右值取别名
左值只能使用左值引用,但是可以使用const修饰来引用右值
右值只能使用右值引用,但是可以使用move(左值对象)来修饰左值
注:move()是一个库中函数模版,目的是将一个左值变成一个右值,类似于强制类型转换。其实有一定缺陷,如果使用move()+移动构造或者移动赋值,会造成左值对象中的数据丢失

我们来写几个代码巩固一下知识:

int x=10;//左值
//右值引用
int&& rrm=10;
string&& rrs=stirng("1111");
int&& rrt=a+b;//a,b是之前已经定义的整形变量
int&& rrx=move(x);//右值引用左值
int&& rrfmin = fmin(x,y);//这里的fmin是c语言库中的函数//左值引用
int& rx=x;
const int& rrx=10;//左值引用右值

左值和右值的参数匹配

这里总结一句话来说,就是如果没有十分匹配的,就只能来匹配这个不是很匹配的,如果有更加匹配的,肯定会匹配更加匹配的,如果没有匹配的,就会报错。

我们来举例一下:

void f(int& x)
{
s	td::cout << "左值引⽤重载 f(" << x << ")\n";
}
void f(const int& x)
{std::cout << "到 const 的左值引⽤重载 f(" << x << ")\n";
}
void f(int&& x)
{std::cout << "右值引⽤重载 f(" << x << ")\n";
}

这三个函数构成重载,如果我们将第一和第三个函数去掉,不管是左值还是右值,我们都会调用第二个,但是我们有了第一和第三个函数,左值会调用第一个,因为更匹配,const的左值会调用第二个,因为更匹配,右值会调用第三个,因为第三个函数更加匹配。
来看示例:

int main()
{int i = 1;const int ci = 2;f(i); // 调⽤ f(int&)f(ci); // 调⽤ f(const int&)f(3); // 调⽤ f(int&&),如果没有 f(int&&) 重载则会调⽤ f(const int&)f(std::move(i)); // 调⽤ f(int&&)// 右值引⽤变量在⽤于表达式时是左值int&& x = 1;f(x);// 调⽤ f(int& x),因为x可以取地址f(std::move(x)); // 调⽤ f(int&& x)return 0;
}

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

左值引用的使用场景

左值引用的目的:在传值传参和传引用传参中减少拷贝的次数,提升代码效率,因为拷贝的时候其实拷贝的是临时对象。有些场景无法解决,如addstring函数和generate函数,只能够被迫使用输出型参数解决(即只能够在函数外面传递一个需要修改的容器)。但是,学了右值引用,我们可以使用右值引用来解决问题。
我们来看这一段代码:

// 这⾥的传值返回拷⻉代价就太⼤了
vector<vector<int>> generate(int numRows) {vector<vector<int>> vv(numRows);for (int i = 0; i < numRows; ++i){vv[i].resize(i + 1, 1);}for (int i = 2; i < numRows; ++i){for (int j = 1; j < i; ++j){vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];}}return vv;
}

假设这里numRows是100000,难道我就要重新创建100000个vector来拷贝构造吗?显然是不可能的,否则就只能在外面传一个vector<vector< int>>,但是会显得十分别扭,来看右值引用是如何解决的:

移动构造和移动赋值

移动构造也是一个构造函数,与拷贝构造不同的是,拷贝构造第一个参数是左值引用,移动构造的第一个参数必须是一个右值引用,若有其他参数,其他参数必须有缺省值
移动赋值也是一个赋值重载,与赋值拷贝不同的是,移动赋值第一个参数是右值引用,赋值拷贝第一个参数是左值引用

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

接下来我们来看代码:

string addStrings(string num1, string num2)
{string str;int end1 = num1.size() - 1, end2 = num2.size() - 1;int next = 0;while (end1 >= 0 || end2 >= 0){int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;int ret = val1 + val2 + next;next = ret / 10;ret = ret % 10;str += ('0' + ret);}if (next == 1)str += '1';reverse(str.begin(), str.end());cout << "******************************" << endl;return str;
}
bit::string ret = bit::addStrings("11111", "2222");

如果我们没有实现右值引用以及右值引用的相关操作,这里的过程是怎样的呢?

如果没有右值引用,首先,两个构造函数+传参的时候有两个拷贝构造(传值传参中间生成临时对象,num1和num2拷贝构造对应的临时对象),随后析构掉这两个临时对象,因为临时对象的生命周期只有产生临时对象的那一行。到return str这句代码时传值返回会有一个临时对象,返回值调用拷贝构造拷贝这个临时对象,函数调用完之后,该临时对象销毁,赋值操作会产生临时对象,此时ret仍然要调用拷贝构造拷贝这个临时对象,拷贝之后该临时对象被销毁。

上述过程我们发现,在str中的内容转移到ret中的时候,调用了两次拷贝构造和两次析构,共产生了两个临时对象,如果说里面是10000个vector的话,这个效率十分低下。

如果此时我书写了右值引用,以及移动赋值和移动构造,前面的位置传参num1和num2的时候产生的临时对象,之前是调用的拷贝构造,但是现在有更匹配的话调用更匹配的函数,因为临时对象是右值,会调用移动构造,移动构造的函数体就是将两个的内容进行交换。
为什么是交换呢?
首先临时对象生命周期只在代码那一行,所以在之后这个临时对象没有任何作用了,而且临时对象中的内容是我需要的,我直接将临时对象中的内容转移出来,即移动出来,这样就能够不用来拷贝拷贝构造了,而且这个swap函数是自己实习拿的。
同理:在addString函数中,str在函数结尾,执行返回代码之后str就要销毁,是可以将str看成一个临时对象,即编译器会将str给move一下,调用移动构造,将临时对象中的内容移到函数的返回值中,随后再来赋值,产生临时对象,再来调用移动构造,最终内容被转移到ret结果之中
注意:
为什么左值必须调用拷贝构造而不能调用移动构造?
我觉得是生命周期的问题,这个左值肯定是这个类实例化的对象,他的生命周期不是只有一行的。

调用移动构造,将内容转移到目的位置,不用再来进行深拷贝,进而提升了效率,在Linux环境下演示该过程:
在这里插入图片描述
在这里插入图片描述
同理:移动赋值也是如此,在赋值的时候会产生临时对象,临时对象具有常性,如果有右值引用和移动赋值的话,就会调用移动赋值。直接将内容给转移,而非将内容拷贝。
在这里插入图片描述

结语

今天的内容就分享到这里,不足之处欢迎指出,感谢大家支持!!
千淘万漉虽辛苦,吹尽狂沙始到金!!加油!!
在这里插入图片描述

http://www.xdnf.cn/news/8006.html

相关文章:

  • C++ stack对象创建、入栈、获取栈顶
  • MySQL高可用实战:PXC集群原理与部署全解析,让数据库永不宕机
  • vue页面实现table动态拆分列功能
  • 江科大TIM定时器hal库实现
  • 自定义属性面板开发指南:公开属性声明、监听回调与基础类型配置
  • Linux:缓冲区
  • BigFoot (DBM) Deadly Boss Mods
  • DL00988-稀疏增强数据transformer船舶AIS轨迹预测含完整数据集
  • 腾讯文档怎么设置多列筛选条件
  • 固定翼无人机抛投技术分析!
  • 从零基础到最佳实践:Vue.js 系列(5/10):《状态管理》
  • 11-帮助中心
  • cmd如何从C盘默认路径切换到D盘某指定目录
  • 前端之vue3创建基本工程,基本登录、注册等功能的完整过程
  • 【IC验证】systemverilog_包
  • 自由开发者计划 001:创建一个用于查看 Jupyter Notebook 的谷歌浏览器插件 Jupyter Peek
  • 常见的LLM
  • 从零基础到最佳实践:Vue.js 系列(2/10):《模板语法与数据绑定》
  • 对抗学习(AL),生成对抗网络(GAN),强化学习,RLHF
  • 【差异分析】t-test
  • React中 lazy与 Suspense懒加载的组件
  • 26、AI 预测性维护 (燃气轮机轴承) - /安全与维护组件/ai-predictive-maintenance-turbine
  • 鸿蒙电脑系统和统信UOS都是自主可控的系统吗
  • 从零开始:Python语言基础之条件语句(if-elif-else)
  • Java虚拟机栈
  • 社会工程与信息收集
  • 左手腾讯CodeBuddy 、华为通义灵码,右手微软Copilot,旁边还有个Cursor,程序员幸福指数越来越高了
  • Human Dil-HDL,使用方法,红色荧光标记人源高密度脂蛋白
  • 循环队列分析及应用
  • 在 Qt 中实现动态切换主题(明亮和暗黑)