1. C11简介在2003年C标准委员会曾经提交了一份技术勘误表(简称TC1)使得C03这个名字已经取代了 C98称为C11之前的最新C标准名称。不过由于C03(TC1)主要是对C98标准中的漏洞 进行修复语言的核心部分则没有改动因此人们习惯性的把两个标准合并称为C98/03标准。相比于 C98/03C11则带来了数量可观的变化其中包含了约140个新特性以及对C03标准中 约600个缺陷的修正这使得C11更像是从C98/03中孕育出的一种新语言。相比较而言C11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全不仅功能更 强大而且能提升程序员的开发效率公司实际项目开发中也用得比较多,所以很重要。C11增加的语法特性非常篇幅非常多主要讲解实际中比较实用的语法。C11 - cppreference.com2. 统一的列表初始化2.1 初始化在C98中标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如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; }C11扩大了用大括号括起的列表(初始化列表)的使用范围使其可用于所有的内置类型和用户自 定义的类型使用初始化列表时可添加等号()也可不添加。struct Point { int _x; int _y; }; int main() { int x1 1; int x2{ 2 }; int array1[]{ 1, 2, 3, 4, 5 }; int array2[5]{ 0 }; Point p{ 1, 2 }; // C11中列表初始化也可以适用于new表达式中 int* pa new int[4]{ 0 }; return 0; }创建对象时也可以使用列表初始化方式调用构造函数初始化class Date { public: Date(int year, int month, int day) :_year(year) ,_month(month) ,_day(day) { cout Date(int year, int month, int day) endl; } private: int _year; int _month int _day; }; int main() { Date d1(2022, 1, 1); // old style // C11支持的列表初始化这里会调用构造函数初始化 Date d2{ 2022, 1, 2 }; Date d3 { 2022, 1, 3 }; return 0; }2.2 std::initializer_liststd::initializer_list的介绍文档 cplusplus.com/reference/initializer_list/initializer_list/C11中新增了initializer_list容器该容器没有提供过多的成员函数。提供了begin和end函数用于支持迭代器遍历。以及size函数支持获取容器中的元素个数。initializer_list本质就是一个大括号括起来的列表如果用auto关键字定义一个变量来接收一个大括号括起来的列表然后以typeid(变量名).name()的方式查看该变量的类型此时会发现该变量的类型就是initializer_list。int main() { // the type of il is an initializer_list auto il { 10, 20, 30 }; cout typeid(il).name() endl; return 0; }initializer_list的使用场景nitializer_list容器没有提供对应的增删查改等接口因为initializer_list并不是专门用于存储数据的而是为了让其他容器支持列表初始化的。比如class Date { public: Date(int year, int month, int day) :_year(year) , _month(month) , _day(day) { cout Date(int year, int month, int day) endl; } private: int _year; int _month; int _day; }; int main() { //用大括号括起来的列表对容器进行初始化 vectorint v { 1, 2, 3, 4, 5 }; listint l { 10, 20, 30, 40, 50 }; vectorDate vd { Date(2022, 8, 29), Date{ 2022, 8, 30 }, { 2022, 8, 31 } }; mapstring, string m{ make_pair(sort, 排序), { insert, 插入 } }; //用大括号括起来的列表对容器赋值 v { 5, 4, 3, 2, 1 }; return 0; }C98并不支持直接用列表对容器进行初始化这种初始化方式是在C11引入initializer_list后才支持的。而这些容器之所以支持使用列表进行初始化根本原因是因为C11给这些容器都增加了一个构造函数这个构造函数就是以initializer_list作为参数的。std::initializer_list一般是作为构造函数的参数C11对STL中的不少容器就增加 std::initializer_list作为参数的构造函数这样初始化容器对象就更方便了。也可以作为operator 的参数这样就可以用大括号赋值。当用列表对容器进行初始化时这个列表被识别成initializer_list类型于是就会调用这个新增的构造函数对该容器进行初始化。这个新增的构造函数要做的就是遍历initializer_list中的元素然后将这些元素依次插入到要初始化的容器当中即可。让模拟实现的vector也支持{}初始化和赋值namespace bit { templateclass T class vector { public: typedef T* iterator; vector(initializer_listT l) { _start new T[l.size()]; _finish _start l.size(); _endofstorage _start l.size(); iterator vit _start; typename initializer_listT::iterator lit l.begin(); while (lit ! l.end()) { *vit *lit; } //for (auto e : l) // *vit e; } vectorT operator(initializer_listT l) { vectorT tmp(l); std::swap(_start, tmp._start); std::swap(_finish, tmp._finish); std::swap(_endofstorage, tmp._endofstorage); return *this; } private: iterator _start; iterator _finish; iterator _endofstorage; }; }3. 声明c11提供了多种简化声明的方式尤其是在使用模板时。3.1 auto在C98中auto是一个存储类型的说明符表明变量是局部自动存储类型但是局部域中定义局 部的变量默认就是自动存储类型所以auto就没什么价值了。C11中废弃auto原来的用法将 其用于实现自动类型推断。这样要求必须进行显示初始化让编译器将定义对象的类型设置为初 始化值的类型。int main() { int i 10; auto p i; auto pf strcpy; cout typeid(p).name() endl; cout typeid(pf).name() endl; mapstring, string dict { {sort, 排序}, {insert, 插入} }; //mapstring, string::iterator it dict.begin(); auto it dict.begin(); return 0; }3.2 decltype关键字decltype将变量的类型声明为表达式指定的类型。// decltype的一些使用使用场景 templateclass T1, class T2 void F(T1 t1, T2 t2) { decltype(t1 * t2) ret; cout typeid(ret).name() endl; } int main() { const int x 1; double y 2.2; decltype(x * y) ret; // ret的类型是double decltype(x) p; // p的类型是int* cout typeid(ret).name() endl; cout typeid(p).name() endl; F(1, a); return 0; }3.3 nullptr由于C中NULL被定义成字面量0这样就可能回带来一些问题因为0既能指针常量又能表示 整形常量。所以出于清晰和安全的角度考虑C11中新增了nullptr用于表示空指针。#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endif4 范围for循环C98中我们要遍历一个数组可以按照以下方式int main() { int arr[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; //将数组元素值全部乘以2 for (int i 0; i sizeof(arr) / sizeof(arr[0]); i) { arr[i] * 2; } //打印数组中的所有元素 for (int i 0; i sizeof(arr) / sizeof(arr[0]); i) { cout arr[i] ; } cout endl; return 0; }对于一个有范围的集合而言循环是多余的有时还容易犯错。所以C11中引入了基于范围的for循环for循环后的括号由冒号分为两部分第一部分是范围内用于迭代的变量第二部分则表示被迭代的范围。比如int main() { int arr[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; //将数组元素值全部乘以2 for (auto e : arr) { e * 2; } //打印数组中的所有元素 for (auto e : arr) { cout e ; } cout endl; return 0; }仍然可用continue来结束本次循环也可以用break来跳出整个循环。范围for的使用条件一、for循环迭代的范围必须是确定的对于数组而言就是数组中第一个元素和最后一个元素的范围对于类而言应该提供begin和end的方法begin和end就是for循环迭代的范围。二、迭代的对象要支持和操作范围for本质上是由迭代器支持的在代码编译的时候编译器会自动将范围for替换为迭代器的形式。而由于在使用迭代器遍历时需要对对象进行和操作因此使用范围for的对象也需要支持和操作。5、STL中一些变化新容器C11中新增了四个容器分别是array、forward_list、unordered_map和unordered_set。用橘色圈起来是C11中的几个新容器但是实际最有用的是unordered_map和unordered_set。array容器array容器本质就是一个静态数组即固定大小的数组。array容器有两个模板参数第一个模板参数代表的是存储的类型第二个模板参数是一个非类型模板参数代表的是数组中可存储元素的个数。比如int main() { arrayint, 10 a1; //定义一个可存储10个int类型元素的array容器 arraydouble, 5 a2; //定义一个可存储5个double类型元素的array容器 return 0; }array容器与普通数组对比array容器与普通数组一样支持通过[]访问指定下标的元素也支持使用范围for遍历数组元素并且创建后数组的大小也不可改变。array容器与普通数组不同之处就是array容器用一个类对数组进行了封装并且在访问array容器中的元素时会进行越界检查。用[]访问元素时采用断言检查调用at成员函数访问元素时采用抛异常检查。而对于普通数组来说一般只有对数组进行写操作时才会检查越界如果只是越界进行读操作可能并不会报错。但array容器与其他容器不同的是array容器的对象是创建在栈上的因此array容器不适合定义太大的数组。forward_list容器forward_list容器本质就是一个单链表。forward_list很少使用原因如下forward_list只支持头插头删不支持尾插尾删因为单链表在进行尾插尾删时需要先找尾时间复杂度为O(N)。forward_list提供的插入函数叫做insert_after也就是在指定元素的后面插入一个元素而不像其他容器是在指定元素的前面插入一个元素因为单链表如果要在指定元素的前面插入元素还要遍历链表找到该元素的前一个元素时间复杂度为O(N)。forward_list提供的删除函数叫做erase_after也就是删除指定元素后面的一个元素因为单链表如果要删除指定元素还需要还要遍历链表找到指定元素的前一个元素时间复杂度为O(N)。因此一般情况下要用链表我们还是选择使用list容器。unordered_map和unordered_set容器后面整合到stl里讲unordered_map和unordered_set容器底层采用的都是哈希表。6、字符串转换函数C11提供了各种内置类型与string之间相互转换的函数比如to_string、stoi、stol、stod等函数。内置类型转换为string将内置类型转换成string类型统一调用to_string函数因为to_string函数为各种内置类型重载了对应的处理函数。string转换成内置类型如果要将string类型转换成内置类型则调用对应的转换函数即可容器中的一些新方法提供了一个以initializer_list作为参数的构造函数用于支持列表初始化。提供了cbegin和cend方法用于返回const迭代器。提供了emplace系列方法并在容器原有插入方法的基础上重载了一个右值引用版本的插入函数用于提高向容器中插入元素的效率。7 右值引用和移动语义7.1 左值引用和右值引用传统的C语法中就有引用的语法而C11中新增了的右值引用语法特性所以从现在开始我们 之前学习的引用就叫做左值引用。无论左值引用还是右值引用都是给对象取别名。什么是左值什么是左值引用左值是一个表示数据的表达式(如变量名或解引用的指针)我们可以获取它的地址可以对它赋 值左值可以出现赋值符号的左边右值不能出现在赋值符号左边。定义时const修饰符后的左 值不能给他赋值但是可以取它的地址。左值引用就是给左值的引用给左值取别名。int main() { // 以下的p、b、c、*p都是左值 int* p new int(0); int b 1; const int c 2; // 以下几个是对上面左值的左值引用 int* rp p; int rb b; const int rc c; int pvalue *p; return 0; }什么是右值什么是右值引用右值也是一个表示数据的表达式如字面常量、表达式返回值函数返回值(这个不能是左值引 用返回)等等右值可以出现在赋值符号的右边但是不能出现出现在赋值符号的左边右值不能 取地址。右值引用就是对右值的引用给右值取别名。int main() { double x 1.1, y 2.2; // 以下几个都是常见的右值 10; x y; fmin(x, y); // 以下几个都是对右值的右值引用 int rr1 10; double rr2 x y; double rr3 fmin(x, y); // 这里编译会报错error C2106: “”: 左操作数必须为左值 10 1; x y 1; fmin(x, y) 1; return 0; }注意右值是不能取地址的但是给右值取别名后会导致右值被存储到特定位置且可 以取到该位置的地址也就是说例如不能取字面量10的地址但是rr1引用后可以对rr1取地 址也可以修改rr1。如果不想rr1被修改可以用const int rr1 去引用是不是感觉很神奇 这个了解一下实际中右值引用的使用场景并不在于此这个特性也不重要。int main() { double x 1.1, y 2.2; int rr1 10; const double rr2 x y; rr1 20; rr2 5.5; // 报错 return 0; }7.2 左值引用与右值引用比较左值引用总结1. 左值引用只能引用左值不能引用右值。2. 但是const左值引用既可引用左值也可引用右值。int main() { // 左值引用只能引用左值不能引用右值。 int a 10; int ra1 a; // ra为a的别名 //int ra2 10; // 编译失败因为10是右值 // const左值引用既可引用左值也可引用右值。 const int ra3 10; const int ra4 a; return 0; }右值引用总结1. 右值引用只能右值不能引用左值。2. 但是右值引用可以move以后的左值。int main() { // 右值引用只能右值不能引用左值。 int r1 10; // error C2440: “初始化”: 无法从“int”转换为“int ” // message : 无法将左值绑定到右值引用 int a 10; int r2 a; // 右值引用可以引用move以后的左值 int r3 std::move(a); return 0; }右值引用使用场景和意义左值引用既能引用左值const左值引用const 还能引用右值为什么C11还要引入右值引用答案为了区分左值与右值从而在合适的时机采用移动语义替代深拷贝大幅提高性能。下面通过一个自定义bit::string类的演变来看左值引用的短板和右值引用如何补齐。一、左值引用的短板以未加入移动语义的string为例假设我们只实现了拷贝构造和拷贝赋值参数为const string// 拷贝构造深拷贝 string(const string s) :_str(nullptr) { cout string(const string s) -- 深拷贝 endl; string tmp(s._str); // 临时对象调用构造函数分配新内存并拷贝数据 swap(tmp); // 与本对象交换tmp析构时释放原空_str } // 拷贝赋值深拷贝 string operator(const string s) { cout string operator(const string s) -- 深拷贝 endl; string tmp(s); // 用s深拷贝出一个tmp swap(tmp); // 交换资源 return *this; }场景问题bit::string GetString() { bit::string str(hello); return str; // 返回局部对象str即将销毁 } bit::string s GetString();如果不开启优化GetString()返回的是一个右值将亡值它马上就会析构。编译器只能用const string去匹配只能调用拷贝构造对临时对象再进行一次深拷贝——浪费。即明知道这个返回的临时对象的资源可以直接“偷”过来用却不得不拷贝一份再销毁原对象这就是左值引用无法区分右值造成的性能短板。二、右值引用与移动语义补齐短板右值引用string只能绑定到右值它的作用就是标记出“即将销毁的对象”让编译器选择移动构造/移动赋值转移资源而非拷贝。1. 移动构造函数新增// 移动构造 —— 参数是右值引用 string(string s) :_str(nullptr) ,_size(0) ,_capacity(0) { cout string(string s) -- 移动语义 endl; swap(s); // 直接交换资源指针 } // 调用场景用一个右值初始化新对象 // s被交换后置空析构时安全地delete[] nullptr要点不分配新内存不拷贝数据。通过swap把临时对象的资源直接转移给当前对象。临时对象被掏空后变成安全状态析构不会有问题。2. 移动赋值运算符新增// 移动赋值 —— 参数是右值引用 string operator(string s) { cout string operator(string s) -- 移动语义 endl; swap(s); // 直接交换资源 return *this; } // 调用场景用一个右值给已有对象赋值 // 被赋值对象原有的资源由s带走赋值后s被析构时释放该资源为什么移动赋值不需要先清空自己直接swap(s)原对象的资源指针交给了ss在函数结束时销毁顺便帮我们释放了旧资源。这等同于“用右值的资源替换自己同时丢弃旧资源”。三、性能对比何时调用移动语义假设类同时提供了拷贝和移动版本表达式实参类别调用的函数string s2(s1);s1是左值拷贝构造const stringstring s3(std::move(s1));右值xvalue移动构造stringstring s4(GetString());右值纯右值移动构造s2 s1;左值拷贝赋值s2 GetString();右值移动赋值s2 std::move(s3);右值移动赋值没有移动语义时以上所有涉及右值的操作都会退化成深拷贝。有移动语义后当源对象是右值编译器会优先匹配移动版本只交换指针/大小等时间复杂度 O(1)没有内存分配和数据拷贝。四、bit::string 类的完整实现拆解namespace bit { class string { public: typedef char* iterator; iterator begin() { return _str; } iterator end() { return _str _size; } // 普通构造函数 string(const char* str ) :_size(strlen(str)), _capacity(_size) { _str new char[_capacity 1]; strcpy(_str, str); } // 交换函数工具 void swap(string s) { ::swap(_str, s._str); ::swap(_size, s._size); ::swap(_capacity, s._capacity); } // ------- 拷贝语义针对左值 ------- // 拷贝构造深拷贝 string(const string s) : _str(nullptr) { // 利用构造函数创建临时对象再交换 string tmp(s._str); swap(tmp); } // 拷贝赋值深拷贝 string operator(const string s) { string tmp(s); swap(tmp); return *this; } // ------- 移动语义针对右值 ------- // 移动构造 string(string s) : _str(nullptr), _size(0), _capacity(0) { swap(s); // 把右值的资源偷过来 } // 移动赋值 string operator(string s) { swap(s); // 右值接管旧资源并自动销毁 return *this; } ~string() { delete[] _str; _str nullptr; } // ... reserve, push_back, operator 等略 private: char* _str; size_t _size; size_t _capacity; // 不含\0 }; }五、使用场景与意义总结返回值优化之外的兜底即使编译器没有做或无法做返回值优化RVO移动构造也能保证返回局部对象时只转移资源。容器操作性能飞跃如vectorstring的push_back(临时string)或者vector自身扩容重新分配内存时如果元素类型支持移动语义则只转移资源效率远高于拷贝。不需要修改拷贝接口与旧代码兼容拷贝重载依然保留给左值移动重载自动为右值服务代码整洁且安全。明确表达“资源所有权转移”使用std::move可以把左值转为右值表示“我不再需要这个对象的内容你可以拿走”这在工厂模式、对象池等场景非常实用。核心结论右值引用的真正意义不是“可以引用右值”const左值引用也能而是通过类型区分出“将亡对象”从而触发移动语义将深拷贝变成廉价的资源交换彻底消除左值引用在这类场景下的性能瓶颈。左值引用的使用场景做参数和做返回值都可以提高效率。void func1(bit::string s) {} void func2(const bit::string s) {} int main() { bit::string s1(hello world); // func1和func2的调用我们可以看到左值引用做参数减少了拷贝提高效率的使用场景和价值 func1(s1); func2(s1); // string operator(char ch) 传值返回存在深拷贝 // string operator(char ch) 传左值引用没有拷贝提高了效率 s1 !; return 0; }左值引用的短板但是当函数返回对象是一个局部变量出了函数作用域就不存在了就不能使用左值引用返回 只能传值返回。例如bit::string to_string(int value)函数中可以看到这里只能使用传值返回 传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。namespace bit { bit::string to_string(int value) { bool flag true; if (value 0) { flag false; value 0 - value; } bit::string str; while (value 0) { int x value % 10; value / 10; str (0 x); } if (flag false) { str -; } std::reverse(str.begin(), str.end()); return str; } } int main() { // 在bit::string to_string(int value)函数中可以看到这里 // 只能使用传值返回传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷 贝构造)。 bit::string ret1 bit::to_string(1234); bit::string ret2 bit::to_string(-1234); return 0; }右值引用和移动语义解决上述问题在bit::string中增加移动构造移动构造本质是将参数右值的资源窃取过来占位已有那么就不 用做深拷贝了所以它叫做移动构造就是窃取别人的资源来构造自己。// 移动构造 string(string s) :_str(nullptr) ,_size(0) ,_capacity(0) { cout string(string s) -- 移动语义 endl; swap(s); } int main() { bit::string ret2 bit::to_string(-1234); return 0; }再运行上面bit::to_string的两个调用我们会发现这里没有调用深拷贝的拷贝构造而是调用 了移动构造移动构造中没有新开空间拷贝数据所以效率提高了。不仅仅有移动构造还有移动赋值在bit::string类中增加移动赋值函数再去调用bit::to_string(1234)不过这次是将 bit::to_string(1234)返回的右值对象赋值给ret1对象这时调用的是移动构造。// 移动赋值 string operator(string s) { cout string operator(string s) -- 移动语义 endl; swap(s); return *this; } int main() { bit::string ret1; ret1 bit::to_string(1234); return 0; } // 运行结果 // string(string s) -- 移动语义 // string operator(string s) -- 移动语义这里运行后我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象 接收编译器就没办法优化了。bit::to_string函数中会先用str生成构造生成一个临时对象但是 我们可以看到编译器很聪明的在这里把str识别成了右值调用了移动构造。然后在把这个临时 对象做为bit::to_string函数调用的返回值赋值给ret1这里调用的移动赋值。STL中的容器都是增加了移动构造和移动赋值cplusplus.com/reference/string/string/string/cplusplus.com/reference/vector/vector/vector/7.4 右值引用引用左值及其一些更深入的使用场景分析按照语法右值引用只能引用右值但右值引用一定不能引用左值吗因为有些场景下可能 真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时可以通过move 函数将左值转化为右值。C11中std::move()函数位于 头文件中该函数名字具有迷惑性 它并不搬移任何东西唯一的功能就是将一个左值强制转化为右值引用然后实现移动语义。templateclass _Ty inline typename remove_reference_Ty::type move(_Ty _Arg) _NOEXCEPT { // forward _Arg as movable return ((typename remove_reference_Ty::type)_Arg); } int main() { bit::string s1(hello world); // 这里s1是左值调用的是拷贝构造 bit::string s2(s1); // 这里我们把s1 move处理以后, 会被当成右值调用移动构造 // 但是这里要注意一般是不要这样用的因为我们会发现s1的 // 资源被转移给了s3s1被置空了。 bit::string s3(std::move(s1)); return 0; }STL容器插入接口函数也增加了右值引用版本cplusplus.com/reference/list/list/push_back/cplusplus.com/reference/vector/vector/push_back/void push_back (value_type val); int main() { listbit::string lt; bit::string s1(1111); // 这里调用的是拷贝构造 lt.push_back(s1); // 下面调用都是移动构造 lt.push_back(2222); lt.push_back(std::move(s1)); return 0; } 运行结果 // string(const string s) -- 深拷贝 // string(string s) -- 移动语义 // string(string s) -- 移动语义7.5 完美转发模板中的不代表右值引用而是万能引用其既能接收左值又能接收右值。templateclass T void PerfectForward(T t) { //... }下面重载了四个Func函数这四个Func函数的参数类型分别是左值引用、const左值引用、右值引用和const右值引用。在主函数中调用PerfectForward函数时分别传入左值、右值、const左值和const右值在PerfectForward函数中再调用Func函数。如下由于PerfectForward函数的参数类型是万能引用因此既可以接收左值也可以接收右值而我们在PerfectForward函数中调用Func函数就是希望调用PerfectForward函数时传入左值、右值、const左值、const右值能够匹配到对应版本的Func函数。但实际调用PerfectForward函数时传入左值和右值最终都匹配到了左值引用版本的Func函数调用PerfectForward函数时传入const左值和const右值最终都匹配到了const左值引用版本的Func函数。根本原因就是右值被引用后会导致右值被存储到特定位置这时这个右值可以被取到地址并且可以被修改所以在PerfectForward函数中调用Func函数时会将t识别成左值。也就是说右值经过一次参数传递后其属性会退化成左值如果想要在这个过程中保持右值的属性就需要用到完美转发。void Fun(int x){ cout 左值引用 endl; } void Fun(const int x){ cout const 左值引用 endl; } void Fun(int x){ cout 右值引用 endl; } void Fun(const int x){ cout const 右值引用 endl; } // 模板中的不代表右值引用而是万能引用其既能接收左值又能接收右值。 // 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力 // 但是引用类型的唯一作用就是限制了接收的类型后续使用中都退化成了左值 // 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发 templatetypename T void PerfectForward(T t) { Fun(t); } int main() { PerfectForward(10); // 右值 int a; PerfectForward(a); // 左值 PerfectForward(std::move(a)); // 右值 const int b 8; PerfectForward(b); // const 左值 PerfectForward(std::move(b)); // const 右值 return 0; }std::forward 完美转发在传参的过程中保留对象原生类型属性要想在参数传递过程中保持其原有的属性需要在传参时调用forward函数。比如void Fun(int x){ cout 左值引用 endl; } void Fun(const int x){ cout const 左值引用 endl; } void Fun(int x){ cout 右值引用 endl; } void Fun(const int x){ cout const 右值引用 endl; } // std::forwardT(t)在传参的过程中保持了t的原生类型属性。 templatetypename T void PerfectForward(T t) { Fun(std::forwardT(t)); } int main() { PerfectForward(10); // 右值 int a; PerfectForward(a); // 左值 PerfectForward(std::move(a)); // 右值 const int b 8; PerfectForward(b); // const 左值 PerfectForward(std::move(b)); // const 右值 return 0; }完美转发实际中的使用场景下面模拟实现了一个简化版的list类类当中分别提供了左值引用版本和右值引用版本的push_back和insert函数。代码如下namespace cl { templateclass T struct ListNode { T _data; ListNode* _next nullptr; ListNode* _prev nullptr; }; templateclass T class list { typedef ListNodeT node; public: //构造函数 list() { _head new node; _head-_next _head; _head-_prev _head; } //左值引用版本的push_back void push_back(const T x) { insert(_head, x); } //右值引用版本的push_back void push_back(T x) { insert(_head, std::forwardT(x)); //完美转发 } //左值引用版本的insert void insert(node* pos, const T x) { node* prev pos-_prev; node* newnode new node; newnode-_data x; prev-_next newnode; newnode-_prev prev; newnode-_next pos; pos-_prev newnode; } //右值引用版本的insert void insert(node* pos, T x) { node* prev pos-_prev; node* newnode new node; newnode-_data std::forwardT(x); //完美转发 prev-_next newnode; newnode-_prev prev; newnode-_next pos; pos-_prev newnode; } private: node* _head; //指向链表头结点的指针 }; }下面定义一个list对象list容器中存储的就是之前模拟实现的string类这里分别传入左值和右值调用不同版本的push_back。比如int main() { cl::listcl::string lt; cl::string s(1111); lt.push_back(s); //调用左值引用版本的push_back lt.push_back(2222); //调用右值引用版本的push_back return 0; }调用左值引用版本的push_back函数插入元素时会调用string原有的operator函数进行深拷贝而调用右值引用版本的push_back函数插入元素时只会调用string的移动赋值进行资源的移动。因为实现push_back函数时复用了insert函数的代码对于左值引用版本的push_back函数在调用insert函数时只能调用左值引用版本的insert函数而在insert函数中插入元素时会先new一个结点然后将对应的左值赋值给该结点因此会调用string原有的operator函数进行深拷贝。而对于右值引用版本的push_back函数在调用insert函数时就可以调用右值引用版本的insert函数在右值引用版本的insert函数中也会先new一个结点然后将对应的右值赋值给该结点因此这里就和调用string的移动赋值函数进行资源的移动。这个场景中就需要用到完美转发否则右值引用版本的push_back接收到右值后该右值的右值属性就退化了此时在右值引用版本的push_back函数中调用insert函数也会匹配到左值引用版本的insert函数最终调用的还是原有的operator函数进行深拷贝。此外除了在右值引用版本的push_back函数中调用insert函数时需要用完美转发保持右值原有的属性之外在右值引用版本的insert函数中用右值给新结点赋值时也需要用到完美转发否则在赋值时也会将其识别为左值导致最终调用的还是原有的operator函数。也就是说只要想保持右值的属性在每次右值传参时都需要进行完美转发实际STL库中也是通过完美转发来保持右值属性的。注意 代码中push_back和insert函数的参数T是右值引用而不是万能引用因为在list对象创建时这个类就被实例化了后续调用push_back和insert函数时参数T中的T已经是一个确定的类型了而不是在调用push_back和insert函数时才进行类型推导的。与STL中的list的区别如果将刚才测试代码中的list换成STL当中的list。调用左值引用版本的push_back插入结点在构造结点时会调用string的拷贝构造函数。调用右值引用版本的push_back插入结点在构造结点时会调用string的移动构造函数。而用我们模拟实现的list时调用的却不是string的拷贝构造和移动构造而对应是string原有的operator和移动赋值。原因是因为我们模拟实现的list容器是通过new操作符为新结点申请内存空间的在申请内存后会自动调用构造函数对进行其进行初始化因此在后续用左值或右值对其进行赋值时就会调用对应的operator或移动赋值进行深拷贝或资源的转移。而STL库中的容器都是通过空间配置器获取内存的因此在申请到内存后不会调用构造函数对其进行初始化而是后续用左值或右值对其进行拷贝构造因此最终调用的就是拷贝构造或移动构造。如果想要得到与STL相同的实验结果可以使用malloc函数申请内存这时就不会自动调用构造函数进行初始化然后在用定位new的方式用左值或右值对申请到的内存空间进行构造这时调用的对应就是拷贝构造或移动构造。
c++11(简介与右值引用)
发布时间:2026/5/28 4:34:05
1. C11简介在2003年C标准委员会曾经提交了一份技术勘误表(简称TC1)使得C03这个名字已经取代了 C98称为C11之前的最新C标准名称。不过由于C03(TC1)主要是对C98标准中的漏洞 进行修复语言的核心部分则没有改动因此人们习惯性的把两个标准合并称为C98/03标准。相比于 C98/03C11则带来了数量可观的变化其中包含了约140个新特性以及对C03标准中 约600个缺陷的修正这使得C11更像是从C98/03中孕育出的一种新语言。相比较而言C11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全不仅功能更 强大而且能提升程序员的开发效率公司实际项目开发中也用得比较多,所以很重要。C11增加的语法特性非常篇幅非常多主要讲解实际中比较实用的语法。C11 - cppreference.com2. 统一的列表初始化2.1 初始化在C98中标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如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; }C11扩大了用大括号括起的列表(初始化列表)的使用范围使其可用于所有的内置类型和用户自 定义的类型使用初始化列表时可添加等号()也可不添加。struct Point { int _x; int _y; }; int main() { int x1 1; int x2{ 2 }; int array1[]{ 1, 2, 3, 4, 5 }; int array2[5]{ 0 }; Point p{ 1, 2 }; // C11中列表初始化也可以适用于new表达式中 int* pa new int[4]{ 0 }; return 0; }创建对象时也可以使用列表初始化方式调用构造函数初始化class Date { public: Date(int year, int month, int day) :_year(year) ,_month(month) ,_day(day) { cout Date(int year, int month, int day) endl; } private: int _year; int _month int _day; }; int main() { Date d1(2022, 1, 1); // old style // C11支持的列表初始化这里会调用构造函数初始化 Date d2{ 2022, 1, 2 }; Date d3 { 2022, 1, 3 }; return 0; }2.2 std::initializer_liststd::initializer_list的介绍文档 cplusplus.com/reference/initializer_list/initializer_list/C11中新增了initializer_list容器该容器没有提供过多的成员函数。提供了begin和end函数用于支持迭代器遍历。以及size函数支持获取容器中的元素个数。initializer_list本质就是一个大括号括起来的列表如果用auto关键字定义一个变量来接收一个大括号括起来的列表然后以typeid(变量名).name()的方式查看该变量的类型此时会发现该变量的类型就是initializer_list。int main() { // the type of il is an initializer_list auto il { 10, 20, 30 }; cout typeid(il).name() endl; return 0; }initializer_list的使用场景nitializer_list容器没有提供对应的增删查改等接口因为initializer_list并不是专门用于存储数据的而是为了让其他容器支持列表初始化的。比如class Date { public: Date(int year, int month, int day) :_year(year) , _month(month) , _day(day) { cout Date(int year, int month, int day) endl; } private: int _year; int _month; int _day; }; int main() { //用大括号括起来的列表对容器进行初始化 vectorint v { 1, 2, 3, 4, 5 }; listint l { 10, 20, 30, 40, 50 }; vectorDate vd { Date(2022, 8, 29), Date{ 2022, 8, 30 }, { 2022, 8, 31 } }; mapstring, string m{ make_pair(sort, 排序), { insert, 插入 } }; //用大括号括起来的列表对容器赋值 v { 5, 4, 3, 2, 1 }; return 0; }C98并不支持直接用列表对容器进行初始化这种初始化方式是在C11引入initializer_list后才支持的。而这些容器之所以支持使用列表进行初始化根本原因是因为C11给这些容器都增加了一个构造函数这个构造函数就是以initializer_list作为参数的。std::initializer_list一般是作为构造函数的参数C11对STL中的不少容器就增加 std::initializer_list作为参数的构造函数这样初始化容器对象就更方便了。也可以作为operator 的参数这样就可以用大括号赋值。当用列表对容器进行初始化时这个列表被识别成initializer_list类型于是就会调用这个新增的构造函数对该容器进行初始化。这个新增的构造函数要做的就是遍历initializer_list中的元素然后将这些元素依次插入到要初始化的容器当中即可。让模拟实现的vector也支持{}初始化和赋值namespace bit { templateclass T class vector { public: typedef T* iterator; vector(initializer_listT l) { _start new T[l.size()]; _finish _start l.size(); _endofstorage _start l.size(); iterator vit _start; typename initializer_listT::iterator lit l.begin(); while (lit ! l.end()) { *vit *lit; } //for (auto e : l) // *vit e; } vectorT operator(initializer_listT l) { vectorT tmp(l); std::swap(_start, tmp._start); std::swap(_finish, tmp._finish); std::swap(_endofstorage, tmp._endofstorage); return *this; } private: iterator _start; iterator _finish; iterator _endofstorage; }; }3. 声明c11提供了多种简化声明的方式尤其是在使用模板时。3.1 auto在C98中auto是一个存储类型的说明符表明变量是局部自动存储类型但是局部域中定义局 部的变量默认就是自动存储类型所以auto就没什么价值了。C11中废弃auto原来的用法将 其用于实现自动类型推断。这样要求必须进行显示初始化让编译器将定义对象的类型设置为初 始化值的类型。int main() { int i 10; auto p i; auto pf strcpy; cout typeid(p).name() endl; cout typeid(pf).name() endl; mapstring, string dict { {sort, 排序}, {insert, 插入} }; //mapstring, string::iterator it dict.begin(); auto it dict.begin(); return 0; }3.2 decltype关键字decltype将变量的类型声明为表达式指定的类型。// decltype的一些使用使用场景 templateclass T1, class T2 void F(T1 t1, T2 t2) { decltype(t1 * t2) ret; cout typeid(ret).name() endl; } int main() { const int x 1; double y 2.2; decltype(x * y) ret; // ret的类型是double decltype(x) p; // p的类型是int* cout typeid(ret).name() endl; cout typeid(p).name() endl; F(1, a); return 0; }3.3 nullptr由于C中NULL被定义成字面量0这样就可能回带来一些问题因为0既能指针常量又能表示 整形常量。所以出于清晰和安全的角度考虑C11中新增了nullptr用于表示空指针。#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endif4 范围for循环C98中我们要遍历一个数组可以按照以下方式int main() { int arr[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; //将数组元素值全部乘以2 for (int i 0; i sizeof(arr) / sizeof(arr[0]); i) { arr[i] * 2; } //打印数组中的所有元素 for (int i 0; i sizeof(arr) / sizeof(arr[0]); i) { cout arr[i] ; } cout endl; return 0; }对于一个有范围的集合而言循环是多余的有时还容易犯错。所以C11中引入了基于范围的for循环for循环后的括号由冒号分为两部分第一部分是范围内用于迭代的变量第二部分则表示被迭代的范围。比如int main() { int arr[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; //将数组元素值全部乘以2 for (auto e : arr) { e * 2; } //打印数组中的所有元素 for (auto e : arr) { cout e ; } cout endl; return 0; }仍然可用continue来结束本次循环也可以用break来跳出整个循环。范围for的使用条件一、for循环迭代的范围必须是确定的对于数组而言就是数组中第一个元素和最后一个元素的范围对于类而言应该提供begin和end的方法begin和end就是for循环迭代的范围。二、迭代的对象要支持和操作范围for本质上是由迭代器支持的在代码编译的时候编译器会自动将范围for替换为迭代器的形式。而由于在使用迭代器遍历时需要对对象进行和操作因此使用范围for的对象也需要支持和操作。5、STL中一些变化新容器C11中新增了四个容器分别是array、forward_list、unordered_map和unordered_set。用橘色圈起来是C11中的几个新容器但是实际最有用的是unordered_map和unordered_set。array容器array容器本质就是一个静态数组即固定大小的数组。array容器有两个模板参数第一个模板参数代表的是存储的类型第二个模板参数是一个非类型模板参数代表的是数组中可存储元素的个数。比如int main() { arrayint, 10 a1; //定义一个可存储10个int类型元素的array容器 arraydouble, 5 a2; //定义一个可存储5个double类型元素的array容器 return 0; }array容器与普通数组对比array容器与普通数组一样支持通过[]访问指定下标的元素也支持使用范围for遍历数组元素并且创建后数组的大小也不可改变。array容器与普通数组不同之处就是array容器用一个类对数组进行了封装并且在访问array容器中的元素时会进行越界检查。用[]访问元素时采用断言检查调用at成员函数访问元素时采用抛异常检查。而对于普通数组来说一般只有对数组进行写操作时才会检查越界如果只是越界进行读操作可能并不会报错。但array容器与其他容器不同的是array容器的对象是创建在栈上的因此array容器不适合定义太大的数组。forward_list容器forward_list容器本质就是一个单链表。forward_list很少使用原因如下forward_list只支持头插头删不支持尾插尾删因为单链表在进行尾插尾删时需要先找尾时间复杂度为O(N)。forward_list提供的插入函数叫做insert_after也就是在指定元素的后面插入一个元素而不像其他容器是在指定元素的前面插入一个元素因为单链表如果要在指定元素的前面插入元素还要遍历链表找到该元素的前一个元素时间复杂度为O(N)。forward_list提供的删除函数叫做erase_after也就是删除指定元素后面的一个元素因为单链表如果要删除指定元素还需要还要遍历链表找到指定元素的前一个元素时间复杂度为O(N)。因此一般情况下要用链表我们还是选择使用list容器。unordered_map和unordered_set容器后面整合到stl里讲unordered_map和unordered_set容器底层采用的都是哈希表。6、字符串转换函数C11提供了各种内置类型与string之间相互转换的函数比如to_string、stoi、stol、stod等函数。内置类型转换为string将内置类型转换成string类型统一调用to_string函数因为to_string函数为各种内置类型重载了对应的处理函数。string转换成内置类型如果要将string类型转换成内置类型则调用对应的转换函数即可容器中的一些新方法提供了一个以initializer_list作为参数的构造函数用于支持列表初始化。提供了cbegin和cend方法用于返回const迭代器。提供了emplace系列方法并在容器原有插入方法的基础上重载了一个右值引用版本的插入函数用于提高向容器中插入元素的效率。7 右值引用和移动语义7.1 左值引用和右值引用传统的C语法中就有引用的语法而C11中新增了的右值引用语法特性所以从现在开始我们 之前学习的引用就叫做左值引用。无论左值引用还是右值引用都是给对象取别名。什么是左值什么是左值引用左值是一个表示数据的表达式(如变量名或解引用的指针)我们可以获取它的地址可以对它赋 值左值可以出现赋值符号的左边右值不能出现在赋值符号左边。定义时const修饰符后的左 值不能给他赋值但是可以取它的地址。左值引用就是给左值的引用给左值取别名。int main() { // 以下的p、b、c、*p都是左值 int* p new int(0); int b 1; const int c 2; // 以下几个是对上面左值的左值引用 int* rp p; int rb b; const int rc c; int pvalue *p; return 0; }什么是右值什么是右值引用右值也是一个表示数据的表达式如字面常量、表达式返回值函数返回值(这个不能是左值引 用返回)等等右值可以出现在赋值符号的右边但是不能出现出现在赋值符号的左边右值不能 取地址。右值引用就是对右值的引用给右值取别名。int main() { double x 1.1, y 2.2; // 以下几个都是常见的右值 10; x y; fmin(x, y); // 以下几个都是对右值的右值引用 int rr1 10; double rr2 x y; double rr3 fmin(x, y); // 这里编译会报错error C2106: “”: 左操作数必须为左值 10 1; x y 1; fmin(x, y) 1; return 0; }注意右值是不能取地址的但是给右值取别名后会导致右值被存储到特定位置且可 以取到该位置的地址也就是说例如不能取字面量10的地址但是rr1引用后可以对rr1取地 址也可以修改rr1。如果不想rr1被修改可以用const int rr1 去引用是不是感觉很神奇 这个了解一下实际中右值引用的使用场景并不在于此这个特性也不重要。int main() { double x 1.1, y 2.2; int rr1 10; const double rr2 x y; rr1 20; rr2 5.5; // 报错 return 0; }7.2 左值引用与右值引用比较左值引用总结1. 左值引用只能引用左值不能引用右值。2. 但是const左值引用既可引用左值也可引用右值。int main() { // 左值引用只能引用左值不能引用右值。 int a 10; int ra1 a; // ra为a的别名 //int ra2 10; // 编译失败因为10是右值 // const左值引用既可引用左值也可引用右值。 const int ra3 10; const int ra4 a; return 0; }右值引用总结1. 右值引用只能右值不能引用左值。2. 但是右值引用可以move以后的左值。int main() { // 右值引用只能右值不能引用左值。 int r1 10; // error C2440: “初始化”: 无法从“int”转换为“int ” // message : 无法将左值绑定到右值引用 int a 10; int r2 a; // 右值引用可以引用move以后的左值 int r3 std::move(a); return 0; }右值引用使用场景和意义左值引用既能引用左值const左值引用const 还能引用右值为什么C11还要引入右值引用答案为了区分左值与右值从而在合适的时机采用移动语义替代深拷贝大幅提高性能。下面通过一个自定义bit::string类的演变来看左值引用的短板和右值引用如何补齐。一、左值引用的短板以未加入移动语义的string为例假设我们只实现了拷贝构造和拷贝赋值参数为const string// 拷贝构造深拷贝 string(const string s) :_str(nullptr) { cout string(const string s) -- 深拷贝 endl; string tmp(s._str); // 临时对象调用构造函数分配新内存并拷贝数据 swap(tmp); // 与本对象交换tmp析构时释放原空_str } // 拷贝赋值深拷贝 string operator(const string s) { cout string operator(const string s) -- 深拷贝 endl; string tmp(s); // 用s深拷贝出一个tmp swap(tmp); // 交换资源 return *this; }场景问题bit::string GetString() { bit::string str(hello); return str; // 返回局部对象str即将销毁 } bit::string s GetString();如果不开启优化GetString()返回的是一个右值将亡值它马上就会析构。编译器只能用const string去匹配只能调用拷贝构造对临时对象再进行一次深拷贝——浪费。即明知道这个返回的临时对象的资源可以直接“偷”过来用却不得不拷贝一份再销毁原对象这就是左值引用无法区分右值造成的性能短板。二、右值引用与移动语义补齐短板右值引用string只能绑定到右值它的作用就是标记出“即将销毁的对象”让编译器选择移动构造/移动赋值转移资源而非拷贝。1. 移动构造函数新增// 移动构造 —— 参数是右值引用 string(string s) :_str(nullptr) ,_size(0) ,_capacity(0) { cout string(string s) -- 移动语义 endl; swap(s); // 直接交换资源指针 } // 调用场景用一个右值初始化新对象 // s被交换后置空析构时安全地delete[] nullptr要点不分配新内存不拷贝数据。通过swap把临时对象的资源直接转移给当前对象。临时对象被掏空后变成安全状态析构不会有问题。2. 移动赋值运算符新增// 移动赋值 —— 参数是右值引用 string operator(string s) { cout string operator(string s) -- 移动语义 endl; swap(s); // 直接交换资源 return *this; } // 调用场景用一个右值给已有对象赋值 // 被赋值对象原有的资源由s带走赋值后s被析构时释放该资源为什么移动赋值不需要先清空自己直接swap(s)原对象的资源指针交给了ss在函数结束时销毁顺便帮我们释放了旧资源。这等同于“用右值的资源替换自己同时丢弃旧资源”。三、性能对比何时调用移动语义假设类同时提供了拷贝和移动版本表达式实参类别调用的函数string s2(s1);s1是左值拷贝构造const stringstring s3(std::move(s1));右值xvalue移动构造stringstring s4(GetString());右值纯右值移动构造s2 s1;左值拷贝赋值s2 GetString();右值移动赋值s2 std::move(s3);右值移动赋值没有移动语义时以上所有涉及右值的操作都会退化成深拷贝。有移动语义后当源对象是右值编译器会优先匹配移动版本只交换指针/大小等时间复杂度 O(1)没有内存分配和数据拷贝。四、bit::string 类的完整实现拆解namespace bit { class string { public: typedef char* iterator; iterator begin() { return _str; } iterator end() { return _str _size; } // 普通构造函数 string(const char* str ) :_size(strlen(str)), _capacity(_size) { _str new char[_capacity 1]; strcpy(_str, str); } // 交换函数工具 void swap(string s) { ::swap(_str, s._str); ::swap(_size, s._size); ::swap(_capacity, s._capacity); } // ------- 拷贝语义针对左值 ------- // 拷贝构造深拷贝 string(const string s) : _str(nullptr) { // 利用构造函数创建临时对象再交换 string tmp(s._str); swap(tmp); } // 拷贝赋值深拷贝 string operator(const string s) { string tmp(s); swap(tmp); return *this; } // ------- 移动语义针对右值 ------- // 移动构造 string(string s) : _str(nullptr), _size(0), _capacity(0) { swap(s); // 把右值的资源偷过来 } // 移动赋值 string operator(string s) { swap(s); // 右值接管旧资源并自动销毁 return *this; } ~string() { delete[] _str; _str nullptr; } // ... reserve, push_back, operator 等略 private: char* _str; size_t _size; size_t _capacity; // 不含\0 }; }五、使用场景与意义总结返回值优化之外的兜底即使编译器没有做或无法做返回值优化RVO移动构造也能保证返回局部对象时只转移资源。容器操作性能飞跃如vectorstring的push_back(临时string)或者vector自身扩容重新分配内存时如果元素类型支持移动语义则只转移资源效率远高于拷贝。不需要修改拷贝接口与旧代码兼容拷贝重载依然保留给左值移动重载自动为右值服务代码整洁且安全。明确表达“资源所有权转移”使用std::move可以把左值转为右值表示“我不再需要这个对象的内容你可以拿走”这在工厂模式、对象池等场景非常实用。核心结论右值引用的真正意义不是“可以引用右值”const左值引用也能而是通过类型区分出“将亡对象”从而触发移动语义将深拷贝变成廉价的资源交换彻底消除左值引用在这类场景下的性能瓶颈。左值引用的使用场景做参数和做返回值都可以提高效率。void func1(bit::string s) {} void func2(const bit::string s) {} int main() { bit::string s1(hello world); // func1和func2的调用我们可以看到左值引用做参数减少了拷贝提高效率的使用场景和价值 func1(s1); func2(s1); // string operator(char ch) 传值返回存在深拷贝 // string operator(char ch) 传左值引用没有拷贝提高了效率 s1 !; return 0; }左值引用的短板但是当函数返回对象是一个局部变量出了函数作用域就不存在了就不能使用左值引用返回 只能传值返回。例如bit::string to_string(int value)函数中可以看到这里只能使用传值返回 传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。namespace bit { bit::string to_string(int value) { bool flag true; if (value 0) { flag false; value 0 - value; } bit::string str; while (value 0) { int x value % 10; value / 10; str (0 x); } if (flag false) { str -; } std::reverse(str.begin(), str.end()); return str; } } int main() { // 在bit::string to_string(int value)函数中可以看到这里 // 只能使用传值返回传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷 贝构造)。 bit::string ret1 bit::to_string(1234); bit::string ret2 bit::to_string(-1234); return 0; }右值引用和移动语义解决上述问题在bit::string中增加移动构造移动构造本质是将参数右值的资源窃取过来占位已有那么就不 用做深拷贝了所以它叫做移动构造就是窃取别人的资源来构造自己。// 移动构造 string(string s) :_str(nullptr) ,_size(0) ,_capacity(0) { cout string(string s) -- 移动语义 endl; swap(s); } int main() { bit::string ret2 bit::to_string(-1234); return 0; }再运行上面bit::to_string的两个调用我们会发现这里没有调用深拷贝的拷贝构造而是调用 了移动构造移动构造中没有新开空间拷贝数据所以效率提高了。不仅仅有移动构造还有移动赋值在bit::string类中增加移动赋值函数再去调用bit::to_string(1234)不过这次是将 bit::to_string(1234)返回的右值对象赋值给ret1对象这时调用的是移动构造。// 移动赋值 string operator(string s) { cout string operator(string s) -- 移动语义 endl; swap(s); return *this; } int main() { bit::string ret1; ret1 bit::to_string(1234); return 0; } // 运行结果 // string(string s) -- 移动语义 // string operator(string s) -- 移动语义这里运行后我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象 接收编译器就没办法优化了。bit::to_string函数中会先用str生成构造生成一个临时对象但是 我们可以看到编译器很聪明的在这里把str识别成了右值调用了移动构造。然后在把这个临时 对象做为bit::to_string函数调用的返回值赋值给ret1这里调用的移动赋值。STL中的容器都是增加了移动构造和移动赋值cplusplus.com/reference/string/string/string/cplusplus.com/reference/vector/vector/vector/7.4 右值引用引用左值及其一些更深入的使用场景分析按照语法右值引用只能引用右值但右值引用一定不能引用左值吗因为有些场景下可能 真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时可以通过move 函数将左值转化为右值。C11中std::move()函数位于 头文件中该函数名字具有迷惑性 它并不搬移任何东西唯一的功能就是将一个左值强制转化为右值引用然后实现移动语义。templateclass _Ty inline typename remove_reference_Ty::type move(_Ty _Arg) _NOEXCEPT { // forward _Arg as movable return ((typename remove_reference_Ty::type)_Arg); } int main() { bit::string s1(hello world); // 这里s1是左值调用的是拷贝构造 bit::string s2(s1); // 这里我们把s1 move处理以后, 会被当成右值调用移动构造 // 但是这里要注意一般是不要这样用的因为我们会发现s1的 // 资源被转移给了s3s1被置空了。 bit::string s3(std::move(s1)); return 0; }STL容器插入接口函数也增加了右值引用版本cplusplus.com/reference/list/list/push_back/cplusplus.com/reference/vector/vector/push_back/void push_back (value_type val); int main() { listbit::string lt; bit::string s1(1111); // 这里调用的是拷贝构造 lt.push_back(s1); // 下面调用都是移动构造 lt.push_back(2222); lt.push_back(std::move(s1)); return 0; } 运行结果 // string(const string s) -- 深拷贝 // string(string s) -- 移动语义 // string(string s) -- 移动语义7.5 完美转发模板中的不代表右值引用而是万能引用其既能接收左值又能接收右值。templateclass T void PerfectForward(T t) { //... }下面重载了四个Func函数这四个Func函数的参数类型分别是左值引用、const左值引用、右值引用和const右值引用。在主函数中调用PerfectForward函数时分别传入左值、右值、const左值和const右值在PerfectForward函数中再调用Func函数。如下由于PerfectForward函数的参数类型是万能引用因此既可以接收左值也可以接收右值而我们在PerfectForward函数中调用Func函数就是希望调用PerfectForward函数时传入左值、右值、const左值、const右值能够匹配到对应版本的Func函数。但实际调用PerfectForward函数时传入左值和右值最终都匹配到了左值引用版本的Func函数调用PerfectForward函数时传入const左值和const右值最终都匹配到了const左值引用版本的Func函数。根本原因就是右值被引用后会导致右值被存储到特定位置这时这个右值可以被取到地址并且可以被修改所以在PerfectForward函数中调用Func函数时会将t识别成左值。也就是说右值经过一次参数传递后其属性会退化成左值如果想要在这个过程中保持右值的属性就需要用到完美转发。void Fun(int x){ cout 左值引用 endl; } void Fun(const int x){ cout const 左值引用 endl; } void Fun(int x){ cout 右值引用 endl; } void Fun(const int x){ cout const 右值引用 endl; } // 模板中的不代表右值引用而是万能引用其既能接收左值又能接收右值。 // 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力 // 但是引用类型的唯一作用就是限制了接收的类型后续使用中都退化成了左值 // 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发 templatetypename T void PerfectForward(T t) { Fun(t); } int main() { PerfectForward(10); // 右值 int a; PerfectForward(a); // 左值 PerfectForward(std::move(a)); // 右值 const int b 8; PerfectForward(b); // const 左值 PerfectForward(std::move(b)); // const 右值 return 0; }std::forward 完美转发在传参的过程中保留对象原生类型属性要想在参数传递过程中保持其原有的属性需要在传参时调用forward函数。比如void Fun(int x){ cout 左值引用 endl; } void Fun(const int x){ cout const 左值引用 endl; } void Fun(int x){ cout 右值引用 endl; } void Fun(const int x){ cout const 右值引用 endl; } // std::forwardT(t)在传参的过程中保持了t的原生类型属性。 templatetypename T void PerfectForward(T t) { Fun(std::forwardT(t)); } int main() { PerfectForward(10); // 右值 int a; PerfectForward(a); // 左值 PerfectForward(std::move(a)); // 右值 const int b 8; PerfectForward(b); // const 左值 PerfectForward(std::move(b)); // const 右值 return 0; }完美转发实际中的使用场景下面模拟实现了一个简化版的list类类当中分别提供了左值引用版本和右值引用版本的push_back和insert函数。代码如下namespace cl { templateclass T struct ListNode { T _data; ListNode* _next nullptr; ListNode* _prev nullptr; }; templateclass T class list { typedef ListNodeT node; public: //构造函数 list() { _head new node; _head-_next _head; _head-_prev _head; } //左值引用版本的push_back void push_back(const T x) { insert(_head, x); } //右值引用版本的push_back void push_back(T x) { insert(_head, std::forwardT(x)); //完美转发 } //左值引用版本的insert void insert(node* pos, const T x) { node* prev pos-_prev; node* newnode new node; newnode-_data x; prev-_next newnode; newnode-_prev prev; newnode-_next pos; pos-_prev newnode; } //右值引用版本的insert void insert(node* pos, T x) { node* prev pos-_prev; node* newnode new node; newnode-_data std::forwardT(x); //完美转发 prev-_next newnode; newnode-_prev prev; newnode-_next pos; pos-_prev newnode; } private: node* _head; //指向链表头结点的指针 }; }下面定义一个list对象list容器中存储的就是之前模拟实现的string类这里分别传入左值和右值调用不同版本的push_back。比如int main() { cl::listcl::string lt; cl::string s(1111); lt.push_back(s); //调用左值引用版本的push_back lt.push_back(2222); //调用右值引用版本的push_back return 0; }调用左值引用版本的push_back函数插入元素时会调用string原有的operator函数进行深拷贝而调用右值引用版本的push_back函数插入元素时只会调用string的移动赋值进行资源的移动。因为实现push_back函数时复用了insert函数的代码对于左值引用版本的push_back函数在调用insert函数时只能调用左值引用版本的insert函数而在insert函数中插入元素时会先new一个结点然后将对应的左值赋值给该结点因此会调用string原有的operator函数进行深拷贝。而对于右值引用版本的push_back函数在调用insert函数时就可以调用右值引用版本的insert函数在右值引用版本的insert函数中也会先new一个结点然后将对应的右值赋值给该结点因此这里就和调用string的移动赋值函数进行资源的移动。这个场景中就需要用到完美转发否则右值引用版本的push_back接收到右值后该右值的右值属性就退化了此时在右值引用版本的push_back函数中调用insert函数也会匹配到左值引用版本的insert函数最终调用的还是原有的operator函数进行深拷贝。此外除了在右值引用版本的push_back函数中调用insert函数时需要用完美转发保持右值原有的属性之外在右值引用版本的insert函数中用右值给新结点赋值时也需要用到完美转发否则在赋值时也会将其识别为左值导致最终调用的还是原有的operator函数。也就是说只要想保持右值的属性在每次右值传参时都需要进行完美转发实际STL库中也是通过完美转发来保持右值属性的。注意 代码中push_back和insert函数的参数T是右值引用而不是万能引用因为在list对象创建时这个类就被实例化了后续调用push_back和insert函数时参数T中的T已经是一个确定的类型了而不是在调用push_back和insert函数时才进行类型推导的。与STL中的list的区别如果将刚才测试代码中的list换成STL当中的list。调用左值引用版本的push_back插入结点在构造结点时会调用string的拷贝构造函数。调用右值引用版本的push_back插入结点在构造结点时会调用string的移动构造函数。而用我们模拟实现的list时调用的却不是string的拷贝构造和移动构造而对应是string原有的operator和移动赋值。原因是因为我们模拟实现的list容器是通过new操作符为新结点申请内存空间的在申请内存后会自动调用构造函数对进行其进行初始化因此在后续用左值或右值对其进行赋值时就会调用对应的operator或移动赋值进行深拷贝或资源的转移。而STL库中的容器都是通过空间配置器获取内存的因此在申请到内存后不会调用构造函数对其进行初始化而是后续用左值或右值对其进行拷贝构造因此最终调用的就是拷贝构造或移动构造。如果想要得到与STL相同的实验结果可以使用malloc函数申请内存这时就不会自动调用构造函数进行初始化然后在用定位new的方式用左值或右值对申请到的内存空间进行构造这时调用的对应就是拷贝构造或移动构造。