
目录一.C11的发展历史二.C列表初始化2.1C98传统的{}2.2 C11中的{}2.3C中的std::initializer_list三、右值引用和移动语义3.1 左值和右值3.2 左值引用和右值引用3.3 引用延长生命周期3.4 左值和右值的参数匹配3.5 右值引用和移动语义的使用场景3.5.1 左值引用主要使用场景回顾3.5.2 移动构造和移动赋值3.5.3 右值引用和移动语义解决传值返回问题一.C11的发展历史C11 是 C 的第二个主要版本并且是从 C98 起的最重要更新。它引入了大量更改标准化了既有实践并改进了对 C 程序员可用的抽象。在它最终由 ISO 在 2011 年 8 月 12 日采纳前人们曾使用名称 “C0x”因为它曾被期待在 2010 年之前发布。C03 与 C11 期间花了 8 年时间故而这是迄今为止最长的版本间隔。从那时起C 有规律地每 3 年更新一次。二.C列表初始化2.1C98传统的{}C98中⼀般数组和结构体可以⽤{}进⾏初始化。2.2 C11中的{}C11 以后想统一初始化方式试图实现一切对象皆可用 {} 初始化{} 初始化也叫做列表初始化注意和构造函数那里的初始化进行区分构造函数那里的{}叫做初始化列表和这里是没有关联关系的。内置类型支持自定义类型也支持自定义类型本质是类型转换中间会产生临时对象最后优化了以后变成直接构造。{} 初始化的过程中可以省略掉 C11 列表初始化的本意是想实现一个大统一的初始化方式其次他在有些场景下带来的不少便利如容器 push/inset 多参数构造的对象时{} 初始化会很方便下面讲的左值右值之类的后面会将这里只是让大家看一下三种构造的区别1.v1.push_back(d1); // 有名对象d1是有名左值对象。参数d1是左值匹配push_back(const Date)vector 获取d1的只读引用在 vector 堆空间开辟一块 Date 内存调用 Date 的拷贝构造函数以d1为模板复制出新对象存入容器执行完毕容器内是独立副本d1继续存在不受影响。2.v1.push_back(Date(2026, 8, 5)); // 匿名对象Date(2026,8,5)是临时匿名对象属于右值。C11 及之后现代标准执行Date(2026,8,5)在栈上创建临时匿名对象临时对象是右值匹配重载push_back(Date)vector 在堆上分配空间调用 Date 的移动构造函数把临时对象资源 “转移” 进容器语句结束栈上临时对象被销毁资源已经转移析构无事可做。3.v1.push_back({ 2026,8,15 }); // 花括号初始化列表这是列表初始化braced-init-list分两层理解{2026,8,15}是不带类型的初始化列表编译器尝试用这组参数构造一个临时Date对象等价于Date{2026,8,15}生成临时 Date 右值后续流程和上面匿名对象完全一致匹配右值版本push_back(Date)调用移动构造将对象移入 vector。写法是否产生临时对象最终调用构造push_back(d1)无临时拷贝构造push_back(Date(y,m,d))栈上临时对象移动构造C11push_back({y,m,d})栈上临时对象移动构造C11emplace_back(y,m,d)无临时直接在容器内构造2.3C中的std::initializer_list上⾯的初始化已经很⽅便但是对象容器初始化还是不太⽅便⽐如⼀个vector对象我想⽤N个 值去构造初始化那么我们得实现很多个构造函数才能⽀持 vector v1 {1,2,3};vector v2 {1,2,3,4,5};C11库中提出了⼀个std::initializer_list的类 auto il { 10, 20, 30 }; // the type of il is an initializer_list 这个类的本质是底层开⼀个数组将数据拷⻉ 过来std::initializer_list内部有两个指针分别指向数组的开始和结束。这是他的⽂档initializer_liststd::initializer_list⽀持迭代器遍历。容器⽀持⼀个std::initializer_list的构造函数也就⽀持任意多个值构成的 初始化。STL中的容器⽀持任意多个值构成的 {x1,x2,x3...} 进⾏ {x1,x2,x3...} 进⾏初始化就是通过 std::initializer_list的构造函数⽀持的。三、右值引用和移动语义C98 的语法中就有引用的语法而 C11 中新增了的右值引用语法特性C11 之后我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用都是给对象取别名。3.1 左值和右值左值是一个表示数据的表达式 (如变量名或解引用的指针甚至可以是一个函数调用所指就是函数调用的返回值)一般是有持久状态存储在内存中我们可以获取它的地址左值可以出现在赋值符号的左边也可以出现在赋值符号右边。定义时 const 修饰符后的左值不能给他赋值不严谨但是可以取它的地址。3.2 左值引用和右值引用Type r1 x; Type rr1 y; 第一个语句就是左值引用左值引用就是给左值取别名第二个就是右值引用同样的道理右值引用就是给右值取别名。左值引用不能直接引用右值但是const左值引用可以引用右值右值引用不能直接引用左值但是右值引用可以引用move(左值)template class T typename remove_reference T ::type move (T arg);move是库里面的一个函数模板本质内部是进行强制类型转换当然他还涉及一些引用折叠的知识这个我们后面会细讲。需要注意的是变量表达式都是左值属性也就意味着一个右值被右值引用绑定后右值引用变量变量表达式的属性是左值表达式类型cout 行为iint*打印内存地址s[0]char*打印字符串内容pint*int*打印内存地址3.3 引用延长生命周期C 里const 左值引用 和 右值引用都能把临时对象的生命周期从 “当前语句” 延长到 “引用的作用域结束”。区别只在于能不能修改这个对象。像 s1 s1 这种表达式会返回一个临时的 std::string 对象其是一个纯右值rvalue用完即毁默认生命周期只到当前语句结束默认情况下不能直接修改它比如 (s1 s1) “Test” 会报错。r3 是一个右值引用右值对象是没办法直接修改的但它是一个有名字的变量C 里有名字的变量都是左值不管它是什么类型的引用所以 r3 本身是左值可以被赋值、被修改也可以被取地址而 r3 绑定的是临时对象所以通过 r3 就能修改这个原本无法直接修改的临时对象。3.4 左值和右值的参数匹配C98 中我们实现一个 const 左值引用作为参数的函数那么实参传递左值和右值都可以匹配。C11 以后分别重载左值引用、const 左值引用、右值引用作为形参的 f 函数那么实参是左值会匹配 f (左值引用)实参是 const 左值会匹配 f (const 左值引用)实参是右值会匹配 f (右值引用)。右值引用变量在用于表达式时属性是左值这个设计这里会感觉很怪下面我们讲右值引用的使用场景时就能体会这样设计的价值了3.5 右值引用和移动语义的使用场景3.5.1 左值引用主要使用场景回顾左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝同时还可以修改实参和修改返回对象的价值。左值引用已经解决大多数场景的拷贝效率问题但是有些场景不能使用传左值引用返回如 addStrings 和 generate 函数C98 中的解决方案只能是被迫使用输出型参数解决。两次拷贝的代价太大所以后面引入了移动构造和移动赋值3.5.2 移动构造和移动赋值移动构造函数是一种构造函数类似拷贝构造函数它是之前拷贝构造的函数重载移动构造函数要求第一个参数是该类类型的引用但是不同的是要求这个参数是右值引用如果还有其他参数额外的参数必须有缺省值。移动赋值是一个赋值运算符的重载他跟拷贝赋值构成函数重载类似拷贝赋值函数移动赋值函数要求第一个参数是该类类型的引用但是不同的是要求这个参数是右值引用。对于像 string/vector 这样的深拷贝的类或者包含深拷贝的成员变量的类移动构造和移动赋值才有意义因为移动构造和移动赋值的第一个参数都是右值引用的类型他的本质是要 “窃取” 引用的右值对象的资源而不是像拷贝构造和拷贝赋值那样去拷贝资源从提高效率。下面的 bit::string 样例实现了移动构造和移动赋值我们需要结合场景理解3.5.3 右值引用和移动语义解决传值返回问题这里用用命名空间与库里面区分是为了方便观察调用的构造.移动构造等#includeiostream #includeassert.h #includestring.h #includealgorithm using namespace std; namespace Jam { class string { public: typedef char* iterator; typedef const char* const_iterator; iterator begin() { return _str; } iterator end() { return _str _size; } const_iterator begin() const { return _str; } const_iterator end() const { return _str _size; } string(const char* str ) :_size(strlen(str)) , _capacity(_size) { cout string(char* str)-构造 endl; _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) { cout string(const string s) -- 拷贝构造 endl; reserve(s._capacity); for (auto ch : s) { push_back(ch); } } // 移动构造 string(string s) { cout string(string s) -- 移动构造 endl; swap(s); } string operator(const string s) { cout string operator(const string s) -- 拷贝赋值 endl; if (this ! s) { _str[0] \0; _size 0; reserve(s._capacity); for (auto ch : s) { push_back(ch); } } return *this; } // 移动赋值 string operator(string s) { cout string operator(string s) -- 移动赋值 endl; swap(s); return *this; } ~string() { cout ~string() -- 析构 endl; delete[] _str; _str nullptr; } char operator[](size_t pos) { assert(pos _size); return _str[pos]; } void reserve(size_t n) { if (n _capacity) { char* tmp new char[n 1]; if (_str) { strcpy(tmp, _str); delete[] _str; } _str tmp; _capacity n; } } void push_back(char ch) { if (_size _capacity) { size_t newcapacity _capacity 0 ? 4 : _capacity * 2; reserve(newcapacity); } _str[_size] ch; _size; _str[_size] \0; } string operator(char ch) { push_back(ch); return *this; } const char* c_str() const { return _str; } size_t size() const { return _size; } private: char* _str nullptr; size_t _size 0; size_t _capacity 0; }; // 传值返回需要拷贝 string addStrings(string num1, string num2) { string str; int end1 num1.size() - 1, end2 num2.size() - 1; // 进位 int next 0; while (end1 0 || end2 0) { int val1 end1 0 ? num1[end1--] - 0 : 0; int val2 end2 0 ? num2[end2--] - 0 : 0; int ret val1 val2 next; next ret / 10; ret ret % 10; str (0 ret); } if (next 1) str 1; reverse(str.begin(), str.end()); return str; } } int main() { Jam::string ret yunze::addStrings(11111, 2222); }也可以看到现代编译器的极致优化就相当于str是ret的别名了二者地址都指向的是一块空间