C++:左值(引用)右值(引用)

2024-05-11 00:44
文章标签 c++ 引用 右值 左值

本文主要是介绍C++:左值(引用)右值(引用),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

〇、前言

本文会讨论C++中的左值,右值,左值引用,右值引用,以及会理清它们之间的关系。

一、左值与右值

(一)概述

1. 左值是一般指表达式结束后依然存在的持久化对象。右值指表达式结束时就不再存在的临时对象。便捷的判断方法:能对表达式取地址、有名字的对象为左值。反之,不能取地址、匿名的对象为右值。

2. C++ 表达式(运算符带上其操作数、字面量、变量名等)有两种独立的属性:类型值类别 。类型指变量声明时的类型,而值类别是表达式结果的类型,它必属于左值、纯右值或将亡值三者之一。如int&& x;其中 x 的类型为右值引用,但作为表达式使用时其值类别为左值(因为有名字,可以取址)。比如:

#include <iostream>
void func(int &&b) {std::cout << "b: " << b << " addr: " << &b << std::endl;b++;std::cout << b << std::endl;
}
int main() {int c = 10;func(std::move(c));std::cout << "c: " << c << " addr: " << &c << std::endl;return 0;
}

上例中的 b 就是一个右值引用,它接受了一个右值 c。在 main() 中,c 被转化为右值引用传入 func(),因此,func() 中的 b 其实就是 main() 中的 c 别名,它们指向的是同一块内存区域。需要注意的是,func() 中的 b 其实是一个左值,换句话说,右值引用如果绑定了一个右值,它会延长这个右值的生命周期。 这种生命周期的延长意味着,尽管原始表达式产生的值是一个右值,一旦它被一个右值引用所绑定,它就不再是一个"即将销毁的临时值",而更像是一个普通的变量。 这允许开发者在保证效率的同时,也能够更灵活地控制这些值。

运行结果:

g++ 1.cxx -o main -std=c++14
./main
b: 10 addr: 0x16ba16e28
11
c: 11 addr: 0x16ba16e28

我们需要注意的是,main() 中的 c 被转换成了右值引用,但是它的状态没有发生任何改变。它在 func 中被修改,并在 func 返回后仍然保持有效。std::move 只是告诉编译器一个对象可以被“安全地”当作右值来使用,这样就允许在移动语义上下文中使用该对象。

(二)右值的分类

1. 纯右值(prvalue):用于识别临时变量和一些不与对象关联的值。函数返回值为非引用类型、表达式临时值(如1+3)、lambda表达式等。

2. 将亡值(xvalue):是与右值引用相关的表达式,通常指将要被移动的对象。如,函数返回类型为T&&std::move的返回值、转换为T&&的类型转换函数的返回值(注意,这些都是与右值引用相关的表达式)或临时对象。

表格形式:

类型分类特征绑定规则和类型安全转换实例和特殊情况
左值 (lvalue)存在变量、函数调用产生的对象,有固定地址。可绑定到左值引用,例如 int& x = a;变量 int a = 10; 函数调用(返回引用)例如 int& foo();
纯右值 (prvalue)不适宜被移动,不具有可识别的地址。可绑定到右值引用或 const 左值引用,例如 int&& x = 5;字面量例如 42,表达式例如 a + b,函数调用(返回非引用)例如 int foo();
将亡值 (xvalue)适宜被移动,具有地址但对象即将被销毁。可绑定到右值引用,例如使用 std::move 例如 int&& x = std::move(a);通过 std::move 生成的将亡值,例如 std::move(a);使用移动构造函数或移动赋值操作时,传递的实际参数。
泛左值 (glvalue)概括左值和将亡值,即拥有特定身份(地址)且可能即将被销毁。可以绑定到左值引用或右值引用,取决于上下文。例如使用 decltype 捕获引用时,decltype((a)) x = a; 捕获的是 a 的引用。

二、左值引用与右值引用

(一)概述

1. 左值引用和右值引用都属于引用类型。无论是声明一个左值引用还是右值引用都必须立即进行初始化(今天考试刚考到)。

2. 左值引用都是左值。但具名的右值引用是左值,而匿名的右值引用是右值(比如上面的 func 中的 b,虽然它是右值引用,但是它是一个左值)。

(二)可绑定的值类型(设T是个具体类型)

1. 左值引用(T&):只能绑定到左值(非常量左值)

