news 2026/3/14 3:14:06

C++引用深入讲解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++引用深入讲解

1. 数据类型

对于任意一种数据类型,在 C++ 里边都有与之相对应的4 种数据类型,以int类型为例:

  1. int:int 类型;
  2. int*:int 的指针类型;
  3. int&:int 的左值引用类型;
  4. int&&:int 的右值引用类型。

如何从代码层面判断是什么类型呢?—— 可用 C++ 的<type_traits>库来判断类型。

#include<iostream>#include<type_traits>usingnamespacestd;intmain(){inta=3;int*ptr_a=&a;int&left_ref=a;int&&right_ref=4;cout<<boolalpha;cout<<"a is base type? "<<is_same<decltype(a),int>()<<endl;// truecout<<"ptr_a is point type? "<<is_pointer<decltype(ptr_a)>::value<<endl;// truecout<<"left_ref is left value reference? "<<is_lvalue_reference<decltype(left_ref)>::value<<endl;// truecout<<"right_ref is right value reference? "<<is_rvalue_reference<decltype(right_ref)>::value<<endl;// truecout<<"int is base type? "<<is_same<int,int>()<<endl;// truecout<<"int* is point type? "<<is_pointer<int*>::value<<endl;// truecout<<"int& is left value reference? "<<is_lvalue_reference<int&>::value<<endl;// truecout<<"int&& is right value reference? "<<is_rvalue_reference<int&&>::value<<endl;// truereturn0;}

2. 左值和右值

左值的英文简写为 “lvalue”,右值的英文简写为 “rvalue”。一般认为它们分别是 “left value”、“right value” 的缩写,其实不然。lvalue 是 “loactor value” 的缩写,可意为存储在内存中、有明确存储地址(可寻址)的数据,而 rvalue 译为 “read value”,指的是那些可以提供数据值的数据。

简单来说:

  1. 所有的具名变量都是左值,左值可以被取地址,左值可以出现在等号的左边,也可以出现在等号的右边。
  2. 所有的匿名变量则是右值,右值不能被取地址,右值只能出现在等号的右边。

数据类型的概念和左值右值的的概念是两回事,不是同一个东西。它们分别从两个维度来描述一个值的属性,也即是说一个值有两个属性,类型属性和此值是左值还是右值的属性。

  • intinit*int&&类型的值可以是左值也可以是右值,当它们作为变量具有名字时就是左值,当它们作为函数返回的临时值或表达式的计算值时就是右值。
  • int&类型的值始终只能是左值,不能是右值,不管是作为变量,还是作为函数的返回值,它都是左值。
  • 非引用返回的临时变量,运算表达式产生的临时变量,原始字面量和 lambda 表达式等都是右值。

示例:

