下面是几个关于引用返回局部变量的代码,我们来进行分析:
int& func() { int n = 0; n++; return n; } int main() { int ret = func(); cout << ret << endl; return 0; }
这个函数用了引用返回,返回的是n的别名,但是n是局部变量,当函数调用结束后,n所在的空间会被销毁以及归还了它的使用权
所以 ret的值是不确定的
如果func函数结束,栈帧销毁,没有清理栈帧,ret的值是侥幸正确的
如果func函数结束,栈帧销毁,清理了栈帧,ret的值是随机的
下面再看一个错误案例:
int& func() { int n = 0; n++; return n; } int main() { int& ret = func(); cout << ret << endl; return 0; }
1
这里ret就是n的别名,函数调用完毕后,函数被销毁,n的空间就被归还给操作系统了
而这里ret还指向原先n的空间,如果此时什么函数都不调用,输出的ret还是正确的值,因为原先的栈帧没被其他函数栈帧覆盖
如果调用了其他函数,原先的栈帧被其他的函数栈帧覆盖,此时的ret就是随机值了
结果如下图:
分析完了几个常见的错误,下面来看一下引用返回的效率
struct A { int a[10000]; }; A a; // 值返回 A Func1() { return a; } // 引用返回 A& Func2() { return a; } void Test() { int begin1 = clock(); for (int i = 0; i < 100000; ++i) Func1(); int end1 = clock(); int begin2 = clock(); for (int i = 0; i < 100000; ++i) Func2(); int end2 = clock(); cout << "值返回" << end1 - begin1 << endl; cout << "引用返回" << end2 - begin2 << endl; } int main() { Test(); }
引用返回还有一个作用就是:修改返回值
在C语言实现的顺序表中,我们如果想修改某位置的值,是需要调用函数的,可能是先通过一个查找函数找到要修改值的位置,然后再调用修改函数,对值进行修改
大致的代码如下:
struct SeqList { int* a; int size; int capacity; }; int find(SeqList* plist, int x) { for (int i = 0; i < plist->size; i++) { if (plist->a[i] == x) return i; } } void modify(SeqList* plist, int pos, int x) { plist->a[pos] = x; } int main() { SeqList list; //查找5所在的下标,然后进行修改 int pos = find(&list, 5); modify(&list, pos, 1); }
这样需要调用2次函数,不是很方便
而在C++中,可以通过引用返回解决这个问题
因为顺序表一般都是动态开辟在堆区的,所以使用引用返回完全没有问题
struct SeqList { int* a; int size; int capacity; }; int& AT(SeqList& list, int x) { for (int i = 0; i < list.size; i++) { if (list.a[i] == x) return list.a[i]; } } int main() { SeqList list; AT(list, 5) += 8;//利用引用可以直接修改 }
总结:
引用传参使用于输出类参数,它也可以提高效率,尤其是深拷贝和大对象
引用返回适用于:1.提高效率(深拷贝和大对象) 2.修改返回值
并且
基本任何场景都可以使用引用传参
谨慎使用引用作为返回值。出了作用域,对象还在,可以使用引用返回,否则不可以使用引用返回。
常引用
const int & = a这种引用被const修饰的叫做常引用 const int a = 10; int &b = a;
1
2
上面的语句编译时会出错,从2种角度可以进行解释:
1.b是a的别名,b的改变会改变a,不符合const的修饰,所以出错
2.引用的过程中,权限不能放大,可以缩小、平移
所以const int a = 10; const int &b = a;才是正确的
根据上面2种解释,我们再来看看接下来的语句:
int a = 10; const int &b = a;//可以成功编译,这里是权限的缩小 const int a = 10; const int &b = a;//权限平移 const int& a = 10;//10是常量,所以要用const接受 int a = 10;//错误,权限放大
我们来观看下面的语句:
const int a = 10;
int b = a;
这个是不是权限放大呢?
答案是:不是
这也可以用2种角度解释:
1.b的改变不会影响a
2.这只是一个拷贝,无权限问题
int main() { double a = 1.11; int& ra = a;//错误 }
不同类型间转换会产生临时变量,临时变量具有常性,所以应该是:const int& ra = a;
前面也说过,值返回也会产生一个临时变量,这个临时变量也有常性
所以用引用接受传值值返回要加上const修饰
int test() { static int n = 10; return n; } int main() { const int& ret = test(); }
引用和指针
在语法层面上,引用就是一个别名,它没有独立的空间,和其他引用公用一个空间
在底层实现上实际是有空间的,引用是通过指针来实现的
int main() { int a = 10; int& ra = a; ra = 20; int* pa = &a; *pa = 20; }
我们看一下引用和指针的汇编代码对比:
对比可以看出:引用和指针在汇编上没有任何区别
这就说明引用是类似指针的方式实现的
这同时也说明了前面提到过的:传引用和传指针效率基本相同
引用和指针的不同:
1.引用概念上定义一个变量的别名,指针是存储一个变量地址
2.引用在定义是必须初始化,指针不必
3.存在空指针,不存在空引用
4.引用在初始化引用了一个实体后,不同再引用其他实体,指针可以在任何时候指向任何一个同类型实体
5.在sizeof的结果不同,引用结果是引用类型的大小,指针始终是4字节(32位)或8字节(64位)
6.引用自加即实体加1,指针自家即指针向后偏移一个类型大小
7.有多级指针,没有多级引用
8、访问实体方式不同:指针需要解引用,引用编译器自己处理
一道面试题:
关于引用以下说法错误的是( )。(阿里巴巴2015笔试题)
A.引用必须初始化,指针不必
B.引用初始化以后不能被改变,指针可以改变所指的对象
C.不存在指向空值的引用,但是存在指向空值的指针
D.一个引用可以看作是某个变量的一个“别名”
E.引用传值,指针传地址
F.函数参数可以声明为引用或指针类型
答案:A
A、B、C、D、F选项都很容易理解
E选项需要说一下:“引用传值,指针传地址”,第一遍读起来感觉没错误,但其实引用表面好像是传值,其本质也是传地址,只是这个工作有编译器来做,所以错误