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

C++11 面试题插入(左值引用和右值引用的区别)移动构造和移动赋值C++新标准如何解决传值返回但对象销毁问题

🎬 胖咕噜的稞达鸭:个人主页

🔥 个人专栏: 《数据结构》《C++初阶高阶》《算法入门》

⛺️技术的杠杆,撬动整个世界!

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

列表初始化

  1. 内置类型初始化
int x{2};
int x1=2;
  1. 自定义类型初始化
  • 2.1 直接构造
    本质是先构造一个Date临时对象,
    再拷贝构造d1;但编译器会优化这个过程,直接用列表参数构造d1(即不会调用拷贝构造函数)。
//2.自定义类型(类)的列表初始化
//		2.1可以直接构造,本质是先构造一个Date临时对象,
//		   再拷贝构造d1;但编译器会优化这个过程,直接用列表参数构造d1(即不会调用拷贝构造函数)。
Date d1 = { 2025,11,01 };
Date d2{ 2025,05,28 };
  • 2.2 绑定引用初始化
    列表{ 2025,12,12 }会先构造一个Date临时对象,然后将这个临时对象的引用绑定到const引用d3(或d4)上。
    因为临时对象的生命周期会被延长,与const引用的生命周期一致,所以这种写法是合法的。
const Date& d3 = { 2025,12,12 };
const Date& d4{ 2025,12,12 };//Date& d4{ 2025,12,12 };// 临时对象 { 2025,12,12} 被绑定到const引用d4,生命周期延长至d4的作用域结束

问题:const Date& d4(2025,10,10)为什么一定要加const,const意义何在?
如果不加const ,就是费const引用,会报错,原因在于非const引用意味着可以修改内部的数据,但是受生命周期影响,被引用&的内容出了作用域就会销毁,再去修改内部的数据,就会报错。加const一来可以防止内部的数据被修改,而来可以让d4一直坚持到生命周期结束再销毁。
“const引用可以延长临时对象的生命周期,使其与引用自身的生命周期一致”,这样就避免了** “对象提前销毁导致引用失效” **的问题。
简单总结一下:
临时对象匿名对象的生命周期都只在一行,const 引用可以延长临时对象+匿名对象的生命周期。

非const引用 + 临时对象 → 语法禁止(因为修改无意义且危险);
const引用 + 临时对象 →语法允许,且延长临时对象生命周期至引用销毁

注意一个小点:只有{ }初始化才可以省略=
像Date d 2025;一定会报错

结合析构函数(复习巩固)
在这里插入图片描述

局部对象和被const引用延长生命周期的临时对象,析构顺序与构造顺序相反(即 “先构造的后析构,后构造的先析构”)。
在这段代码中,构造顺序大致是:d1 → d2 → d3 → d4;因此析构顺序是:d4 → d3 → d2 → d1,与打印的顺序完全一致。

//构造
vector<int>v1 = { 1,2,3,4,5,6 };
vector<int>v2{ 1,2,3,4,5,6 };const vector<int>& v3 = { 9,8,7,6,5,4 };
const vector<int>& v4({ 9,8,7,6,5,4 });//构造+拷贝构造+优化
vector<int>v5({ 6,7,8 });
//({6,7,8})通过列表构造函数创建临时对象,然后用该临时对象直接构造v5。
//编译器会触发拷贝构造优化(返回值优化,RVO),
//避免临时对象的拷贝,直接在v5的内存地址上构造对象,最终等价于一次构造。

列表初始化+pair隐式类型转换

map<string, int>map1 = { { "apple",5}, {"bule" ,9} };
//map<string, int> & map2({ "apple",5 }, { "bule" ,9 });报错没有匹配的构造函数的类型

右值引用和移动语义

左值和右值的区别:右值不可以取地址,左值可以取地址。
在这里插入图片描述
左值引用给左值取别名,右值引用给右值取别名。

//左值引用给左值取别名
int& r1 = b;
int*& r2 = p;
int& r3 = *p;
string& r4 = s;
char& r5 = s[0];//右值引用给右值取别名
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
string&& rr4 = string("11111");

左值不能直接引用右值,但是加了const的左值可以引用;
右值也不能直接引用左值,但是可以引用move左值。
补充:move的本质就是进行强制类型转换。
在这里插入图片描述
在这里插入图片描述
注意:
从语义和内存本质的角度,左值引用和右值引用都是 “别名”,本身不额外开辟新的对象存储空间,在底层实现上通常以指针的形式存在(汇编层面会体现为指针操作)
**右值引用可以给右值起别名,但右值引用变量本身是左值属性。**所以变量表达式都是左值属性。

引用延长生命周期

总结:
右值引用可以延长被引用对象的生命周期,被引用对象可以通过非const的引用修改;
左值引用不能延长对象生命周期,但是const左值引用可以延长生命周期,
被引用对象不能通过到const的引用修改。

