做网站需要域名 域名是啥,求几个微信推广平台,邵阳做网站,wordpress西瓜前言#xff1a;
本期#xff0c;我们将要的介绍有关 C右值引用 的相关知识。对于本期知识内容#xff0c;大家是必须要能够掌握的#xff0c;在面试中是属于重点考察对象。 目录
#xff08;一#xff09;左值引用和右值引用
1、什么是左值#xff1f;什么是左值引用…前言
本期我们将要的介绍有关 C右值引用 的相关知识。对于本期知识内容大家是必须要能够掌握的在面试中是属于重点考察对象。 目录
一左值引用和右值引用
1、什么是左值什么是左值引用
2、什么是右值什么是右值引用
二左值引用与右值引用比较
三右值引用使用场景和意义
四完美转发
1、概念
2、模板中的 万能引用
3、std::forward
总结 一左值引用和右值引用 传统的C语法中就有引用的语法而C11中新增了的右值引用语法特性所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用都是给对象取别名。 1、什么是左值什么是左值引用
C98/03 标准中就有引用使用 表示。但此种引用方式有一个缺陷即正常情况下只能操作 C中的左值无法对右值添加引用。举个例子
int main()
{int num 10;int b num; //正确int c 10; //错误return 0;
}
输出展示 【解释说明】
如上所示编译器允许我们为 num 左值建立一个引用但不可以为 10 这个右值建立引用。因此C98/03 标准中的引用又称为左值引用。 那么到底什么是左值什么是左值引用呢 左值是一个表示数据的表达式(如变量名或解引用的指针)我们可以获取它的地址可以对它赋值左值可以出现赋值符号的左边右值不能出现在赋值符号左边定义时const修饰符后的左值不能给他赋值但是可以取它的地址。左值引用就是给左值的引用给左值取别名。
注意虽然 C98/03 标准不支持为右值建立非常量左值引用但允许使用常量左值引用操作右值。也就是说常量左值引用既可以操作左值也可以操作右值例如
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;
} 2、什么是右值什么是右值引用 我们知道右值往往是没有名称的因此要使用它只能借助引用的方式。这就产生一个问题实际开发中我们可能需要对右值进行修改实现移动语义时就需要显然左值引用的方式是行不通的。 为此C11 标准新引入了另一种引用方式称为右值引用用 表示 右值也是一个表示数据的表达式如字面常量、表达式返回值函数返回值(这个不能是左值引用返回)等等右值可以出现在赋值符号的右边但是不能出现出现在赋值符号的左边右值不能取地址右值引用就是对右值的引用给右值取别名。 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;
}
输出展示 但是如果是下面这几个表达式就会发生报错现象
10 1;
x y 1;
fmin(x, y) 1;
输出显示 需要注意的是右值是不能取地址的但是给右值取别名后会导致右值被存储到特定位置且可 以取到该位置的地址。例如下面代码所示
int main()
{double x 1.1, y 2.2;int rr1 10;double rr2 x y;cout rr1 rr2 endl;rr1 20;rr2 5.5;cout rr1 rr2 endl;return 0;
}
输出展示 当我们不想被修改时我们可以加上 【const】关键字 【解释说明】
不能取字面量10的地址但是rr1引用后可以对rr1取地址也可以修改rr1如果不想rr1被修改可以用const int rr1 去引用是不是感觉很神奇这个了解一下实际中右值引用的使用场景并不在于此这个特性也不重要 二左值引用与右值引用比较 左值引用总结
1. 左值引用只能引用左值不能引用右值。2. 但是const左值引用既可引用左值也可引用右值 int main()
{// 左值引用只能引用左值不能引用右值。int a 10;int ra1 a; // ra为a的别名return 0;
}
输出展示 又例如以下示例
int main()
{// 左值引用只能引用左值不能引用右值。int a 10;int ra2 10; // 编译失败因为10是右值return 0;
}
输出展示 左值引用只能引用左值不能引用右值。但是当我们加上 const 时此时左值引用可以给右值取别名
int main()
{int a 10;// const左值引用既可引用左值也可引用右值。const int ra3 10;const int ra4 a;return 0;
}
输出展示 【解释说明】
值得一提的是虽然C 语法上是支持定义常量右值引用的但这种定义出来的右值引用并无实际用处
const int ra3 10;
一方面右值引用主要用于移动语义和完美转发其中前者需要有修改右值的权限其次常量右值引用的作用就是引用一个不可修改的右值这项工作完全可以交给常量左值引用完成。 右值引用总结
1. 右值引用只能右值不能引用左值。2. 但是右值引用可以move以后的左值。
代码展示 引用左值会发生报错行为 通过 move 可以支持将左值转换为右值引用 在C中move是一个函数模板可以将给定的对象转换为对应的右值引用。它并不执行实际的内存移动操作而是将对象标记为可以进行移动操作的右值。这样用户可以利用该标记来实现更高效的移动语义。 三右值引用使用场景和意义 前面我们可以看到左值引用既可以引用左值和又可以引用右值那为什么C11还要提出右值引用呢是不是化蛇添足呢下面我们来看看左值引用的短板右值引用是如何补齐这个短板的 现有以下代码 【解释说明】
首先对于上述代码中的 res1 和 res2 它们分别为左值和右值
紧接着大家想想我们对左值和对右值拷贝有没有什么区别呢
如果是内置类型他们其实区别不是很大但是对于自定类型他们的区别可就很大了因为自定义类型的右值一般很多地方又把它叫做将亡值。通常都是一些表达式的返回值、一个函数调用等而对于右值又分为 纯右值一般来说是内置类型和将亡值一般来说是自定义类型
对于上述的 res1它作为一个左值我们不能对其进行操作只能去做深拷贝。因为虽然看起来这里是一个赋值其实应该是拷贝构造
而对于 res2 来说它本身是右值假如是自定义类型作为一个将亡值我们就没有必要去对其进行拷贝操作。此时就引出了关于右值引用实现引动构造的概念。 例如现在有这样一个我们手写模拟的 string namespace zp
{class string{public:typedef char* iterator;iterator begin(){return _str;}iterator end(){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);}// s1.swap(s2)void swap(string s){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}// 拷贝构造string(const string s):_str(nullptr){cout string(const string s) -- 深拷贝 endl;string tmp(s._str);swap(tmp);}// 赋值重载string operator(const string s){cout string operator(string s) -- 深拷贝 endl;string tmp(s);swap(tmp);return *this;}~string(){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];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)string operator(char ch){push_back(ch);return *this;}string operator(char ch){string tmp(*this);tmp ch;return tmp;}const char* c_str() const{return _str;}private:char* _str;size_t _size;size_t _capacity; // 不包含最后做标识的\0};
}
当我们再这样的场景在来观察上述的 res1 和res2 我们可以发现此处的右值发生的是相应的深拷贝这样显然是会造成不必要的浪费的。为了解决上述这样的问题我们就可以引入 “移动构造” 这样的概念
// 移动构造
string(string s):_str(nullptr)
{cout string(string s) -- 移动拷贝 endl;swap(s);
}紧接着再次运行上述代码我们可以发现编译器会去自动识别 此时当我们就是想把 s1 转为右值可以怎么做呢其实很简单这里就体现了move 输出展示 我们通过调试也可以发现此时确实达到了预期的效果 【小结】
通过上述我们可以发现左值引用的好处就是直接减少拷贝
左值引用的使用场景可以分为以下两个部分
做参数和做返回值都可以提高效率 左值引用的短板
但是当函数返回对象是一个局部变量出了函数作用域就不存在了就不能使用左值引用返回只能传值返回。
例如现在有以下这样的代码 zp::string to_string(int value){bool flag true;if (value 0){flag false;value 0 - value;}zp::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;}
【说明】
zp::string to_string(int value)函数中可以看到这里只能使用传值返回传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。 紧接着我们去打印看结果是什么 此时传值返回带来的代价得到了极大的解决 右值引用和移动语义解决上述问题
在zp::string中增加移动构造移动构造本质是将参数右值的资源窃取过来占位已有那么就不用做深拷贝了所以它叫做移动构造就是窃取别人的资源来构造自己 再运行上面zp::to_string的两个调用我们会发现这里没有调用深拷贝的拷贝构造而是调用了移动构造移动构造中没有新开空间拷贝数据所以效率提高了。 c11 不仅仅有移动构造还有移动赋值 在zp::string类中增加移动赋值函数再去调用zp::to_string(1234)不过这次是zp::to_string(1234)返回的右值对象赋值给ret1对象这时调用的是移动构造。
输出展示 【解释说明】
这里运行后我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象接收编译器就没办法优化了。zp::to_string函数中会先用str生成构造生成一个临时对象但是我们可以看到编译器很聪明的在这里把str识别成了右值调用了移动构造。然后在把这个临时对象做为 zp::to_string 函数调用的返回值赋值给ret1这里调用的移动赋值。 四完美转发
1、概念
完美转发perfect forwarding是C11引入的一项特性旨在实现在函数模板中对参数类型进行精确传递的能力它主要用于保留传递到函数模板的实参的值类别并将其转发到内部调用的函数从而实现类型和值类别的完全保持
举个例子
templatetypename T
void PerfectForward(T t)
{Fun(t);
}
【解释说明】
如上所示PerfectForward() 函数模板中调用了 Func() 函数在此基础上完美转发指的是如果 PerfectForward() 函数接收到的参数 t 为左值那么该函数传递给 Func() 的参数 t 也是左值反之如果 function() 函数接收到的参数 t 为右值那么传递给 Func() 函数的参数 t 也必须为右值。 使用任何一种引用形式可以实现转发但无法保证完美。因此如果使用 C 98/03 标准下的 C 语言我们可以采用函数模板重载的方式实现完美转发例如
templatetypename T
void Func(T arg)
{cout 左值引用 arg endl;
}templatetypename T
void Func(T arg)
{cout 右值引用 arg endl;
}templatetypename T
void PerfectForward(T arg)
{Func(arg); // 利用重载的process函数进行处理
}int main()
{int value 42;PerfectForward(value); // 传递左值PerfectForward(123); // 传递右值return 0;
}
输出展示 【解释说明】
在上述示例中我们定义了两个重载的函数模板 Func一个接收左值引用参数T arg另一个接收转发引用参数T arg然后我们再定义一个模板函数PerfectForward其参数也是转发引用T arg。在PerfectForward函数内部我们通过调用Func函数来处理传递的参数通过函数重载的机制传递的左值参数将匹配到接收左值引用的Func函数传递的右值参数则匹配到接收转发引用的Func函数从而正确地进行区分和处理通过函数模板的重载我们可以根据参数类型将左值和右值区分开来并分别处理实现了针对不同值类别的精确匹配和操作。 2、模板中的 万能引用 显然上述使用重载的模板函数实现完美转发也是有弊端的此实现方式仅适用于模板函数仅有少量参数的情况否则就需要编写大量的重载函数模板造成代码的冗余。为了方便用户更快速地实现完美转发C 11 标准中允许在函数模板中使用右值引用来实现完美转发。 还是以 PerfectForward() 函数为例在 C11 标准中实现完美转发只需要编写如下一个模板函数即可
//模板中的不代表右值引用而是万能引用其既能接收左值又能接收右值。
templatetypename T
void PerfectForward(T t)
{Fun(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;
}
输出展示 【解释说明】
模板中的不代表右值引用而是万能引用其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力但是引用类型的唯一作用就是限制了接收的类型后续使用中都退化成了左值我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发 3、std::forward C11 标准的开发者已经帮我们想好的解决方案该新标准还引入了一个模板函数 forwordT()我们只需要调用该函数就可以很方便地解决此问题。 完美转发通常与转发引用forwarding reference和 std::forward 函数一起使用转发引用是一种特殊的引用类型使用语法进行声明用于在函数模板中捕获传递的实参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;
}templatetypename T
void PerfectForward(T t)
{// forwardT(t)在传参的过程中保持了t的原生类型属性。Fun(std::forwardT(t));
}
int main()
{PerfectForward(10); // 右值int a;PerfectForward(a); // 左值PerfectForward(move(a)); // 右值const int b 8;PerfectForward(b); // const 左值PerfectForward(move(b)); // const 右值return 0;
}
程序执行结果为 通过完美转发我们可以在函数模板中正确处理传递实参的值类别并将其转发到内部函数以达到类型和值类别的完全保持提高代码的灵活性和效率。 总结
学到这里一些读者可能无法记清楚左值引用和右值引用各自可以引用左值还是右值这里给大家一张表格方便大家记忆 表中Y 表示支持N 表示不支持。 以上便是关于左值引用和右值引用的全部知识讲解感谢大家的观看与支持