C++ | 引用

时间:2023-02-12 15:07:54

1.1 创建引用变量

引用是已定义的变量的别名(另一个名称)。

int a;
int &b = a; // 将b作为a变量的别名

C和C++使用&符号来指示变量的地址。C++给&符号赋予了另一个含义,将其用来声明引用。其中,&不是取址运算符,而是类型标识符的一部分(就像int*指的是指向int的指针一样,int&指的是指向int的引用)。上述代码中的引用声明允许将a和b互换——它们指向相同的值和内存单元,二者可以交替使用。

1.2 引用的注意事项

1)引用声明中的&符号是类型标识符的一部分,不是取址操作。

#include<iostream>

int main(){
	using namespace std;
	int a = 10;
	int &b = a; // 将b作为a变量的别名
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	b++;
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "address(a) = " << &a << endl;
	cout << "address(b) = " << &b << endl;
	return 0;
}

输出:

a = 10
b = 10
a = 11
b = 11
address(a) = 0x62fe14
address(b) = 0x62fe14

2)必须在声明引用变量时进行初始化,且引用初始化之后无法改变。

一旦引用在初始化时与某个变量关联起来,就将一直效忠于它,无法更改。

#include<iostream>

int main(){
	using namespace std;
	int a = 10;
	int &b = a; // 将b作为a变量的别名
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "address(a) = " << &a << endl;
	cout << "address(b) = " << &b << endl;
	int c = 20;
	b = c; // 试图将b改为c的引用
	cout << "c = " << c << endl;
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "address(c) = " << &c << endl;
	cout << "address(a) = " << &a << endl;
	cout << "address(b) = " << &b << endl;
	return 0;
}

输出:

a = 10
b = 10
address(a) = 0x62fe14
address(b) = 0x62fe14
c = 20
a = 20
b = 20
address(c) = 0x62fe10
address(a) = 0x62fe14
address(b) = 0x62fe14

最初,b引用的是变量a,但随后程序试图将b改为c的引用。乍一看,这种意图似乎成功了,因为b的值由10变为了20.但是a的值也变为了20,并且a与b的地址相同但与c的地址不同。由于b是a的别名,因此上述 b = c 的赋值语句等效于下面的语句:

a = c; // 将变量c的值赋给变量a

1.3 数组的引用

#include<iostream>

int main(){
	using namespace std;

	int arr[] = {1, 2, 3, 4, 5};

	/*方法1*/
	// step1.定义数组类型
	typedef int(MY_ARR)[5];
	// step2.建议引用
	MY_ARR &arref1 = arr;

	/*方法2*/
	// 直接定义引用
	int (&arref2)[5] = arr;

	/*方法3*/
	// step1.定义数组引用类型
	typedef int(&MY_ARRREF)[5];
	MY_ARRREF arref3 = arr;

	for(int i=0; i<5; i++){
		cout << arref1[i] << '\t';
	}
	cout << endl;
	for(int i=0; i<5; i++){
		cout << arref2[i] << '\t';
	}
	cout << endl;
	for(int i=0; i<5; i++){
		cout << arref3[i] << '\t';
	}
	cout << endl;
	return 0;
}

输出:

1       2       3       4       5
1       2       3       4       5
1       2       3       4       5

02.引用的本质

引用的本质在 c++ 内部实现是一个常量指针.

c++编译器在编译过程中使用常指针作为引用的内部实现,因此引用所占用的空间大小与指针相同,只是这个过程是编译器内部实现,用户不可见。

int &b = a;

实际上是下述代码的伪装表示:

int* const pr = &a; 

其中,引用b扮演的角色与表达式*pr相同。

正因为引用本质是一个常量指针,所以创建时必须进行初始化。

//发现是引用,转换为 int* const ref = &a;
void testFunc(int& ref){
    ref = 100; // ref 是引用,转换为*ref = 100
}
int main(){
    int a = 10;
    int& aRef = a; //自动转换为 int* const aRef = &a;这也能说明引用为什么必须初始化
    aRef = 20; //内部发现 aRef 是引用,自动帮我们转换为: *aRef = 20;
    cout << "a:" << a << endl;
    cout << "aRef:" << aRef << endl;
    testFunc(a);
    return 0;
}

03.指针的指针与指针的引用

摘自:https://www.cnblogs.com/li-peng/p/4116349.html

char *p1 = "hello";
char* &p2 = p1; // 将p2作为指针p1的别名,即指针的引用

C++ | 引用

3.1 为什么需要使用它们

当我们把一个指针做为参数传一个方法时,其实是把指针的复本传递给了方法,也可以说传递指针是指针的值传递。如果我们在方法内部修改指针会出现问题,在方法里做修改只是修改的指针的copy而不是指针本身,原来的指针还保留着原来的值。