在这里插入图片描述

int main()
{std::string s1 = "happy";std::string&& a1 = std::move(s1);const std::string& a2 = s1 + s1;//到const的左值引用延长生命周期//a2 += "happy";//error:没有与这些操作数匹配的 "+=" 运算符//错误:不能通过到 const 的引用修改std::cout << a2 << '\n';//print:happyhappystd::string&& a3 = s1 + s1;// 右值引用延长生存期a3 += "exersice";// 能通过到非 const 的引用修改std::cout << a3 << '\n';//print:happyhappyexersicereturn 0;
}

左值和右值的参数匹配

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

#include<iostream>
#include<string>
using namespace std;
void f(int& x)
{cout << "左值引用重载调用" << endl;
}void f(const int& x)
{cout << "const左值引用重载调用" << endl;
}void f(int&& x)
{cout << "左值引用重载调用" << endl;
}
int main()
{int i = 1;const int ci = 2;int&& x = 3;f(1);//print:左值引用重载调用f(ci);//print:const左值引用重载调用f(x);//print:左值引用重载调用f(std::move(x)); // print:右值引用重载调用(原则上,但是编译器版本太新了会做优化调用左值引用return 0;
}

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

面试题插入:左值引用和右值引用的区别:

左值引用是 “对象的别名”,用于共享访问和避免拷贝;
右值引用是 “临时资源的所有权标识”,用于移动语义和完美转发,是 C++11 后提升性能的关键特性。
左值引用和右值引用的目的都是减少拷贝,提高效率。

优点:
左值引用可以修改参数/返回值,方便使用;
缺点:
但是在有些场景下,左值引用在对象函数栈帧中结束了会销毁,不能使用左值引用返回,当前函数局部对象,出了当前函数作用域生命周期到了就销毁了,不能用左值返回,只能传值返回。
问题:
那么如何解决只能传值返回,但是返回对象在函数栈帧结束之后会销毁的问题?

解决场景:
方案一:
不用返回值,用输出型参数。不足:一定程度上牺牲了可读性;
方案二:
编译器优化
方案三:
新标准新语法处理(右值引用),而右值引用可以延长函数对象的生命周期

编译器不优化的场景是拷贝构造+拷贝赋值,VS2019debug版本传参有优化,,对于ret赋值的没有优化到还是有拷贝构造和拷贝赋值,二代优化只有一次拷贝赋值,没有出现临时对象,没有拷贝构造。将构造和拷贝构造合二为一。
C++11之后彻底不优化,但是调用了移动赋值和移动构造,这样会直接调用移动构造和移动赋值,传值返回的代价约等于0.

总结:

在没有移动构造和移动赋值的情况下:

构造场景:

编译器如果不优化:
先产生一个临时对象,str函数哈哈栈帧结束后,临时对象接收str中存储的数据(一次拷贝构造),临时对象又会将内部存储的值拷贝构造给main()函数栈帧中的ret.总体上来说是两次拷贝构造。
在这里插入图片描述

string addstring1(string nums1,string nums2)
{string str;int end1=nums1.size()-1,end2=nums2.size()-1;int next=0;...str+=('0'+ret);...return str;
}int main()
{
//构造场景string ret=addstring1("xndx","lzdx");cout<<ret.c_str()<<endl;return 0;
}

不优化:会调用很多次构造先构造参数,每一次参数的构造完成,还会调用拷贝构造;
一代优化:合二为一,没有临时对象的产生,只产生了一次拷贝构造,构造+拷贝构造。
二代优化:合三为一,直接构造。

赋值场景:

在这里插入图片描述

string addstring1(string nums1,string nums2)
{string str;int end1=nums1.size()-1,end2=nums2.size()-1;int next=0;...str+=('0'+ret);...return str;
}int main()
{
//赋值场景string ret;ret=addstring1("xndx","lzdx");cout<<ret.c_str()<<endl;return 0;
}

不优化版本下:
产生一个临时对象,str在函数栈帧结束之前将内部资源拷贝构造给临时对象,由临时对象拷贝赋值给main函数中的ret。参数构造+拷贝构造,最后还有一次拷贝赋值

一代优化:多个参数构造,一次拷贝构造+一次拷贝赋值;
二代优化:拷贝构造和构造合二为一,最后一次拷贝赋值。

有移动构造和移动赋值C++11环境下:

构造场景:

在这里插入图片描述

不优化版本:str先将内部资源移动构造给临时对象,main函数中ret接收临时对象中的资源,也是移动构造,每一个参数构造+移动构造

