一、引用概念
引用即为某个已存在的变量取别名,引用变量与被引用变量公用一块内存空间,比如西红柿、番茄都是同一物品的不同命名。通过在数据类型后、变量名前添加“&”符号来定义引用类型。
int ival = 1024;
int &rename_ival = ival;//rename_ival为变量ival的引用(rename_ival为ival的别名)
二、引用特性
1、引用必须初始化
引用实质为“取别名”,故在取别名前必须已有名字,那么其实也要求了被引用的必须为一个对象。
2、一旦引用一个对象将无法引用其他对象
一般在初始化变量时,初始值会被拷贝到新建对象,而在定义引用时则是将初始值与引用进行绑定,故引用一旦引用一个对象将无法引用其他对象,对引用的赋值实质为对被引用对象的赋值,以下代码即可证明。
#include <iostream>
int main()
{
int ival = 1024;
int& rename_ival = ival;
int new_ival = 2048;
rename_ival = new_ival;
std::cout << "ival = " << ival << std::endl;
std::cout << "rename_ival = " << rename_ival << std::endl;
return 0;
}
3.一个变量可有多个引用
一个变量可以有多个引用,可通俗理解为一个人可以有多个昵称。
三、const引用
const引用即将引用与const对象绑定。由于const引用是为const对象取别名,故无法通过修改const引用修改被引用的const对象。
//使用场景1
const int ival = 1024;
const int &reival1 = ival;//正确
int &reival2 = ival;//错误:将非常量引用指向常量对象,将被引用对象的只读特性改变为可读可写,是权限的放大,不被允许
int ival3 = 2048;
const int &ival4 = ival3;//const引用可引用非常量对象,允许权限的缩小。
//使用场景2
#include <iostream>
int main()
{
double ival1 = 1024.1024;
//int& ival2 = ival1;错误
const int& ival2 = ival1;
std::cout << ival2 << std::endl;
return 0;
}
针对使用场景2,可以发现变量ival3引用了一个int类型的变量,但ival2却为double类型的变量,发生了隐式类型转换。在此情况下使用非const引用会提示错误。
为了确保让ival3绑定一个整形对象,编译器将相关代码做了如下变换:
const int temp = ival1;
const int &ival2 = temp;
在此情况下,ival2实际引用了一个临时变量,该临时变量暂存了表达式的结果。因为临时变量具有常性,所以必须使用const引用。
四、使用场景
1、做参数
#include <iostream>
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
int main()
{
int left = 1024;
int right = 2048;
Swap(left, right);
std::cout << "left = " << left << std::endl;
std::cout << "right = " << right << std::endl;
return 0;
}
如上代码所示使用引用作为形参,在函数被调用时实质就是传递了实参。
2、做返回值
在理解引用返回前,先看一下传值返回,代码如下:
#include <iostream>
int Add(int num1, int num2)
{
int sum = num1 + num2;
return sum;
}
int main()
{
int ret = Add(1, 2); //int& ret = Add(1, 2);
return 0;
}
通过上述代码可能会比较直接的理解为在main函数中调用了Add函数,两数求和后将返回值sum赋值给ret。但事实并非如此,实际上传值返回并未直接把sum的值赋值给ret,而是先将其值拷贝存放进了临时变量,通过临时变量再赋值给ret。将ret设置为引用类型后,不难证明却有临时变量的存在。临时变量若较大则会存放于调用层函数的栈帧中,若临时变量较小(一般在4/8byte内)则会存放在寄存器中。
那么再来看看传引用返回,与传值返回不同的是返回了sum对象的引用,虽然与传值返回一样产生了临时变量,但此时临时变量只是返回了一个名称,而并不是一块具体的空间。通过如下代码同样可以验证。
#include <iostream>
int& Add(int num1, int num2)
{
int sum = num1 + num2;
return sum;
}
int main()
{
int& ret = Add(1, 2);
return 0;
}
然而,这上述代码却存在一个很明显的问题,即当Add函数调用完毕后,函数栈帧将会销毁,那么sum的取值就会存在非法访问,取值有可能是3也有可能会是一串随机值,这取决于sum空间中的内容是否有改动,倘若在ret所在行的后一行添加一串代码,那么ret的值将会被修改为随机值。
于是,对于引用返回便有了如下的使用规则:若返回对象在函数调用结束后还会继续存在则可以使用引用返回,如静态变量,反之则不宜使用。
五、引用和指针
1、相同点
#include <iostream>
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
void Swap(int* left, int* right)
{
int temp = *left;
*left = *right;
*right = temp;
}
int main()
{
int left = 1024;
int right = 2048;
Swap(&left,&right);
Swap(left, right);
std::cout << "left = " << left << std::endl;
std::cout << "right = " << right << std::endl;
return 0;
引用和指针从底层来看,并没有什么区别,引用在底层实现上与指针相同。引用只是c++语法糖,可以将其看作编译器自动完成取地址、解引用的常量指针。
void Swap(int& left, int& right)//引用:汇编指令
{
00E119B0 push ebp
00E119B1 mov ebp,esp
00E119B3 sub esp,0CCh
00E119B9 push ebx
00E119BA push esi
00E119BB push edi
00E119BC lea edi,[ebp-0Ch]
00E119BF mov ecx,3
00E119C4 mov eax,0CCCCCCCCh
00E119C9 rep stos dword ptr es:[edi]
00E119CB mov ecx,offset _19C4B5D4_list@cpp (0E1F066h)
00E119D0 call @__CheckForDebuggerJustMyCode@4 (0E11384h)
int temp = left;
00E119D5 mov eax,dword ptr [left]
00E119D8 mov ecx,dword ptr [eax]
00E119DA mov dword ptr [temp],ecx
left = right;
00E119DD mov eax,dword ptr [left]
00E119E0 mov ecx,dword ptr [right]
00E119E3 mov edx,dword ptr [ecx]
00E119E5 mov dword ptr [eax],edx
right = temp;
00E119E7 mov eax,dword ptr [right]
00E119EA mov ecx,dword ptr [temp]
00E119ED mov dword ptr [eax],ecx
}
void Swap(int* left, int* right)//指针:汇编指令
{
00BB19B0 push ebp
00BB19B1 mov ebp,esp
00BB19B3 sub esp,0CCh
00BB19B9 push ebx
00BB19BA push esi
00BB19BB push edi
00BB19BC lea edi,[ebp-0Ch]
00BB19BF mov ecx,3
00BB19C4 mov eax,0CCCCCCCCh
00BB19C9 rep stos dword ptr es:[edi]
00BB19CB mov ecx,offset _19C4B5D4_list@cpp (0BBF066h)
00BB19D0 call @__CheckForDebuggerJustMyCode@4 (0BB1384h)
int temp = *left;
00BB19D5 mov eax,dword ptr [left]
00BB19D8 mov ecx,dword ptr [eax]
00BB19DA mov dword ptr [temp],ecx
*left = *right;
00BB19DD mov eax,dword ptr [left]
00BB19E0 mov ecx,dword ptr [right]
00BB19E3 mov edx,dword ptr [ecx]
00BB19E5 mov dword ptr [eax],edx
*right = temp;
00BB19E7 mov eax,dword ptr [right]
00BB19EA mov ecx,dword ptr [temp]
00BB19ED mov dword ptr [eax],ecx
}
2、不同点
1、在语法概念上,引用只是定义了对象的别名,并未新空间;指针存储对象的地址。
2、引用必须初始化,而指针并无要求。
3、有空指针,无空引用。
4、引用与被引用对象绑定,一个对象可被引用多次,但一个引用不可引用多个对象;指针可指向不同的对象。
5、自增自减意义不同,引用自增自减即对被引用对象进行修改;指针则指指针运算,表明指针的偏移。
6、引用的使用比指针安全。
7、引用没有顶层const修饰,如 int& const。因为引用本身就与被引用对象绑定不可变,顶层const就失去了其意义;指针既有顶层const又有底层const修饰。
8、访问实体方式不同,指针需要显式解引用,引用则由编译器自动完成取地址、解引用。
9、在sizeof中含义不同,引用结果为被引用对象类型的大小,而sizeof指针的大小则为4或8byte。
EOF