在文章的开始可以先看这样一段C++代码:
int main(void)
{
int a = 0;
int& ra = a;
int n = 0;
int* pn = &n;
return 0;
}
这段代码转换为汇编代码是这样的(由于没有在CSDN的代码选项里面找到汇编,我直接复制重要部分的汇编代码吧)
int a = 0;
00007FF7E6EF224D mov dword ptr [a],0
int& ra = a;
00007FF7E6EF2254 lea rax,[a]
00007FF7E6EF2258 mov qword ptr [ra],rax
int n = 0;
00007FF7E6EF225C mov dword ptr [n],0
int* pn = &n;
00007FF7E6EF2263 lea rax,[n]
00007FF7E6EF2267 mov qword ptr [pn],rax
可以看到,定义引用和定义指针的汇编代码并无区别。所以,我们可以把引用看作指针去理解,但是它又与指针有着许多的区别:
1.初始化
指针不初始化,代码可以运行,虽然会被编译器口头警告;但引用不一样,如果引用不进行初始化,代码是直接无法运行,并且引用的初始化赋值只能是一个对象,无法用NULL或者是nullptr对其初始化。
2.赋值
定义一个可读可写的指针,这个指针和变量一样是可以重新被赋值的 。 引用是否也可以被重新赋值呢?
#include<iostream>
using std::cout;
int main(void)
{
int p = 9;
int& b = p;
int a = 10;
b = a;
//int& b = a;
cout << b;
return 0;
}
可以看到,b = a;这步操作只是把a的值赋给了b,而下面的int& b = a;这一操作是不被编译器所允许的——重复初始化。 从这里得出的结论是——引用无法用以上操作重新赋值。事实也确实如此,在C++语法中,引用无法重新被赋值。
3.多级引用?
指针有多级指针,那么引用是否也有呢?🤔
答案是没有,int ** p = &b 不会报错,但是int && d = b会。
4.大小
众所周知,指针大小是固定的(在同一平台)。那么引用呢?本质上也是指针,是否也是固定的呢?
#include<iostream>
using namespace std;
int main(void)
{
char c = 0;
int a = 0;
char& rc = c;
int& ra = a;
cout << sizeof(ra) << endl;
cout << sizeof(rc) << endl;
return 0;
}
可以看到,输出的值随着类型的变化而变化。
5.计算
引用本质上作为指针,对它进行计算操作会怎么样呢?
#include<iostream>
using namespace std;
int main(void)
{
int a = 0;
int& ra = a;
cout << ++ra << endl;
return 0;
}
可以看到,是对其引用对象进行了计算操作。
到此为止,相信你对引用已经有了一个大概的认识,接下来就是我认为较难理解的部分细节。
1.权限
int main(void)
{
const int a = 0;
int& ra = a;
return 0;
}
这段代码在VS中会报错:qualifiers dropped in binding reference of type "int &" to initializer of type "const int"
嗯......大概意思是:用int&类型引用const int 类型缺少修饰词
确实,将代码int& ra = a;改为const int&ra;就不会报错。
所以当权限不一样时就不行?继续看如下代码:
int main(void)
{
int a = 0;
const int& ra = a;
return 0;
}
虽然权限不一样,但这样却不会报错。所以可以理解为:引用既定对象时,无法扩大权限,但可缩小。
再看:
int main(void)
{
int a = 1;
float b = a;
float& rb = b;
float& ra = a;
return 0;
}
在VS上跑一下,编译器直接指出 a reference of type "float &" (not const-qualified) cannot be initialized with a value of type "int" ——需要const修饰(报错代码为:float& ra = a;)
果不其然,加上修饰词const就会报错了(当然,隐式转换会有警告,但无关紧要)
这又是为什么呢?
拙见——a为int类型,如果允许通过ra对变量a进行修改,就是将一个浮点型数据写入整形变量中,很明显:这种操作是绝对不被允许的。所以,加上const修饰就好了,只能从a所在内存读出数据。
2.引用可做参数
非常好理解,就像传地址调用一样,但不同类型定义有一些出入,直接看代码:
void Test_n(int& num)
{
}
void Test_p(int*& ptr)
{
}
void Test_a( int (&arr)[5])
{
}
int main(void)
{
int n = 0;
int* p = nullptr;
int myarr[5] = { 0 };
Test_n(n);
Test_p(p);
Test_a(myarr);
return 0;
}
3.引用可做返回值
#include<iostream>
using namespace std;
int& Test(int a)
{
int c = a + 1;
return c;
}
int main(void)
{
int num = 0;
int& tmp = Test(num);
Test(2);
cout << tmp << endl;
printf("%d\n",tmp);
cout << tmp << endl;
return 0;
}
运行结果如下:
很奇怪,但是可以解释。部分汇编代码如下:
int num = 0;
00007FF7E5CE1A7B mov dword ptr [num],0
int& tmp = Test(num);
00007FF7E5CE1A82 mov ecx,dword ptr [num]
00007FF7E5CE1A85 call Test (07FF7E5CE1519h)
00007FF7E5CE1A8A mov qword ptr [tmp],rax
拓展:在 X64调用约定中,ecx寄存器用于调用第一个整型参数,函数返回值(如果是整形或者是指针)存储在rax寄存器中。
这段代码其实有一个警告: warning C4172: returning address of local variable or temporary: c
——返回值可能是地址或者是临时变量c。
所以,在主函数中用什么类型的变量接收,返回值就是什么类型。这里我们用了一个引用变量来接收,返回的自然就是指针。
我们或许又会有疑问——返回指针输出的不应该是地址吗?并非如此,用引用变量接收后,它就变成了一个纯正的引用变量,编译器会自动处理,不再像指针一样需要自己去进行解引用操作。
如此一来,我们就可以解释了,引用变量可以直接访问到函数所在栈区(为编译器与操作系统协同工作),但是在函数调用结束后,函数所在栈区被销毁(操作权还给操作系统)。
这里可以看出VS的编译器在函数调用结束后并不会清空其中数据,但会随着其他函数的调用导致其中的数据被修改。所以这里也能看出—— printf(其实是所有的C语言库函数)会调用栈区,但cout不会。(这是我们以后探讨的问题了)。
刚刚提到过,返回值分为两种,如果我们用整形变量来接收呢。答案是——确实解决了非法访问的问题,但是为什么不直接用int定义函数呢?
返回值如果为非引用类型,编译器就会创建一个临时变量来持有返回值,引用类型返回值则不会(具体原因和证明就不再赘述,以后再详细展开吧)所以为了追求极致的性能,C++在能用的地方肯定是选择引用类型的返回值。
那有没有解决办法呢?也肯定是有的
1.动态内存分配
#include<iostream>
using namespace std
int& Test(int a)
{
int* c = new int(a + 1);
return *c;
}
int main()
{
int num = 0;
int& tmp = Test(num);
cout << tmp << endl;
delete &tmp;
return 0;
}
但是开辟内存和释放内存是否又会增加时间成本呢?
2.用静态变量
#include<iostream>
using namespace std;
int& Test(int a)
{
static int c = a + 1;
return c;
}
int main(void)
{
int num = 0;
int& tmp = Test(num);
Test(2);
cout << tmp << endl;
printf("%d\n",tmp);
cout << tmp << endl;
return 0;
}
这样也可以解决,将返回值单独放在静态区,典型的以空间换时间。
综上,就是我目前对引用的全部理解了,欢迎大家补充。