北京市规划网站沈阳网络推广
2026/1/16 19:15:44 网站建设 项目流程
北京市规划网站,沈阳网络推广,代写,it网站建设方案1. 数据类型 对于任意一种数据类型#xff0c;在 C 里边都有与之相对应的4 种数据类型#xff0c;以 int 类型为例#xff1a; int#xff1a;int 类型#xff1b;int*#xff1a;int 的指针类型#xff1b;int#xff1a;int 的左值引用类型#xff1b;int在 C 里边都有与之相对应的4 种数据类型以int类型为例intint 类型int*int 的指针类型intint 的左值引用类型intint 的右值引用类型。如何从代码层面判断是什么类型呢—— 可用 C 的type_traits库来判断类型。#includeiostream#includetype_traitsusingnamespacestd;intmain(){inta3;int*ptr_aa;intleft_refa;intright_ref4;coutboolalpha;couta is base type? is_samedecltype(a),int()endl;// truecoutptr_a is point type? is_pointerdecltype(ptr_a)::valueendl;// truecoutleft_ref is left value reference? is_lvalue_referencedecltype(left_ref)::valueendl;// truecoutright_ref is right value reference? is_rvalue_referencedecltype(right_ref)::valueendl;// truecoutint is base type? is_sameint,int()endl;// truecoutint* is point type? is_pointerint*::valueendl;// truecoutint is left value reference? is_lvalue_referenceint::valueendl;// truecoutint is right value reference? is_rvalue_referenceint::valueendl;// truereturn0;}2. 左值和右值左值的英文简写为 “lvalue”右值的英文简写为 “rvalue”。一般认为它们分别是 “left value”、“right value” 的缩写其实不然。lvalue 是 “loactor value” 的缩写可意为存储在内存中、有明确存储地址可寻址的数据而 rvalue 译为 “read value”指的是那些可以提供数据值的数据。简单来说所有的具名变量都是左值左值可以被取地址左值可以出现在等号的左边也可以出现在等号的右边。所有的匿名变量则是右值右值不能被取地址右值只能出现在等号的右边。数据类型的概念和左值右值的的概念是两回事不是同一个东西。它们分别从两个维度来描述一个值的属性也即是说一个值有两个属性类型属性和此值是左值还是右值的属性。intinit*int类型的值可以是左值也可以是右值当它们作为变量具有名字时就是左值当它们作为函数返回的临时值或表达式的计算值时就是右值。int类型的值始终只能是左值不能是右值不管是作为变量还是作为函数的返回值它都是左值。非引用返回的临时变量运算表达式产生的临时变量原始字面量和 lambda 表达式等都是右值。示例intmain(){5;// 5是右值3.14;// 3.14是右值Bar(6);// Bar(6)是右值inta;// a是左值intb1;// b是左值intca;// c也是左值虽然c的类型是左值引用但c本身是左值因为c有名字intd5;// d也是左值虽然d的类型是右值引用但d本身是左值因为d有名字intec;// 合法虽然c的类型是左值引用但c本身是左值左值引用是对左值的引用因此赋值语句合法intfd;// 合法虽然d的类型是右值引用但d本身是左值左值引用是对左值的引用因此赋值语句合法intgd;// 非法虽然d的类型是右值引用但d本身是左值而右值引用是对右值的引用d不是右值因此赋值语句非法return0;}2.1. 左值到右值的转换一个左值可以被转换convert为右值这完全合法且经常发生。让我们先用操作符作为一个例子根据 C 的规范specification它使用两个右值作为参数并返回一个右值。示例intx1;inty3;intzxy;// Rightx和y是左值但是加法操作符需要右值作为参数发生了什么答案很简单x和y经历了一个隐式implicit的左值到右值lvalue-to-rvalue的转换许多其它的操作符也有同样的转换例如减法、加法、除法等。2.2. 右值到左值的转换相反呢一个右值可以被转化为左值吗不可以它不是技术所限而是 C 编程语言就是那样设计的。因为右值是没有地址的左值是有地址的把右值变成左值需要为右值分配地址如果这么做了这就和右值的语义不符了。2.3. 函数返回值一般是右值但也可以是左值2.3.1. 示例intglobal1;intfun1(){returnglobal;}intfun2(){return1;}intmain(){fun1()2;// Right: fun1返回的是global的左值引用左值引用都是左值因此可以对一个左值进行重新赋值甚至取地址fun2()3;// Error: fun2返回的是右值不能对右值赋值编译错误return0;}2.3.2. 结论当函数的返回类型是左值引用类型时函数的返回值是左值否则函数的返回值都是右值。3. 引用的概念引用分为左值引用和右值引用左值引用大家比较熟悉这里就不在累述了主要学习一下右值引用的基本概念和注意点。关于右值引用网上这篇文章讲解的比较好看这篇就够了 —— 从4行代码看右值引用。关于此篇文章的总结所有的具名变量或对象都是左值而匿名变量则是右值。C11 中所有的值必属于左值、将亡值、纯右值三者之一。纯右值在表达式结束之后就销毁了。通过右值引用右值又 “重获新生”其生命周期与右值引用类型变量的生命周期一样长只要该变量还活着该右值临时量将会一直存活下去。右值引用独立于左值和右值意思是右值引用类型的变量可能是左值也可能是右值。T t在发生自动类型推断的时候它是未定的引用类型当它被右值初始化时它是右值引用类型其它情况都是左值引用类型。常量左值引用是一个 “万能” 的引用类型接受** “左值、右值、常量左值和常量右值左值引用右值引用常量左值引用常量右值引用” **。右值引用T是一个未定的引用类型可以接受左值或者右值正是这个特性让它适合作为一个参数的路由然后再通过std::forward按照参数的实际类型去匹配对应的重载函数最终实现完美转发。所有的右值引用叠加到右值引用上仍然还是一个右值引用所有的其它引用类型之间的叠加都将变成左值引用。4. 实例分析与讲解4.1. 测试代码4.1.1. 类定义后续测试几乎全部依据此类。#includefunctional#includeiostream#includememory#includetype_traits#includevectorusingnamespacestd;classBar{public:Bar(){coutempty constructorendl;}Bar(intx){value_ptr_newint(x);coutnormal constructorendl;}Bar(constBarx){if(x.value_ptr_!nullptr){value_ptr_newint(*x.value_ptr_);}coutcopy constructorendl;}Bar(Barx){if(this-value_ptr_x.value_ptr_){coutmove constructor, is the same objectendl;return;}if(x.value_ptr_!nullptr){deletevalue_ptr_;value_ptr_x.value_ptr_;x.value_ptr_nullptr;}coutmove constructorendl;}~Bar(){coutdestructorendl;if(value_ptr_!nullptr){deletevalue_ptr_;}}Baroperator(constBarx){if(x.value_ptr_!nullptr){value_ptr_newint(*x.value_ptr_);}coutcopy operatorendl;return*this;}Baroperator(Barx){if(this-value_ptr_x.value_ptr_){coutmove operator, is the same objectendl;return*this;}if(x.value_ptr_!nullptr){deletevalue_ptr_;value_ptr_x.value_ptr_;x.value_ptr_nullptr;}coutmove operatorendl;return*this;}intGetValue()const{if(value_ptr_nullptr){return-1;}return*value_ptr_;}voidSetValue(intx){*value_ptr_x;}staticvoidFunA(Bar x){coutFunAendl;}staticvoidFunB(constBar x){coutFunBendl;}staticvoidFunC(Barx){coutFunCendl;}staticvoidFunD(constBarx){coutFunDendl;}staticvoidFunE(Barx){coutFunEendl;}staticvoidFunF(constBarx){coutFunFendl;}staticfunctionvoid()funG(){coutfunGendl;Bara(5);coutthe content of a: a.GetValue()endl;coutthe address of a:aendl;// 注意这里仅仅定义task不会运行task。//[a]表示按值传递a因此要调用复制构造函数。functionvoid()task[a](){// 这里的a和外部的a不是一个a这个a是通过复制构造而来的然后存于lambda的栈空间。coutthe content of a: a.GetValue()endl;// 因为这里的a和外部的a不是一个a所以地址不一样。coutthe address of a:aendl;};// 这里仅返回定义的task不会运行task真正运行的地方为显示调用task_g()的地方。returntask;}staticfunctionvoid()funH(){coutfunHendl;Bara(5);coutref_a is left value reference? is_lvalue_referencedecltype(a)()endl;// falsecoutthe content of a: a.GetValue()endl;coutthe address of a:aendl;//[a]表示按引用传递afunctionvoid()task[a](){/* 这里的a就是外部a都是Bar类型。 * 因为funH()仅定义了task而真正运行这段代码地方是在funH()之外显示调用task_h()的地方a在funH()结束之后就被 * 析构了而真正运行task_h()的地方又引用了已经被析构的a所以这里获取不到值。 * */coutref_a is left value reference? is_lvalue_referencedecltype(a)()endl;// falsecoutthe content of a: a.GetValue()endl;// a虽然在funH()结束之后被析构了但这里还引用的是那块内存所以地址是一样的只不过无效了。coutthe address of a:aendl;};returntask;}staticfunctionvoid()funI(){coutfunIendl;Bara(5);Barref_aa;coutref_a is left value reference? is_lvalue_referencedecltype(ref_a)()endl;// truecoutthe content of ref_a: ref_a.GetValue()endl;coutthe address of ref_a:ref_aendl;//[ref_a]这里表示按值传递ref_a会调用a的复制构造函数。functionvoid()task[ref_a](){// 注意这里的ref_a依然是左值引用类型但引用的是一个新a而不是上边的a因此地址不一样。coutref_a is left value reference? is_lvalue_referencedecltype(ref_a)()endl;// truecoutthe content of a: ref_a.GetValue()endl;coutthe address of a:ref_aendl;};returntask;}staticfunctionvoid()funJ(){coutfunJendl;Bara(5);Barref_aa;coutref_a is left value reference? is_lvalue_referencedecltype(ref_a)()endl;// truecoutthe content of ref_a: ref_a.GetValue()endl;coutthe address of ref_a:ref_aendl;//[ref_a]这里表示按引用传递ref_a。functionvoid()task[ref_a](){/* 这里ref_a就是指向a与funH()情况一样a在funJ()结束后就被析构了因此这里是非法引用。 * 注意和funI()的区别。[ref_a]传参会调用复制构造函数构造一个新a导致ref_a指向新a。而[ref_a]传参不会构造新a * ref_a还是指向的同一个a。 * */coutref_a is left value reference? is_lvalue_referencedecltype(ref_a)()endl;// truecoutthe content of a: ref_a.GetValue()endl;coutthe address of a:ref_aendl;};returntask;}staticfunctionvoid()funK(){coutfunKendl;Bara(5);coutthe content of ref_a: a.GetValue()endl;coutthe address of ref_a:aendl;/* [a move(a)]会调用移动构造函数把外部的a(等号右边的a)的数据转移到lambda内部的a(等号左边的a)。 * 等号左右两边的a是不一样的左边是lambda内部的a右边是funK()的a。 * */functionvoid()task[amove(a)](){// a的地址变了。coutthe content of a: a.GetValue()endl;coutthe address of a:aendl;};returntask;}// 普通函数的定义staticvoidfunL(Bar a){coutfunLendl;}// 普通函数的定义staticvoidfunM(Bara){coutfunMendl;}// 普通函数的定义staticvoidfunN(Barx){coutfunNendl;Bar amove(x);}// 普通函数的定义staticvoidfunN2(Barx){coutfunN2endl;/* 注意这里x的类型是右值引用而x本身属于左值所以赋给a时若想调用move constructor则必须写成Bar a * move(x)若写成Bar a x则会触发copy constructor。move constructor是给右值用的 不是给右值引用用的。 * */coutx is right value reference? is_rvalue_referencedecltype(x)()endl;// trueBar amove(x);}staticBarfunO(){coutfunOendl;Bara(5);returna;}staticBarfunP(){coutfunPendl;staticBara(5);returna;}staticconstBarfunQ(){coutfunQendl;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_TYPEDebug ..; make # # # Build arm release version: # # rm -rf build; mkdir build; cd build; cmake -Darcharm ..; make # # # Build arm debug version: # # rm -rf build; mkdir build; cd build; cmake -Darcharm -DCMAKE_BUILD_TYPEDebug ..; 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. */templatetypename_Tpconstexprtypenamestd::remove_reference_Tp::typemove(_Tp__t)noexcept{returnstatic_casttypenamestd::remove_reference_Tp::type(__t);}/// remove_referencetemplatetypename_Tpstructremove_reference{typedef_Tp type;};templatetypename_Tpstructremove_reference_Tp{typedef_Tp type;};templatetypename_Tpstructremove_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 行的重载经过typedef后type的类型就是int类型。场景 2若_Tp的类型是左值引用例如int a5; int lref_a a;。若此时_Tp是lref_a那么_Tp就是左值引用会调用第 17 行的重载那么经过第 18 行之后_Tp的左值引用类型被去掉了经过typedef后type的类型就是int类型。场景 3若_Tp的类型是右值引用例如int rref_a 5;。若此时_Tp是rref_a那么_Tp就是右值引用会调用第 21 行的重载那么经过第 22 行之后_Tp的右值引用类型被去掉了经过typedef后type的类型就是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. 调用规则根据一般参数创建新变量时调用一般构造函数。根据同类型左值变量创建新变量时调用复制构造函数。根据同类型右值变量创建新变量时调用移动构造函数。4.3.2. 示例 1intmain(){Bar e;// empty constructorBarn(1);// normal constructorBarc1(n);// copy constructorBar c2n;// copy constructorBarm3(move(n));// move constructorBar m4move(n);// move constructorreturn0;}解析第 69 行分析虽然这里用的是但依然调用的是复制构造函数而不是赋值运算符重载。因为此时变量c2不存在需要构造既然是构造就要调用构造函数。第 70 行分析上述讲解move时叙说了move的作用就是把一个变量变成右值这里move(n)就是把n变成右值根据构造函数重载的参数类型这里匹配到的是移动构造函数而不是赋值构造函数。第 71 行分析这里类似第 69 行m4不存在不存在就要构造所以这里调用的是移动构造函数而不是移动运算符重载。4.3.3. 示例 2intmain(){Bar a5;return0;}输出normal constructor move constructor destructor destructor解析仔细看这段代码乍一看应该是语法错误为什么因为5是int类型而a是Bar类型那为什么可以把int类型能转换成Bar类型—— 因为发生了隐式转换。这就是我们常说的explicit关键字。现有代码的构造函数如下Bar(intx){value_ptr_newint(x);coutnormal constructorendl;}构造函数没有加explicit关键子所以允许隐式转换我们再来分析运行Bar a 5;这行代码的行为先把5进行隐式转换调用Bar的 “normal constructor”把5变成Bar(5)因此这段代码等价于Bar a Bar(5)。返回的Bar(5)属于右值因此调用 “move constructor” 把Bar(5)给Bar a。假如我们修改构造函数为如下代码explicitBar(intx){value_ptr_newint(x);coutnormal constructorendl;}这里加了explicit关键字就不允许隐式转换所以再写Bar a 5;这样的代码属于语法错误编译不通过。以下是编译错误提示xxx/main.cpp:244:13:错误conversion from ‘int’ to non-scalar type ‘Bar’ requested Bar a5;^gmake[3]:***[CMakeFiles/main.dir/build.make:82CMakeFiles/main.dir/main.cpp.o]错误1gmake[2]:***[CMakeFiles/Makefile2:95CMakeFiles/main.dir/all]错误2gmake[1]:***[CMakeFiles/Makefile2:102CMakeFiles/main.dir/rule]错误2关于隐式转换的更多讲解参看explicit构造函数。4.4. 等号运算符重载等号运算符重载分为赋值运算符重载移动运算符重载。4.4.1. 调用规则用左值变量赋值给已经存在的变量调用赋值运算符重载。用右值变量赋值给已经存在的变量调用移动运算符重载。4.4.2. 示例 1intmain(){Barn(1);// normal constructorBar e1,e2;// empty constructore1n;// copy operatore1move(n);// move operatorreturn0;}解析因为e1和e2已经存在存在就不需要构造所以第 6 和 7 行调用的是等号运算符重载而不是构造函数。因为n是左值所以第 6 行调用赋值运算符重载。因为move(n)是右值所以第 7 行调用移动运算符重载。4.4.3. 示例 2intmain(){Bar e;e5;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 xaBar::FunD(a);// not call any constructor, just run const Bar xaBar::FunE(move(a));// not call any constructor, just run Bar xaBar::FunF(move(a));// not call any constructor, just run const Bar xareturn0;}解析第 5 行和第 6 行因为是按照值传递所以调用复制构造函数。第 7~10 行函数参数是左值引用或右值引用类型不会调用任何构造函数仅仅运行引用绑定。4.6. lambda 函数引用传参4.6.1. 示例intmain(){autotask_gBar::funG();task_g();autotask_hBar::funH();task_h();autotask_iBar::funI();task_i();autotask_jBar::funJ();task_j();autotask_kBar::funK();task_k();return0;}解析详细分析看 funG()~funK() 的函数注释。4.6.2. lambda 函数和普通函数的区别与联系首先要明确一个概念运行一个函数的完整流程是定义函数参数入栈运行函数。lambda 函数把 “定义函数” 和 “参数入栈” 放在 lambda 函数的定义阶段把 “运行函数” 放在 lambda 函数的运行阶段。而普通函数则是把 “定义函数” 放在普通函数的定义阶段把 “参数入栈” 和 “运行函数” 放在普通函数的运行阶段。也即是说 lambda 函数在定义阶段做了两件事情定义 lambda 函数体即{ ... }中的内容但是并不运行此函数体跟普通函数的定义一样只是定义而不是运行。通过[ ... ]把局部变量的参数放入 lambda 的函数栈。这点和普通函数不一样普通函数运行时才会把传递的参数放入函数栈而 lambda 函数是在定义时入栈。你想啊lambda 函数的出现就是为了用局部变量若此时不入栈等运行 lambda 函数的时候局部变量已经被析构了还怎么入栈lambda 函数与普通函数的联系intmain(){//场景1按值传递Bara1(5);functionvoid()taskL[a1](){couttaskLendl;};// lambda函数的定义taskL();// lambda函数的运行//等同于如下代码Barb1(5);Bar::funL(b1);//普通函数的运行//场景2按左值引用传递Bara2(5);functionvoid()taskM[a2](){couttaskMendl;};// lambda函数的定义taskM();// lambda函数的运行//等同于如下代码Barb2(5);Bar::funM(b2);//普通函数的运行//场景3按右值引用传递Bara3(5);functionvoid()taskN[a3move(a3)](){couttaskNendl;};// lambda函数的定义taskN();// lambda函数的运行//等同于如下代码Barb3(5);Bar::funN(b3);//普通函数的运行//亦等同于如下代码Barb32(5);Bar::funN2(move(b32));//普通函数的运行return0;}关于 lambda 函数的更多用法参看C11 lambda匿名函数用法详解4.7. 函数返回时发生了什么运行return语句时函数先把return的值赋值给一个临时变量然后再把临时变量赋值给接收的变量。4.7.1. 示例intmain(){Bar bBar::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分析在funO()中先运行Bar a(5);此时触发 “normal constructor”接着运行return a此时把a赋值给一个临时变量假定为temp运行如下类似代码Bar temp a因为temp不存在此时触发 “move constructor” 注意若没定义 “move constructor” 则这里触发 “copy constructor” 函数funO()结束析构a触发 “destructor”运行Bar b Bar::funO();把temp的值赋值给b等价于运行如下类似代码Bar b temp因为b不存在所以触发 “move constructor” 注意若没定义 “move constructor” 则这里触发 “copy constructor” 接着临时变量temp被析构触发 “destructor”当main()运行结束析构b再次触发 “destructor”。4.7.2. 示例用引用接收函数的返回值就真的是引用吗intmain(){constBaraBar::funP();constBarbBar::funQ();Bar cBar::funQ();return0;}输出funP normal constructor copy constructor funQ normal constructor destructor destructor destructor分析funP()本身返回的不是引用类型在const Bar a Bar::funP();中虽然用引用接收函数的返回值但其实还是发生了拷贝行为。只有当函数本身的定义是返回引用类型时才能用引用或用非引用接收函数的返回值。当用引用接收时不会发生复制行为例如变量b而当使用非引用接收时会发生复制行为例如变量c。4.8. 类对象的创建与赋值4.8.1. 结论对类进行构造其实就是对类内数据成员的构造相应的调用数据成员的构造函数类对象之间的赋值其实就是对类内数据成员的赋值相应的调用数据成员的移动构造或复制构造函数。4.8.2. 示例classWrapperBar{private:Bar x_;Bar y_1;};voidfun1(){coutfun1endl;// 构造WrapperBar a其实就是构造类WrapperBar的数据成员x_和y_WrapperBar a;}WrapperBarfun2(){coutfun2endl;WrapperBar a;returna;}WrapperBarfun3(WrapperBara){coutfun3endl;returna;}intmain(){fun1();// fun2中的WrapperBar a是临时变量// 且类WrapperBar的数据成员Bar实现了移动构造函数这里会调用移动构造函数把临时值转移给b// 若类WrapperBar的数据成员Bar只实现了复制构造函数则这里调用复制构造函数// 即若移动构造和复制构造函数同时存在则能调用移动构造函数完成的任务优先调用移动构造函数而非复制构造函数WrapperBar bfun2();// b不是临时变量不能移走因此这里调用复制构造函数WrapperBar cfun3(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 destructor4.9. auto 类型推断4.9.1. 结论auto可以自动推导出常量const属性与指针*属性但是不能推导出引用属性。4.9.2. 示例#includeiostream#includetype_traitsusingnamespacestd;classA{private:inta1;int*ba;public:intget1(){returna;}constintget2(){returna;}int*get3(){returnb;}constint*get4(){returnb;}};intmain(){A a;autor1a.get1();// r1为int类型autor2a.get1();// r2为int类型autor3a.get2();// r3为int类型autor4a.get2();// r4为const int类型autor5a.get3();// r5为int*类型autor6a.get4();// r6为const int*类型coutboolalpha;coutis_samedecltype(r1),int()endl;// turecoutis_samedecltype(r2),int()endl;// turecoutis_samedecltype(r3),int()endl;// turecoutis_samedecltype(r4),constint()endl;// turecoutis_samedecltype(r5),int*()endl;// turecoutis_samedecltype(r6),constint*()endl;// turereturn0;}输出r1 r2 r3 r4 r5 r6分析函数返回值其实要先赋值给一个临时变量然后再把临时变量赋值给接收变量。例如auto r1 a.get1()可以分解为如下步骤构造局部变量a运行int temp a运行auto r1 temp则r1当然为int型而不是int类型。这段分析可以参考《函数返回时发生了什么》章节。4.10. T引用折叠4.10.1. 示例templatetypenameTvoidfun1(Ta){coutboolalphaendl;coutis_sameint,decltype(a)::valueendl;// truecoutis_sameint,decltype(a)::valueendl;// falsecoutis_rvalue_referencedecltype(a)::valueendl;// true}templatetypenameTvoidfun2(Ta){coutboolalphaendl;coutis_sameint,decltype(a)::valueendl;// falsecoutis_sameint,decltype(a)::valueendl;// truecoutis_lvalue_referencedecltype(a)::valueendl;// true}templatetypenameTvoidfun3(Ta){coutboolalphaendl;coutis_sameint,decltype(a)::valueendl;// falsecoutis_sameint,decltype(a)::valueendl;// truecoutis_lvalue_referencedecltype(a)::valueendl;// true}templatetypenameTvoidfun4(Ta){coutboolalphaendl;coutis_sameint,decltype(a)::valueendl;// falsecoutis_sameint,decltype(a)::valueendl;// truecoutis_lvalue_referencedecltype(a)::valueendl;// true}intmain(){inta1;// a是左值intba;// b是左值虽然b的类型是左值引用但b本身是左值因为b有名字intc2;// c是左值虽然c的类型是右值引用但c本身是左值因为c有名字fun1(3);// true因为3是右值fun2(a);// true因为a是左值fun3(b);// true因为b是左值fun4(c);// true因为c是左值return0;}4.10.2. 结论若传给 T的是右值则 T推导后的结果为右值引用类型若传给 T的是左值则 T推导后的结果为左值引用类型4.11. forward 完美转发4.11.1. 示例 1非完美转发#includeiostream#includetype_traitsusingnamespacestd;templatetypenameTvoidfun2(Tx){coutboolalphais_rvalue_referencedecltype(x)::valueendl;// falsecoutboolalphais_lvalue_referencedecltype(x)::valueendl;// true}templatetypenameTvoidfun1(Tx){coutboolalphais_rvalue_referencedecltype(x)::valueendl;// truefun2(x);}intmain(){fun1(1);return0;}分析虽然在fun1中推导出T是右值引用类型但用的是非完美转发到fun2中T变成了左值引用类型。4.11.2. 示例 2完美转发#includeiostream#includetype_traits#includeutilityusingnamespacestd;templatetypenameTvoidfun2(Tx){coutboolalphais_rvalue_referencedecltype(x)::valueendl;// truecoutboolalphais_lvalue_referencedecltype(x)::valueendl;// false}templatetypenameTvoidfun1(Tx){coutboolalphais_rvalue_referencedecltype(x)::valueendl;// truefun2(forwardT(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);coutsomething else for 5endl;BaraBar(6);coutsomething else for 6endl;return0;}输出normal constructor destructor somethingelsefor5normal constructor somethingelsefor6destructor分析Bar(5)是右值在第 3 行运行结束后就被销毁了但同样是右值的Bar(6)变得和变量a的生命周期一样长。4.13. 引用的引用4.13.1. 结论可以对左值引用进行左值引用可以对右值引用进行左值引用但不能对右值引用进行右值引用。4.13.2. 示例intmain(){inta15;inta2a1;// a2左值引用a1inta3a2;// a3左值引用a2但其实还是左值引用a1实际等效于inta3 a1intb16;// b1右值引用6intb2b1;// b2左值引用b1intb3b1;// 编译错误cannot bind rvalue reference of type ‘int’ to lvalue of type ‘int’return0;}4.14. 万能引用类型 const常量左值引用const常量左值引用是一个 “万能” 的引用类型可以接受 “左值、右值、常量左值和常量右值左值引用右值引用常量左值引用常量右值引用”。4.14.1. 示例intmain(){inta1;constinta11;intba;constintb1a;intc1;constintc11;// const int绑定左值constintd1a;intd2a;// const int绑定常量左值constintd3a1;intd4a1;// Error: Binding reference of type int to value of type const int drops const qualifier// const int绑定右值constinte11;inte21;// Error: Non-const lvalue reference to type int cannot bind to a temporary of type int// const int绑定常量右值constinte311;inte411;// Error: Non-const lvalue reference to type int cannot bind to a temporary of type int// const int绑定左值引用constintf1b;intf2b;// const int绑定常量左值引用constintf3b1;intf4b1;// Error: Binding reference of type int to value of type const int drops const qualifier// const int绑定右值引用constintg1c;intg2c;// const int绑定常量右值引用constintg3c1;intg4c1;// Error:Binding reference of type int to value of type const int drops const qualifierreturn0;}4.15. 右值引用独立于左值和右值右值引用独立于左值和右值意思是右值引用类型的变量可能是左值也可能是右值。4.15.1. 示例 1右值引用类型作为左值intmain(){inta1;// a的类型为右值引用类型但a本身是左值所有具名变量都是左值因此可以修改a的值coutaendl;a2;// Right可以修改a的值coutaendl;return0;}4.15.2. 示例 2右值引用类型作为右值intmymove(intx){returnstatic_castint(x);}voidfun1(intx){}voidfun2(intx){}intmain(){inta1;// Right:// mymove函数返回的类型是右值引用但它本身是一个右值因为它是函数返回的临时值而临时值是右值fun1参数是右值引用类型// 右值引用类型绑定右值因此语法正确coutmymove(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 和示例 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 bmove(a);return0;}分析第 3 行通过 “normal constructor” 定义了变量a。构造函数在堆内存动态分配了一个空间存放5假设这块堆内存的地址是0X5577也就是说value_ptr_指向0X5577这块地址。第 4 行的意思是我不想要变量a了我想把a持有的堆资源0X5577转移给别人那么我此时就调用move先把a变成一个右值然后等号就会触发 “move constructor”。根据 “move constructor” 的实现可以看出来是把a中value_ptr_的地址给了b然后把 a 的value_ptr_赋值为nullptr这就实现了资源转移。若我第 4 行想要的效果不是转移资源而仅仅是把资源复制一份给b那么我们就应该用如下代码Bar b a这里会调用复制构造函数 “copy constructor”再申请一块堆内存并把此内存的值赋值成5把此内存的地址赋值给b的value_ptr_此时我a还是持有原来的资源。因此说白了move搭配 “move constructor” 只有在想转移堆资源时才有意义。因为堆内存的申请和释放是比较耗时的相比栈内存慢得多。所以为了避免堆内存的重复申请和释放引入了右值的概念用于堆资源转移右值就是为了堆资源转移而生的。若没有move和 “move constructor” 想要实现堆资源转移b就需要先申请一块堆内存然后把a堆内存的值赋值给b然后再释放a的堆内存就引入了堆内存的重新申请和释放开销比较耗时。理解了原理我们就知道了move和 “move constructor” 的本质它们只应该用在那些堆资源需要转移的地方用在栈上是毫无意义的考虑如下代码intmain(){inta3;intbmove(a);return0;}这段代码毫无意义因为a和b都是栈内存用move毫无意义。4.17. 移动构造函数, 移动赋值函数, 析构函数的注意事项移动构造函数和移动赋值函数在资源转移后必须要把老的指针赋值为nullptr好的析构函数中必须要有判断是否为nullptr的逻辑。4.17.1. 示例 1若修改上述类Bar的移动构造函数和移动赋值函数为如下则会引入资源重复释放问题Bar(Barx){if(x.value_ptr_!nullptr){value_ptr_x.value_ptr_;// x.value_ptr_ nullptr;}coutmove constructorendl;}Baroperator(Barx){if(x.value_ptr_!nullptr){value_ptr_x.value_ptr_;// x.value_ptr_ nullptr;}coutmove operatorendl;return*this;}运行intmain(){Bara(5);Bar bmove(a);return0;}输出free():doublefree detected in tcache2normal constructor move constructor destructor destructor因为a和b的value_ptr_指向同一块堆内存a和b析构时触发自身的析构函数会重复释放两次这块堆内存。4.17.2. 示例 2修改上述类Bar的析构函数为如下代码虽然不会出错但这是一种不好的写法~Bar(){coutdestructorendl;deletevalue_ptr_;}考虑资源移动场景把a的资源转给了b此时a的value_ptr_为nullptr因此析构a时这里实际执行的是delete nullptrdelete一个nullptr是不会出错的但这种写法不好最好还是进行if判断。4.18. std::vector 行为分析4.18.1. 示例 1push_back运行intmain(){cout----1endl;Bara(1);Barb(2);cout----2endl;vectorBarlist;cout----3endl;list.push_back(a);cout----4endl;list.push_back(b);cout----5endl;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. 示例 2push_back reserve运行intmain(){cout----1endl;Bara(1);Barb(2);cout----2endl;vectorBarlist;list.reserve(2);cout----3endl;list.push_back(a);cout----4endl;list.push_back(b);cout----5endl;return0;}输出----1normal constructor normal constructor----2----3copy constructor----4copy constructor----5destructor destructor destructor destructor用reserve预留 2 个空间这样push_back几次就调用几次copy constructor。4.18.3. 示例 3push_back reserve move运行intmain(){cout----1endl;Bara(1);Barb(2);cout----2endl;vectorBarlist;list.reserve(2);cout----3endl;list.push_back(move(a));cout----4endl;list.push_back(move(b));cout----5endl;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----1endl;Bara(1);Barb(2);cout----2endl;vectorBarlist;cout----3endl;list.emplace_back(a);cout----4endl;list.emplace_back(b);cout----5endl;return0;}输出----1normal constructor normal constructor----2----3copy constructor----4copy constructor copy constructor destructor----5destructor destructor destructor destructor运行intmain(){cout----1endl;Bara(1);Barb(2);cout----2endl;vectorBarlist;list.reserve(2);cout----3endl;list.emplace_back(a);cout----4endl;list.emplace_back(b);cout----5endl;return0;}输出----1normal constructor normal constructor----2----3copy constructor----4copy constructor----5destructor destructor destructor destructor这两个例子都是提前构造好Bar a和Bar b这时候emplace_back退化为和push_back一样的行为。4.18.5. 示例 4原地构造 emplace_back运行intmain(){vectorBarlist;cout----1endl;list.emplace_back(1);cout----2endl;list.emplace_back(2);cout----3endl;return0;}输出----1normal constructor----2normal constructor copy constructor destructor----3destructor destructoremplace_back相当于就是原地进行构造了。至于多的 1 次copy constructor和示例 1 一样。这里不会调用move constructor因为旧的堆空间要释放因此只能是拷贝操作把资源从老的堆空间转移到新的堆空间。4.18.6. 示例 4原地构造 emplace_back reserve运行intmain(){vectorBarlist;list.reserve(2);cout----1endl;list.emplace_back(1);cout----2endl;list.emplace_back(2);cout----3endl;return0;}输出----1normal constructor----2normal constructor----3destructor destructor这个例子更加说明了emplace_back就是原地构造不会调用额外的move constructor函数。4.19. std::unique_ptr 行为分析运行intmain(){unique_ptrBarpa(newBar(1));Bar*raw_papa.get();unique_ptrBarpbmove(pa);Bar*raw_pbpb.get();if(raw_paraw_pb){coutraw_pa raw_pbendl;}if(pa.get()nullptr){coutpa.get() nullptrendl;}return0;}输出normal constructor raw_paraw_pb pa.get()nullptrdestructor说明把一个智能指针a通过move操作赋值给另一个智能指针b的时候调用的是智能指针的移动构造函数而不会调用所管理对象的移动构造函数。在智能指针的移动构造函数中只是把所管理对象的指针交接从a交接给b并把a的指针赋为nullptr。4.20. std::vector std::unique_ptr 行为分析4.20.1. 示例 1push_back运行intmain(){unique_ptrBarpa(newBar(1));unique_ptrBarpb(newBar(2));vectorunique_ptrBarlist;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_ptrBarpa(newBar(1));unique_ptrBarpb(newBar(2));vectorunique_ptrBarlist;list.emplace_back(move(pa));list.emplace_back(move(pb));return0;}输出normal constructor normal constructor destructor destructoremplace_back退化为和push_back一样的行为。4.20.3. 示例 3原地构造 emplace_back运行intmain(){vectorunique_ptrBarlist;list.emplace_back(newBar(1));list.emplace_back(newBar(2));return0;}输出normal constructor normal constructor destructor destructor说明已经通过指针管理了提不提前构造都一样。4.20.4. vector 元素析构顺序解析运行#includeiostream#includememory#includevectorusingnamespacestd;classFoo{public:Foo(intval):val_(val){coutconstructor valendl;}~Foo(){coutdestructor val_endl;}private:intval_0;};intmain(){{vectorFootest1;test1.reserve(4);test1.emplace_back(1);test1.emplace_back(2);test1.emplace_back(3);test1.emplace_back(4);}{vectorunique_ptrFootest2;test2.reserve(4);test2.emplace_back(make_uniqueFoo(1));test2.emplace_back(make_uniqueFoo(2));test2.emplace_back(make_uniqueFoo(3));test2.emplace_back(make_uniqueFoo(4));}return0;}输出constructor1constructor2constructor3constructor4destructor1destructor2destructor3destructor4constructor1constructor2constructor3constructor4destructor1destructor2destructor3destructor4说明 vector 元素的析构顺序与构造顺序相同先构造的先析构后构造的后析构。4.21. 成员函数声明后加constconst 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 成员函数不能修改 valueintgetValue()const{// value 10; // 编译错误returnvalue;}// 非 const 成员函数可以修改 valuevoidsetValue(intv){valuev;}};voidtest_const_qualifier(){constMyClasscobj(5);MyClassobj(1);cobj.getValue();// OKconst 对象只能调用 const 函数// cobj.setValue(10); // 编译错误obj.getValue();// OK非 const 对象可以调用 const 函数obj.setValue(10);// OK}4.21.2. 引用限定符,const ,,const 引用限定符 (,) 是 C11 引入的用于根据调用对象的类型左值或右值重载 (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_dataHello;public:// 1. 左值版本 (Lvalue Reference Qualifier):// 只能被非 const 的左值对象调用。// 返回一个可修改的左值引用允许用户修改内部数据。std::stringdata(){std::cout- 被非 const 左值调用 (允许修改)std::endl;returninternal_data;}// 2. 常量左值版本 (Const Lvalue Reference Qualifier):// 只能被 const 的左值对象调用。// 返回一个 const 引用不允许用户修改内部数据。conststd::stringdata()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 s1DataWrapper().data();// 调用常量右值版本 (const DataWrapper() 是一个 const 临时对象即 const 右值)std::string s2static_castconstDataWrapper(DataWrapper()).data();}

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询