四、使用场景
1.做参数
引用可以做参数,可以代替指针的作用
如Swap函数,指针的写法如下,有多处使用"*"解引用
1. //指针写法 2. void Swap(int* left, int* right) 3. { 4. int temp = *left; 5. *left = *right; 6. *right = *left; 7. }
如果用引用做参数,不需要解引用,不需要使用"*"
1. //引用写法 2. void Swap(int& left, int& right) 3. { 4. int temp = left; 5. left = right; 6. right = left; 7. }
2.做返回值
先了解传值返回,再了解传引用返回
(1)传值返回
传值返回,返回的是返回对象c的拷贝,不拿c做返回值,c只是Add的临时变量,Add函数调用结束后,c就不在了,因此不能返回c,返回的是临时变量。
1. #include<iostream> 2. using namespace std; 3. 4. int Add(int a, int b) 5. { 6. int c = a + b; 7. return c; 8. } 9. 10. int main() 11. { 12. int ret = Add(1, 2); 13. 14. return 0; 15. }
如何证明产生了这个临时变量?如下,使用ret接收返回值
1. #include<iostream> 2. using namespace std; 3. 4. int Add(int a, int b) 5. { 6. int c = a + b; 7. return c; 8. } 9. 10. int main() 11. { 12. int& ret = Add(1, 2); 13. 14. return 0; 15. }
但编译不通过
根据常引用声明,这是权限被放大导致的编译错误。c没有使用const进行修饰,ret也没有使用const进行修饰,为什么会导致权限放大呢?这是由于中间产生了临时变量,临时变量具有常性,不可修改。
所以ret也应该被const修饰,传引用返回的c的引用,即临时变量的名称,也是c的别名,把c的别名给了ret。
1. #include<iostream> 2. using namespace std; 3. 4. int Add(int a, int b) 5. { 6. int c = a + b; 7. return c; 8. } 9. 10. int main() 11. { 12. cosnt int& ret = Add(1, 2);//用const修饰引用 13. 14. return 0; 15. }
传值返回生成的临时对象是需要返回的对象的拷贝:
①如果需要返回的对象比较小,一般会使用寄存器存储这个临时对象
②如果这个对象比较大,这个临时对象通常会存在于上一个函数的栈帧中。假如c比较大,那么临时对象会存在main函数中。
(2)传引用返回
传引用返回,返回的是返回对象c的引用(别名),当调用完Add函数,return返回后,Add的栈帧被销毁了,但是它使用的内存空间还在,只是使用权不属于它了,所以再去访问那块内存空间时,内存空间有可能没被清空,也有可能被清空了,因此有可能是以前的值,也有可能是随机值,所以代码的运行结果是不确定的。
1. #include<iostream> 2. using namespace std; 3. 4. int& Add(int a, int b) 5. { 6. int c = a + b; 7. return c; 8. } 9. 10. int main() 11. { 12. int& ret = Add(1, 2); 13. Add(5, 7); 14. 15. cout << "ret = "<<ret << endl; 16. 17. return 0; 18. }
当调用Add(5,7) 时,调用的是同一个函数,在同一位置上再建立一个栈帧,栈帧大小是一样的,c的位置也一样,仅仅只是把c的位置的内容由3改成了12,因此ret的值也为12。
但如果调用Add(1,2) 后,下次调用的不是Add函数,是其他函数,那么c的位置的值会被改成其他内容。比如调用Add(1,2)后,再打印HelloWorld,那么会打印随机值。
1. #include<iostream> 2. using namespace std; 3. 4. int& Add(int a, int b) 5. { 6. int c = a + b; 7. return c; 8. } 9. 10. int main() 11. { 12. int& ret = Add(1, 2); 13. printf("Hello world\n"); 14. 15. cout << "ret = "<<ret << endl; 16. 17. return 0; 18. }
因此实际中,如果出了函数作用域,返回对象就不存在了,不能用引用返回。如果非要用引用返回,就要使用static关键字,延长变量的生命周期,Add函数调用完毕,栈帧销毁,但是静态区不会销毁,栈帧销毁对函数局部变量没有影响。
1. #include<iostream> 2. using namespace std; 3. 4. int& Add(int a, int b) 5. { 6. static int c = a + b;//栈帧销毁对c没有影响 7. return c; 8. } 9. 10. int main() 11. { 12. const int& ret = Add(1, 2); 13. Add(5, 7); 14. 15. cout << "ret = " << ret << endl; 16. 17. return 0; 18. }
什么时候使用传值返回,什么时候使用传引用返回呢?
如果函数返回时,出了函数作用域,如果返回对象还在(还没还给操作系统) ,可以使用引用返回,如果已经还给系统了,必须要用传值返回。
传值传参和传值返回、传引用传参和传引用返回
TestFunc1传值,TestFunc2传引用
1. #include<iostream> 2. #include<time.h> 3. using namespace std; 4. 5. struct A 6. { 7. int a[10000]; 8. }; 9. 10. void TestFunc1(A a) 11. { 12. } 13. 14. void TestFunc2(A& a) 15. { 16. } 17. 18. void TestRefAndValue() 19. { 20. A aa; 21. 22. //以值作为函数参数 23. size_t begin1 = clock(); 24. for (size_t i = 0; i < 10000; i++) 25. { 26. TestFunc1(aa); 27. } 28. size_t end1 = clock(); 29. 30. //以引用作为函数参数 31. size_t begin2 = clock(); 32. for (size_t i = 0; i < 10000; i++) 33. { 34. TestFunc2(aa); 35. } 36. size_t end2 = clock(); 37. 38. //分别计算两个函数运行结束后的时间 39. cout << "TestFun1(A)-time:" << end1 - begin1 << endl; 40. cout << "TestFun2(A)-time:" << end2 - begin2 << endl; 41. 42. } 43. int main() 44. { 45. TestRefAndValue(); 46. return 0; 47. }
传值传参和传值返回:在传参和返回期间,形参是实参的一份拷贝,返回变量是变量本身的一份拷贝,实参和变量本身占多大空间,形参和返回变量就占多大空间,在函数内操作副本,对该变量的修改并不会修改函数外部的变量。
传引用传参:形参和实参是同一个东西,实际操作的就是该变量,由于引用是指向某个变量的,对引用的操作其实就是对他指向的变量的操作,因此函数内对变量进行修改的话,外部变量也会被相应修改。
如果不希望修改变量的值,需要选择传值而不是传引用。但传引用往往比传值效率要高。
五、引用和指针
1. int a = 10; 2. 3. //引用:在语法上,这里给a这块空间取了一个别名,没有开辟新空间 4. int& ra = a; 5. ra = 20; 6. 7. //指针:在语法上,这里定义了一个指针变量,开了4个字节空间,存储a的地址 8. int* pa = &a; 9. *pa = 20;
1. 引用是别名,指针是地址
2. 引用在定义时必须初始化,指针没有要求
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体,指针可以被重新赋值以指向另一个不同的变量。
4. 没有NULL引用,但有NULL指针
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占 4个字节)
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7. 有多级指针,但是没有多级引用
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
9. 引用比指针使用起来相对更安全。 这是由于,从编译上看,程序在编译时分别将指针和引用添加到 符号表上,符号表上记录变量名、变量地址。指针变量在符号表上对应的地址值为指针变量的地址,而 引用在符号表上对应的地址值为 引用对象的地址值。符号表生成后就不会再更改,指针可以改变指向的对象(指针变量中的值可以改),而引用对象不能改(第(3)条)。因此引用比指针更安全。如:
1. int a= 10; 2. int b = 30; 3. int*& rpa = pa;//rpa是pa即int*的别名,此时pa指向a 4. rpa = &b;//rpa作为pa的引用,rpa的值被修改了,因此pa的值也被修改了,现在rpa和pa的值都为b的地址 5. 6. printf("&a = %p\n", &a); 7. printf("&b = %p\n", &b); 8. printf("rpa = %p\n", rpa); 9. printf("pa = %p\n", pa);