【重学C++】【引用】一文看懂引用的本质与右值引用存在的意义

简介: 【重学C++】【引用】一文看懂引用的本质与右值引用存在的意义

大家好,我是 同学小张,持续学习C++进阶知识和AI大模型应用实战案例,持续分享,欢迎大家点赞+关注,共同学习和进步。

重学C++系列文章,在会用的基础上深入探讨底层原理和实现,适合有一定C++基础,想在C++方向上持续学习和进阶的同学。争取让你每天用5-10分钟,了解一些以前没有注意到的细节。


引用是C++中一个重要的特性,它使原来在C中必须用指针实现的功能有了另一种实现的选择,在书写形式上更为简洁。本文我们就从引用的本质、以及现代C++中最常使用的左值引用与右值引用方面入手,深入理解这些概念、底层原理及用法细节。

1. 引用的本质

先说结论:在C++中,引用本质上是一个指针常量。

1.1 引用的一些解释和性质

C++中的引用可以理解为一个变量的别名,它的本质在底层实现上其实是一个指针常量。也就是,一旦指向一个值,那这个指向就不能变。这就是为什么引用在声明时必须被初始化的原因。

尽管引用在语法层面上与普通的变量使用方式几乎相同,可以对其进行赋值等操作,但在底层实现上,这些操作都是通过指针间接完成的。当对引用进行操作时,编译器会自动将其转化为对指针所指向的对象的操作。

需要注意的是,尽管引用在底层实现上与指针有相似之处,但在C++的语法层面,引用和指针还是有明显的区别的。例如,引用总是指向一个有效的对象,没有空引用,而且引用一旦被初始化为指向一个对象,就不能再指向其他对象。这些特性使得引用在某些情况下比指针更为安全易用。

1.2 引用是否占用内存空间?

答案是:占用。与指针占用相同的内存空间。但是其不允许寻址。

(1)引用占用内存空间的验证

可以使用一个结构体来验证引用是否占用内存空间:

struct Student
{
    int age;
    int& a;
    int& b;
};
std::printf("Student的大小:%llu\n", sizeof(Student)); // Student的大小:24

以上输出 Student 的大小为 24(64位系统),我们都知道int的大小是4,而64位系统中,地址的大小是8,引用 ab占用的内存为 2*8=16,在Struct中存在内存对齐,4字节的int向8字节对齐,所以 age 也占8,一共是24。也证明了引用确实占用内存空间。

(2)引用不能寻址

以下测试代码:

int a = 10;
int &b = a;
int c = 20;
int *const d = &a;
std::printf("a的地址:%p\n", &a);
std::printf("b的地址:%p\n", &b);
std::printf("c的地址:%p\n", &c);
std::printf("d的地址:%p\n", &d);
std::printf("d指向的值:%d\n", *d);

输出为:

引用b&b为其指向的地址,而非自身的地址。而正常的指针d&d代表的是指针自身的地址。

1.3 面试:引用与指针的区别

总结一下C++中引用与指针的区别,常见面试八股文

  1. 初始化和绑定:
  • 引用:在声明时必须被初始化,并且一旦一个引用被绑定到一个对象,就不能再被重新绑定到另一个对象。
  • 指针:可以未初始化,并且可以指向任何类型的对象,也可以重新指向另一个对象。
  1. 间接访问:
  • 引用:通过引用名直接访问其所绑定的对象。
  • 指针:通过指针间接访问其所指向的对象。
  1. 空值:
  • 引用:不能为空。
  • 指针:可以设置为nullptr或NULL。
  1. 解引用操作:
  • 引用:不能进行解引用操作。
  • 指针:可以解引用来访问其所指向的对象。
  1. 指针算术:
  • 指针:可以进行指针算术操作,例如加、减等。
  • 引用:不能进行此类操作。
  1. 函数参数传递:
  • 引用:可以通过引用来传递大型对象,而不会造成复制开销。
  • 指针:也可以用于传递大型对象,但可能增加错误使用(如野指针)的风险。
  1. const 修饰符:
  • 引用:可以通过const引用来确保不修改所绑定的对象。
  • 指针:需要使用const修饰指针本身和指针所指向的内容,来确保不修改。
  1. 语法差异:
  • 声明引用时使用"&"符号,例如int a = 10; int& ref = a;
  • 声明指针时使用"*"符号,例如int* ptr = &a;
  1. 寻址差异:
  • 指针可以看到自身的地址
  • 引用看不到自身的地址
  1. 转换差异:
  • 理论上,引用的代码都可以替换成指针常量来实现
  • 指针常量有时候无法替换成引用来实现

