引用不是新定义一个变量,而是给已存在的变量取一个别名,编译器不会因为引用变量而开辟内存空间,它和它引用的变量公用同一块空间。
相当于是给被引用的变量取了一个小名,但是相当于是同一个变量。
void TestRef()
{
int a = 10;
int& ra = a;// 定义引用类型
printf("%p\n",&a);
printf("%p\n",&ra);
}
注意:引用类型必须和引用实体是同种类型的数据。
1.交换函数
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 10, b = 20;
Swap(a,b);
cout << "a = " << a << endl << "b = " << b;
}
2.单链表
本来,对于单链表的pushback而言,我们要传入二级指针
typedef struct SListNode
{
SLDataType data;
struct SListNode* next;
}SLnode;
void SLPushBack(SLNode** pphead, SLDateType x)
{
SLNode** pphead, SLDateType x;
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLNode* tail = *pphead;
while(tail -> next != NULL)
{
tail = tail -> next;
}
tail -> next = newnode;
}
}
有了引用,我们就可以这样写
typedef struct SListNode
{
SLDataType data;
struct SListNode* next;
}SLnode;
void SLPushBack(SLNode*& phead, SLDateType x)
{
if (phead == NULL)
{
phead = newnode;
}
else
{
SLNode* tail = phead;
while(tail -> next != NULL)
{
tail = tail -> next;
}
tail -> next = newnode;
}
}
1.引用在定义时必须初始化
void TestRef()
{
int a = 10;
// int&ra ;//该条语句编译时会出错
int& ra = a;
int& rra = a;
}
2.一个变量可以有多个引用
还可以给别名再取别名,这样,所有的名字其实都指的是同一个变量。
3.引用一旦引用一个实体,再不能引用其他的实体
void TestConstRef()
{
const int a = 10;
//int& ra = a; //该语句编译时会出错,因为a为常量
const int& ra = a;
//int& b = 10; //该句编译时会出错,因为b为常量
const int& b = 10;
double b = 12.34;
//int& rd = d; //该语句编译时会出错,因为类型不同
const int& rd = d;
}
正常情况下的传值返回:
int Count()
{
int n = 0;
n++;
return n;
}
int main()
{
int ret = Count();
cout << ret << endl;
}
如果将代码改成
int& Count()
{
int n = 0;
n++;
return n;
}
int main()
{
int ret = Count();
cout << ret << endl;
cout << ret << endl;
}
此时,相当于是Count函数的返回值是n的一个引用
虽然短期内可以返回正确的值
但是,我们知道,出了Count函数后,n变量就会被销毁,此时我们利用它的引用取访问它,其实就相当于之前的野指针问题,会有很大的安全问题。
如果将代码再改成下面的情况:
int& Count()
{
int n = 0;
n++;
return n;
}
int main()
{
int& ret = Count();
cout << ret << endl;
cout << ret << endl;
}
相当于ret和返回值都是n这个变量
第二次访问的时候出现了随机值。
因为cout输出本身也是调用了函数,在Count函数调用完后,它的栈帧销毁了(其中也包括n的那块空间),紧接着cout开始调用函数,这时,cout调用的函数的栈帧就可能会将之间的Count所在的栈帧进行覆盖,同理,n所在的那块栈帧就可能会被各种值覆盖,因此,第二次通过n的引用访问n时,就会有可能得到一个随机值。
错误示范
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1,2);
Add(3,4);
cout << "Add(1,2) = " << ret << endl;
return 0;
}
同样的道理,在不清栈帧的情况下,Add(1,2)函数执行的时候开辟的栈帧的c所在的空间,会被后面再次调用Add(3,4)时覆盖,得到7。
结论:
如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
1.返回的变量是全局对象(这样出了函数,变量并不会被销毁)
2.返回的对象是静态对象
1.提高效率
2.输出型参数(形参的修改,影响的实参)
1. 提高效率
2.可以修改返回对象:
小例:单链表的访问和修改:
单链表:
struct SepList
{
int *a;
int size;
int capacity;
};
c语言:
//访问第i个位置的值
int SLAT(struct SepList* ps, int i)
{
assert(i < ps->size);
return ps->a[i];
}
//将第i个位置的值修改为x
void SLModify(struct SepList* ps, int i, int x)
{
assert(i < ps-> size);
ps->a[i] = x;
}
c++引用:
//访问或修改第i个位置的值
int& STAT(struct SepList* ps, int i)
{
assert(i < ps -> size);
return ps->a[i];
}
此时,引用返回如果想要修改第i个位置的值,则直接修改即可,因为函数的返回是ps->a[i]的一个引用,所以此函数既可以访问,也可以修改。
STAT(ps,i) = 1;
小例:
int main()
{
const int a = 0;
//权限的放大:本来a变量不可以修改的,但是引用变量b是可以被修改的,所以这里不能这么写
int& b = a;
//权限的平移:这里b和a都不能被修改,所以权限是相同的,可以这样写
const int& b = a;
//权限的缩小:本来a是可以被修改的,但是引用变量b不可以,这样就会把a的权限缩小,但是这样是合法的
int a = 0;
const int& b = a;
return 0;
}
还有一种情况:
int a = 7;
double& b = a;
这样写也是会报错的:
因为int类型的变量赋值给double时,不是直接给的,中间会有一个double类型的临时变量,也就是隐式转换,先将int类型的a转换成double类型,赋值给临时变量,然后将这个临时变量的值给b。
而我们知道,临时变量具有常量性,所以如果这样写,会有权限放大的现象。
如果改为:
int a = 7;
const double& b = a;
就不会报错了。
类似的:
int fun()
{
int a = 0;
return a;
}
int main()
{
int& ret = fun();
return 0;
}
这里也会报错:
因为,fun函数不是直接返回a,而是返回a的一份临时拷贝,也是具有常量性,所以这里的int& ret = fun()也存在权限放大的问题。
但是改为const int& ret = fun()就可以了。
这里的临时变量也会因为这里的引用被自动延长生命周期,直到不再使用为止。
从汇编角度来看,引用其实和指针在底层其实是一种操作,也就是说底层其实没有引用,只有指针。
执行++操作的步骤也是一样的。
1.引用概念上定义一个变量的别名,指针存储了一个变量指针。
2.引用在定义时必须初始化,指针没有要求。
3.引用在初始化时引用一个实体后,就不能再引用其他实体了,而指针可以在任何一个同类型实体
4.没有NULL引用,但又NULL指针。
5.在sizeof中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
6.引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。
7.有多级指针,但是没有多级引用。
8.访问实体方式不同,指针需要显示解引用,引用编译器自己处理。
9.引用比指针使用起来相对更安全。