「地表最强」C++核心编程(二)引用

时间:2022-12-17 07:52:26

环境:
编译器:CLion2021.3;操作系统:macOS Ventura 13.0.1


地表最强C++系列传送门:
「地表最强」C++核心编程(一)内存分区模型
「地表最强」C++核心编程(二)引用
「地表最强」C++核心编程(三)函数提高——暂未更新
「地表最强」C++核心编程(四)类和对象——暂未更新
「地表最强」C++核心编程(五)文件操作——暂未更新

一、引用的基本语法

可以将引用简单理解为给一个变量起的别名,他和原变量共享同一块空间,基本语法如下:

数据类型 &别名 = 原名;

//举例
    int a = 10;
    int &b = a;//此处便是定义了一个引用
    cout << a << endl;//10
    cout << b << endl;//10

    b = 20;
    cout << a << endl;//20
    cout << b << endl;//20

通过举例可以看出,对引用的操作会直接改变原变量的值。留意一下这一点,本文第三点将会进一步举例说明。

二、引用的注意事项

2.1 引用必须初始化

	int a = 10;
	//int &b;//err,因为未初始化
	int &b = a;//ok,进行了初始化

2.2 引用初始化后不可更改

这里强调一下,前边说过,引用可以简单理解为别名。这里的不可更改的意思是指:假设定义了一个变量a,用引用b作为a的别名即int &b = a; 那么b将不可以再作为其他变量的别名,但是其内容是可以修改的,不要将二者混淆

	int a = 10;
	int &b = a;//定义了一个引用并初始化,此时b是a的别名
	int c = 20;
/*
有人认为下边的一行是在更改引用,实际上这个想法是错误的。
要时刻记住,引用就是别名,因此此行代码实际上就相当于a = c;这只是一个赋值操作,显然是没有问题的。
*/
	b = c;//ok,这只是个赋值操作。
	
/*
	那什么操作是在更改引用呢?我查看了一些文章都没有这种错误举例,这里便举两种可能的错误:
	int &b = c;//有的文章把这个作为修改引用的反例,但我觉得不妥,这个错误应该是重复定义引用b。
	&b = c;//这个操作不知道会不会有人写,这是编译器报的错误是不可赋值。
*/

三、引用做函数参数

以交换变量值函数为例。在了解引用之前,我们常用值传递和地址传递:

//值传递
void mySwap01(int a,int b){
	int temp = a;
	a = b;
	b = temp;
}

//地址传递
void mySwap02(int* pa,int* pb){
	int temp = *a;
	*a = *b;
	*b = temp;
}

之所以要用地址传递就是因为值传递的时候,其实是函数又自己给形参开辟了新的空间,实际上交换的也只是形参的值,实参并没有发生改变。但我们使用此来函数往往需要实参发生改变。而地址传递虽然也给自己的形参开辟了新的空间,但这个形参是实参的地址,因此函数可以通过这个地址直接操作实参,从而实现实参的值的交换。
如果文字介绍看不清楚,可以移步「地表最强」C语言(六)函数查看6.3的图解,说不定会对你有帮助。

有了引用之后,我们不需要地址传递也可以实现交换实参的值,只需要将引用作为参数传递也可以完成实参的交换,对于不喜欢使用指针的兄弟们很友好:

void mySwap03(int& a,int& b){
	int temp = a;
	a = b;
	b = temp;
}
int main(){
	int a = 10;
	int b = 20;
	//调用引用传参的函数的时候,参数直接写变量名。把引用想简单一些,就是别名。
	mySwap03(a,b);
	cout << "a = " << a <<endl;//20
	cout << "b = " << b <<endl;//10	 成功交换实参
	return 0;
}

四、引用做函数的返回值

4.1 不要返回局部变量的引用

int& test01() {//引用作为返回值
    int a = 10;//局部变量,函数调用结束后就会释放
    return a;//返回了局部变量的引用
}

int main(){    
	int &ref = test01();
    cout << "ref= " << ref << endl;//err。部分编译器下可以正常运行,但这是编译器的功劳,这种操作本身是非法的,因为局部变量已经被释放了
	return 0;
}

4.2 返回值是引用的函数调用可以作为左值

int& test02() {
    static int a = 10;//静态变量存放在全局区,程序结束后才释放
    return a;//返回的是静态变量
}

int main(){    
    int &ref = test02();//ref是a的别名
    cout << "ref= " << ref << endl;//注意与4.1的区别
    //4.1中返回的局部变量在打印时已经被释放,而此处返回的是静态变量,函数调用后其空间不会被释放,因此可以合法访问

    test02() = 1000;//test02()的返回值是引用,可以作为左值。这里其实相当于操作a
    cout << "ref= " << ref << endl;//1000
	return 0;
}

五、引用的本质

首先来介绍两个概念:
指针常量int* const p:该指针指向的内容可以改变,而指针的指向不可以改变;
常量指针const int* p:该指针指向的内容不可以改变,而指针的指向可以改变。
引用的本质就是指针常量,这也是为什么引用一旦初始化后,就不可以修改指向,但是内容却可以更改。

void func(int &ref) {//更改指向的内容
    ref = 100;
}

int main(){
    int a = 10;
    int &ref = a;//自动转换为 int* const ref = &a;   可以看出引用是指针常量,不能修改指向,因此引用不可更改
    ref = 20;//发现ref是引用,自动转换为*ref = 20; 更改的是内容而非指向
    cout << "a= " << a << endl;//20
    cout << "ref= " << ref << endl;//20
    
    func(a);//更改了内容
    cout << "a= " << a << endl;//100
    cout << "ref= " << ref << endl;//100
	return 0;
}

六、常量引用

通过本文第三节我们知道了引用作文函数参数传递时,可以修改实参。但有时我们并不希望实参被修改,此时就需要常量引用来修饰形参,防止误操作。
也就是说:
1.引用一经初始化,其指向不可更改,但内容可以修改;
2.常量引用不但指向不可更改,内容同样不可更改。

void showValue(const int &val) {//形参是常量引用
//    val = 1000;//err,常量引用不可修改其内容,防止了误操作
    cout << "val= " << val << endl;
}

引用和常量引用的另一个区别是:不可以用字面值常量给引用赋值,但是可以用字面值常量给常量引用赋值:

int main(){
    int a = 10;
//    int& ref = 10;//err,引用必须引一块合法的内存空间(栈或堆),不能直接引字面常量
    const int& ref = 10;//ok,自动转换为  int temp = 10;  const int& ref = temp;
//    ref = 20;//err,此时相当于const int* const ref,指向和内容都不能修改
	return 0;
}