引用是C++中一个重要的特性,它使原来在C中必须用指针实现的功能有了另一种实现的选择,在书写形式上更为简洁。本文我们就从引用的本质、以及现代C++中最常使用的左值引用与右值引用方面入手,深入理解这些概念、底层原理及用法细节。 1. 引用的本质先说结论:在C++中,引用本质上是一个指针常量。 1.1 引用的一些解释和性质C++中的引用可以理解为一个变量的别名,它的本质在底层实现上其实是一个指针常量。也就是,一旦指向一个值,那这个指向就不能变。这就是为什么引用在声明时必须被初始化的原因。 尽管引用在语法层面上与普通的变量使用方式几乎相同,可以对其进行赋值等操作,但在底层实现上,这些操作都是通过指针间接完成的。当对引用进行操作时,编译器会自动将其转化为对指针所指向的对象的操作。 需要注意的是,尽管引用在底层实现上与指针有相似之处,但在C++的语法层面,引用和指针还是有明显的区别的。例如,引用总是指向一个有效的对象,没有空引用,而且引用一旦被初始化为指向一个对象,就不能再指向其他对象。这些特性使得引用在某些情况下比指针更为安全易用。 1.2 引用是否占用内存空间?答案是:占用。与指针占用相同的内存空间。但是其不允许寻址。 (1)引用占用内存空间的验证 可以使用一个结构体来验证引用是否占用内存空间:
以上输出 (2)引用不能寻址 以下测试代码:
输出为: ![]() 引用 1.3 面试:引用与指针的区别总结一下C++中引用与指针的区别,常见面试八股文:
例如,下面的代码是合法的:
而如下代码是非法的:
2. 右值引用知道了引用的一些概念和用法,下面我们来看现代C++中最常用的一个高级特性:右值引用。 在右值引用之前,我们有必要认识一下什么是右值,什么是左值。 2.1 左值和右值2.1.1 定义和区分有的书中将左值和右值定义为:
这个定义有一半是正确的,但有一半是错误的。在我看来,左值和右值并不是通过在赋值操作符的左边和右边来区分的。左值可以放在赋值操作符的左边,但也可以放在右边。 正确的描述应该是:
2.1.2 一些左值和右值举例下面举几个左值右值的例子,看看就好,记不住也没关系,实际中,用到的区分左值右值的情况并不是很多。 2.1.2.1 左值(1)普通变量: (2)指针、指针访问的数据成员、数组、数组访问的元素:*p, p->value, arr[n] (3)返回引用类型的函数,例如下面的例子,程序员可以拿到该函数返回值的地址
2.1.2.2 右值(1)字面量:如 42、true等 (2)返回值不是引用类型的函数:int get_value() (3)this 指针 (4)lambda:{ return x * x; } 2.1.2.3 下面这些例子,你能分得清吗?(1) 前者,对i加1后再赋给i,最终的返回值就是i,所以,++i的结果是具名的,名字就是i;而对于i++而言,是先对i进行一次拷贝,将得到的副本作为返回结果,然后再对i加1,由于i++的结果是对i加1前i的一份拷贝,所以它是不具名的。 (2)“hello” 这些字符串的字面量是左值,42等非字符串的字面量是右值 2.2 右值引用的意义右值引用存在的意义,主要是用来提高程序的运行效率,避免不必要的拷贝。 举个例子,C++11之后,std::string的赋值操作符实现有以下两种形式:
第一种是将字符串拷贝一份,这样实际内存中就存在两份一样的字符串。 第二种是将字符串的内存数据作交换,这样不会进行拷贝,只相当于交换了一下指针指向。这种情况避免了拷贝中的性能消耗,但是破坏了原来的字符串(指向了空)。 对于右值来说,编译器知道其不会被程序员使用,也知道其用完之后应该销毁,所以它可以放心地使用第二种方式。 而对于左值,编译器不知道这个原来的值什么时候应该被销毁,所以其只能通过第一种方式完成赋值。 这也是为什么会有左值和右值的区分和右值引用的意义所在。 看到这,应该也能理解,我前面说的,区分左值和右值,看看就好,记不住也没关系了:对于大多数的情况来说,这些左值和右值都是编译器来自行区分的,根本用不到我们。 我们需要做的,是从我们的程序中,识别出哪些是赋值之后就不需要的,这些值可以通过 3. 总结本文我们深入理解了引用的本质,以及学习了左值和右值的概念,还有右值引用存在的意义。 对于左值和右值,我认为不需要去特别的区分,这是编译器的工作。我们更多需要做的,是识别出我们程序中变量的生命周期,如有可能,将左值通过 4. 参考
公众号内文章一览 |
|