1代优化:没有产生临时对象,编译器识别到 str 是 “即将被返回的局部对象”,会将其视为右值;执行 return str; 时,触发移动构造:直接将 str 的资源 “转移” 给临时对象(而非拷贝);
进一步优化中,临时对象和 main 中的 ret 会 “合二为一”(省略临时对象的构造),最终 str 的资源直接移动构造到 ret 中。
此过程仅触发一次移动构造,资源转移效率极高(无额外拷贝)。构造参数+最终一次移动构造

2代优化(比如在VS2022编译器下)中甚至没有产生str,只有ret,也就是说str本质是ret对象的引用,实际上没有产生str,一旦产生,函数栈帧销毁,局部对象str销毁,ret就是野引用,所以str没有产生,其底层使用指针形式实现,这个时候打印出str和ret的地址cout<<&ret<<endl;cout<<&str<<endl;会发现他们的地址是相同的。
这时候会直接构造,没有移动构造。

赋值的场景:

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

string addstring1(string nums1,string nums2)
{string str;int end1=nums1.size()-1,end2=nums2.size()-1;int next=0;...str+=('0'+ret);...return str;
}int main()
{
//赋值场景string ret;ret=addstring1("xndx","lzdx");cout<<ret.c_str()<<endl;return 0;
}

不优化版本下:
产生一个临时对象,str在函数栈帧结束之前将内部资源拷贝构造给临时对象,由临时对象拷贝赋值给main函数中的ret。参数构造+移动构造,最后还有一次移动赋值
一代优化多次构造,一次移动构造+一次移动赋值
二代优化:构造和移动构造合二为一,只有构造和移动赋值

总体来说:
如果代码中有拷贝构造和拷贝赋值,也有移动构造和移动赋值,编译器一定会优先执行移动构造和移动赋值,因为选择的都是效率高的。
当编译器升级+C++支持移动构造和移动赋值,传值返回的效率变高。

问题:那么移动构造和移动赋值的效率这么高,需不需要在每一个类型中都实现?
对于深拷贝的自定义类型(vector/string/map…),实现移动构造和移动赋值的价值很大,一定程度上比拷贝构造和拷贝赋值的效率高很多。
对于浅拷贝的自定义类型(如Date/pair<int,int>…)不需要额外实现移动构造和 移动赋值,因为浅拷贝的传值返回和移动构造移动赋值相比,浅拷贝没有指向资源,拷贝代价不大,效率差不多。

类别:

在这里插入图片描述
在这里插入图片描述
引用折叠

类型别名的引用折叠:

int main()
{typedef int& lref; //lref 是 “int 的左值引用” 类型typedef int&& rref;//rref 是 “int 的右值引用” 类型。int n = 0;lref& r1 = n;// r1 的类型是 int&lref&& r2 = n;// r2 的类型是 int&rref& r3 = n;// r3 的类型是 int&rref&& r4 = 1;// r4 的类型是 int&&return 0;
}

总结:
只要嵌套中存在左值引用(&),最终结果就是左值引用;只有纯右值引用(&&)嵌套时,才是右值引用。
二、模板函数的引用行为

// 由于引用折叠限定,f1实例化以后总是一个左值引用
template<class T>
void f1(T& x)//模板参数是左值引用,实例化的时候只能接受左值(n),无论T是什么类型,x始终是左值引用(无折叠空间,因为参数已固定为& )。
{}
// 由于引用折叠限定,f2实例化后可以是左值引用,也可以是右值引用
template<class T>
void f2(T&& x)//模板参数是万能引用(T&&)
{}

f1(T& x):模板参数是左值引用,实例化时只能接受左值(如 n),且无论 T 推导为何,x 始终是左值引用(无折叠空间,因为参数已固定为 &)。
f2(T&& x):模板参数是万能引用(T&&)
实例化时:
传入左值(如 n),T 推导为 int&,x 类型为 int& && → 折叠为 int&(左值引用)。
传入右值(如 1),T 推导为 int,x 类型为 int&&(右值引用)。

结论:f1 只能处理左值引用,f2 可通过引用折叠同时处理左值和右值引用(万能引用的特性)。
在这里插入图片描述
f1(T& x) 本质是左值引用参数,显式实例化后仍为左值引用,非const时仅接受左值,const时可接受左值和右值。

在这里插入图片描述
f2(T&& x) 是万能引用,显式实例化后通过引用折叠可变为左值或右值引用,需严格匹配实参的左值 / 右值属性。

总结:左值只能绑定左值引用,右值引用可以绑定右值引用和const左值引用。

