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

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的引用修改。

在这里插入图片描述
intmain(){ 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:happyhappy std::string&& a3 = s1 + s1;// 右值引用延长生存期 a3 +="exersice";// 能通过到非 const 的引用修改 std::cout << a3 <<'\n';//print:happyhappyexersicereturn0;}

左值和右值的参数匹配

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

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

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

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

左值引用是 “对象的别名”,用于共享访问和避免拷贝;
右值引用是 “临时资源的所有权标识”,用于移动语义和完美转发,是 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;}intmain(){//构造场景 string ret=addstring1("xndx","lzdx"); cout<<ret.c_str()<<endl;return0;}

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

赋值场景:

在这里插入图片描述
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;}intmain(){//赋值场景 string ret; ret=addstring1("xndx","lzdx"); cout<<ret.c_str()<<endl;return0;}

不优化版本下:
产生一个临时对象,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;}intmain(){//赋值场景 string ret; ret=addstring1("xndx","lzdx"); cout<<ret.c_str()<<endl;return0;}

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

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

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

类别:

在这里插入图片描述


在这里插入图片描述


引用折叠

类型别名的引用折叠:

intmain(){typedefint& lref;//lref 是 “int 的左值引用” 类型typedefint&& 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&&return0;}

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

// 由于引用折叠限定,f1实例化以后总是一个左值引用template<classT>voidf1(T& x)//模板参数是左值引用,实例化的时候只能接受左值(n),无论T是什么类型,x始终是左值引用(无折叠空间,因为参数已固定为& )。{}// 由于引用折叠限定,f2实例化后可以是左值引用,也可以是右值引用template<classT>voidf2(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<classT>voidf1(T& x)//模板参数是左值引用,实例化的时候只能接受左值(n),无论T是什么类型,x始终是左值引用(无折叠空间,因为参数已固定为& )。{}// 由于引用折叠限定,f2实例化后可以是左值引用,也可以是右值引用template<classT>voidf2(T&& x)//模板参数是万能引用(T&&){}intmain(){typedefint& lref;//lref 是 “int 的左值引用” 类型typedefint&& 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<constint&>(n);//T=const int& → 参数类型为const int&(const左值引用)。 f1<constint&>(0);//左值(n)和右值(0)都能绑定到const int& → 均合法。// 折叠 f1<constint&&>(n);//const右值引用 f1<constint&&>(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(右值)可绑定 → 前者报错。return0;}

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

在这里插入图片描述
template<classT>voidFunction(T && t)//模板参数是万能引用(T&&){int a =0; T x = a;//x++;// 所以Function内部会编译报错,x不能++,error:不能给常量赋值 cout <<&a << endl; cout <<&x << endl << endl;}intmain(){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)constint 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)return0;}
在这里插入图片描述

Read more

「C++」多态

「C++」多态

目录 前言 多态概念 多态的定义及实现 情景导入 多态构成条件 虚函数及重写/覆盖 重写其他注意点 协变(了解) 析构函数的虚函数重写 override和final关键字 重载/隐藏/重写的对比 纯虚函数和抽象类 多态的原理 虚表指针 多态的原理 动态绑定与静态绑定 虚函数表 后记 前言 本篇继承自「C++」继承-ZEEKLOG博客内容,继续介绍三大特性中的多态部分,而继承部分中提到的virtual关键字也将获得一个新功能,并在多态中有具有极其重要的地位。继承中所说析构函数名的特殊处理也将在本篇解释。 多态概念 多态分为编译时多态(静态多态)和运行时多态(动态多态) 编译时多态:主要指的就是我们之前学习的,函数重载、模板等内容。它们通过传递不同的参数类型,在编译时调用对应参数类型的重载函数、生成不同的模板函数,从而达到不同的多种形态。叫编译时多态,是因为这其中的实参与形参参数类型的匹配是在编译时就完成的。而编译时一般就归为静态,运行时归为动态; 运行时多态:主要指的就是在程序运行过程中,

By Ne0inhk

C++模块化编程:告别#include的新时代

我看网上的一些文档,里面有很多人讲import,对此来讲一下个人见解。 事先声明:本文仅作技术讨论与交流,部分术语可能为非标准表述(如“接口污染”等概念),旨在通俗化描述技术问题。文中观点或示例可能存在不严谨之处,欢迎专业人士指正。 第一章 差异变化 从C++98到C++17,C++一直沿用基于 #include的“库导入”方式来组织代码。这种方式本质上是文本替换:预编译器将头文件的内容原封不动地复制到源文件中,形成一个巨大的编译单元。如: //C++98及以后的标准 #include<iostream>//导入库 int main()//程序入口 { std::cout<<"Hello,World!"; //隐式加入return 0; } 这里是将代码变为: // iostream standard header

By Ne0inhk

C++26任务优先级机制全面曝光(下一代并发编程革命)

第一章:C++26任务优先级机制全面曝光(下一代并发编程革命) C++26即将引入原生的任务优先级调度机制,标志着标准库在并发编程领域迈出了革命性的一步。该特性允许开发者为线程或协程任务显式指定执行优先级,从而更精细地控制资源分配与响应行为。 任务优先级的声明方式 通过新的 std::priority 枚举和配套的执行器(executor)扩展,任务可被标记为高、中、低等优先级。例如: // 声明一个高优先级异步任务 auto high_priority_task = std::async( std::launch::async | std::priority::high, []() { // 关键实时处理逻辑 return perform_critical_work(); } ); 上述代码中,std::priority::high 指示运行时应尽可能优先调度该任务,适用于延迟敏感型操作。 优先级枚举定义 标准库将提供如下优先级等级: * std::priority::low:后台任务,

By Ne0inhk
【STL】C++ list 模拟实现:从底层链表到容器封装

【STL】C++ list 模拟实现:从底层链表到容器封装

前言 作为 C++ 学习者,光会用 STL list 总觉得差点意思 —— 这次手写模拟实现,就是想从底层搞懂它:双向链表节点咋设计?迭代器为啥能 “++/--”?插入删除咋做到不影响其他元素? 这篇笔记是我的实践记录:从节点、迭代器到容器接口,一步步还原 list 的核心逻辑,把 “用容器” 变成 “懂容器”。 目录 一、List的介绍 二、默认成员函数 1、List的节点结构、容器结构 ℡. 节点结构 ℡. 迭代器结构 链表的迭代器为啥不能直接用原生指针? 迭代器结构为啥用struct? 迭代器为啥不能写析构函数? ℡. 链表结构 2、List构造函数 3、List拷贝构造函数 4、List赋值运算符重载 5、List析构函数 三、迭代器 1、begin/

By Ne0inhk