
目录一统一的列表初始化1-1初始化列表1-2initializer_list初始化容器二类型声明2-1auto用法的修改2-2decltype关键字三STL容器的变化四右值引用和移动语义4-1左值引用和右值引用4-2左值引用与右值引用比较4-3左值引用和右值引用的使用场景4-4右值引用的深入4-5完美转发介绍本文重点讲解C11标准语法由于C11增加的语法特性篇幅非常多这里没办法一一讲解本文重点介绍C11的初始化列表、声明、左右值引用和移动语义。一统一的列表初始化1-1初始化列表这里所说的初始化列表不是构造函数中的初始化列表而是大括号 “{}”即用于初始化数据的大括号叫做初始化列表。C11标准之前大括号{}只能对特定的场景进行初始化如数组、结构体等常用于多元素的初始值设定。C11扩大了大括号{}初始化的使用范围使其可用于所有的内置类型和用户自 定义的类型使用初始化列表时可添加等号()也可不添加。#include iostreamusing namespace std;struct Point{int _x;int _y;};class Date{public:Date(int year, int month, int day):_year(year) ,_month(month) ,_day(day){ }private:int _year;int _month;int _day;};int main(){//1大括号可用于所有的内置类型和用户自定义类型的初始化//2使用大括号初始化可不加等号//自定义类型int a 0;int b { 1 };int c{ 2 };int arr[]{ 1,2,3 };//内置类型Point p{ 1, 2 };int* pa new int[4]{ 0 };//调用构造函数初始化Date d2{ 2022, 1, 2 };Date d3 { 2022, 1, 3 };return 0;}C11标准在这里强调的是一切皆可使用{}初始化包括调用自定义类型的构造函数初始化。在初始化时也可不加等号。补平常使用 “()” 初始化是专门调用构造函数初始化的如int a(1); int e int(2)。这不是在C11标准后才开始支持的语法在C有了模板之后就支持了构造和拷贝构造即支持“()”。1-2initializer_list初始化容器initializer_list其实并不是一个真正的容器它可以理解为一种适配器用于表示使用特定类型值的数组常用于构造函数初始化对象。initializer_list提供了对初始化列表中元素的访问就像访问数组一样它用于表示对象的列表初始化它使得程序员能够使用花括号{}来初始化对象。如vector容器中我们通常可直接vectorint v { 1,2,3,4 };一次性初始化任意数量的int型数据。它底层结构就使用了initializer_list才具有此功能。其它类似容器同理。这里我们以vector为例运用initializer_list实现vector的构造函数如下示例1templateclass Tclass vector {public:typedef T* iterator;/*像vectorint v { 1,2,3,4 };vectorchar v { a,b,c }等以数组的形式进行初始化将会调用以下构造函数*/vector(initializer_listT l){_start new T[l.size()];_finish _start l.size();_capacity _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;}private:iterator _start;iterator _finish;iterator _capacity;};示例2#include iostream#include initializer_listclass A{public:A(std::initializer_listint list) {for (const auto value : list) {std::cout value ;}std::cout std::endl;}};int main(){A a{ 1,2,3,4,5 };//使用 initializer_list 进行初始化return 0;}下面我们来分析initializer_list占用的空间大小。initializer_list内部维护了一个指向数组的指针以及该数组的长度但不拥有所指向数组的内存。它内部有一个成员函数size()——用于获取初始化列表中元素的数量以及一个成员函数begin()和end()——用于获取指向初始化列表开始和结束位置的迭代器。因此initializer_list本身并不占用除指针和长度之外的额外空间它本身占用的空间相对较小主要是指针和长度的开销。数组真正占用的存储空间是由其引用的初始化列表所占用的。template class Tclass initializer_list {private:const T* _arr; // 指向一段连续内存数组首地址size_t _size; // 元素个数public:// 构造、迭代器等...};initializer_list所占用的空间主要取决于它引用的初始化列表的大小。这个列表通常是在创建initializer_list对象时传递的并且存储在堆或其他适当的内存区域中。initializer_list仅保存指向这个列表的指针和列表的长度以便在需要时可以访问和迭代这些元素。#include iostream#include vector#include initializer_listusing namespace std;int main(){auto il1 { 1, 2, 3, 4, 5, 6, 7, 8, 9 };initializer_listint il2 { 1, 2, 3, 4, 5, 6, 7, 8, 9 };cout sizeof(il1) endl;//输出8没有存储初始化列表数据的空间cout sizeof(il2) endl;//输出8没有存储初始化列表数据的空间vectorint v1 { 1,2,3,4,5,6,7,8,9 };//调用内部initializer_listfor (auto e : v1){cout e ;}cout endl;v1 { 10, 20,30,40,50 };//调用内部initializer_listfor (auto e : v1){cout e ;}cout endl;return 0;}二类型声明2-1auto用法的修改C98及之前auto只是一个存储类型的说明符表示变量是局部自动存储类型但是局部域中定义局部变量默认就是自动存储类型所以auto就没什么价值了。C11中废弃auto原来的用法将其用于实现自动类型推断。这样要求必须进行显示初始化让编译器将定义对象的类型设置为初始化值的类型。平常我们使用auto来接收类型时编译器必须更新C11标准。这里唯一要注意的是auto关键字不能出现在函数参数中因为编译器在编译阶段需要确定auto的具体类型。2-2decltype关键字关键字auto用于接收类型而关键字decltype的作用是将变量的类型声明为表达式指定的类型。#include iostream#include vectorusing namespace std;templateclass T1, class T2void F(T1 t1, T2 t2){decltype(t1 * t2) ret;cout typeid(ret).name() endl;}int main(){const int a 1;const int* b a;const int* const c b;cout typeid(a).name() endl;//输出intcout typeid(b).name() endl;//输出int const *cout typeid(c).name() endl;//输出int const *cout endl;const int x 1;double y 2.2;decltype(x * y) ret;//decltype声明ret类型ret的类型是doubledecltype(x) p;//decltype声明p类型p的类型是const int*cout typeid(ret).name() endl;//输出doublecout typeid(p).name() endl;//输出int const *F(1, a);//创建x类型的顺序表vvectordecltype(x) v;return 0;}2-3using关键字C11新增using作用它除了可以用于引入命名空间中的成员外还可以用来定义类型的别名将复杂类型的名称简化与typedef类同。在C11中using与typedef都是用于创建类型别名的工具但它们在语法和功能上存在一些差异。以下是对两者的详细比较以及对using的详细说明using与typedef的差异typedef的语法是typedef existing_data_type new_name; 例如typedef int Integer;将int类型别名为Integer。using的语法是using new_name existing_data_type; 例如using Integer int;同样将int类型别名为Integer。相比typedefusing的语法更加直观和现代。typedef不支持模板化别名而using则支持。这使得using在模板编程中更加灵活可以用于定义模板别名。templatetypename Tusing V std::vectorT;这里定义了一个模板的别名V它是std::vectorT的简写。三STL容器的变化首先C11标准在STL中增添了部分容器下面用橘色圈起来是C11中的一些几个新容器但是实际最有用的是容器unordered_map和unordered_set。其他的只需了解即可。容器新增用法的缺陷C11标准发布后STL容器增添了不少新用法这些用法有些意义重大但有一说一部分用法的加入可以说无关痛痒意义不大如提供了cbegin和cend方法返回const迭代器等等begin和end也是可以返回const迭代器的专门实现cbegin和cend方法完全没必要。其次对于C11增添的用法如果我们再细细去观察会发现基本每个容器中都增加了一些但是其实很多都是用得比较少的可以说C的缺陷之一就是语法杂而不华复杂而又不精华的语法和特性令人头疼。容器新增用法的优点C11为容器底层增添了initializer_list用法及一系列的构造大大优化了容器的用法。其次容器还新增右值引用的用法如push、insert等、移动构造和移动赋值这提高了STL的效率减少了拷贝。四右值引用和移动语义4-1左值引用和右值引用首先我们先明白什么是左值什么是右值。左值一个表示数据的表达式通常指的是有持久存储期的对象表示一个可以被修改的内存位置。比如变量、数组元素、结构体的成员、解引用的指针等。我们可以获取左值的地址且对它赋值除此之外它还可以出现赋值符号的左边或右边。平常我们操作最多的数据以及对象基本都是左值。右值它也是一个表示数据的表达式通常指的是一个临时的、不可修改的值表示一个临时的、不可修改的值。如字面常量、表达式返回值等等一系列临时对象如表达式的结果这类没有持久存储期的对象。右值可以出现在赋值符号的右边但是不能出现出现在赋值符号的左边。右值通常也不能取其地址但某些右值引用类型的变量可以。#include iostreamusing namespace std;/*返回右值。func1返回的是其静态局部变量x的一个临时拷贝。临时拷贝是一个临时的数值属于右值*/int func1(){static int x 5;return x;}/*返回左值。func2的返回类型是int它是一个引用类型。func2返回的是对静态局部变量 x的一个引用表示对象x是一个具有持久存储周期的变量属于左值*/int func2(){static int x 5;return x;}int main(){//左值。类似于以下具有持久存储期的对象double x 1.1, y 2.2;func2();//以下几个都是常见的右值10;//字面常量x y;//表达式返回值func1();//函数返回值//编译报错表达式必须是可修改的左值或函数指示符//不能赋值/*10 1;x y 1;func1() 1;//不能取地址10;(x y);func1();*/const int a 7;const int* p a;//a还是左值仍可取地址return 0;}下面我们来观看左值引用与右值引用。左值引用就是对左值的引用给左值取别名。我们平常使用单个 “” 的引用叫做左值引用。右值引用就是对右值的引用给右值取别名。右值引用的符号是 “”。右值本身不能取地址和进行赋值操作但是给右值取别名后会导致右值被存储到特定位置这样就可以取到该位置的地址和修改其值右值引用底层指针存储的就是这块空间的地址。#include iostreamusing namespace std;int main(){int a 4, b 2;int p a b;cout p ;//输出6p 7;cout p endl;//输出7int pp 10;cout pp ;//输出10pp 0;cout pp endl;//输出0return 0;}以上右值引用特性了解即可实际中右值引用的使用场景并不在于此这个特性也不重要。右值引用主要用于移动语义。4-2左值引用与右值引用比较一般情况下左值引用只能引用左值右值引用只能引用右值。但是const左值引用既可引用左值也可引用右值即常引用。右值引用要想引用左值必须使用move函数。在C中move的主要作用是强制将左值转换为右值引用从而允许使用移动语义来优化资源管理。需注意的是move本身并不会真正移动任何东西而是通过类型转换告诉编译器“这个对象可以被移动”。#include iostreamusing namespace std;//返回右值int func1(){static int x 5;return x;}//返回左值int func2(){static int x 5;return x;}int main(){double x 1.1, y 2.2;//右值引用int rr1 10;//错误使用: int rr1 10;double rr2 x y;//错误使用: double rr2 x y;double rr3 func1();//错误使用: double rr3 func1();//右值引用取地址操作int* p rr1;//左值引用引用右值const int r1 10;const double r2 x y;const double r3 func1();//move处理左值右值引用引用左值double pp1 move(x);double pp2 move(y);return 0;}4-3左值引用和右值引用的使用场景无论是左值引用还是右值引用都是引用包含了引用的大多数优势。左值引用与右值引用主要针对于大型数据结构的返回和传递情况。对于大型数据结构如果直接传递或返回可能会导致大量的内存复制操作从而降低程序的性能。通过使用引用可以避免这种复制提高程序的运行效率。 由于const左值引用即可接收左值也可接收右值所以const左值引用能够解决所有的传递拷贝情况但返回值拷贝的情况不能全部解决如函数返回局部对象一旦出了作用域就不存在了不能使用左值引用返回。右值引用的增入主要解决此种问题。下面我们来观察在没用右值引用返回局部对象的情况。平常我们使用多参构造时实际上其实进行了两次构造。首先从多参数构造成对象类型然后从对象类型进行拷贝构造。有些编译器会对此进行优化直接转成构造或拷贝构造#include iostream#include stringusing namespace std;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 d2 { 2024, 3, 23 };//多参数的隐式类型转换//构造 拷贝构造 - 优化直接构造string s2 1111;//多参数的隐式类型转换//C98标准支持 () 的语法运用构造函数Date d1(2024, 3, 23);string s1(1111);//引用临时变量以下将无法进行优化const string s 123;return 0;}函数返回局部对象数据结构时同理先进行拷贝构造生成临时变量然后又通过此临时变量拷贝构造给接收对象若是优化的编译器这里会直接进行一次拷贝构造给接收对象但即便是这样仍没有解决值拷贝的效率问题。由于返回值是临时对象这里无法使用引用返回但是返回大型数据结构时通常涉及到构造函数、拷贝构造函数或赋值运算符的调用。左引用实现这些方法时都是深拷贝实现这样一来跟传统的按值返回函数构造临时对象返回值的拷贝一样而临时对象的生命即将结束深拷贝下来可能会非常昂贵尤其是当对象包含大量数据或资源如动态分配的内存时至少要进行一次构造如上图般。右值引用解决这块问题时依靠的是移动语义。右值引用和移动语义允许我们“窃取”临时对象的资源而不是复制它们。这通过“移动构造函数”和“移动赋值操作符”实现这些特殊移动语义函数允许对象将其资源如内存、文件句柄等转移给另一个对象进行的只是资源的移动而不是复制它们。若便编译器没有优化这里也只是进行了一次拷贝——拷贝临时对象一次移动——资源移动给指定对象没有像左值引用那般进行多次值拷贝即避免了多次深拷贝若编译器优化后直接变成了一次移动没有了临时对象的拷贝与上图类似。移动赋值同理如下//移动赋值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;}场景二#include iostream#include utilityusing namespace std;class A{public://移动构造A(int data) :_data(data){cout A(int data)—移动构造 endl;}A(A _A){cout A(A _A)—移动拷贝 endl;swap(_A);}//拷贝构造A(const int data 0) :_data(data){cout A(const int data 0)—拷贝构造 endl;}A(const A _A){_data _A._data;cout A(A _A)—深拷贝 endl;}//移动赋值A operator(A _A){cout A operator(A _A)—移动赋值 endl;swap(_A);return *this;}//深拷贝A operator(const A _A){cout A operator(const A _A)—深拷贝赋值 endl;A a(_A);swap(a);return *this;}void swap(A _A)//注意: 使用void swap(A _A)交换这里会存在拷贝构造即深拷贝{std::swap(_A._data, _data);}int _data;};A func1(){A a 10;return a;}int func2(){int a 1;return a;}int main() {//调用函数这里不会发生值拷贝而是使用移动语义A _c func1();cout endl;A _d func2();cout endl;_d _c;return 0;}左值使用move函数后它的返回属性即表达式就从左值变成了右值但它本身还是左值也就是说左值也可按照个人意愿调用移动语义但是此用法一定要慎重因为右值引用的本质是将右值即将结束生命里的资源全部转移的另一个对象中右值对象即将“死亡”我们可随意动用它的资源。左值引用由于左值生命周期还很长不能轻易移动它的资源一点移动它的资源后面再次运用时就会出问题因为move本身并不执行任何移动操作它只是产生一个右值引用。实际的移动操作即资源的窃取是由移动构造函数或移动赋值运算符来执行的。这点在使用时需注意。#include iostream#include stringusing namespace std;int main(){string s1(abc);// 这里s1是左值调用的是拷贝构造cout s1 endl;//输出abc/*将s1 move处理以后它会被当成右值此时构造函数调用的是库中的移动构造s1的全部资源转移给了s3s1被置空了*/string s2(move(s1));//s1还是左值调用拷贝构造cout s1 endl;//资源被转移输出空return 0;}通过以上的运用可总结一点浅拷贝的类不需要移动构造因为一般情况下浅拷贝的对象消耗不是很大如int型、指针型等深拷贝的类要观看是否需要使用移动构造来转移资源提高效率。C11之前编译器在这方面做了不少努力在很多场景下直接进行优化成一次拷贝但效率上还是没有得到完全的解决。C11增加了右值引用使用了移动语义配合编译器的优化将这块深拷贝的问题完全解决。STL的容器增加了移动语义即移动构造和移动赋值处理大型数据结构的拷贝问题。4-4右值引用的深入首先来说明右值引用的属性。右值被右值引用后右值引用的属性其实是左值。右值引用的作用在于移动语义移动语义的作用在于资源的转移避免大型结构的深拷贝。若右值引用属性还是右值那么右值根本无法被改变资源也就无法被转移。其次左值加上const后属性还是左值这一点不要被const的作用迷惑。const关键字只是限制对象本身不能被修改并没有修改对象的属性我们仍可对其操作和取地址。最后右值引用并没有延长右值的生命周期也没有开辟空间它只是将右值资源的进行了移动。#include iostreamusing namespace std;int main(){const int a 7;const int* p a;//a还是左值仍可取地址int pp 10;pp;//可以对右值引用进行操作属于左值return 0;}4-5完美转发C11引入右值引用后模板中的引用运用出现了两种情况即左值引用传入和右值引用传入。如下template typename Tclass Func{public:void func(T t);//左值引用或const左值引用void func(T t);//右值引用/*注意这里一般不会有const右值引用。通常const型右值引用通常没有实际意义因为右值引用的主要目的是允许移动语义而const会阻止这种移动现实中根本不会出现这里了解即可*/}这样一来显然比较麻烦。C11直接使用万能引用包含了所有情况符号“”。当调用万能引用时它会根据传入的参数属性实例化出特定的属性。如下template typename T//万能引用T直接包含了左值和右值的所有情况。void func(T t);//万能引用。传入参数时它会根据参数的属性实例化出三个版本之一左值引用、右值引用、const左值引用注意模板中的不代表右值引用而是万能引用。万能引用的核心在于模板参数的推导。如果函数本身不是模板函数或者模板参数已经明确指定则不会触发万能引用的行为。下面来说明下万能引用的场景要点templateclass Tclass A{public:void fun(T a);// 不是万能引用};template class T intvoid func(T a);// 是万能引用上面代码中fun的定义没有模板参数推导。虽然类 A 是一个模板类但成员函数 fun 并不是一个模板函数。类模板参数 T 已经在类定义时确定了例如当实例化 Aint 时T 就被确定为 int。因此fun 中的 T 不会触发模板参数推导T 也就是一个明确的右值引用。func的定义中虽然默认模板参数 T int 但这并不影响万能引用的判定因为模板参数仍然可以被显式指定或通过推导确定。最后再次强调下只有当类型推导发生时右值引用才能成为万能引用。万能引用既能接收左值或const左值又能接收右值。当传入左值时万能引用解析为左值引用当传入右值时万能引用解析为右值引用当传入const左值时万能引用解析为const左值引用。#include iostream#include stringusing namespace std;/*三个版本之所以不实现T版本是因为一旦传入左值时T版本比万能引用更合适系统会优先选择T版本*/template typename Tvoid func(T a){cout void func(T a) endl;}/*const型都传入此版本中。万能引用也可引用const但是此版本更为合适const型系统会优先选择*/template typename Tvoid func(const T a){cout void func(const T a) endl;}int main(){//以下三个都输出void func(T a)func(10);//右值int a 10;func(a);//左值string b bit;func(b);//左值//以下三个都输出void func(const T a)const int _a 0;func(_a);//const型const string _b bit;func(_b);//const型func(123);//常量字符串属于const char*即const型return 0;}C实际运用中万能引用最常用的地方是模板编程中右值引用的使用场景是没有模板参数的参与。//样例一void f(int data);//没有模板参数属于右值引用//样例二templatetypename Tvoid f(vectorT param);//参数类型是vectorT不是T属于vectorT的右值引用//样例三templatetypename Tclass vector{public:void push_back(T x);//不是模板函数属于右值引用}//样例四templatetypename Tvoid f(T data);//模板函数参数类型是模板参数属于万能引用平常运用中出现情况最多的也就以上几种这里可对比万能引用与右值引用的条件。万能引用固然很方便使用但是万能引用却存在更改对象属性的问题。具体形式如下#include iostreamusing namespace std;//Fun的四个版本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; }//万能引用template typename Tvoid func(T a)//注意引用类型a的属性全部是左值使用move(a)后属性全部是右值{Fun(a);}int main(){func(10);//右值int a 0;func(a);//左值func(move(a));//右值const int b 8;func(b);//const 左值func(move(b));//const 右值return 0;}若是使用Fun(move(a))将会输出以下模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力但归根到底还是引用。无论是左值引用还是右值引用它们的属性都是左值后续的使用中都变成了左值运用导致原本对象的属性改变。C11加入了forward来解决这种问题。forward保持了原本属性。//forward的使用代码如上这里只将Fun参数使用forward版本Fun(forwardT(a));改变后运行结果如下总 C是一门极度追求效率的语言所以在某些方面增添了许多语法如右值引用但在某些方面的增添确实不太实用如初始化列表“{}”这将导致语法复杂多样因此很多人对这么语言都有褒贬看法。