// 由于引用折叠限定,f1实例化以后总是一个左值引用
template<class T>
void f1(T& x)//模板参数是左值引用,实例化的时候只能接受左值(n),无论T是什么类型,x始终是左值引用(无折叠空间,因为参数已固定为& )。
{}
// 由于引用折叠限定,f2实例化后可以是左值引用,也可以是右值引用
template<class T>
void f2(T&& x)//模板参数是万能引用(T&&)
{}
int main()
{typedef int& lref; //lref 是 “int 的左值引用” 类型typedef int&& rref;//rref 是 “int 的右值引用” 类型。lref& r1 = n;// r1 的类型是 int&lref&& r2 = n;// r2 的类型是 int&rref& r3 = n;// r3 的类型是 int&rref&& r4 = 1;// r4 的类型是 int&&int n = 0;//没有折叠->实例化为void f1(int& x)f1<int>(n);//n是左值,可绑定到int&;f1<int>(0);//0是右值,无法绑定到非const的int& → 报错。// 折叠f1<int&>(n);//n是左值,可以绑定到int& &f1<int&>(0);// 0是右值,无法绑定,改正:f1<const int&>(0);// 折叠f1<int&&>(n);//T=int&& → 参数类型为(int&&)&(引用折叠为int&)。n(左值)可绑定到int& ;f1<int&&>(0);// //0(右值)不可 → 后者报错。// 折叠->实例化为void f1(const int& x)f1<const int&>(n);//T=const int& → 参数类型为const int&(const左值引用)。f1<const int&>(0);//左值(n)和右值(0)都能绑定到const int& → 均合法。// 折叠f1<const int&&>(n);//const右值引用f1<const int&&>(0);// 没有折叠->实例化为void f2(int&& x)f2<int>(n);// T = int → 参数类型为int && (右值引用)。f2<int>(0);// 0是右值,参数类型是int &&(右值引用)// 折叠->实例化为void f2(int& x)f2<int&>(n);//T = int& &&,n是左值,左值引用和左值,不报错f2<int&>(0);//左值引用和0会报错,左值无法引用右值,报错改正:f2<const int&>(0);// 折叠->实例化为void f2(int&& x)f2<int&&>(n);// T=int&& → 参数类型为(int&&)&&(折叠为int&&,右值引用)。n(左值)无法绑定到int&& ;f2<int&&>(0);//0(右值)可绑定 → 前者报错。return 0;
}

Function(T&& t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模板参数T的推导int&,再结合引用折叠规则,就实现了
实参是左值,实例化出左值引用版本形参的Function,
实参是右值,实例化出右值引用版本形参的Function。
在这里插入图片描述

template<class T>
void Function(T && t)//模板参数是万能引用(T&&)
{int a = 0;T x = a;//x++;// 所以Function内部会编译报错,x不能++,error:不能给常量赋值cout << &a << endl;cout << &x << endl << endl;
}
int main()
{ Function(10);// 10是右值,推导出T为int,模板实例化为void Function(int&& t)int a;Function(a);// a是左值,推导出T为int&,引用折叠,模板实例化为void Function(int& t)Function(std::move(a));// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)const int b = 8;Function(b);//b是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int&& t)Function(std::move(b));// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t)return 0;
}

在这里插入图片描述

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

相关文章:

  • 住房城乡与建设厅网站首页智慧团建团员注册入口
  • 网站地图可以自己做么杭州网站制作 乐云践新
  • Qt5设定编译生成程序的路径和文件名
  • SpringBoot18-文件上传助手MultipartResolver
  • python学习之os,sys,time,random模块
  • 排序算法实战:从插入排序到希尔排序的实现与性能对决
  • Harmony鸿蒙开发0基础入门到精通Day10--JavaScript篇
  • VMware安装CentOS7操作系统
  • 搬瓦工做网站方法wordpress数据类型
  • 常德网站网站建设软件工程师英文
  • Win11超精简定制版本Tiny11安装教程来袭
  • 【第1章>第2节】图像“腐蚀”处理的理论分析与MATLAB仿真测试
  • 如何将BOOST库集成到VS2019中去使用呢?
  • 黑龙江做网站公司网站建设方案书网络部署方案
  • 乐清微网站建设做网络运营需要掌握什么
  • java学习--冒泡排序
  • iis7.5 网站配置简述网站建设基本步骤
  • visual studio 获取并输出 $(ProjectDir) 的所在的具体路径
  • wordpress网站搜索引擎微信公众号运营模式
  • 海洋捕食算法的详细原理,公式,应用案例MPA-BP
  • 动态规划的解题套路1-泰波那契模型
  • 高端建站咨询京津冀协同发展英文
  • 【Ubuntu】ubuntu虚拟机磁盘不够扩容后开机黑屏-解决方案
  • 网站建设 教学视频教程网站wap版影响权重么
  • Efficient Memory Management for Large Language Model with PagedAttention
  • 东莞网站建设推广费用wordpress上不去了
  • 网站301重定向$cms和wordpress
  • 网站建设实训进程计划九龙坡区网站建设
  • 【LeetCode 每日一题】1414. 和为 K 的最少斐波那契数字数目
  • 怎么用一个主机做多个网站制作网站报价单