我们用下边的代码说明一下问题:

#include<iostream>

int m_value = 1;

void func(int *p){
    p = &m_value;
}

int main(){
	using namespace std;
    int n = 2;
    int *pn = &n;
    cout << *pn << endl;
    func(pn);
    cout << *pn <<endl;
    return 0;
}

输出:

2
2

3.2 使用指针的指针

展示一下使用""指针的指针"(二级指针)做为参数:

#include<iostream>

int m_value = 1;

void func(int **p){
    *p = &m_value;
	// 也可以根据你的需求分配内存
    *p = new int;
    **p = 5;
}

int main(){
	using namespace std;
    int n = 2;
    int *pn = &n;
    cout << *pn << endl;
    func(&pn);
    cout << *pn <<endl;
    return 0;
}

输出:

2
5

我们看一下 func(int **p)这个方法

  • p: 是一个指针的指针,在这里我们不会去对它做修改,否则会丢失这个指针指向的指针地址;
  • *p: 是被指向的指针,是一个地址。如果我们修改它,修改的是被指向的指针的内容。换句话说,我们修改的是main()方法里指针pn
  • **p: 两次解引用是指向main()方法里的pn,即指针pn*指向内当中的具体内容。

3.3 指针的引用

再看一下指针的引用代码:

#include<iostream>

int m_value = 1;

void func(int *&p){
    p = &m_value;
	// 也可以根据你的需求分配内存
    p = new int;
    *p = 5;
}

int main(){
	using namespace std;
    int n = 2;
    int *pn = &n;
    cout << *pn << endl;
    func(pn);
    cout << *pn <<endl;
    return 0;
}

输出:

2
5

看一下func(int *&p)方法

  • p: 是指针的引用,main()方法里的 指针pn
  • *p:是main()方法里的pn指向的内容。

04.引用的使用场景

引用变量的主要用途是作为函数的形参,通过将引用变量用作参数,函数将使用原始数据,而不是其副本。

C++ | 引用

现在通过一个常见案例——交换两个变量的值进行演示。

交换函数必须能够修改调用程序中变量的值。这意味着按值传递将不管用,因为函数将交换原始变量副本的内容,而不是变量本身的内容。但传递引用时,函数将可以使用原始数据。另一种方法是,传递指针访问原始数据。

#include<iostream>
void swapr(int & a, int & b); // 传递引用进行变量值交换
void swapp(int * p, int * q); // 传递指针进行变量值交换
void swapv(int a, int b); // 传递副本进行变量值交换(不可行)

int main(){
	using namespace std;
	int a = 111;
	int b = 999;
	cout << "a = " << a;
	cout << " b = " << b << endl;

	cout << "Using references to swap contents:\n";
	swapr(a, b);
	cout << "a = " << a;
	cout << " b = " << b << endl;

	cout << "Using pointers to swap contents:\n";
	swapp(&a, &b);
	cout << "a = " << a;
	cout << " b = " << b << endl;

	cout << "Trying to use passing by value:\n";
	swapv(a, b);
	cout << "a = " << a;
	cout << " b = " << b << endl;
    return 0;
}

void swapr(int & a, int & b){
	int tmp;
	tmp = a;
	a = b;
	b = tmp;
}
void swapp(int * a, int * b){
	int tmp;
	tmp = *a;
	*a = *b;
	*b = tmp;
}
void swapv(int a, int b){
	int tmp;
	tmp = a;
	a = b;
	b = tmp;
}

输出:

a = 111 b = 999
Using references to swap contents:
a = 999 b = 111
Using pointers to swap contents:
a = 111 b = 999
Trying to use passing by value:
a = 111 b = 999

05.常量引用

5.1 概述

#include<iostream>
double cube_a(double x); // 接受double类型的参数
double cube_b(double &rx); // 接受double引用

int main(){
	using namespace std;
	double a = 3;
	cout << cube_a(a);
	cout << " = cube of " << a << endl;
	cout << cube_b(a);
	cout << " = cube of " << a << endl;
    return 0;
}

double cube_a(double x){
	x *= x * x;
	return x;
}
double cube_b(double &rx){
	rx *= rx * rx;
	return rx;
}

输出:

27 = cube of 3
27 = cube of 27

cube_b修改了main()中的变量a的值,而cube_a没有。这是因为cube_b使用了引用参数,因此修改rx实际上就是修改a

如果程序员想让函数仅传递变量信息,而不对变量本身进行修改,同时又想使用引用,则应该使用常量引用

double cube_b(const double &rx); // const 修饰的引用,不能修改

这样做,当编译器发现代码修改了rx的值时,将报错。

5.2 临时变量&左值右值

1)左值右值

按值传递的函数,如上述的cube_a,可使用多种类型的实参,如下的调用都是合法的:

