1.引用
1.1 引用的概念
引用不是新定义一个变量,而是给已存在的变量取一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用一块内存空间。
比如:李逵,在家称为铁牛,江湖人称黑旋风;这三个名字都代表了同一个人;
使用方法:
类型& 引用变量名(对象名)= 引用实体;
int main()
{
int a = 10;
int& ra = a;
cout << a << endl;
cout << ra << endl;
return 0;
}
注意:引用类型必须和引用实体必须是同种类型;
1.2 引用特性
1.引用时必须初始化
int main()
{
int a = 10;
//错误示范
//int& ra;
//正确使用
int& ra = a;
return 0;
}
2.一个变量可以有多个引用
int main()
{
int a = 10;
//都是a的别名
int& ra = a;
int& rra = ra;
return 0;
}
3.引用一旦引用一个实体,就不能引用其他实体
int main()
{
int a = 10;
int b = 20;
int& ri = a;
//并不是将ri修改为b的别名
//而是将b赋值给ri,也就是赋值给a
ri = b;
return 0;
}
2.3 引用的使用场景
1.做参数
//left和right皆是实参的别名,可以直接对实参进行修改
void swap(int& left, int& right)
{
int tmp = left;
left = right;
right = tmp;
}
做参数来代替指针的部分功能:在用C语言实现链表的头插时,我们需要传递二级指针来实现对头节点指针的修改;现在我们可以用引用来代替二级指针,如下:
//C语言实现
//单链表的尾插
void SListPushBack(SListNode** pphead, SLTDataType val)
{
assert(pphead);
SListNode* new_node = BuySListNode(val);
//链表为空
//二级指针的主要用处
if (*pphead == NULL)
{
*pphead = new_node;
return;
}
//链表不为空
SListNode* tail = *pphead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = new_node;
}
//用C++实现
//单链表的尾插 使用引用代替二级指针
void SListPushBack(SListNode*& phead, SLTDataType val)
{
assert(phead);
SListNode* new_node = BuySListNode(val);
//链表为空
if (phead == NULL)
{
//修改引用就是修改实参
phead = new_node;
return;
}
//链表不为空
SListNode* tail = phead;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = new_node;
}
2.做返回值
//传值返回
int Count()
{
static int n = 0;
n++;
return n;
}
int main()
{
int ret = Count();
return 0;
}
//两个函数不构成函数重载
//传引用返回
int& Count()
{
static int n = 0;
n++;
return n;
}
int main()
{
int ret = Count();
return 0;
}
上述两个函数的区别,我们用图给出:
可以看到,传引用返回与传值返回的区别就在于传引用返回的返回值不需要开辟空间,而传值返回需要;
上述代码中,如果去掉static关键字,传引用返回会有什么变化吗?
答案是:肯定会有变化!当去掉static关键字后,变量n就存在于栈区中,当函数调用结束后栈帧自动销毁,存储n的空间也会被回收,我们也失去了访问它的权限(有的编译器能访问,有的会报错);它的数据可能被其他栈帧所覆盖,也有可能被操作系统初始化为随机值,也有可能不变,但可以肯定的是它的数据值是不确定的!
内存空间销毁意味着什么?
1.空间还在吗?当然还在,只是使用权已经被回收,我们的数据不被保护;
2.我们还能访问吗? 能!但是确是越界访问!读写的数据也是不确定的!
下面举个例子:当函数栈帧销毁后,数据被其他栈帧所覆盖
结论:
当出了作用域后,返回变量不存在了,就不能使用引用返回,因为引用返回的结果是未定义的;出了作用于后,返回变量仍存在,才能使用引用返回;
引用返回不仅能减少拷贝,提高效率;还能用来修改返回值
例如:修改顺序表中的偶数
//查看顺序表元素个数
int SeqListSize(SeqList* psl)
{
return psl->size;
}
//返回指定下标元素
SLDataType& SeqListAt(SeqList* psl, size_t pos)
{
assert(psl);
assert(psl->size > pos);
return psl->a[pos];
}
#include"SeqList.h"
int main()
{
SeqList sl;
SeqListInit(&sl);
SeqListPushBack(&sl, 1);
SeqListPushBack(&sl, 2);
SeqListPushBack(&sl, 3);
SeqListPushBack(&sl, 4);
for (int i = 0; i < SeqListSize(&sl); i++)
{
if (SeqListAt(&sl,i) % 2 == 0)
SeqListAt(&sl, i) *= 2;//通过引用修改元素
}
SeqListPrint(&sl);
return 0;
}
2.4 常引用
引用同指针一样,都有权限的大小,当你使用const修饰引用时,权限就会变小;
int main()
{
//正确
const int c = 30;
const int& rc = c;//权限平移
//正确
int a = 10;
const int& ra = a;//权限缩小
//错误
const int b = 20;
//int& rb = b;//权限放大
return 0;
}
思考下,下面代码能否正确运行,如果不行,为什么?
int Add(int left ,int right)
{
return left + right;
}
int main()
{
int& ret = Add(1, 2);
return 0;
}
当我们运行后,发现无法正确运行!它报出的错误是:非常量引用的初始值必须是左值;
为什么会这样呢?还记得函数返回值是返回一个临时变量这个概念吗?临时变量其实就是一个常量值,常量值无法被修改,其权限是被缩小的;所以我们就需要缩小引用本身的权限来接收这个临时变量,所以我们就得使用常引用!
正确玩法:
int Add(int left ,int right)
{
return left + right;
}
int main()
{
const int& ret = Add(1, 2);
return 0;
}
再思考一个问题,引用能否初始化成常量?
这个问题和前面那个如出一辙,常量无法被改变,所以必须缩小引用权限才能接收,必须使用常引用;
//错误写法
int & a = 10;
//正确写法
const int& a = 10;
在写函数参数时,大部分都会使用const修饰,因为缩小权限,能接收不同权限的实参;如果不用const修饰,就只能接收不被const修饰的实参;
注意:
引用本身权限的缩小,引用的这个变量的本身权限不变;(即我引用你,我的权限缩小,你的权限不变);
int main()
{
int a = 10;
const int& ra = a;//权限缩小
//ra++;//error
a++;//ok
return 0;
}
结论:
引用的权限同指针一样,只能缩小或者平移,不能被放大;
如果所引用的变量为常量,就得使用常引用;
如果所设计的函数不需要修改实参的值,尽可能多的使用常引用参数,因为其可以接收不同权限的实参;
2.5 引用和指针的区别
在语法上,引用只是一个别名,没有独立的空间,与其引用本体共用一块空间;
但在底层实现上,引用是有空间的,因为引用是按指针的实现方式来实现的;
从上图可以看到,指针和引用在汇编中,其实现方式是一摸一样的,这也就说明了引用和指针在底层实现方式上是一致的;
指针和引用的不同点:
1.引用概念上定义一个变量的别名,指针存储一个变量地址;
2.引用在定义时必须初始化,指针没有要求;
3.引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体;
4.没有NULL引用,但有NULL指针;
5.在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节大小(4或8);
6.引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小;
7.有多级指针,但是没有多级引用;
8.访问实体方式不同,指针需要显式解引用,引用编译器自己处理;
9.引用比指针使用起来相对更安全;