Cpp-Reference

167 阅读2分钟

Reference


什么是引用

在 C++ 中,引用即取别名,例如:孙悟空、齐天大圣、美猴王、孙行者都指的是孙悟空;本质都表示一个已存在变量或对象的别名,它们都访问同一块内存空间。引用底层仍然采用指针实现,在一些情况下优化了代码质量。


引用的性质

int a = 0;
int& b = a;

以上代码表示 b 是 a 的引用,即 b 是 a 的别名。

int a = 0;
int& b = a;

a = 1;
std::cout << a << " ";

b = 2;
std::cout << a;

以上代码输出结果为:1 2 可知更改别名也可更改变量本身,即如果孙悟空吃饭了,那么齐天大圣也吃饭了,美猴王也吃饭了,是一个道理。

int a = 0;
int& b = a;
int& c = b;
int& d = b;

多次引用也是允许的,别名是可以起多个的。

double a = 0;
int& ra = a;

不同类型是不可以引用的(a 生成临时变量给 ra 时存在权限放大,相当于把 const int& 给 int&,这里因为已经编译错误,不深究),当然如果你愿意可以强制类型转换,但请尽量杜绝这种操作:

char a = 0;
int& ra = (int&)a;

ra = 10000;

这样会导致程序崩溃,非法内存访问。

引用在定义时必须初始化

int a = 0;

//error
int& ra;
ra = a;

//correct
int* pa;
pa = &a;

指针可以先定义不初始化,后续再赋值。但引用必须在定义时就初始化,不允许赋值,这也就是下一条性质。

引用不允许改变引用对象

int a = 0;
int b = 1;

int& ra = a;
ra = b; //1.赋值 //2.改变引用对象

int* pa = &a;
pa = &b;

引用在定义时必须初始化,换言之即引用在定义时必须确定引用对象,一旦确定就不可更改,所以这一步是给 ra 别名赋值为 1。但指针是可以改变指向的。


函数中的引用

引用做函数形式参数

由于引用的特性,它可以在一定程度上替代指针,拿 swap 函数举例:

void swap(int* e1, int* e2) //指针版本
{
	int tmp = *e1;
	*e1 = *e2;
	*e2 = tmp;
}

void swap(int& e1, int& e2) //引用版本
{
    int tmp = e1;
    e1 = e2;
    e2 = tmp;
}

可知引用省去了指针繁琐的解引用操作。

void func(int a);
void func(int& a);

以上两个函数是构成函数重载的,一个是 int 类型,一个是 int 引用类型,类型不同而构成重载,但如果有:func(1) 这样的调用会导致调用不明确。

引用做函数返回参数

对比引用返回和传值返回:

int func()
{
    static int a = 0;
    ++a;
    
    return a;
}

int& func()
{
    static int a = 0;
    ++a;
    
    return a;
}

对于传值返回,a 只会随着调用次数的增加而变大:

std::cout << func() << " ";
std::cout << func() << " ";
std::cout << func() << " ";

输出结果为:1 2 3 ,无法通过其它手段更改 a 的值。

在传引用返回时,可以直接对 a 进行更改:

func() += 100;

std::cout << func() << " ";
std::cout << func() << " ";
std::cout << func() << " ";

输出结果为:102 103 104

首先观测传值返回的汇编代码:

//程序代码如下:
int func()
{
    static int a = 0;
    ++a;

    return a;
}

int main()
{
    int ret = func();

	return 0;
}
int func()
{
;函数入栈操作
00007FF7BE232250  push        rbp  
00007FF7BE232252  push        rdi  
00007FF7BE232253  sub         rsp,0E8h  
00007FF7BE23225A  lea         rbp,[rsp+20h]  
00007FF7BE23225F  lea         rcx,[__385AFBA2_test@cpp (07FF7BE243068h)]  
00007FF7BE232266  call        __CheckForDebuggerJustMyCode (07FF7BE2313E8h)  
;入栈完成

    static int a = 0;
    ++a;
00007FF7BE23226B  mov         eax,dword ptr [a (07FF7BE23D170h)]  
00007FF7BE232271  inc         eax  
00007FF7BE232273  mov         dword ptr [a (07FF7BE23D170h)],eax  

    return a;

00007FF7BE232279  mov         eax,dword ptr [a (07FF7BE23D170h)]  ;将 a 的值移入寄存器 eax
}
;函数出栈操作
00007FF7BE23227F  lea         rsp,[rbp+0C8h]  
00007FF7BE232286  pop         rdi  
00007FF7BE232287  pop         rbp  
00007FF7BE232288  ret 
;出栈完成

int main()
{
00007FF7BE230FD2  push        rdi  
00007FF7BE230FD3  sub         rsp,0E8h  
00007FF7BE230FDA  lea         rbp,[rsp+20h]  
00007FF7BE230FDF  lea         rcx,[__385AFBA2_test@cpp (07FF7BE243068h)]  
00007FF7BE230FE6  call        __CheckForDebuggerJustMyCode (07FF7BE2313E8h)  
    int ret = func();
00007FF7BE230FEB  call        func (07FF7BE231253h)  
00007FF7BE230FF0  mov         dword ptr [ret],eax ;将 eax 的值给 ret 完成函数调用

	return 0;
00007FF7BE230FF3  xor         eax,eax  
}
00007FF7BE230FF5  lea         rsp,[rbp+0C8h]  
00007FF7BE230FFC  pop         rdi  
00007FF7BE230FFD  pop         rbp  
00007FF7BE230FFE  ret  

