引用不是定义一个新的变量,而是给已有的变量定义一个别名,编译器不会给引用开辟内存空间,它和它引用的变量共用一块空间。
类型& 引用变量名(对象名) = 引用实体;
比如:
int a = 10;
int& ai = a;
//正如一个人可以取多个外号,实体也可以有多个引用。
int& aii = a;
ai 就是 a的别名,ai 是 a的另一个称号。
所以有很明显的几个规则:
看代码:
输出型参数
可以有效避免实参的拷贝。
void swap(int& x, int& y)
{
int temp = x;
x = y;
y = temp;
}
//一些OJ题中,参数返回值
int* func(int* a, int size, &returnSize)
{}
有效避免二级指针的使用
typedef struct ListNode
{
struct ListNode* next;
int val;
}LTNode, *PLTNode;
//struct ListNode*& phead
void SlistPushBack(PLTNode& phead, int x)
{}
可见,有些指针能实现的地方,引用也能实现。
引用的正确使用:
传值返回
先看一串代码:
int Count()
{
static int n = 0;
n++;
return n;
}
int main()
{
int ret = Count();
return 0;
}
要想清晰的理解这段代码,我们得借助函数栈帧知识。
在图中栈帧往下开辟,静态变量n存储在静态区,Count函数在返回后销毁,销毁之前会为一个临时变量开辟空间,返回的n=1作为一个临时变量放在寄存器中(数据较小将放入CPU的寄存器中,如果数据较大就会在上一个栈帧中开好空间存放),再将寄存器的内容返回给ret。
为什么要返回临时变量?
因为如果是单单普通的Int n 返回,函数中销毁后n也销毁,获取的n也就没有意义了。
引用返回
int& Count()
{
static int n = 0;
n++;
return n;
}
int main()
{
int ret = Count();
return 0;
}
Count栈帧销毁后,由于n存储在静态区,直接返回n的引用,这一过程虽然产生了临时变量但是返回引用所以不开辟空间。
引用的错误使用:
传值返回(这是正确的)
int Count()
{
int n = 0;
n++;
return n;
}
int main()
{
int ret = Count();
return 0;
}
与之前传值相比,n的值存放在栈区随着Count函数销毁也会被销毁,但销毁之前,也会传一个临时变量给ret,这个临时变量开辟空间。
注意:这里的销毁指的不是开辟Count的那块区域被销毁了,而是依然能访问也能修改,只是非法的,并且那块区域的数据已经不受保护了,读取的数据都是不确定的,相当酒店退房一样。
引用返回
这是一个错误示范
int& Count()
{
int n = 0;
n++;
return n;
}
int main()
{
int ret = Count(); //非法访问
return 0;
}
由于引用返回产生的临时变量不开辟空间,那么在n销毁后,返回的n的引用也就构成了非法访问。
总结:
减少拷贝,提高效率,能修改参数
对函数传参,并不会对函数直接传实参,而是会传递一个临时拷贝的形参,尤其当数据量大时,造成效率低下。通过传引用不需要拷贝,很好的解决效率问题。
#include
struct A { int a[10000]; };
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void TestRefAndValue()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 1000000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 1000000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
4w字节的数据,100w次的调用。
减少拷贝,提高效率,能修改参数
由于函数在返回值的时候,需要返回一个开辟空间的临时变量,在数据量足够多的时候,效率是比较低下的,而返回引用值的时候,返回的临时变量不需要开辟内存空间,这就提高了效率,但是得正确使用引用。
#include
struct A{ int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a;}
// 引用返回
A& TestFunc2(){ return a;}
void TestReturnByRefOrValue()
{
// 以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "TestFunc1 time:" << end1 - begin1 << endl; //
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
当数据量或返回次数越多,这个差距也会越大。
const使得变量不能修改,使得变量只有只读权限,如果引用该变量,那么对应的引用类型也必须带上const,权限也只有只读。(权限不能放大)
如果变量有读和写的权限,那么引用的权限可以只读。(权限的缩小)
int main()
{
int a = 0;
// 权限平移
int& ra = a;
// 指针和引用赋值中,权限可以缩小,但是不能放大。
const int b = 1;
//拷贝
a = b;
//权限放大不行
//int& rb = b;
//权限缩小可以
const int& rra = a;
//rra++ //err
a++; //改变rra和a
return 0;
}
const引用避免传参的限制
看代码:
因为权限的问题,在对参数是引用的函数进行传参,需要考虑实参权限是否可以对应。
为了在多种场景下都能使用该函数,许多的函数实现在设计引用参数时都加上了const起到保证作用。
//void Func(int& x){}
void Func(const int& x){}
int main()
{
int a = 0;
// 权限平移
int& ra = a;
// 指针和引用赋值中,权限可以缩小,但是不能放大。
const int b = 1;
//拷贝
a = b;
//权限放大不行
//int& rb = b;
//权限缩小可以
const int& rra = a;
//权限平移
const int& rb = b;
Func(a);
Func(ra);
Func(rra);
Func(b);
Func(10); //下一节会提到为什么可以
return 0;
}
以上的函数调用都能在func(const int&x)中成功。
并且上面两个函数构成函数重载。
const 引用可以初始化引用常量
const int& b = 10;
所以之前Func(10)为什么可以调用,也就解决了。
截断、强转和整型提升都会产生临时变量
double d = 12.34;
cout << (int)d << endl;//打印临时变量
int i = d; //(截断) 实际临时变量12赋予给了i
临时变量有常量的特性
int Count()
{
int n = 0;
n++;
return n;
}
int main()
{
const int& b = 10;
double d = 12.34;
//int& ri = d; //不行
const int& ri = d; //引用临时变量 临时变量具有常性
const int& ret = Count(); //函数返回临时变量
}
在语法概念上引用和本体共用一块空间,指针需要开辟一块内存存储地址。
在底层实现中,有空间的,它们实现都是一样的。本质引用和地址都是传地址,只是引用是由编译器做。
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}
具体不同点:
引用概念上定义一个变量的别名,指针存储一个变量地址。
引用在定义时必须初始化,指针没有要求
没有NULL引用,但有NULL指针
在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
有多级指针,但是没有多级引用
访问实体方式不同,指针需要显式解引用,引用编译器自己处理
引用比指针使用起来相对更安全,