2. 右值引用(T&&):只能绑定到右值(非常量右值)

3. 常量左值引用(const T&):常量左值引用是个“万能”的引用类型:它既可以绑定到左值也可以绑定到右值,它像右值引用一样可以延长右值的生命期。不过相比于右值引用所引用的右值,常量左值引用的右值在它的“余生”中只能是只读的。对于这点,可以参考这个例子:

#include <iostream>
void func(const int &b) {std::cout << "b: " << b << " addr: " << &b << std::endl;b++; // 会报错std::cout << b << std::endl;
}
int main() {int c = 10;func(std::move(c));std::cout << "c: " << c << " addr: " << &c << std::endl;return 0;
}

编译出错:

g++ 2.cxx -o main -std=c++14
2.cxx:4:6: error: cannot assign to variable 'b' with const-qualified type 'const int &'b++;~^
2.cxx:2:22: note: variable 'b' declared const here
void func(const int &b) {~~~~~~~~~~~^
1 error generated.

4. 常量右值引用(const T&&):可绑定到右值或常量右值。由于移动语义需要右值可以被修改,因此常量右值引用没有实际用处。如果需要引用右值且让其不可更改,则常量左值引用就足够了。

三、万能引用(universal reference)

(一)T&&的含义

1. 当T是一个具体的类型时,T&& 表示右值引用,只能绑定到右值。

2. 当涉及T类型推导时,T&& 为万能引用。若用右值初始化万能引用,则T&&为右值引用。若用左值初始化万能引用,则T&&为左值引用。但不管哪种情况,T&&都是一种引用类型

(二)万能引用

1. T&&是万能引用的两个条件:

(1)必须涉及类型推导

(2)声明的形式也必须正好形如 T&&并且该形式被限定死了,任何对其修饰都将剥夺T&&成为万能引用的资格

2. 万能引用使用的场景

(1)函数模板形参
    
(2)auto&&

一个例子:

#include <iostream>
#include <vector>using namespace std;class Widget {};void func1(Widget &&param){}; // param为右值引用类型(不涉及类型推导)template <typename T>
void func2(T &&param) {} // param为万能引用(涉及类型推导)template <typename T>
void func3(std::vector<T> &&param) {
}template <typename T>
void func4(const T &&param) {}
template <class T> class MyVector {public:void push_back(T &&x) {} // x为右值引用。因为当定义一个MyVector对象后,T己确定。当调用该函数时T的类型不用再推导!template <class... Args>void emplace_back(Args&&...args){}; // args为万能引用,因为Args独立于T的类型,当调用该函数时,需推导Args的类型。
};int main() {Widget w;func2(w);            // 万能引用, func2(T&& param),param为Widget&(左值引用)func2(std::move(w)); // 万能引用, param为Widget&&,是个右值引用。int x = 0;Widget &&var1 = Widget(); // var1为右值引用(不涉及类型推导)auto &&var2 = var1; //万能引用,auto&&被推导为Widget&(左值引用)auto &&var3 = x; //万能引用,被推导为int&;(左值引用)// 3. 计算任意函数的执行时间:auto&&用于lambda表达式形参(C++14)auto timefunc = [](auto &&func, auto &&...params) {//计时器启动//调用func(param...)函数std::forward<decltype(func)>(func)( //根据func的左右值特性来调用相应的重载&或&&版本的成员函数std::forward<decltype(params)>(params)... //保持参数的左/右值特性);//计时器停止并记录流逝的时间};timefunc(func1, std::move(w)); //计算func1函数的执行时间return 0;
}

在你提供的代码中,main 函数涉及到万能引用的语句如下:

  1. func2(w);func2(std::move(w)); — 这里的 func2 函数模板参数 T&& 是一个万能引用。它可以绑定到左值和右值。在这两个调用中,第一次调用时 T 被推导为 Widget&(因为 w 是一个左值),而第二次调用时 T 被推导为 Widget(因为 std::move(w) 产生一个右值)。

  2. auto &&var2 = var1; — 这里使用了 auto&&,它也是一个万能引用。var1 是一个左值(尽管它本身是一个绑定到临时对象的右值引用),所以 var2 被推导为 Widget&

  3. auto &&var3 = x; — 这同样使用了 auto&&,这是一个万能引用。由于 x 是一个左值,var3 被推导为 int&

  4. timefunc lambda 表达式的定义中,参数列表 (auto &&func, auto &&...params) 使用了万能引用。这里 auto&& 用于单个参数和参数包,允许这个 lambda 接受任意数量的任意类型的参数,并保持他们的值类别(左值或右值)。

这些例子展示了万能引用在模板类型推导中的强大功能,尤其是在泛型编程和函数重载解析中的应用。通过万能引用,可以写出更灵活的函数和模板,使得它们能够同时接受左值和右值,而无需重载函数。

四、参考

这里。

这篇关于C++:左值(引用)右值(引用)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/978030

相关文章

从入门到精通C++11 <chrono> 库特性

《从入门到精通C++11<chrono>库特性》chrono库是C++11中一个非常强大和实用的库,它为时间处理提供了丰富的功能和类型安全的接口,通过本文的介绍,我们了解了chrono库的基本概念... 目录一、引言1.1 为什么需要<chrono>库1.2<chrono>库的基本概念二、时间段(Durat

C++20管道运算符的实现示例

《C++20管道运算符的实现示例》本文简要介绍C++20管道运算符的使用与实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录标准库的管道运算符使用自己实现类似的管道运算符我们不打算介绍太多,因为它实际属于c++20最为重要的

Visual Studio 2022 编译C++20代码的图文步骤

《VisualStudio2022编译C++20代码的图文步骤》在VisualStudio中启用C++20import功能,需设置语言标准为ISOC++20,开启扫描源查找模块依赖及实验性标... 默认创建Visual Studio桌面控制台项目代码包含C++20的import方法。右键项目的属性:

c++中的set容器介绍及操作大全

《c++中的set容器介绍及操作大全》:本文主要介绍c++中的set容器介绍及操作大全,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录​​一、核心特性​​️ ​​二、基本操作​​​​1. 初始化与赋值​​​​2. 增删查操作​​​​3. 遍历方

解析C++11 static_assert及与Boost库的关联从入门到精通

《解析C++11static_assert及与Boost库的关联从入门到精通》static_assert是C++中强大的编译时验证工具,它能够在编译阶段拦截不符合预期的类型或值,增强代码的健壮性,通... 目录一、背景知识:传统断言方法的局限性1.1 assert宏1.2 #error指令1.3 第三方解决

C++11委托构造函数和继承构造函数的实现

《C++11委托构造函数和继承构造函数的实现》C++引入了委托构造函数和继承构造函数这两个重要的特性,本文主要介绍了C++11委托构造函数和继承构造函数的实现,具有一定的参考价值,感兴趣的可以了解一下... 目录引言一、委托构造函数1.1 委托构造函数的定义与作用1.2 委托构造函数的语法1.3 委托构造函

C++11作用域枚举(Scoped Enums)的实现示例

《C++11作用域枚举(ScopedEnums)的实现示例》枚举类型是一种非常实用的工具,C++11标准引入了作用域枚举,也称为强类型枚举,本文主要介绍了C++11作用域枚举(ScopedEnums... 目录一、引言二、传统枚举类型的局限性2.1 命名空间污染2.2 整型提升问题2.3 类型转换问题三、C

C++链表的虚拟头节点实现细节及注意事项

《C++链表的虚拟头节点实现细节及注意事项》虚拟头节点是链表操作中极为实用的设计技巧,它通过在链表真实头部前添加一个特殊节点,有效简化边界条件处理,:本文主要介绍C++链表的虚拟头节点实现细节及注... 目录C++链表虚拟头节点(Dummy Head)一、虚拟头节点的本质与核心作用1. 定义2. 核心价值二

C++ 检测文件大小和文件传输的方法示例详解

《C++检测文件大小和文件传输的方法示例详解》文章介绍了在C/C++中获取文件大小的三种方法,推荐使用stat()函数,并详细说明了如何设计一次性发送压缩包的结构体及传输流程,包含CRC校验和自动解... 目录检测文件的大小✅ 方法一:使用 stat() 函数(推荐)✅ 用法示例:✅ 方法二:使用 fsee

Windows下C++使用SQLitede的操作过程

《Windows下C++使用SQLitede的操作过程》本文介绍了Windows下C++使用SQLite的安装配置、CppSQLite库封装优势、核心功能(如数据库连接、事务管理)、跨平台支持及性能优... 目录Windows下C++使用SQLite1、安装2、代码示例CppSQLite:C++轻松操作SQ