【C++】引用和右值引用

目录

1. 引用

1.1 引用的概念

1.2 引用的特性

1.3 引用的使用场景

1.3.1 作为参数

1.3.2 作为返回值

1.4 常量引用

1.5 引用和指针的区别

2. 左值和右值

3. 右值引用

3.1 右值引用的概念

3.2 左值持久;右值短暂

3.3 变量是左值

3.4 标准库move函数


1. 引用

C++11中新增了一种引用:右值引用(rvalue reference),主要用于内置类。严格来说,当我们使用术语“引用(reference)”时,指的其实是“左值引用(lvalue reference)”。

1.1 引用的概念

引用(reference)不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。

int a = 10;
int& ra = a;
//a为目标原名称,ra为目标引用名
//相当于给a取了一个别名叫ra
//a的地址和ra的地址相同

1.2 引用的特性

  1. 引用在定义时必须初始化
  2. 一个变量可以有多个引用
  3. 引用一旦引用一个实体,再不能引用其他实体
int a = 10;
int b = 20;

int& ra = a;//ok
int& ra;//err 引用在定义时必须初始化
int& c= a;//ok 一个变量可以有多个引用
int& ra = b;//err 引用一旦引用一个实体,再不能引用其他实体

1.3 引用的使用场景

1.3.1 作为参数

交换两个数的函数:

C语言版:修改变量,传变量的地址,再通过解引用,对变量进行操作

void Swap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

C++版:修改变量,传变量的引用,对引用进行操作,就是对变量进行操作

void Swap(int& r1, int& r2)
{
    int tmp = r1;
    r1 = r2;
    r2 = tmp;
}

1.3.2 作为返回值

返回一个值(非引用):存在拷贝

【C++】引用和右值引用_第1张图片

【C++】引用和右值引用_第2张图片

返回引用:

1. 减少拷贝

【C++】引用和右值引用_第3张图片

2. 调用者可以修改返回对象

//静态顺序表
#define N 10

typedef struct Array
{
	int a[N];
	int size;
}AY;

int& PosAt(AY& ay, int i)
{
	assert(i < N);
	return ay.a[i];
}

int main()
{
	AY ay;
	for (int i = 0; i < N; ++i)
	{
		PosAt(ay, i) = i * 10;//修改返回对象
	}
	for (int i = 0; i < N; ++i)
	{
		cout << PosAt(ay, i) << " ";
	}
	cout << endl;
	return 0;
}

如果出了函数作用域,对象还存在,则可以返回引用。

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。

1.4 常量引用

const修饰的引用,称为常量引用或常引用。不能通过常量引用改变对应的对象的值。

const int ci = 10;
const int &r1 = ci;//ok r1是常量引用,对应的对象ci也是常量
r1 = 20;//err 不能通过常量引用改变对应的对象的值
int &r2 = ci;//err ci不能改变,当然也就不能通过引用去改变ci
             //假设合法,则可以通过r2来改变ci,这是不合法的

在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式。

int i = 50;
const int& r1 = i;//ok 将const int&绑定到一个普通int对象上
const int& r2 = 10;//ok 将const int&绑定到一个int字面值上
const int& r3 = r1 * 2;//ok 将const int&绑定到一个表达式上
int& r4 = r1 * 2;//err

我们要清楚当一个常量引用被绑定到另外一种类型上时到底发生了什么:

double pi = 3.14;
const int& rpi = pi;//ok 将const int&绑定到一个普通double对象上

为什么rpi能够绑定pi?为了让rpi绑定一个整型对象,编译器把代码处理为:

const int temp = pi;//隐式类型转换 double->const int
const int& rpi = temp;//让rpi绑定这个临时量

所以当一个常量引用被绑定到另外一种类型上时,常量引用绑定的其实是相同类型的临时量。

如果函数的返回值是一个值而非引用,那么返回的是一个临时变量,且具有常量性。如果要用引用接收该值,只能用常量引用。

【C++】引用和右值引用_第4张图片

【C++】引用和右值引用_第5张图片

int Count()
{
	static int n = 0;
	n++;
	return n;
}

int& ret = Count();//err
const int& ret = Count();//ok

1.5 引用和指针的区别

  1. 引用概念上定义一个变量的别名,指针存储一个变量地址
  2. 引用在定义时必须初始化,指针没有要求
  3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
  4. 没有NULL引用,但有NULL指针
  5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(4/8B)
  6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
  7. 有多级指针,但是没有多级引用
  8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  9. 引用比指针使用起来相对更安全

2. 左值和右值

