📝个人主页🌹:Eternity._
⏩收录专栏⏪:C++ “ 登神长阶 ”
🤡往期回顾🤡:位图与布隆过滤器
🌹🌹期待您的关注 🌹🌹
❀C++11
前言:在C++的悠久历史中,每一次标准的更新都如同为这门强大的编程语言注入了新的活力。C++11,作为这一进程中的一个重要里程碑,不仅带来了众多新特性,还深刻改变了C++编程的范式,其中右值引用(Rvalue References)无疑是最为引人注目的特性之一
在传统的C++编程中,我们习惯于通过左值(Lvalues)来引用和操作对象,这些左值通常指向具有持久身份的对象。然而,随着C++应用的日益复杂和对性能要求的不断提高,如何高效地处理临时对象(即右值,Rvalues)成为了亟待解决的问题。C++11引入的右值引用,正是为了填补这一空白,它允许我们直接引用即将被销毁的临时对象,从而开启了C++编程的新纪元
本篇将带您深入探索C++11中的右值引用及其相关特性,包括移动语义(MoveSemantics)、完美转发(Perfect Forwarding)等。我们将从基础概念讲起,逐步深入到实际应用和最佳实践,旨在帮助您全面理解并掌握这一强大的编程工具
让我们一起踏上学习的旅程,探索它带来的无尽可能!
📒1. C++11简介
C++11是C++编程语言的一个重大更新版本,也被称为C++标准第三版,正式名称为ISO/IEC 14882:2011 - Information technology – Programming languages – C++
相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个重点去学习
C++11标准的发布对C++编程产生了深远的影响,推动了C++语言的现代化和性能提升。随着各大主流编译器(如GCC、Clang、MSVC等)对C++11语法的支持逐渐完善,越来越多的项目开始采用C++11标准进行开发。同时,相关的技术书籍和教程也相继更新,以支持C++11的新特性
总之,C++11是C++编程语言发展历程中的一个重要里程碑,它带来了众多新特性和改进,为C++程序员提供了更加强大和灵活的工具来编写高效、可维护的代码
📜2. 统一的列表初始化
在C++11及以后的版本中,引入了统一的列表初始化(Uniform Initialization)或称为初始化列表(
Initialization List
),这是一种新的初始化语法,使用大括号{}来初始化变量。统一的列表初始化不仅提高了代码的一致性和可读性,还解决了之前初始化语法中的一些歧义和限制
🌸{ }初始化
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定
代码示例 (C++):
// C++98 struct Pxt { int _x; int _y; }; int main() { Pxt p = { 1, 2 }; return 0; }
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加
代码示例 (C++):
// C++11 struct Pxt { int _x; int _y; }; int main() { int a = 1; int b{ 2 }; Pxt p{ 1, 2 }; // C++11中列表初始化也可以适用于new表达式中 int* pa = new int[4]{ 0 }; return 0; }
我们的自定义创建的对象也可以使用列表初始化方式调用构造函数初始化
代码示例 (C++):
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(2024, 7, 28); // old style // C++11支持的列表初始化,这里会调用构造函数初始化 Date d2{ 2024, 7, 29 }; Date d3 = { 2024, 7, 30 }; // 这里的 vector 和上面的 Date 不太一样 vector<int> v = { 1,2,3,4,5 }; return 0; }
列表初始化时,必须要跟对应的构造函数参数个数匹配,
Data
中只能有三个参数,但是vector
的参数可以有很多个,列表初始化也支持隐式类型转换
vector<Data> vd = { {2024, 7, 28},{2024, 7, 29},{2024, 7, 30} };
🌺initializer_list
initializer_list
是 C++11 引入的一个特性,它提供了一种方式来初始化容器类对象或函数参数列表,使得可以使用花括号 { } 来直接初始化对象或传递参数。initializer_list
是一个轻量级的模板类,它用于表示一个给定类型的值的数组,但大小是固定的,且生命周期与包含它的对象相同
代码示例 (C++):
int main() { auto it = { 1 ,2 }; // initializer_list cout << typeid(it).name() << endl; return 0; }
注意:initializer_list的迭代器就是原生指针
std::initializer_list
一般是作为构造函数的参数,C++11对STL中的不少容器就增加std::initializer_list
作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=
的参数,这样就可以用大括号赋值
我们当初在模拟实现这些STL容器时,并没有实现
initializer_list
,今天我们以vector为例子,实现一下initializer_list
的构造
代码示例 (C++):
vector(initializer_list<T> lt) { reverse(lt.size()); for(auto& e : lt) { push_back(e); } }
📚3. decltype与新容器 array
🎩decltype
decltype 是 C++11 引入的一个关键字,它作为操作符用于查询表达式的数据类型。这个操作符主要用于泛型编程中,特别是在模板编程中,当需要推导表达式的类型但又不想实际执行该表达式时,decltype 显得尤为有用
代码示例 (C++):
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; return 0; }
关键字decltype
将变量的类型声明为表达式指定的类型
🎈新容器 array
在C++中,
std::array
是一个固定大小的容器,它提供了类似于数组的接口,但它是标准库的一部分,因此提供了更多的安全性和灵活性。std::array
定义在头文件<array>
中,是一个模板类,可以存储任何类型的固定数量元素
array<int, 10> a; // a[10] vector<int> v(10, 0) // 因为有vector的存在,让array的出现有点“小丑”,而且vector 似乎比array好用
因为有
vector
的存在,让array
的出现有点“小丑”,而且vector
似乎比array
好用,这里我就不细说了,想了解的朋友可以了解一下
📝4. 右值引用和移动语义
右值引用
- 在C++中,表达式根据它们是否可以被修改分为左值(lvalue)和右值(rvalue)。左值是可以被取地址的表达式,通常对应于具有持久状态的实体(如变量)。而右值则是不可以被取地址的临时对象或字面值,它们通常表示计算的结果或函数返回的临时对象。
- 右值引用是C++11引入的一种新类型的引用,它通过类型后加&&来表示。右值引用可以绑定到右值上,但也可以绑定到左值上(需要std::move来显式转换)。右值引用的主要目的是允许函数或操作以“移动”而不是“复制”的方式处理资源,这通常意味着资源的所有权从源对象转移到目标对象,源对象则变为一个安全可销毁的状态。
移动语义
移动语义允许对象通过转移其资源(如动态分配的内存)而不是复制它们来初始化或赋值另一个对象。这通常是通过一个特殊的成员函数——移动构造函数和移动赋值操作符来实现的。这两个函数都接受右值引用作为参数,表示它们可以从一个即将被销毁的对象中“窃取”资源。
- 移动构造函数: 接受一个右值引用参数,用于初始化新对象,通过转移源对象的资源而不是复制它们,从而避免不必要的资源分配和复制。
- 移动赋值操作符: 同样接受一个右值引用参数,用于将一个对象的资源转移到另一个已经存在的对象上,并将源对象置于一个可析构的状态。
⛰️左值引用和右值引用
左值引用
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋
值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左
值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名
代码示例 (C++):
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; }
右值引用
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回) 等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名
代码示例 (C++):
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); return 0; }
左操作数必须为左值,否则就会报错
int main() { // 这里编译会报错:error C2106: “=”: 左操作数必须为左值 10 = 1; x + y = 1; fmin(x, y) = 1; }
注意:
- 不能用一个值能不能修改来区分左右值,右值是不能取地址的
- 给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址
🌄左值引用与右值引用比较
左值引用:
- 左值引用只能引用左值,不能引用右值
- 但是const左值引用既可引用左值,也可引用右值
代码示例 (C++):
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; }
右值引用:
- 右值引用只能右值,不能引用左值
- 但是右值引用可以
move
以后的左值
代码示例 (C++):
int main() { // 右值引用只能右值,不能引用左值。 int&& r1 = 10; // error C2440: “初始化”: 无法从“int”转换为“int &&” // message : 无法将左值绑定到右值引用 int a = 10; int&& r2 = a; // 右值引用可以引用move以后的左值 int&& r3 = std::move(a); return 0; }
move
当你对一个对象使用
move
时,你实际上是在告诉编译器:“这个对象我之后可能不再需要了,或者我可以接受它处于某种未定义状态,所以你可以安全地‘窃取’它的资源。”,从而变成将亡值
,然后,编译器会寻找接收该对象的函数是否支持移动语义(即是否有一个接受右值引用的构造函数或赋值运算符)
代码示例 (C++):
int main() { string s1("hello world"); string s2 = s1; string s3 = move(s1); return 0; }
直接将s1中的资源“移动”到了s3中
🌞右值引用使用场景和意义
右值引用让我们能够在一些函数中直接使用右值,而不用开空间而节省资源,而在STL的很多容器中也够重载了右值引用
右值引用的使用可以让很多场景得到更高的效率来实现功能
移动构造,移动赋值代码示例 (string为例):
// 移动构造 string(string&& s) :_str(nullptr) ,_size(0) ,_capacity(0) { cout << "string(string&& s) -- 移动语义" << endl; swap(s); } // 移动赋值 string& operator=(string&& s) { cout << "string& operator=(string&& s) -- 移动语义" << endl; swap(s); return *this; }
左值引用做参数可以减少拷贝,提高效率的使用场景和价值,但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回
代码示例 :
string to_string(int x) { string ret; while (x) { int val = x % 10; x /= 10; ret += ('0' + val); } reverse(ret.begin(), ret.end()); return ret; } int main() { string ret = to_string(1234); return 0; }
to_string
的返回值是一个右值,用这个右值构造ret2,如果没有移动构造,调用就会匹配调用拷贝构造,因为const左值引用是可以引用右值的,这里就是一个深拷贝
- 移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己,而移动构造中没有新开空间,拷贝数据,所以效率就提高了
- 如果既有拷贝构造又有移动构造调用就会匹配调用移动构造,因为编译器会选择最匹配的参数调用。那么这里就是一个移动语义
有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过
move
函数将左值转化为右值。该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义
代码示例 :
void push_back (value_type&& val); int main() { list<pxt::string> lt; pxt::string s1("1111"); // 这里调用的是拷贝构造 lt.push_back(s1); // 下面调用都是移动构造 lt.push_back("2222"); lt.push_back(std::move(s1)); return 0; }
⭐完美转发
“完美转发”(Perfect Forwarding)是C++11及以后版本中引入的一个特性,它允许函数模板以完全相同的类型(包括const限定符和引用类型)转发其参数到另一个函数或模板。这通常通过模板和std::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; } template<typename 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; }
神奇的一幕发生了,我们运行发现结果全是左值引用
我们有以下结论:
- 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值
- 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力
- 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值
那我们如何能够在传递过程中保持它的左值或者右值的属性, 就需要用我们用到完美转发
完美转发
forward
它允许函数模板将参数转发到另一个函数时,保持其值类别(左值或右值)不变。这是通过模板的隐式类型转换和引用折叠规则实现的,完美转发在传参的过程中保留对象原生类型属性
template<typename T> void PerfectForward(T&& t) { Fun(forward<T>(t)); }
📙5. 新的类功能
C++11在原来的基础上新增了两个默认成员函数:移动构造函数和移动赋值运算符重载
关于这两个函数需要注意:
- 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任
意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类
型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,
如果实现了就调用移动构造,没有实现就调用拷贝构造。- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中
的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内
置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋
值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造
完全类似)- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值
禁止生成默认函数的关键字delete
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁
已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即
可,该语法指示编译器不生成对应函数的默认版本,称=delete
修饰的函数为删除函数
class Person { public: Person(const char* name = "", int age = 0) :_name(name) , _age(age) {} Person(const Person& p) = delete; private: string _name; int _age; };
📖6. 总结
在探索C++11的广阔特性时,右值引用无疑是一个令人兴奋且意义深远的新特性。它不仅为C++带来了移动语义和完美转发的能力,还极大地增强了C++代码的性能和灵活性。通过深入学习和实践右值引用,我们学会了如何更有效地管理资源,减少了不必要的拷贝操作,从而提高了程序的运行效率
在学习过程中,我们见证了右值引用如何与移动构造函数、移动赋值操作符以及std::move函数等配合使用,共同构建起了一套完整的移动语义体系。这套体系不仅优化了STL容器的性能,还为我们编写高性能的C++代码提供了强有力的支持
随着C++标准的不断演进,我们期待看到更多基于右值引用的新特性和优化,C++11的内容我们还没有完全了解,愿我们都能保持好奇心和求知欲,不断探索C++的无限可能,我们下期见!
希望本文能够为你提供有益的参考和启示,让我们一起在编程的道路上不断前行!
谢谢大家支持本篇到这里就结束了,祝大家天天开心!