以上操作可知,在传值返回时,返回的是 a 的临时拷贝 eax,也称之为临时变量。

传引用返回时(去除了函数入栈和出栈的代码,若有需求见传值返回情况):

//程序代码如下:
int& func()
{
    static int a = 0;
    ++a;

    return a;
}

int main()
{
    int& ret = func();

	return 0;
}
int& func()
{
    static int a = 0;
    ++a;
00007FF6A1D81E5B  mov         eax,dword ptr [a (07FF6A1D8D170h)]  
00007FF6A1D81E61  inc         eax  
00007FF6A1D81E63  mov         dword ptr [a (07FF6A1D8D170h)],eax  

    return a;
00007FF6A1D81E69  lea         rax,[a (07FF6A1D8D170h)]  ;将 a 的地址给了 rax (lea = load effective address)
}

int main()
{ 
    int& ret = func();
00007FF7374D204B  call        func (07FF7374D145Bh)  
00007FF7374D2050  mov         qword ptr [ret],rax  ;直接将 rax 存储的地址给了 ret,使得 ret 可以更改这个地址中的值

	return 0;
00007FF6A1D82055  xor         eax,eax  
}

以上操作可知,传引用返回没有发生临时拷贝,而是直接把 a 的地址传回来(如果表层来讲,就是把 a 的引用传回来给了 ret 这个别名),ret 接收到这个地址并可对其值实施更改(如果表层来讲,ret 就是 a 的别名),这也揭示了引用的底层实现是靠指针完成的。

如果将以上代码替换为:

int* func()
{
    static int a = 0;
    ++a;

    return &a;
}

int main()
{
    int* ret = func();

	return 0;
}

会发现其汇编代码和传引用返回一模一样

int* func()
{
    static int a = 0;
    ++a;
00007FF757AA1E5B  mov         eax,dword ptr [a (07FF757AAD170h)]  
00007FF757AA1E61  inc         eax  
00007FF757AA1E63  mov         dword ptr [a (07FF757AAD170h)],eax  

    return &a;
00007FF757AA1E69  lea         rax,[a (07FF757AAD170h)]  
}

int main()
{
    int* ret = func();
00007FF757AA204B  call        func (07FF757AA1460h)  
00007FF757AA2050  mov         qword ptr [ret],rax  

	return 0;
00007FF757AA2054  xor         eax,eax  
}

故更进一步揭示了指针和引用的紧密关系。同时,会发现返回值为指针时和传引用返回一样没有发生临时拷贝,故C++ 一旦发现返回值是指针或引用便不会生成临时拷贝,哪怕需要的正确返回是 int**,而你返回了 int* 也不会发生临时拷贝。

当然,以此衍生出的问题需要提点一些,例如:

传引用返回后用 int 类型接收会如何?这个过程相当于:

int a = 0;
int& ra = a;
int b = ra;

很明显,b所得到的是 a 的值,进行了赋值操作而已。而 int& 和 int 不是同一类型,编译器会发生隐式类型转换,在这一过程中,底层实际上是对 ra 地址解引用获得所指向的值,然后赋值给临时变量 eax(或许,反正是某一寄存器),再由 eax 赋值给 b。

在进行类似这样的比较时:

int a = 1;
double b = 1.11;

if(b > a) {std::cout << "1";}

比较时 a 就会发生隐式类型转换,和 b 比较的实际上是 a 所赋值的 double 类型的临时变量。

如果:

int func() 
{
    int a = 521;
    return a;
}

int main()
{
    //int& ra = func(); //error
    const int& ra = func();
    
    return 0;
}

尽管这是一个错误代码(func 结束后 a 已销毁),但可以揭示:临时变量具有常性


常引用权限问题

const int a = 0;
int& b = a; // error

此代码编译不通过,a 是一个常变量,而 b 是对变量的引用,这样会导致权限放大,是禁止的。

const int a = 0;
const int& b = a;

此代码编译通过,a 是常变量,b 是对常变量的引用,这属于权限的平移,是允许的。

int a = 0;
const int& b = a;

此代码编译通过,a 是变量,b 是对常变量的引用,这属于权限的缩小,是允许的。(更改 a 的值也会更改 b,只是通过 b 无法更改 a)

权限是可以被平移和缩小的,权限不能放大。


总结

  • 引用不可以改变引用对象。
  • 一个变量或对象可以有多个引用。
  • 引用在定义时必须初始化。
  • 不同类型不可以引用。
  • 权限不允许放大,可以缩小和平移。
  • 临时变量具有常性。

补充说明

  • 不对比指针和引用的区别,总结引用相关特性后即可明白,无需赘述。