例如,下面的代码是合法的:

int i=5, j=6;
int* const array[]={&i,&j};

而如下代码是非法的:

int i=5, j=6;
int& array[]={i,j};

2. 右值引用

知道了引用的一些概念和用法,下面我们来看现代C++中最常用的一个高级特性:右值引用。

在右值引用之前,我们有必要认识一下什么是右值,什么是左值。

2.1 左值和右值

2.1.1 定义和区分

有的书中将左值和右值定义为:

  • 左值(lvalue):指的是那些可以放在赋值操作符左边的值,它们通常对应着内存中有明确存储位置的对象,比如变量。
  • 右值(rvalue):指的是那些不能放在赋值操作符左边的值,通常是临时的、匿名的或者不可地址化的值,例如常量和临时对象。

这个定义有一半是正确的,但有一半是错误的。在我看来,左值和右值并不是通过在赋值操作符的左边和右边来区分的。左值可以放在赋值操作符的左边,但也可以放在右边。

正确的描述应该是:

  • 左值:存放在内存中,有内存地址,可以通过地址访问(变量名、指针或者引用)
  • 右值:不在内存中,没有内存地址。无法通过地址(变量名、指针或者引用)来访问

2.1.2 一些左值和右值举例

下面举几个左值右值的例子,看看就好,记不住也没关系,实际中,用到的区分左值右值的情况并不是很多。

2.1.2.1 左值

(1)普通变量:int a, std::string b

(2)指针、指针访问的数据成员、数组、数组访问的元素:*p, p->value, arr[n]

(3)返回引用类型的函数,例如下面的例子,程序员可以拿到该函数返回值的地址

int g_test_a = 10;
int& get_value()
{
    return g_test_a;
}
// main函数中打印:
std::printf("get value function返回值的地址:%p\n", &get_value());
std::printf("g_test_a的地址:%p\n", &g_test_a);
// 输出:
// get value function返回值的地址:00007ff7e864a000
// g_test_a的地址:00007ff7e864a000
2.1.2.2 右值

(1)字面量:如 42、true等

(2)返回值不是引用类型的函数:int get_value()

(3)this 指针

(4)lambda:{ return x * x; }

2.1.2.3 下面这些例子,你能分得清吗?

(1)++i是左值,i++是右值

前者,对i加1后再赋给i,最终的返回值就是i,所以,++i的结果是具名的,名字就是i;而对于i++而言,是先对i进行一次拷贝,将得到的副本作为返回结果,然后再对i加1,由于i++的结果是对i加1前i的一份拷贝,所以它是不具名的。

(2)“hello” 这些字符串的字面量是左值,42等非字符串的字面量是右值

2.2 右值引用的意义

右值引用存在的意义,主要是用来提高程序的运行效率,避免不必要的拷贝。

举个例子,C++11之后,std::string的赋值操作符实现有以下两种形式:

string& operator=(conststring& str) 
{
  复制 string
  return *this;
}
string& operator=(string&& str) noexcept
{
  交换 string;
  return *this;
}

第一种是将字符串拷贝一份,这样实际内存中就存在两份一样的字符串。

