大家好,我是 同学小张,持续学习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
,引用 a
和 b
占用的内存为 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++中引用与指针的区别,常见面试八股文:
- 初始化和绑定:
- 引用:在声明时必须被初始化,并且一旦一个引用被绑定到一个对象,就不能再被重新绑定到另一个对象。
- 指针:可以未初始化,并且可以指向任何类型的对象,也可以重新指向另一个对象。
- 间接访问:
- 引用:通过引用名直接访问其所绑定的对象。
- 指针:通过指针间接访问其所指向的对象。
- 空值:
- 引用:不能为空。
- 指针:可以设置为nullptr或NULL。
- 解引用操作:
- 引用:不能进行解引用操作。
- 指针:可以解引用来访问其所指向的对象。
- 指针算术:
- 指针:可以进行指针算术操作,例如加、减等。
- 引用:不能进行此类操作。
- 函数参数传递:
- 引用:可以通过引用来传递大型对象,而不会造成复制开销。
- 指针:也可以用于传递大型对象,但可能增加错误使用(如野指针)的风险。
- const 修饰符:
- 引用:可以通过const引用来确保不修改所绑定的对象。
- 指针:需要使用const修饰指针本身和指针所指向的内容,来确保不修改。
- 语法差异:
- 声明引用时使用"&"符号,例如
int a = 10; int& ref = a;
。 - 声明指针时使用"*"符号,例如
int* ptr = &a;
。
- 声明引用时使用"&"符号,例如
- 寻址差异:
- 指针可以看到自身的地址
- 引用看不到自身的地址
- 转换差异:
- 理论上,引用的代码都可以替换成指针常量来实现
- 指针常量有时候无法替换成引用来实现
例如,下面的代码是合法的:
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 指针
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. 参考
- https://blog.csdn.net/shulianghan/article/details/132436972
- https://mp.weixin.qq.com/s/CKEdPD0tmaPxb8QxqqU_lw
- https://mp.weixin.qq.com/s/F0IYsVBbSgYHP66bbTDH6w
- https://mp.weixin.qq.com/s/7Aj3FEdBpsw67VU2IdMjPw
- https://www.cnblogs.com/lyxtech/articles/15216107.html
如果觉得本文对你有帮助,麻烦点个赞和关注呗 ~~~
- 大家好,我是 同学小张,持续学习C++进阶知识和AI大模型应用实战案例
- 欢迎 点赞 + 关注 👏,持续学习,持续干货输出。
- +v: jasper_8017 一起交流💬,一起进步💪。
- 微信公众号也可搜【同学小张】 🙏
本站文章一览: