3

C++ | 引用 - 就良同学

 1 year ago
source link: https://www.cnblogs.com/lijiuliang/p/17113812.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

01.引用概述

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;
}

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的别名,即指针的引用
image

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.引用的使用场景

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

image

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

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

#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,将会报错。

image

因为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引用使函数能够正确生成并使用临时变量。

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK