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++中unordered_set哈希集合的实现

《C++中unordered_set哈希集合的实现》std::unordered_set是C++标准库中的无序关联容器,基于哈希表实现,具有元素唯一性和无序性特点,本文就来详细的介绍一下unorder... 目录一、概述二、头文件与命名空间三、常用方法与示例1. 构造与析构2. 迭代器与遍历3. 容量相关4

C++中悬垂引用(Dangling Reference) 的实现

《C++中悬垂引用(DanglingReference)的实现》C++中的悬垂引用指引用绑定的对象被销毁后引用仍存在的情况,会导致访问无效内存,下面就来详细的介绍一下产生的原因以及如何避免,感兴趣... 目录悬垂引用的产生原因1. 引用绑定到局部变量,变量超出作用域后销毁2. 引用绑定到动态分配的对象,对象

Python内存管理机制之垃圾回收与引用计数操作全过程

《Python内存管理机制之垃圾回收与引用计数操作全过程》SQLAlchemy是Python中最流行的ORM(对象关系映射)框架之一,它提供了高效且灵活的数据库操作方式,本文将介绍如何使用SQLAlc... 目录安装核心概念连接数据库定义数据模型创建数据库表基本CRUD操作创建数据读取数据更新数据删除数据查

C++读写word文档(.docx)DuckX库的使用详解

《C++读写word文档(.docx)DuckX库的使用详解》DuckX是C++库,用于创建/编辑.docx文件,支持读取文档、添加段落/片段、编辑表格,解决中文乱码需更改编码方案,进阶功能含文本替换... 目录一、基本用法1. 读取文档3. 添加段落4. 添加片段3. 编辑表格二、进阶用法1. 文本替换2

C++中处理文本数据char与string的终极对比指南

《C++中处理文本数据char与string的终极对比指南》在C++编程中char和string是两种用于处理字符数据的类型,但它们在使用方式和功能上有显著的不同,:本文主要介绍C++中处理文本数... 目录1. 基本定义与本质2. 内存管理3. 操作与功能4. 性能特点5. 使用场景6. 相互转换核心区别

C++右移运算符的一个小坑及解决

《C++右移运算符的一个小坑及解决》文章指出右移运算符处理负数时左侧补1导致死循环,与除法行为不同,强调需注意补码机制以正确统计二进制1的个数... 目录我遇到了这么一个www.chinasem.cn函数由此可以看到也很好理解总结我遇到了这么一个函数template<typename T>unsigned

C++统计函数执行时间的最佳实践

《C++统计函数执行时间的最佳实践》在软件开发过程中,性能分析是优化程序的重要环节,了解函数的执行时间分布对于识别性能瓶颈至关重要,本文将分享一个C++函数执行时间统计工具,希望对大家有所帮助... 目录前言工具特性核心设计1. 数据结构设计2. 单例模式管理器3. RAII自动计时使用方法基本用法高级用法

深入解析C++ 中std::map内存管理

《深入解析C++中std::map内存管理》文章详解C++std::map内存管理,指出clear()仅删除元素可能不释放底层内存,建议用swap()与空map交换以彻底释放,针对指针类型需手动de... 目录1️、基本清空std::map2️、使用 swap 彻底释放内存3️、map 中存储指针类型的对象

C++ STL-string类底层实现过程

《C++STL-string类底层实现过程》本文实现了一个简易的string类,涵盖动态数组存储、深拷贝机制、迭代器支持、容量调整、字符串修改、运算符重载等功能,模拟标准string核心特性,重点强... 目录实现框架一、默认成员函数1.默认构造函数2.构造函数3.拷贝构造函数(重点)4.赋值运算符重载函数

C++ vector越界问题的完整解决方案

《C++vector越界问题的完整解决方案》在C++开发中,std::vector作为最常用的动态数组容器,其便捷性与性能优势使其成为处理可变长度数据的首选,然而,数组越界访问始终是威胁程序稳定性的... 目录引言一、vector越界的底层原理与危害1.1 越界访问的本质原因1.2 越界访问的实际危害二、基