探秘C++系列——引用

前言

首先得知道一条声明语句由一个基本数据类型(base type)和紧随其后的声明符(declarator)列表组成。
数据类型好理解,就是int,double,char,string之类的,那么这个声明符列表是什么东西呢?
我们初学C++时要定义一个变量,就用
“数据类型+变量名”的形式,例如int a;
a就是我们给变量起的名字。像这种简单的声明语句,变量名就等于声明符列表(当然可以用int a,b,c; 的形式定义多个变量,符合列表的概念)
但实际上,可以有更复杂的声明符。每个声明符可以命名一个变量并指定该变量的类型为与前面定义的数据类型有关的某种类型 这种类型也叫复合类型。C++中的引用类型就是一种复合类型,那么接下来,我们就来好好聊一下引用到底是什么玩意儿。


初识引用

引用也可以理解为变量的别名。通常将声明符写为&x 的形式。x为我们给变量取的名字。其定义方式如下

int num=1;
int &ref=num;

我们在第一行定义了一个int 类型的变量,然后在第二行把它赋给一个int类型的引用。ref是引用的名字,然后我们在它前面加个&代表ref变量的类型是基于int类型的一种复合类型,这个ref就成了int类型的引用。
注意:&也是声明符的一部分。为什么要强调这点呢?来看下面这个语句:

int i=1;
int &a=i,b;

请问:a和b分别是什么类型?
如果我们记住了&是声明符的一部分,那么就很好理解了。这个声明语句定义了2个声明符:&a和b,还定义了数据类型为int。a前面有个&,说明它是int类型的引用,是种复合类型,而b就仅仅是一个普通的int类型。
通常我们把&和变量名连在一起,这样便于理解,不过这不是强制规定。

//以下两种定义引用的写法不报错
int i=1;
int& ref=i;
int & ref1=i;

但是像 int& a=i,b=i; 这种写法可能会让人误认为a和b都是引用。实际上这里只有a是引用,我们要看的是声明符具体是什么。

回到我们的第一个例子

int num=1;
int &ref=num;

我们尝试输出ref的值

cout<<ref<<endl; 

输出结果为1,和num的值一模一样。
这个挺好理解,因为之前我们说过可以把引用当成变量的别名,就好比我们给一个人起外号,我们叫这个外号实际上也是在叫这个人。但需要注意的是定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。
比如int &ref=i ; 和int ref=i; 它们的区别是后者将初始值(i的值)拷贝,然后在内存中开辟新的空间用于存放ref,而引用没有单独的内存空间,它只是指代赋予的那个变量,给原变量一个其他的名字,对引用操作相当于对原变量进行操作。
我们用一个例子来证明这点:

int i=5;
int &ref=i;
//输出地址
cout<<&i<<endl;
cout<<&ref<<endl;
int a=i;
cout<<&a<<endl;
//输出结果:
0x7fff59cca598
0x7fff59cca598
0x7fff59cca59c

可以看到引用的内存地址和原变量是一样的,把变量赋给另一变量会开辟新的内存空间。
这里不知大家有没有注意到,为什么我在输出语句里加个&就会输出一串奇怪的东西(实际上就是变量的内存地址啦),&x 的形式之前说了不是引用的意思吗?
实际上,&在不同的情况下会有不同的含义(C++的指针中接触到的* 也是如此)。它既可以作为表达式里的运算符,又可以作为声明的一部分。上面那个例子中输出语句里的&代表的是取地址运算符,得到的是变量的地址。这个其实有学过C语言的小伙伴应该会比较熟悉。在我刚开始学C++的时候,靠的是C语言的老本,所以会误以为&x这种形式全是取地址的意思,等我学到“引用”这个概念时才知道引用和取地址是两个不同的概念,看来当时还是太年轻了哈哈。

引用和取地址的区别

和C语言相比,引用是C++特有的概念。
当&紧随类型名出现时,如int &ref=num , 这时&作为声明的一部分,是引用的意思。
当&出现在表达式中时,(表达式即为一个或多个运算对象的组合,对象间可用运算符连接)如 &num, 这里&作为一元运算符,是取地址的运算符号