第二种是将字符串的内存数据作交换,这样不会进行拷贝,只相当于交换了一下指针指向。这种情况避免了拷贝中的性能消耗,但是破坏了原来的字符串(指向了空)。

对于右值来说,编译器知道其不会被程序员使用,也知道其用完之后应该销毁,所以它可以放心地使用第二种方式。

而对于左值,编译器不知道这个原来的值什么时候应该被销毁,所以其只能通过第一种方式完成赋值。

这也是为什么会有左值和右值的区分和右值引用的意义所在。

看到这,应该也能理解,我前面说的,区分左值和右值,看看就好,记不住也没关系了:对于大多数的情况来说,这些左值和右值都是编译器来自行区分的,根本用不到我们。

我们需要做的,是从我们的程序中,识别出哪些是赋值之后就不需要的,这些值可以通过 std::move 函数将左值转换成右值,告诉编译器,这个变量可以当作右值来用,从而优化性能。

3. 总结

本文我们深入理解了引用的本质,以及学习了左值和右值的概念,还有右值引用存在的意义。

对于左值和右值,我认为不需要去特别的区分,这是编译器的工作。我们更多需要做的,是识别出我们程序中变量的生命周期,如有可能,将左值通过 std::move 转换成右值,从而避免拷贝过程,提高程序性能。

4. 参考

如果觉得本文对你有帮助,麻烦点个赞和关注呗 ~~~


  • 大家好,我是 同学小张,持续学习C++进阶知识AI大模型应用实战案例
  • 欢迎 点赞 + 关注 👏,持续学习持续干货输出
  • +一起交流💬,一起进步💪。
  • 微信公众号也可搜【同学小张】 🙏

本站文章一览:

相关文章
|
2月前
|
编译器 C++
C++ 11新特性之右值引用
C++ 11新特性之右值引用
43 1
|
4月前
|
存储 安全 C++
浅析C++的指针与引用
虽然指针和引用在C++中都用于间接数据访问,但它们各自拥有独特的特性和应用场景。选择使用指针还是引用,主要取决于程序的具体需求,如是否需要动态内存管理,是否希望变量可以重新指向其他对象等。理解这二者的区别,将有助于开发高效、安全的C++程序。
32 3
|
4月前
|
存储 自然语言处理 编译器
【C++入门 三】学习C++缺省参数 | 函数重载 | 引用
【C++入门 三】学习C++缺省参数 | 函数重载 | 引用
|
5月前
|
存储 安全 编译器
【C++航海王:追寻罗杰的编程之路】引用、内联、auto关键字、基于范围的for、指针空值nullptr
【C++航海王:追寻罗杰的编程之路】引用、内联、auto关键字、基于范围的for、指针空值nullptr
66 5
|
5月前
|
C++
C++引用
C++引用
|
4月前
|
C++
C++基础知识(二:引用和new delete)
引用是C++中的一种复合类型,它是某个已存在变量的别名,也就是说引用不是独立的实体,它只是为已存在的变量取了一个新名字。一旦引用被初始化为某个变量,就不能改变引用到另一个变量。引用的主要用途包括函数参数传递、操作符重载等,它可以避免复制大对象的开销,并且使得代码更加直观易读。
|
4月前
|
存储 自然语言处理 编译器
|
4月前
|
安全 C++
|
5月前
|
编译器 C++ 开发者
C++一分钟之-右值引用与完美转发
【6月更文挑战第25天】C++11引入的右值引用和完美转发增强了资源管理和模板灵活性。右值引用(`&&`)用于绑定临时对象,支持移动语义,减少拷贝。移动构造和赋值允许有效“窃取”资源。完美转发通过`std::forward`保持参数原样传递,适用于通用模板。常见问题包括误解右值引用只能绑定临时对象,误用`std::forward`,忽视`noexcept`和过度使用`std::move`。高效技巧涉及利用右值引用优化容器操作,使用完美转发构造函数和创建通用工厂函数。掌握这些特性能提升代码效率和泛型编程能力。
47 0