引用概念
引用不是新定义一个变量,而是给已存在的变量取了一个别名,编译器不会给变量开辟内存空间,它和它所引用的变量共用同一块内存空间。
比如,孙悟空、齐天大圣和孙行者。
类型& 引用变量名(对象名) = 引用实体
int main()
{
int a = 10;
int& ra = a;//定义引用类型
cout << "a = " << a << endl;
cout << "ra = " << ra << endl;
return 0;
}
注意:引用类型必须和引用实体是同一种类型的
引用特性
- 引用在定义时必须初始化
- 一个变量可以用多个引用(就和一个人可以有多个称号一样)
- 在当前引用的声明周期内,引用一旦引用一个实体,就再也不能引用其他实体
int main()
{
int a = 10;
//int& ra;这条语句编译时会报错,因为该引用在定义时没有初始化
int& ra = a;
int& rra = ra;
printf("a的地址为%p\n", &a);
printf("ra的地址为%p\n", &ra);
printf("rra的地址为%p\n", &rra);
return 0;
}
可以看到,a、ra和rra虽然名称不同,但都是同一块空间的标识符
常引用
引用中的权限问题
int main()
{
const int a = 10;
int& ra = a;
}
当运行时,会出现报错
这里报错是因为引用(取别名)不能放大权限,那么什么叫做放大权限?
原来用const
修饰的变量a
拥有了常属性,不能再被任意修改。
当别的引用变量来引用这个不能被修改的a
时,这个引用变量并没有被const
修饰,即ra
可以被修改,相当于修改权限被放大了(以前不能被修改,但是换了名字就可以被修改)
这种权限放大的限制只存在于引用,对于指针之间的赋值是不存在的
所以,要想正确引用,就应该在引用变量前面加上const
修饰,避免权限放大
int main()
{
const int a = 10;
const int& ra = a;
}
加上const修饰之后,ra
和a
的属性就完全一样了(都具有常属性),都不能被修改
另外,还要提一点的就是,虽然在使用引用的时候不可以实现权限放大,但是可以实现权限缩小。举个例子:
int main()
{
int a = 20;
const int& ra = a;
return 0;
}
在这段代码中,ra作为a的别名,但是ra不能被修改,相当于是权限被缩小了,这在引用的使用中是被允许的
综上,在引用的使用中,权限不能放大,但是权限可以平移和缩小
引用实体为常量
引用也可以给常量取别名,当然,引用变量需要加上const
修饰
int main()
{
const int& a = 10;
return 0;
}
引用中的类型转换
先看一段代码
int main()
{
double d = 12.54;
int& rd = d;
return 0;
}
可以看到,报错显示出现了类型不匹配的问题
但是,如果对这个代码稍微修改一下……
int main()
{
double d = 12.54;
const int& rd = d;
return 0;
}
运行会发现,这次就不会发生报错了,但是这又是为什么呢?
这里要从类型转换说起
类型转换深度分析
int main()
{
int x = 10;
unsigned int y = 1;
if (x > y)
{
//...
}
return 0;
}
在上面代码中,当类型不同的x
和y
进行运算时,类型会首先发生变化(不同类型的对象在进行运算符运算时类型会进行提升)
我们知道,有符号和无符号的进行运算,有符号会优先提升为无符号的
但是,这里的类型提升并不是直接将x
的类型提升为unsigned int
类型,而是会生成一个临时变量,x将它的值赋值给这个临时变量,然后将这个临时变量看成是unsigned int
类型再和无符号类型y
进行运算
类型转换,无论是隐式类型转换还是强制类型转换,中间都会产生临时变量,而这个临时变量具有常属性
了解到了这些,回到先前的代码
int main()
{
double d = 12.54;
const int& rd = d;
return 0;
}
与其说是rd
引用了d
,倒不如说是rd
引用的是中间所产生的临时变量
为了验证,我分别打印了d和rd的地址
int main()
{
double d = 12.54;
const int& rd = d;
printf("d的地址是%p\n", &d);
printf("rd的地址是%p\n", &rd);
return 0;
}
看到这里有人可能就要问了
int main()
{
int a = 10;
int& ra = a;
return 0;
}
为什么这里的引用不需要加上const呢?
额,因为这里根本就没有发生类型转换啊,就没有产生常属性的临时变量呀
引用的使用场景
做参数
void swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
其效果等同于C语言的:
void swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
做返回值
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
cout << ret << endl;
Add(4, 5);
cout << ret << endl;
return 0;
}
在VS2022下运行结果如下:
有的读者可能觉得第一次打印结果为3可以理解,但是第二次打印结果为9就有点匪夷所思:明明没有用ret接收,为什么ret的值还会改变?
其实,这两次打印的结果都应该是随机值,具体分析如下:
当第一次函数将c的别名返回时,main函数用ret接收,于是ret就是形参c的别名,但是此时c的空间已经被回收了(函数结束,函数所占用的空间(函数的栈帧)被系统回收),所以ret实际上是引用的一块已经被回收了的空间,即第一次打印的结果是不可知的。
当第二次调用Add函数时,因为两次的Add函数大小相同,且原来的空间并没有被使用,所以第二次Add使用的空间就是第一次Add使用过后的空间,第二次Add函数中的形参c的地址又恰好是原来形参c的地址,这里我做一个验证
可以看到,两次产生的形参c的地址是完全一样的,又因为ret是c的别名,所以,形参c所对应的内存空间其实也就是ret所对应的内存空间,形参c的值发生什么变化,只要ret是c的引用,那么ret也对应要发生改变
所以,在将引用作为返回值时,需要注意:
如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回
引用和指针的区别
引用在语法概念上就是一个别名,没有独立空间,和其引用实体共用一块空间
在底层实现上实际上是有空间的,因为引用是按照指针方式来实现的
引用和指针的不同点:
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全