double a = 3;
double z;
z = cube_a(a + 2.0);
z = cube_a(8.0);

但是,若将上面的参数传递给接受引用参数的函数,如上述的cube_b,将会报错。

C++ | 引用

因为double cube_b(double &rx);rx是某个变量的别名,则实参必须是该变量。而表达式a+2.0并不是变量,8.0也不是变量而是字面量

字面量:https://www.runoob.com/note/38919

如图所示,错误提示信息为非常量引用的初始值必须为左值,接下来介绍左值右值的概念。

  • 左值:左值参数是指可被引用的数据对象(可通过地址进行访问的数据对象),例如,变量、数组元素、结构成员、引用和解除引用的指针都属于左值;
  • 右值:包括字面常量(用引号括起的字符串除外,它们由其地址表示)和包含多项的表达式。

在C语言中,左值最初是指可出现在赋值语句左边的实体,但这是引入关键字 const 之前的情况。现在,常规变量和const变量都可视为左值,因为可通过地址访问它们。

但是常规变量属于可修改的左值,而const变量属于不可修改的左值。

2)临时变量

但是,如果将函数cube_b(double &rx)改为如下实现,上述不合法的参数传递将变为合法的:

double cube_b(const double &rx){ // 接收常量引用
    return rx * rx * rx; // 常量引用不允许修改rx的值,否则报错,所以将rx *= rx * rx改为rx * rx * rx
}
#include<iostream>
double cube_b(const double &rx); // 接受double引用

int main(){
	using namespace std;
	double a = 3;

	cout << cube_b(a + 2.0);
	cout << " = cube of " << a + 2.0 << endl;

	cout << cube_b(8.0);
	cout << " = cube of " << 8.0 << endl;

    return 0;
}

double cube_b(const double &rx){
	return rx * rx * rx;
}

输出:

125 = cube of 5
512 = cube of 8

这是因为C++为a+2.08.0各自生成了一个临时变量。仅当参数为常量引用时,C++才会这样做。

当函数接收的是常量引用时,C++创建临时变量的2种情形:

  • 实参的类型正确,但不是左值;
  • 实参的类型不正确,但可以转换为正确的类型。
#include<iostream>
double cube_b(const double &rx); // 接受double常引用

int main(){
	using namespace std;
	double side = 3.0;
	double *pd = &side;
	double &rd = side;
	int edge = 5;
	double lens[4] = {2.0, 5.0, 10.0, 12.0};

	double c1 = cube_b(side); 		  // rx is side
	double c2 = cube_b(lens[2]);	  // rx is lens[2]
	double c3 = cube_b(rd);			  // rx is rd is side
	double c4 = cube_b(*pd);		  // rx is *pd is side
	double c5 = cube_b(edge);		  // rx is temporary variable
	double c6 = cube_b(7.0);		  // rx is temporary variable
	double c7 = cube_b(side + 10.0);  // rx is temporary variable

	cout << "c1 = " << c1 << endl;
	cout << "c2 = " << c2 << endl;
	cout << "c3 = " << c3 << endl;
	cout << "c4 = " << c4 << endl;
	cout << "c5 = " << c5 << endl;
	cout << "c6 = " << c6 << endl;
	cout << "c7 = " << c7 << endl;

    return 0;
}

double cube_b(const double &rx){
	return rx * rx * rx;
}

输出:

c1 = 27
c2 = 1000
c3 = 27
c4 = 27
c5 = 125
c6 = 343
c7 = 2197
  1. 参数sidelens[2]rd*pd都是有名称的且类型为double的数据对象,因此可以为其创建引用,而不需要临时变量。
  2. edge虽然是变量,但是类型却不正确,double引用不能指向int,此时编译器将生成一个临时匿名变量。
  3. 参数7.0side+10.0的类型都正确,但没有名称(即二者均为右值),此时编译器都将生成一个临时匿名变量,并让rx指向它。这些临时变量只在函数调用期间存在,函数调用结束将被销毁。

也就是说,cube_b(side + 10.0)等价于double temp = 200; const double &rx = temp;

如果函数调用的参数不是左值或与相应的const引用参数的类型不匹配,则C++将创建类型正确的临时匿名变量,将函数调用的参数的值传递给该临时变量,并让参数来引用该变量。

5.3 尽可能使用const

将引用参数声明为常量引用的理由:

  • 使用const可以避免无意中修改数据的编程错误;

  • 使用const使得函数能够处理const和非const实参,否则将只能接收非const数据;

    这是因为C++相对于C语言而言,类型匹配更加严格,对一个赋值语句,要求左右类型必须一致。而C语言则会进行自动类型转换。

  • 使用const引用使函数能够正确生成并使用临时变量。

因此,应尽可能将引用形参声明为常量引用。