引用的使用注意点

1)引用必须在声明时初始化,不能先声明后赋值。
比如单独写个int &ref; 是错误的,必须要指定引用绑定的是谁

2)因为引用是和变量(对象)绑定在一起的,(后续讲的对象和变量意思差不多)
所以为引用赋值实际上是把值赋给了引用绑定的对象。获取引用的值,实际上是获取了引用所绑定的对象的值。以引用作为初始值,实际上就是以引用绑定的对象的值作为初始值。
一旦引用的声明完成,将无法再令引用重新绑定在另一个对象
我们用例子来证实上面的话:

int num=1;
int &ref=num;
int &ref2=ref; //此时ref2的值是num的值,也就是1
ref2=3; //因为ref2也绑定到了num,此时就是修改num的值,num=3
int a=2;
ref=a;  //注意,这里相当于把a的值赋给num,并不是把ref绑定到a的意思
cout<<num<<endl; //此时num=2
cout<<ref<<" "<<ref2<<endl; //输出2 2 ,因为ref和ref2都是绑定在num上

3)非常量引用只能绑定在对象上,不能与字面值或某个表达式的计算结果绑定在一起
例如:

int a=1,b=2;
int &ref=2;  //错误的语法,2是一个字面值
int &ref=a*b;  //错误的语法,a*b是个表达式

在visual studio中,像上面这样写会报一个“非常量的引用的初始值必须为左值”的错。
在C++中有左值,右值的概念。我们可以简单地理解为:
左值在内存中有具体的地址,如创建好的变量。
右值一般是不可寻址的常量,或在表达式求值过程中创建的无名临时对象,短暂性的
而对于const修饰的常量引用,并没有这种限制,这个后面会写一篇文章对const进行更详细的介绍。

4)大多数情况下,引用的类型要与绑定的对象严格匹配
如 int i=1; double &d=i; 就会报错,因为类型不匹配

引用传参

我们要经常看到定义函数时规定传入的形参是引用类型,例如:

void swap(int &num1, int &num2){
    
    
	int temp=num1;
	num1=num2;
	num2=temp;
}
int main(){
    
    
	int a=1,b=2;
	swap(a,b);
	return 0;
}

这是一个简单的交换数值的函数。我们把形参定义成引用类型有什么好处呢?
如果不加&,实际传入函数的参数会通过值传递的形式传给形参,函数执行时会为形参在栈中开辟一块空间,形参接收的只是实参数值的副本
(可以简单地认为形参是函数定义时括号里定义的参数,参数前面先定义具体类型; 实参是实际调用这个函数时传入的参数)
因为在函数的作用域中,操作的是形参,所以改变形参的值不会影响到实参。从而无法实现数值交换的功能。

但是如果把形参定义为引用,我们知道变量本身就是一个保存数据的内存地址的名称,而引用是变量的另一个名称。传递引用时,函数的形参依旧会作为局部变量在栈中开辟内存空间,但是因为我们是把别名传进来,这个别名指代的就是对应的变量的名字,这个名字就相当于数据的内存地址。因此函数新开辟的空间存放的就是实参的内存地址,我们对形参的数据进行修改,实参内存空间中的数据也会被修改。

引用和指针的区别

指针“指向”内存中的某个对象,而引用“绑定到”内存中的某个对象,它们都实现了对其他对象的间接访问,二者的区别主要有两个方面:

1)指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的声明周期内它可以指向几个不同的对象;引用不是一个对象,无法令引用重新绑定到另外一个对象。

2)指针无需在定义时赋初值,和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值;引用则必须在定义时赋初值。


写在最后:
本篇文章是我在学习C++时做的一些笔记,可能会有一些更深入的点还未学习到,如果有补充的,后续会进行更新。

猜你喜欢

转载自blog.csdn.net/qq_46044366/article/details/119280490
今日推荐