C++的表达式要不然是右值(rvalue,读作"are-value"),要不然就是左值(lvalue,读作"ell-value")。这两个名词是从C语言继承过来的,原本是为了帮助记忆:左值可以位于赋值语句的左侧,右值则不能。

在C++语言中,二者的区别就没那么简单了。一个左值表达式的求值结果是一个对象或者一个函数,然而以常量对象为代表的某些左值实际上不能作为赋值语句的左侧运算对象。此外,虽然某些表达式的求值结果是对象,但它们是右值而非左值。可以做一个简单的归纳:当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。

不同的运算符对运算对象的要求各不相同,有的需要左值运算对象、有的需要右值运算对象;返回值也有差异,有的得到左值结果、有的得到右值结果。一个重要的原则(除右值引用)是在需要右值的地方可以用左值来代替,但是不能把右值当成左值(也就是位置)使用。当一个左值被当成右值使用时,实际使用的是它的内容(值)。到目前为止,已经有几种我们熟悉的运算符是要用到左值的。

  • 赋值运算符需要一个(非常量)左值作为其左侧运算对象,得到的结果也仍然是一个左值。
  • 取地址符作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值。
  • 内置解引用运算符、下标运算符、迭代器解引用运算符、string和vector的下标运算符的求值结果都是左值。
  • 内置类型和迭代器的递增递减运算符作用于左值运算对象,其前置版本所得的结果也是左值。

3. 右值引用

3.1 右值引用的概念

为了支持移动操作,新标准引入了一种新的引用类型——右值引用(rvalue reference)。所谓右值引用就是必须绑定到右值的引用。我们通过&&而不是&来获得右值引用。如我们将要看到的,右值引用有一个重要的性质——只能绑定到一个将要销毁的对象。因此,我们可以自由地将一个右值引用的资源“移动”到另一个对象中。

左值和右值是表达式的属性。一些表达式生成或要求左值,而另外一些则生成或要求右值。一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。

类似任何引用,一个右值引用也不过是某个对象的另一个名字而已。如我们所知,对于常规引用(为了与右值引用区分开来,我们可以称之为左值引用(lvalue reference)),我们不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式。右值引用有着完全相反的绑定特性:我们可以将一个右值引用绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上:

int i = 42;
int &r = i;            //正确:r引用i
int &&rr = i;          //错误:不能将一个右值引用绑定到一个左值上
int &r2 = i * 42;      //错误:i*42是一个右值
const int &r3 = i * 42;//正确:我们可以将一个const的引用绑定到一个右值上
int &&rr2 = i * 42;    //正确:将rr2绑定到乘法结果上

返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是返回左值的表达式的例子。我们可以将一个左值引用绑定到这类表达式的结果上。

返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值。我们不能将一个左值引用绑定到这类表达式上,但我们可以将一个const的左值引用或者一个右值引用绑定到这类表达式上。

3.2 左值持久;右值短暂

考察左值和右值表达式的列表,两者相互区别之处就很明显了:左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。

由于右值引用只能绑定到临时对象,我们得知

  • 所引用的对象将要被销毁
  • 该对象没有其他用户

这两个特性意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。

右值引用指向将要被销毁的对象。因此,我们可以从绑定到右值引用的对象“窃取”状态。

3.3 变量是左值

变量可以看作只有一个运算对象而没有运算符的表达式,虽然我们很少这样看待变量。类似其他任何表达式,变量表达式也有左值/右值属性。变量表达式都是左值。带来的结果就是,我们不能将一个右值引用绑定到一个右值引用类型的变量上,这有些令人惊讶:

int &&rr1 = 42; //正确:字面常量是右值
int &&rr2 = rr1;//错误:表达式rr1是左值!

其实有了右值表示临时对象这一观察结果,变量是左值这一特性并不令人惊讶。毕竟,变量是持久的,直至离开作用域时才被销毁。

变量是左值,因此我们不能将一个右值引用直接绑定到一个变量上,即使这个变量是右值引用类型也不行。

3.4 标准库move函数

虽然不能将一个右值引用直接绑定到一个左值上,但我们可以显式地将一个左值转换为对应的右值引用类型。我们还可以通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用,此函数定义在头文件utility中。move函数返回给定对象的右值引用。

int &&rr3 = std::move(rr1);//ok

move调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。我们必须认识到,调用move就意味着承诺:除了对rr1赋值或销毁它外,我们将不再使用它。在调用move之后,我们不能对移后源对象的值做任何假设。

我们可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值。

如前所述,与大多数标准库名字的使用不同,对move我们不提供using声明。我们直接调用std::move而不是move。

使用move的代码应该使用std::move而不是move。这样做可以避免潜在的名字冲突。

你可能感兴趣的:(C++,c++)