intmain(){5;// 5是右值3.14;// 3.14是右值Bar(6);// Bar(6)是右值inta;// a是左值intb=1;// b是左值int&c=a;// c也是左值,虽然c的类型是左值引用,但c本身是左值,因为c有名字int&&d=5;// d也是左值,虽然d的类型是右值引用,但d本身是左值,因为d有名字int&e=c;// 合法,虽然c的类型是左值引用,但c本身是左值,左值引用是对左值的引用,因此赋值语句合法int&f=d;// 合法,虽然d的类型是右值引用,但d本身是左值,左值引用是对左值的引用,因此赋值语句合法int&&g=d;// 非法,虽然d的类型是右值引用,但d本身是左值,而右值引用是对右值的引用,d不是右值,因此赋值语句非法return0;}

2.1. 左值到右值的转换

一个左值可以被转换(convert)为右值,这完全合法且经常发生。让我们先用+操作符作为一个例子,根据 C++ 的规范(specification),它使用两个右值作为参数并返回一个右值。

示例:

intx=1;inty=3;intz=x+y;// Right

xy是左值,但是加法操作符需要右值作为参数:发生了什么?答案很简单:xy经历了一个隐式(implicit)的左值到右值(lvalue-to-rvalue)的转换,许多其它的操作符也有同样的转换,例如:减法、加法、除法等。

2.2. 右值到左值的转换?

相反呢?一个右值可以被转化为左值吗?不可以,它不是技术所限,而是 C++ 编程语言就是那样设计的。因为右值是没有地址的,左值是有地址的,把右值变成左值需要为右值分配地址,如果这么做了,这就和右值的语义不符了。

2.3. 函数返回值一般是右值但也可以是左值

2.3.1. 示例

intglobal=1;int&fun1(){returnglobal;}intfun2(){return1;}intmain(){fun1()=2;// Right: fun1返回的是global的左值引用,左值引用都是左值,因此可以对一个左值进行重新赋值,甚至取地址fun2()=3;// Error: fun2返回的是右值,不能对右值赋值,编译错误return0;}

2.3.2. 结论

当函数的返回类型是左值引用类型时,函数的返回值是左值,否则函数的返回值都是右值。

3. 引用的概念

引用分为左值引用和右值引用,左值引用大家比较熟悉,这里就不在累述了,主要学习一下右值引用的基本概念和注意点。

关于右值引用网上这篇文章讲解的比较好,看这篇就够了 —— 从4行代码看右值引用。

关于此篇文章的总结:

  1. 所有的具名变量或对象都是左值,而匿名变量则是右值。
  2. C++11 中所有的值必属于左值、将亡值、纯右值三者之一。
  3. 纯右值在表达式结束之后就销毁了。
  4. 通过右值引用,右值又 “重获新生”,其生命周期与右值引用类型变量的生命周期一样长,只要该变量还活着,该右值临时量将会一直存活下去。
  5. 右值引用独立于左值和右值,意思是右值引用类型的变量可能是左值也可能是右值。
  6. T&& t在发生自动类型推断的时候,它是未定的引用类型,当它被右值初始化时它是右值引用类型,其它情况都是左值引用类型。
  7. 常量左值引用是一个 “万能” 的引用类型,接受** “左值、右值、常量左值和常量右值,左值引用,右值引用,常量左值引用,常量右值引用” **。
  8. 右值引用T&&是一个未定的引用类型,可以接受左值或者右值,正是这个特性让它适合作为一个参数的路由,然后再通过std::forward按照参数的实际类型去匹配对应的重载函数,最终实现完美转发。
  9. 所有的右值引用叠加到右值引用上仍然还是一个右值引用,所有的其它引用类型之间的叠加都将变成左值引用。

4. 实例分析与讲解

4.1. 测试代码

4.1.1. 类定义

后续测试几乎全部依据此类。

#include<functional>#include<iostream>#include<memory>#include<type_traits>#include<vector>usingnamespacestd;classBar{public:Bar(){cout<<"empty constructor"<<endl;}Bar(intx){value_ptr_=newint(x);cout<<"normal constructor"<<endl;}Bar(constBar&x){if(x.value_ptr_!=nullptr){value_ptr_=newint(*x.value_ptr_);}cout<<"copy constructor"<<endl;}Bar(Bar&&x){if(this->value_ptr_==x.value_ptr_){cout<<"move constructor, is the same object"<<endl;return;}if(x.value_ptr_!=nullptr){deletevalue_ptr_;value_ptr_=x.value_ptr_;x.value_ptr_=nullptr;}cout<<"move constructor"<<endl;}~Bar(){cout<<"destructor"<<endl;if(value_ptr_!=nullptr){deletevalue_ptr_;}}Bar&operator=(constBar&x){if(x.value_ptr_!=nullptr){value_ptr_=newint(*x.value_ptr_);}cout<<"copy operator="<<endl;return*this;}Bar&operator=(Bar&&x){if(this->value_ptr_==x.value_ptr_){cout<<"move operator=, is the same object"<<endl;return*this;}if(x.value_ptr_!=nullptr){deletevalue_ptr_;value_ptr_=x.value_ptr_;x.value_ptr_=nullptr;}cout<<"move operator="<<endl;return*this;}intGetValue()const{if(value_ptr_==nullptr){return-1;}return*value_ptr_;}voidSetValue(intx){*value_ptr_=x;}staticvoidFunA(Bar x){cout<<"FunA"<<endl;}staticvoidFunB(constBar x){cout<<"FunB"<<endl;}staticvoidFunC(Bar&x){cout<<"FunC"<<endl;}staticvoidFunD(constBar&x){cout<<"FunD"<<endl;}staticvoidFunE(Bar&&x){cout<<"FunE"<<endl;}staticvoidFunF(constBar&&x){cout<<"FunF"<<endl;}staticfunction<void()>funG(){cout<<"funG"<<endl;Bara(5);cout<<"the content of a: "<<a.GetValue()<<endl;cout<<"the address of a:"<<&a<<endl;// 注意:这里仅仅定义task,不会运行task。//[a]表示按值传递a,因此要调用复制构造函数。function<void()>task=[a](){// 这里的a和外部的a不是一个a,这个a是通过复制构造而来的,然后存于lambda的栈空间。cout<<"the content of a: "<<a.GetValue()<<endl;// 因为这里的a和外部的a不是一个a,所以地址不一样。cout<<"the address of a:"<<&a<<endl;};// 这里仅返回定义的task,不会运行task,真正运行的地方为显示调用task_g()的地方。returntask;}staticfunction<void()>funH(){cout<<"funH"<<endl;Bara(5);cout<<"ref_a is left value reference? "<<is_lvalue_reference<decltype(a)>()<<endl;// falsecout<<"the content of a: "<<a.GetValue()<<endl;cout<<"the address of a:"<<&a<<endl;//[&a]表示按引用传递afunction<void()>task=[&a](){/* 这里的a就是外部a,都是Bar类型。 * 因为funH()仅定义了task,而真正运行这段代码地方是在funH()之外显示调用task_h()的地方,a在funH()结束之后就被 * 析构了,而真正运行task_h()的地方又引用了已经被析构的a,所以这里获取不到值。 * */cout<<"ref_a is left value reference? "<<is_lvalue_reference<decltype(a)>()<<endl;// falsecout<<"the content of a: "<<a.GetValue()<<endl;// a虽然在funH()结束之后被析构了,但这里还引用的是那块内存,所以地址是一样的,只不过无效了。cout<<"the address of a:"<<&a<<endl;};returntask;}staticfunction<void()>funI(){cout<<"funI"<<endl;Bara(5);Bar&ref_a=a;cout<<"ref_a is left value reference? "<<is_lvalue_reference<decltype(ref_a)>()<<endl;// truecout<<"the content of ref_a: "<<ref_a.GetValue()<<endl;cout<<"the address of ref_a:"<<&ref_a<<endl;//[ref_a],这里表示按值传递ref_a,会调用a的复制构造函数。function<void()>task=[ref_a](){// 注意:这里的ref_a依然是左值引用类型,但引用的是一个新a,而不是上边的a,因此地址不一样。cout<<"ref_a is left value reference? "<<is_lvalue_reference<decltype(ref_a)>()<<endl;// truecout<<"the content of a: "<<ref_a.GetValue()<<endl;cout<<"the address of a:"<<&ref_a<<endl;};returntask;}staticfunction<void()>funJ(){cout<<"funJ"<<endl;Bara(5);Bar&ref_a=a;cout<<"ref_a is left value reference? "<<is_lvalue_reference<decltype(ref_a)>()<<endl;// truecout<<"the content of ref_a: "<<ref_a.GetValue()<<endl;cout<<"the address of ref_a:"<<&ref_a<<endl;//[&ref_a],这里表示按引用传递ref_a。function<void()>task=[&ref_a](){/* 这里ref_a就是指向a,与funH()情况一样,a在funJ()结束后就被析构了,因此这里是非法引用。 * 注意和funI()的区别。[ref_a]传参会调用复制构造函数,构造一个新a,导致ref_a指向新a。而[&ref_a]传参不会构造新a, * ref_a还是指向的同一个a。 * */cout<<"ref_a is left value reference? "<<is_lvalue_reference<decltype(ref_a)>()<<endl;// truecout<<"the content of a: "<<ref_a.GetValue()<<endl;cout<<"the address of a:"<<&ref_a<<endl;};returntask;}staticfunction<void()>funK(){cout<<"funK"<<endl;Bara(5);cout<<"the content of ref_a: "<<a.GetValue()<<endl;cout<<"the address of ref_a:"<<&a<<endl;/* [a = move(a)]会调用移动构造函数把外部的a(等号右边的a)的数据转移到lambda内部的a(等号左边的a)。 * 等号左右两边的a是不一样的,左边是lambda内部的a,右边是funK()的a。 * */function<void()>task=[a=move(a)](){// a的地址变了。cout<<"the content of a: "<<a.GetValue()<<endl;cout<<"the address of a:"<<&a<<endl;};returntask;}// 普通函数的定义staticvoidfunL(Bar a){cout<<"funL"<<endl;}// 普通函数的定义staticvoidfunM(Bar&a){cout<<"funM"<<endl;}// 普通函数的定义staticvoidfunN(Bar&x){cout<<"funN"<<endl;Bar a=move(x);}// 普通函数的定义staticvoidfunN2(Bar&&x){cout<<"funN2"<<endl;/* 注意:这里x的类型是右值引用,而x本身属于左值,所以赋给a时若想调用"move constructor"则必须写成Bar a = * move(x),若写成Bar a = x,则会触发"copy constructor"。"move constructor"是给右值用的, 不是给右值引用用的。 * */cout<<"x is right value reference? "<<is_rvalue_reference<decltype(x)>()<<endl;// trueBar a=move(x);}staticBarfunO(){cout<<"funO"<<endl;Bara(5);returna;}staticBarfunP(){cout<<"funP"<<endl;staticBara(5);returna;}staticconstBar&funQ(){cout<<"funQ"<<endl;staticBara(5);returna;}private:int*value_ptr_=nullptr;};

4.1.2. CMakeLists.txt

# Three basic variables: # - arch: The platform of the bin file will run. Select from [x86, arm], default value is `x86`. # # Using Example: # Build x86 release version: # ``` # rm -rf build; mkdir build; cd build; cmake ..; make # ``` # # Build x86 debug version: # ``` # rm -rf build; mkdir build; cd build; cmake -DCMAKE_BUILD_TYPE=Debug ..; make # ``` # # Build arm release version: # ``` # rm -rf build; mkdir build; cd build; cmake -Darch=arm ..; make # ``` # # Build arm debug version: # ``` # rm -rf build; mkdir build; cd build; cmake -Darch=arm -DCMAKE_BUILD_TYPE=Debug ..; make # ``` cmake_minimum_required(VERSION 3.0) if (NOT (UNIX OR LINUX)) message(FATAL_ERROR "This CMakeLists.txt only supports Linux platforms, please write it yourself for other platforms.") endif () if (arch STREQUAL "arm") set(CMAKE_C_COMPILER /opt/GoldenOS-SDK-aarch64-tda4-NeuSAR-release/build/toolchain/aarch64_eabi_gcc9.2.0_glibc2.31.0_fp/bin/aarch64-unknown-linux-gnueabi-gcc) set(CMAKE_CXX_COMPILER /opt/GoldenOS-SDK-aarch64-tda4-NeuSAR-release/build/toolchain/aarch64_eabi_gcc9.2.0_glibc2.31.0_fp/bin/aarch64-unknown-linux-gnueabi-g++) message("Build arm version.") else () message("Build x86 version.") endif () if (NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release) endif () message("Build ${CMAKE_BUILD_TYPE} version.") set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) project(std_move LANGUAGES C CXX) if (PROJECT_BINARY_DIR STREQUAL PROJECT_SOURCE_DIR) message(FATAL_ERROR "The binary directory of CMake cannot be the same as source directory!") endif () add_compile_options(-Wall -fno-elide-constructors) aux_source_directory(. source) add_executable(${CMAKE_PROJECT_NAME} ${source})

说明:

-fno-elide-constructors选项用于关闭返回值优化效果,便于分析代码的真实行为。

4.2. std::move 原理

首先,必须弄清楚std::move的原理,才能进行后续讲解。

4.2.1. std::move 源码

/** * @brief Convert a value to an rvalue. * @param __t A thing of arbitrary type. * @return The parameter cast to an rvalue-reference to allow moving it. */template<typename_Tp>constexprtypenamestd::remove_reference<_Tp>::type&&move(_Tp&&__t)noexcept{returnstatic_cast<typenamestd::remove_reference<_Tp>::type&&>(__t);}/// remove_referencetemplate<typename_Tp>structremove_reference{typedef_Tp type;};template<typename_Tp>structremove_reference<_Tp&>{typedef_Tp type;};template<typename_Tp>structremove_reference<_Tp&&>{typedef_Tp type;};

4.2.2. std::move 源码分析

从源码分析可知,std::move就是把传进来的变量__t通过static_cast转换后返回,而转换的类型是typename std::remove_reference<_Tp>::type&&。那么remove_reference具体又干了什么?从源码中可以看到remove_reference有三个重载,分为三种场景:

场景 1:

_Tp的类型是右值,例如数字5,那么就会调用第 13 行的重载,经过typedeftype的类型就是int类型。

场景 2:

_Tp的类型是左值引用,例如:int a=5; int& lref_a = a;。若此时_Tplref_a,那么_Tp就是左值引用,会调用第 17 行的重载,那么经过第 18 行之后_Tp的左值引用类型&被去掉了,经过typedeftype的类型就是int类型。

场景 3:

_Tp的类型是右值引用,例如:int& rref_a = 5;。若此时_Tprref_a,那么_Tp就是右值引用,会调用第 21 行的重载,那么经过第 22 行之后_Tp的右值引用类型&&被去掉了,经过typedeftype的类型就是int类型。

承接上述三种场景的例子继续分析,当std::remove_reference<_Tp>::type推导出type的类型后,type的类型始终为int类型,此时后边再加两个&&,那么就变成了右值引用类型int&&,所以typename std::remove_reference<_Tp>::type&&的运行结果始终会推导出int&&类型,最终static_cast__t转变成右值引用类型int&&返回,同时因为是函数的返回值,所以返回的值是一个右值。

经过上述分析,std::move接受一个变量,并把它转换为右值返回,并不会调用此类型的移动构造函数或者移动赋值函数!

4.3. 构造函数

构造函数分为:一般构造函数,复制构造函数,移动构造函数。

4.3.1. 调用规则

  1. 根据一般参数创建新变量时调用一般构造函数
  2. 根据同类型左值变量创建新变量时调用复制构造函数
  3. 根据同类型右值变量创建新变量时调用移动构造函数

4.3.2. 示例 1

intmain(){Bar e;// empty constructorBarn(1);// normal constructorBarc1(n);// copy constructorBar c2=n;// copy constructorBarm3(move(n));// move constructorBar m4=move(n);// move constructorreturn0;}

解析:

  1. 第 69 行分析:虽然这里用的是=,但依然调用的是复制构造函数而不是赋值运算符重载。因为此时变量c2不存在,需要构造,既然是构造就要调用构造函数
  2. 第 70 行分析:上述讲解move时叙说了move的作用就是把一个变量变成右值,这里move(n)就是把n变成右值,根据构造函数重载的参数类型,这里匹配到的是移动构造函数而不是赋值构造函数
  3. 第 71 行分析:这里类似第 69 行,m4不存在,不存在就要构造,所以这里调用的是移动构造函数而不是移动运算符重载。

4.3.3. 示例 2

intmain(){Bar a=5;return0;}

输出:

normal constructor move constructor destructor destructor

解析:

仔细看这段代码,乍一看应该是语法错误,为什么?因为5int类型,而aBar类型,那为什么可以把int类型能转换成Bar类型?—— 因为发生了隐式转换。这就是我们常说的explicit关键字。

现有代码的构造函数如下:

Bar(intx){value_ptr_=newint(x);cout<<"normal constructor"<<endl;}

构造函数没有加explicit关键子,所以允许隐式转换,我们再来分析运行Bar a = 5;这行代码的行为:

  1. 先把5进行隐式转换,调用Bar的 “normal constructor”,把5变成Bar(5),因此这段代码等价于Bar a = Bar(5)
  2. 返回的Bar(5)属于右值,因此调用 “move constructor” 把Bar(5)Bar a

假如我们修改构造函数为如下代码:

explicitBar(intx){value_ptr_=newint(x);cout<<"normal constructor"<<endl;}

这里加了explicit关键字,就不允许隐式转换,所以再写Bar a = 5;这样的代码属于语法错误,编译不通过。以下是编译错误提示:

xxx/main.cpp:244:13:错误:conversion from ‘int’ to non-scalar type ‘Bar’ requested Bar a=5;^gmake[3]:***[CMakeFiles/main.dir/build.make:82:CMakeFiles/main.dir/main.cpp.o]错误1gmake[2]:***[CMakeFiles/Makefile2:95:CMakeFiles/main.dir/all]错误2gmake[1]:***[CMakeFiles/Makefile2:102:CMakeFiles/main.dir/rule]错误2

关于隐式转换的更多讲解参看:explicit构造函数。

4.4. 等号运算符重载

等号运算符重载分为:赋值运算符重载,移动运算符重载。

4.4.1. 调用规则

  1. 用左值变量赋值给已经存在的变量调用赋值运算符重载。
  2. 用右值变量赋值给已经存在的变量调用移动运算符重载。

4.4.2. 示例 1

intmain(){Barn(1);// normal constructorBar e1,e2;// empty constructore1=n;// copy operator=e1=move(n);// move operator=return0;}

解析:

  1. 因为e1e2已经存在,存在就不需要构造,所以第 6 和 7 行调用的是等号运算符重载,而不是构造函数
  2. 因为n是左值,所以第 6 行调用赋值运算符重载。
  3. 因为move(n)是右值,所以第 7 行调用移动运算符重载。

4.4.3. 示例 2

intmain(){Bar e;e=5;return0;}

输出:

empty constructor normal constructor move operator= destructor destructor

解析:

这里也是发生了隐式转换,分析参看《构造函数》章节的示例 2。

4.5. 函数引用传参

4.5.1. 示例

intmain(){Bara(1);// normal constructorBar::FunA(a);// call copy constructorBar::FunB(a);// call copy constructorBar::FunC(a);// not call any constructor, just run Bar& x=aBar::FunD(a);// not call any constructor, just run const Bar& x=aBar::FunE(move(a));// not call any constructor, just run Bar&& x=aBar::FunF(move(a));// not call any constructor, just run const Bar&& x=areturn0;}

解析:

  1. 第 5 行和第 6 行,因为是按照值传递,所以调用复制构造函数。
  2. 第 7~10 行,函数参数是左值引用或右值引用类型,不会调用任何构造函数,仅仅运行引用绑定

4.6. lambda 函数引用传参

4.6.1. 示例

intmain(){autotask_g=Bar::funG();task_g();autotask_h=Bar::funH();task_h();autotask_i=Bar::funI();task_i();autotask_j=Bar::funJ();task_j();autotask_k=Bar::funK();task_k();return0;}

解析:

  1. 详细分析看 funG()~funK() 的函数注释。

4.6.2. lambda 函数和普通函数的区别与联系

首先要明确一个概念,运行一个函数的完整流程是:定义函数,参数入栈,运行函数。

lambda 函数把 “定义函数” 和 “参数入栈” 放在 lambda 函数的定义阶段,把 “运行函数” 放在 lambda 函数的运行阶段。而普通函数则是把 “定义函数” 放在普通函数的定义阶段,把 “参数入栈” 和 “运行函数” 放在普通函数的运行阶段。

也即是说 lambda 函数在定义阶段做了两件事情:

  1. 定义 lambda 函数体,即{ ... }中的内容,但是并不运行此函数体,跟普通函数的定义一样,只是定义而不是运行。
  2. 通过[ ... ]把局部变量的参数放入 lambda 的函数栈。这点和普通函数不一样,普通函数运行时才会把传递的参数放入函数栈,而 lambda 函数是在定义时入栈。你想啊,lambda 函数的出现就是为了用局部变量,若此时不入栈,等运行 lambda 函数的时候,局部变量已经被析构了,还怎么入栈?

lambda 函数与普通函数的联系:

intmain(){//场景1:按值传递Bara1(5);function<void()>taskL=[a1](){cout<<"taskL"<<endl;};// lambda函数的定义taskL();// lambda函数的运行//等同于如下代码Barb1(5);Bar::funL(b1);//普通函数的运行//场景2:按左值引用传递Bara2(5);function<void()>taskM=[&a2](){cout<<"taskM"<<endl;};// lambda函数的定义taskM();// lambda函数的运行//等同于如下代码Barb2(5);Bar::funM(b2);//普通函数的运行//场景3:按右值引用传递Bara3(5);function<void()>taskN=[a3=move(a3)](){cout<<"taskN"<<endl;};// lambda函数的定义taskN();// lambda函数的运行//等同于如下代码Barb3(5);Bar::funN(b3);//普通函数的运行//亦等同于如下代码Barb32(5);Bar::funN2(move(b32));//普通函数的运行return0;}

关于 lambda 函数的更多用法参看:C++11 lambda匿名函数用法详解

4.7. 函数返回时发生了什么?

运行return语句时,函数先把return的值赋值给一个临时变量,然后再把临时变量赋值给接收的变量。

4.7.1. 示例

intmain(){Bar b=Bar::funO();return0;}

输出:

funO normal constructor move constructor destructor move constructor destructor destructor

若我们删除类Bar中的移动构造函数Bar(Bar&& x)则这里的输出为:

funO normal constructor copy constructor destructor copy constructor destructor destructor

分析:

  1. funO()中先运行Bar a(5);,此时触发 “normal constructor”;
  2. 接着运行return a,此时把a赋值给一个临时变量,假定为temp,运行如下类似代码:Bar temp = a,因为temp不存在,此时触发 “move constructor” (注意:若没定义 “move constructor” 则这里触发 “copy constructor” );
  3. 函数funO()结束,析构a,触发 “destructor”;
  4. 运行Bar b = Bar::funO();,把temp的值赋值给b,等价于运行如下类似代码:Bar b = temp,因为b不存在,所以触发 “move constructor” (注意:若没定义 “move constructor” 则这里触发 “copy constructor” );
  5. 接着临时变量temp被析构,触发 “destructor”;
  6. main()运行结束,析构b,再次触发 “destructor”。

4.7.2. 示例

用引用接收函数的返回值就真的是引用吗?

intmain(){constBar&a=Bar::funP();constBar&b=Bar::funQ();Bar c=Bar::funQ();return0;}

输出:

funP normal constructor copy constructor funQ normal constructor destructor destructor destructor

分析:

  1. funP()本身返回的不是引用类型,在const Bar &a = Bar::funP();中虽然用引用接收函数的返回值,但其实还是发生了拷贝行为。
  2. 只有当函数本身的定义是返回引用类型时,才能用引用或用非引用接收函数的返回值。当用引用接收时不会发生复制行为,例如变量b;而当使用非引用接收时会发生复制行为,例如变量c

4.8. 类对象的创建与赋值

4.8.1. 结论

  1. 对类进行构造,其实就是对类内数据成员的构造,相应的调用数据成员的构造函数;
  2. 类对象之间的赋值,其实就是对类内数据成员的赋值,相应的调用数据成员的移动构造或复制构造函数。

4.8.2. 示例

classWrapperBar{private:Bar x_;Bar y_=1;};voidfun1(){cout<<"fun1"<<endl;// 构造`WrapperBar a`其实就是构造类WrapperBar的数据成员`x_`和`y_`WrapperBar a;}WrapperBarfun2(){cout<<"fun2"<<endl;WrapperBar a;returna;}WrapperBarfun3(WrapperBar&a){cout<<"fun3"<<endl;returna;}intmain(){fun1();// fun2中的`WrapperBar a`是临时变量,// 且类WrapperBar的数据成员Bar实现了移动构造函数,这里会调用移动构造函数把临时值转移给b,// 若类WrapperBar的数据成员Bar只实现了复制构造函数,则这里调用复制构造函数,// 即,若移动构造和复制构造函数同时存在,则能调用移动构造函数完成的任务优先调用移动构造函数而非复制构造函数WrapperBar b=fun2();// b不是临时变量,不能移走,因此这里调用复制构造函数WrapperBar c=fun3(b);return0;}

运行结果:

fun1 empty constructor normal constructor destructor destructor fun2 empty constructor normal constructor move constructor move constructor destructor destructor fun3 copy constructor copy constructor destructor destructor destructor destructor

4.9. auto 类型推断

4.9.1. 结论

auto可以自动推导出常量const属性与指针*属性,但是不能推导出引用&属性。

4.9.2. 示例

#include<iostream>#include<type_traits>usingnamespacestd;classA{private:inta=1;int*b=&a;public:int&get1(){returna;}constint&get2(){returna;}int*get3(){returnb;}constint*get4(){returnb;}};intmain(){A a;autor1=a.get1();// r1为int类型auto&r2=a.get1();// r2为int&类型autor3=a.get2();// r3为int类型auto&r4=a.get2();// r4为const int&类型autor5=a.get3();// r5为int*类型autor6=a.get4();// r6为const int*类型cout<<boolalpha;cout<<is_same<decltype(r1),int>()<<endl;// turecout<<is_same<decltype(r2),int&>()<<endl;// turecout<<is_same<decltype(r3),int>()<<endl;// turecout<<is_same<decltype(r4),constint&>()<<endl;// turecout<<is_same<decltype(r5),int*>()<<endl;// turecout<<is_same<decltype(r6),constint*>()<<endl;// turereturn0;}

输出:

r1 r2 r3 r4 r5 r6

分析:

函数返回值其实要先赋值给一个临时变量,然后再把临时变量赋值给接收变量。例如:auto r1 = a.get1()可以分解为如下步骤:

  1. 构造局部变量a
  2. 运行int& temp = a
  3. 运行auto r1 = temp,则r1当然为int型而不是int&类型。
  4. 这段分析可以参考《函数返回时发生了什么?》章节。

4.10. T&&引用折叠

4.10.1. 示例

template<typenameT>voidfun1(T&&a){cout<<boolalpha<<endl;cout<<is_same<int&&,decltype(a)>::value<<endl;// truecout<<is_same<int&,decltype(a)>::value<<endl;// falsecout<<is_rvalue_reference<decltype(a)>::value<<endl;// true}template<typenameT>voidfun2(T&&a){cout<<boolalpha<<endl;cout<<is_same<int&&,decltype(a)>::value<<endl;// falsecout<<is_same<int&,decltype(a)>::value<<endl;// truecout<<is_lvalue_reference<decltype(a)>::value<<endl;// true}template<typenameT>voidfun3(T&&a){cout<<boolalpha<<endl;cout<<is_same<int&&,decltype(a)>::value<<endl;// falsecout<<is_same<int&,decltype(a)>::value<<endl;// truecout<<is_lvalue_reference<decltype(a)>::value<<endl;// true}template<typenameT>voidfun4(T&&a){cout<<boolalpha<<endl;cout<<is_same<int&&,decltype(a)>::value<<endl;// falsecout<<is_same<int&,decltype(a)>::value<<endl;// truecout<<is_lvalue_reference<decltype(a)>::value<<endl;// true}intmain(){inta=1;// a是左值int&b=a;// b是左值,虽然b的类型是左值引用,但b本身是左值,因为b有名字int&&c=2;// c是左值,虽然c的类型是右值引用,但c本身是左值,因为c有名字fun1(3);// true,因为3是右值fun2(a);// true,因为a是左值fun3(b);// true,因为b是左值fun4(c);// true,因为c是左值return0;}

4.10.2. 结论

  1. 若传给 T&&的是右值,则 T&&推导后的结果为右值引用类型
  2. 若传给 T&&的是左值,则 T&&推导后的结果为左值引用类型

4.11. forward 完美转发

4.11.1. 示例 1:非完美转发

#include<iostream>#include<type_traits>usingnamespacestd;template<typenameT>voidfun2(T&&x){cout<<boolalpha<<is_rvalue_reference<decltype(x)>::value<<endl;// falsecout<<boolalpha<<is_lvalue_reference<decltype(x)>::value<<endl;// true}template<typenameT>voidfun1(T&&x){cout<<boolalpha<<is_rvalue_reference<decltype(x)>::value<<endl;// truefun2(x);}intmain(){fun1(1);return0;}

分析:

虽然在fun1中推导出T是右值引用类型,但用的是非完美转发,到fun2T变成了左值引用类型。

4.11.2. 示例 2:完美转发

#include<iostream>#include<type_traits>#include<utility>usingnamespacestd;template<typenameT>voidfun2(T&&x){cout<<boolalpha<<is_rvalue_reference<decltype(x)>::value<<endl;// truecout<<boolalpha<<is_lvalue_reference<decltype(x)>::value<<endl;// false}template<typenameT>voidfun1(T&&x){cout<<boolalpha<<is_rvalue_reference<decltype(x)>::value<<endl;// truefun2(forward<T>(x));}intmain(){fun1(1);return0;}

分析:

fun1中推导出T是右值引用类型,然后使用forward完美转发,到fun2中,T还是右值引用类型。

4.11.3. 结论

当使用T&&类型推导时,使用forward可以完美转发右值引用类型。

4.12. 右值的生命周期

4.12.1. 结论

一般来说右值在表达式结束后就要被销毁,但通过右值引用,右值的生命周期得到了延续,变得和右值引用变量的生命周期一样长。

4.12.2. 示例

intmain(){Bar(5);cout<<"something else for 5"<<endl;Bar&&a=Bar(6);cout<<"something else for 6"<<endl;return0;}

输出:

normal constructor destructor somethingelsefor5normal constructor somethingelsefor6destructor

分析:

Bar(5)是右值,在第 3 行运行结束后就被销毁了,但同样是右值的Bar(6)变得和变量a的生命周期一样长。

4.13. 引用的引用

4.13.1. 结论

  1. 可以对左值引用进行左值引用;
  2. 可以对右值引用进行左值引用,但不能对右值引用进行右值引用。

4.13.2. 示例

intmain(){inta1=5;int&a2=a1;// a2左值引用a1int&a3=a2;// a3左值引用a2,但其实还是左值引用a1,实际等效于int&a3 = a1int&&b1=6;// b1右值引用6int&b2=b1;// b2左值引用b1int&&b3=b1;// 编译错误:cannot bind rvalue reference of type ‘int&&’ to lvalue of type ‘int’return0;}

4.14. 万能引用类型 const&(常量左值引用)

const&(常量左值引用)是一个 “万能” 的引用类型,可以接受 “左值、右值、常量左值和常量右值,左值引用,右值引用,常量左值引用,常量右值引用”。

4.14.1. 示例

intmain(){inta=1;constinta1=1;int&b=a;constint&b1=a;int&&c=1;constint&&c1=1;// const int&绑定左值constint&d1=a;int&d2=a;// const int&绑定常量左值constint&d3=a1;int&d4=a1;// Error: Binding reference of type 'int' to value of type 'const int' drops 'const' qualifier// const int&绑定右值constint&e1=1;int&e2=1;// Error: Non-const lvalue reference to type 'int' cannot bind to a temporary of type 'int'// const int&绑定常量右值constint&e3=1+1;int&e4=1+1;// Error: Non-const lvalue reference to type 'int' cannot bind to a temporary of type 'int'// const int&绑定左值引用constint&f1=b;int&f2=b;// const int&绑定常量左值引用constint&f3=b1;int&f4=b1;// Error: Binding reference of type 'int' to value of type 'const int' drops 'const' qualifier// const int&绑定右值引用constint&g1=c;int&g2=c;// const int&绑定常量右值引用constint&g3=c1;int&g4=c1;// Error:Binding reference of type 'int' to value of type 'const int' drops 'const' qualifierreturn0;}

4.15. 右值引用独立于左值和右值

右值引用独立于左值和右值,意思是右值引用类型的变量可能是左值也可能是右值。

4.15.1. 示例 1:右值引用类型作为左值

intmain(){int&&a=1;// a的类型为右值引用类型,但a本身是左值(所有具名变量都是左值),因此可以修改a的值cout<<a<<endl;a=2;// Right,可以修改a的值cout<<a<<endl;return0;}

4.15.2. 示例 2:右值引用类型作为右值

int&&mymove(int&x){returnstatic_cast<int&&>(x);}voidfun1(int&&x){}voidfun2(int&x){}intmain(){inta=1;// Right:// mymove函数返回的类型是右值引用,但它本身是一个右值(因为它是函数返回的临时值,而临时值是右值),fun1参数是右值引用类型,// 右值引用类型绑定右值,因此语法正确cout<<mymove(a)<<endl;fun1(mymove(a));// Error:// mymove函数返回的类型是右值引用,但它本身是一个右值(因为它是函数返回的临时值,而临时值是右值),fun2参数是左值引用类型,// 左值引用类型绑定左值,而mymove的返回值是右值,因此语法错误,编译器提示:Candidate function not viable: expects an// lvalue for 1st argumentfun2(mymove(a));return0;}

4.15.3. 结论

  1. 示例 1 和示例 2 测试的都是右值引用类型,但它们本身可以是左值,也可以是右值。

4.16. move 需要搭配移动构造函数或移动赋值函数使用

《std::move原理》章节分析了std::move的原理,它就是把一个值变成右值并返回,除此之外什么也不干。而移动构造函数或移动赋值函数根据函数重载的定义可以看出,它就是要接收一个右值作为参数。所以二者需要搭配使用。要想实现移动语义进行资源转移,就必须要实现移动构造函数/移动赋值函数,否则你只写个T x = move(a) 或 T x; x = move(a)是没有任何意义的,这句话正确运行的前提必须是类型T定义了移动构造函数/移动赋值函数。

4.16.1. 示例

intmain(){Bara(5);Bar b=move(a);return0;}

分析:

  1. 第 3 行,通过 “normal constructor” 定义了变量a。构造函数在堆内存动态分配了一个空间存放5,假设这块堆内存的地址是0X5577,也就是说value_ptr_指向0X5577这块地址。
  2. 第 4 行的意思是,我不想要变量a了,我想把a持有的堆资源0X5577转移给别人,那么我此时就调用move先把a变成一个右值,然后等号就会触发 “move constructor”。根据 “move constructor” 的实现可以看出来,是把avalue_ptr_的地址给了b,然后把 a 的value_ptr_赋值为nullptr,这就实现了资源转移。若我第 4 行想要的效果不是转移资源,而仅仅是把资源复制一份给b,那么我们就应该用如下代码Bar b = a,这里会调用复制构造函数 “copy constructor”,再申请一块堆内存,并把此内存的值赋值成5,把此内存的地址赋值给bvalue_ptr_,此时我a还是持有原来的资源。

因此说白了,move搭配 “move constructor” 只有在想转移堆资源时才有意义。因为堆内存的申请和释放是比较耗时的,相比栈内存慢得多。所以为了避免堆内存的重复申请和释放,引入了右值的概念用于堆资源转移,右值就是为了堆资源转移而生的。若没有move和 “move constructor” 想要实现堆资源转移,b就需要先申请一块堆内存,然后把a堆内存的值赋值给b,然后再释放a的堆内存,就引入了堆内存的重新申请和释放开销,比较耗时。

理解了原理,我们就知道了move和 “move constructor” 的本质,它们只应该用在那些堆资源需要转移的地方,用在栈上是毫无意义的,考虑如下代码:

intmain(){inta=3;intb=move(a);return0;}

这段代码毫无意义,因为ab都是栈内存,用move毫无意义。

4.17. 移动构造函数, 移动赋值函数, 析构函数的注意事项

  1. 移动构造函数和移动赋值函数在资源转移后必须要把老的指针赋值为nullptr
  2. 好的析构函数中必须要有判断是否为nullptr的逻辑。

4.17.1. 示例 1

若修改上述类Bar的移动构造函数和移动赋值函数为如下,则会引入资源重复释放问题:

Bar(Bar&&x){if(x.value_ptr_!=nullptr){value_ptr_=x.value_ptr_;// x.value_ptr_ = nullptr;}cout<<"move constructor"<<endl;}Bar&operator=(Bar&&x){if(x.value_ptr_!=nullptr){value_ptr_=x.value_ptr_;// x.value_ptr_ = nullptr;}cout<<"move operator="<<endl;return*this;}

运行:

intmain(){Bara(5);Bar b=move(a);return0;}

输出:

free():doublefree detected in tcache2normal constructor move constructor destructor destructor

因为abvalue_ptr_指向同一块堆内存,ab析构时触发自身的析构函数,会重复释放两次这块堆内存。

4.17.2. 示例 2

修改上述类Bar的析构函数为如下代码虽然不会出错,但这是一种不好的写法

~Bar(){cout<<"destructor"<<endl;deletevalue_ptr_;}

考虑资源移动场景,把a的资源转给了b,此时avalue_ptr_nullptr,因此析构a时这里实际执行的是delete nullptrdelete一个nullptr是不会出错的,但这种写法不好,最好还是进行if判断。

4.18. std::vector 行为分析

4.18.1. 示例 1:push_back

运行:

intmain(){cout<<"----1"<<endl;Bara(1);Barb(2);cout<<"----2"<<endl;vector<Bar>list;cout<<"----3"<<endl;list.push_back(a);cout<<"----4"<<endl;list.push_back(b);cout<<"----5"<<endl;return0;}

输出:

----1normal constructor normal constructor----2----3copy constructor----4copy constructor copy constructor destructor----5destructor destructor destructor destructor

明明只有两次push_back操作,但是为什么调用了三次copy constructor?因为,list初始化时的堆空间大小是 0,当第一次push_back后堆空间大小变为 1,在进行第二次push_back操作时堆空间不够了,需要重新分配一个 2 的空间,把老数据搬移过去,在把新数据放在尾巴上。搬运的时候额外调用了一次copy constructor

4.18.2. 示例 2:push_back + reserve

运行:

intmain(){cout<<"----1"<<endl;Bara(1);Barb(2);cout<<"----2"<<endl;vector<Bar>list;list.reserve(2);cout<<"----3"<<endl;list.push_back(a);cout<<"----4"<<endl;list.push_back(b);cout<<"----5"<<endl;return0;}

输出:

----1normal constructor normal constructor----2----3copy constructor----4copy constructor----5destructor destructor destructor destructor

reserve预留 2 个空间,这样push_back几次就调用几次copy constructor

4.18.3. 示例 3:push_back + reserve + move

运行:

intmain(){cout<<"----1"<<endl;Bara(1);Barb(2);cout<<"----2"<<endl;vector<Bar>list;list.reserve(2);cout<<"----3"<<endl;list.push_back(move(a));cout<<"----4"<<endl;list.push_back(move(b));cout<<"----5"<<endl;return0;}

输出:

----1normal constructor normal constructor----2----3move constructor----4move constructor----5destructor destructor destructor destructor

增加了move操作,这样push_back就会变成调用move constructor,达到和原地emplace_back一样的效果。

4.18.4. 示例 3:提前构造 + emplace_back

运行:

intmain(){cout<<"----1"<<endl;Bara(1);Barb(2);cout<<"----2"<<endl;vector<Bar>list;cout<<"----3"<<endl;list.emplace_back(a);cout<<"----4"<<endl;list.emplace_back(b);cout<<"----5"<<endl;return0;}

输出:

----1normal constructor normal constructor----2----3copy constructor----4copy constructor copy constructor destructor----5destructor destructor destructor destructor

运行:

intmain(){cout<<"----1"<<endl;Bara(1);Barb(2);cout<<"----2"<<endl;vector<Bar>list;list.reserve(2);cout<<"----3"<<endl;list.emplace_back(a);cout<<"----4"<<endl;list.emplace_back(b);cout<<"----5"<<endl;return0;}

输出:

----1normal constructor normal constructor----2----3copy constructor----4copy constructor----5destructor destructor destructor destructor

这两个例子都是提前构造好Bar aBar b,这时候emplace_back退化为和push_back一样的行为。

4.18.5. 示例 4:原地构造 + emplace_back

运行:

intmain(){vector<Bar>list;cout<<"----1"<<endl;list.emplace_back(1);cout<<"----2"<<endl;list.emplace_back(2);cout<<"----3"<<endl;return0;}

输出:

----1normal constructor----2normal constructor copy constructor destructor----3destructor destructor

emplace_back相当于就是原地进行构造了。至于多的 1 次copy constructor和示例 1 一样。这里不会调用move constructor,因为旧的堆空间要释放,因此只能是拷贝操作把资源从老的堆空间转移到新的堆空间。

4.18.6. 示例 4:原地构造 + emplace_back + reserve

运行:

intmain(){vector<Bar>list;list.reserve(2);cout<<"----1"<<endl;list.emplace_back(1);cout<<"----2"<<endl;list.emplace_back(2);cout<<"----3"<<endl;return0;}

输出:

----1normal constructor----2normal constructor----3destructor destructor

这个例子更加说明了emplace_back就是原地构造,不会调用额外的move constructor函数。

4.19. std::unique_ptr 行为分析

运行:

intmain(){unique_ptr<Bar>pa(newBar(1));Bar*raw_pa=pa.get();unique_ptr<Bar>pb=move(pa);Bar*raw_pb=pb.get();if(raw_pa==raw_pb){cout<<"raw_pa == raw_pb"<<endl;}if(pa.get()==nullptr){cout<<"pa.get() == nullptr"<<endl;}return0;}

输出:

normal constructor raw_pa==raw_pb pa.get()==nullptrdestructor

说明把一个智能指针a通过move操作赋值给另一个智能指针b的时候,调用的是智能指针的移动构造函数,而不会调用所管理对象的移动构造函数。在智能指针的移动构造函数中,只是把所管理对象的指针交接从a交接给b,并把a的指针赋为nullptr

4.20. std::vector + std::unique_ptr 行为分析

4.20.1. 示例 1:push_back

运行:

intmain(){unique_ptr<Bar>pa(newBar(1));unique_ptr<Bar>pb(newBar(2));vector<unique_ptr<Bar>>list;list.push_back(move(pa));list.push_back(move(pb));return0;}

输出:

normal constructor normal constructor destructor destructor
  • 这里必须调用move,因为unique_ptr不允许赋值构造,只能移动构造。
  • 这里调不调用reverse都一样,就算有旧堆到新堆的转换,那么也只是unique_ptr资源交接的行为,本身已经构造的两个Bar对象在另外的地方,不在list中占用空间。

4.20.2. 示例 2:提前构造 + emplace_back

运行:

intmain(){unique_ptr<Bar>pa(newBar(1));unique_ptr<Bar>pb(newBar(2));vector<unique_ptr<Bar>>list;list.emplace_back(move(pa));list.emplace_back(move(pb));return0;}

输出:

normal constructor normal constructor destructor destructor

emplace_back退化为和push_back一样的行为。

4.20.3. 示例 3:原地构造 + emplace_back

运行:

intmain(){vector<unique_ptr<Bar>>list;list.emplace_back(newBar(1));list.emplace_back(newBar(2));return0;}

输出:

normal constructor normal constructor destructor destructor

说明已经通过指针管理了,提不提前构造都一样。

4.20.4. vector 元素析构顺序解析

运行:

#include<iostream>#include<memory>#include<vector>usingnamespacestd;classFoo{public:Foo(intval):val_(val){cout<<"constructor "<<val<<endl;}~Foo(){cout<<"destructor "<<val_<<endl;}private:intval_=0;};intmain(){{vector<Foo>test1;test1.reserve(4);test1.emplace_back(1);test1.emplace_back(2);test1.emplace_back(3);test1.emplace_back(4);}{vector<unique_ptr<Foo>>test2;test2.reserve(4);test2.emplace_back(make_unique<Foo>(1));test2.emplace_back(make_unique<Foo>(2));test2.emplace_back(make_unique<Foo>(3));test2.emplace_back(make_unique<Foo>(4));}return0;}

输出:

constructor1constructor2constructor3constructor4destructor1destructor2destructor3destructor4constructor1constructor2constructor3constructor4destructor1destructor2destructor3destructor4

说明 vector 元素的析构顺序与构造顺序相同,先构造的先析构,后构造的后析构。

4.21. 成员函数声明后加const&const &&&const &&的含义

在 C++ 中,成员函数声明后可以添加const,&,const &,&&const &&。这些修饰符被称为引用限定符 (Ref-Qualifiers)常量性 (Const-Qualifiers),它们主要用来限定成员函数可以被哪种类型的对象(左值、右值、常量左值、常量右值)调用,以及在函数内部是否可以修改对象的状态。

4.21.1. 常量限定符(const

含义:限制成员函数不能修改成员变量。它允许函数被const对象(左值或右值)和const对象调用。

用途:用于实现观察者 (Observer)行为,确保函数只读取对象的状态而不改变它。

示例:

classMyClass{private:intvalue;public:MyClass(intv):value(v){}// const 成员函数:不能修改 'value'intgetValue()const{// value = 10; // 编译错误!returnvalue;}// 非 const 成员函数:可以修改 'value'voidsetValue(intv){value=v;}};voidtest_const_qualifier(){constMyClasscobj(5);MyClassobj(1);cobj.getValue();// OK,const 对象只能调用 const 函数// cobj.setValue(10); // 编译错误!obj.getValue();// OK,非 const 对象可以调用 const 函数obj.setValue(10);// OK}

4.21.2. 引用限定符(&,const &,&&,const &&

引用限定符 (&,&&) 是 C++11 引入的,用于根据调用对象的类型(左值或右值)重载 (overload)成员函数。

声明形式作用允许调用的对象类型典型用途
const保证函数不修改对象状态const和非const的左值/右值观察者 (Observer) 函数
&限制只能被非const左值调用const左值允许修改对象并阻止临时对象被修改
const &限制只能被const左值调用const左值搭配const,提供常量左值操作
&&限制只能被非const右值调用const右值 (临时对象)启用移动语义,用于优化性能
const &&限制只能被const右值调用const右值 (常量临时对象)搭配const,处理常量右值

通常,我们会结合const和引用限定符来提供最完整的重载集,尤其是在需要根据对象是左值还是右值来执行不同操作(例如,移动语义 vs 复制语义)时。

考虑一个返回对象内部数据成员的函数data()

classDataWrapper{private:std::string internal_data="Hello";public:// 1. 左值版本 (Lvalue Reference Qualifier):// 只能被非 const 的左值对象调用。// 返回一个可修改的左值引用,允许用户修改内部数据。std::string&data()&{std::cout<<"-> 被非 const 左值调用 (允许修改)"<<std::endl;returninternal_data;}// 2. 常量左值版本 (Const Lvalue Reference Qualifier):// 只能被 const 的左值对象调用。// 返回一个 const 引用,不允许用户修改内部数据。conststd::string&data()const&{std::cout<<"-> 被 const 左值调用 (只读)"<<std::endl;returninternal_data;}// 3. 右值版本 (Rvalue Reference Qualifier):// 只能被非 const 的右值对象调用 (临时对象)。// 通常用于实现 '移动' 语义,将内部数据移出,避免复制。std::stringdata()&&{std::cout<<"-> 被右值调用 (执行移动)"<<std::endl;returnstd::move(internal_data);// 返回值可以是右值,允许调用者移动}// 4. 常量右值版本 (Const Rvalue Reference Qualifier):// 只能被 const 的右值对象调用。// 由于是 const,只能执行 '复制'。conststd::stringdata()const&&{std::cout<<"-> 被 const 右值调用 (执行复制)"<<std::endl;returninternal_data;}};voidtest_ref_qualifiers(){DataWrapper lvalue_obj;// 左值constDataWrapper const_lvalue_obj;// const 左值// 调用左值版本lvalue_obj.data()="World";// 调用常量左值版本const_lvalue_obj.data();// 调用右值版本 (DataWrapper() 是一个临时对象,即右值)std::string s1=DataWrapper().data();// 调用常量右值版本 (const DataWrapper() 是一个 const 临时对象,即 const 右值)std::string s2=static_cast<constDataWrapper>(DataWrapper()).data();}
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/10 19:18:02

DT_digital_twin_ROS+Grazebo仿真

在 ROS 2 Humble&#xff08;对应Ubuntu 22.04&#xff09;环境下&#xff0c;推荐安装Gazebo 版本是 Gazebo Fortress &#xff08;也称为Gazebo Classic 的继任者&#xff0c;属于 Ignition Gazebo / Gazebo Sim 系列&#xff09;。注意&#xff1a;自ROS 2 Humble起&#xf…

作者头像 李华
网站建设 2026/3/13 20:04:27

毕业设计项目 stm32与深度学习口罩佩戴检测系统(源码+硬件+论文)

文章目录 0 前言1 主要功能2 硬件设计(原理图)3 核心软件设计4 实现效果5 最后 0 前言 &#x1f525; 这两年开始毕业设计和毕业答辩的要求和难度不断提升&#xff0c;传统的毕设题目缺少创新和亮点&#xff0c;往往达不到毕业答辩的要求&#xff0c;这两年不断有学弟学妹告诉…

作者头像 李华
网站建设 2026/3/13 23:11:23

如何快速掌握Marketch插件:从安装到高效使用的完整指南

如何快速掌握Marketch插件&#xff1a;从安装到高效使用的完整指南 【免费下载链接】marketch Marketch is a Sketch 3 plug-in for automatically generating html page that can measure and get CSS styles on it. 项目地址: https://gitcode.com/gh_mirrors/ma/marketch …

作者头像 李华
网站建设 2026/3/12 2:19:01

千元级路由器选购:从Wi-Fi 7技术到硬件配置的核心考量

在千元级别路由器这一市场范围之内&#xff0c;存在着多样选择情况供消费者去面对&#xff0c;此价位区间将诸多品牌的中高端甚至部分旗舰型号都聚集在了一起&#xff0c;它是追求稳定性能、前瞻技术以及高性价比的一个平衡点所在之处。针对家庭里不断增长的智能设备情况、高带…

作者头像 李华
网站建设 2026/3/12 9:35:03

Android应用开发实战指南:完整项目资源解析

Android应用开发实战指南&#xff1a;完整项目资源解析 【免费下载链接】Android开发期末大作业资源文件 本仓库提供了一个Android开发期末大作业的资源文件&#xff0c;文件名为android开发期末大作业.zip。该资源文件包含了项目源码、任务书、实验大报告以及apk文件。通过这些…

作者头像 李华
网站建设 2026/3/13 7:53:34

LangChain4j流式AI交互终极指南:5大实战技巧与避坑方案

LangChain4j流式AI交互终极指南&#xff1a;5大实战技巧与避坑方案 【免费下载链接】langchain4j langchain4j - 一个Java库&#xff0c;旨在简化将AI/LLM&#xff08;大型语言模型&#xff09;能力集成到Java应用程序中。 项目地址: https://gitcode.com/GitHub_Trending/la…

作者头像 李华