2.2 使用场景:
做参数:
引用和指针在简单传参(后续有复杂的自定义对象传参,效果更加明显)是的对比.
做返回值:
(1)我们之前的一般返回是这样的.
//普通返回 int test() { static int a = 0; a += 5; return a; } int main() { int& c = test(); cout << c << endl; return 0; }
(2)改用引用作为返回值后:
//引用返回 int& test()//注意看返回值的类型 { static int a = 0; a += 5; return a;//返回的是a的别名 } int main() { int& c = test();//此时c是作为别名接收返回值 cout << c << endl; return 0; }
图解:
引用作为返回值的写法减少了拷贝,所以明显效率更高.
那以后我们都用引用作为返回值吗?(友情提示引用虽好,可不要贪杯哦!);
我们看一下下面的情况:
//引用返回 int& test() { int a = 0; a += 5; return a; } int add(int a, int b) { int sum = a + b; return sum; } int main() { int& c = test(); add(22, 44); cout << c << endl; return 0; }
结果:
66
c 是函数test()的返回值,我们打印的是c,为啥结果确实add(22,44)的返回值,是巧合吗?
这就要考虑所在环境了,还是画图比较好理解,上图解!
如果引用做返回值时,返回的空间是被系统收回的,那就很危险.
引用作为返回值的时候,可以修改返回值,或者获取返回值,而不是获取返回值的拷贝(临时变量).
(1)普通返回:
//普通返回 int test() { static int a = 0; a += 5; return a; } int main() { int c = test(); c = 95;//对c不会影响a,因为它只是a的拷贝返回 cout << test() << endl;//调用了两次,所以结果为10 return 0; }
运行结果:
10
(2)引用返回:
//引用返回 int& test() { static int a = 0; a += 5; return a; } int add(int a, int b) { int sum = a + b; return a + b; } int main() { int& c = test(); c = 95;//对c修改,就是对a修改. cout << test() << endl; return 0; }
运行结果:100
可以通过调试发现,test返回的是a的别名,所以c是a的别名的别名.
小结:
1.引用做参数基本上所有场景都可以.
(1)方便做输出型参数(例如:swap()函数),可以用传引用改变实参.
(2)传参是以别名的形式,中间没有拷贝,可以提高效率.
2.引用做返回值时需要特别注意,如果出了作用域,对象空间被系统收回了,就不能用引用返回.其它情况建议用引用返回,可以减少拷贝,提高效率.
(1)同样,可以减少拷贝,提高效率.
(2)可以修改返回值,或者是获取返回值.(后续会遇到这种情况).
2.3 常引用?
(1)权限放大:
从只读–>可读,可写,权限放大会报错.因为不安全.
//情况1 const int a = 6; //错误写法 int& ra = a; //该语句编译时会报错,因为a变量具有常性,而ra是可读可写,权限不能放大. //正确写法: const int& ra = a;//权限的平移 //情况2 //错误写法 int& b = 5; // 该语句编译时会出错,b为常量 //正确写法 const int& b = 5;//权限的平移 //情况3 double d = 13.14; //错误写法 int& rd = d; // 这里会发生隐式类型转换,而产生的临时变量具有常性. //正确写法 const int& rd = d; //情况4 int Test_Const() { int x = 2, y = 3; int sum = x + y; return sum; } int main() { //错误写法 int& ret=Test_Const();//返回值是临时变量(因为函数栈帧被销毁了,需要借助寄存器,或者别的产生临时变量返回)的拷贝,临时变量具有常性. //正确写法 const int& ret = Test_Const(); return 0; }
情况3的特别说明:隐式转换(操作数两边数据类型不同时,要保证数据两边的类型不变,需要借助临时变量).
(2)权限缩小:
从可读可写–>只读,权限缩小,更加安全.
int& Test_Const() { int x = 2, y = 3; int sum = x + y; return sum; } int main() { //权限平移 int& ret = Test_Const();//注意Test_Const函数的返回值是sum的别名,并不是具有常性的临时变量,所以这里不会报错 //缩小 const int& ret = Test_Const();//从可读可写-->只读,权限缩小,更加安全,不会报错. return 0; }
小结:
权限可以平移和缩小,但是不可以放大,使用时需要注意.
2.4 从底层探究引用和指针.
示例代码:
int main() { int a = 4; //从语法上看,引用不开空间,而是用别名直接对a操作 int& b = a; b = 5; //从语法看,指针需要开空间存储a的地址,然后通过地址去找a int* p = &a; *p=20; return 0; }
我们通过调试窗口,打开反汇编窗口,观察汇编代码:
我们发现,在底层引用和指针的实现逻辑是一样的.都需要开空间,但是在语法上,我们依旧认为它是没有开空间的.
就好比:
老婆饼里面没有老婆,红烧狮子头里面没有狮子,娃娃菜里面没有娃娃.
- 引用语法概念上定义一个变量的别名,而指针是存储一个变量地址。
- 引用在定义时必须初始化,否则编译器不知道是谁的“别名”,而指针没有规定,只不过我们习惯性初始化为NULL而已.
- 引用在初始化时引用一个实体后,就不能再引用其他实体(上面有讲到),而指针可以在任何时候指向任何一个同类型实体,并不受限制.
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 对引用的赋值,就是对引用实体进行修改,而指针+1或者赋值则是对实体的地址编号操作,并不会影响实体.
- 不存在多级引用,但是可以多个引用可以引用一个实体,即一个实体有多个别名.指针可以有多级指针.
- 访问实体方式不同,指针需要显式解引用,引用编译器会自己转换处理.
- 安全性:引用比指针使用起来相对更安全 ,好歹不存在空指针吧!😂😂😂
三、重新认识一下auto关键字
3.1 auto关键字的介绍
在c语言中:
auto是C语言的一个关键字,关键字主要用于声明变量的生存期为自动,这个关键字不怎么多写,因为所有的局部变量默认就是auto的。
int a=0;//默认就是自动生存期 //等价于下面的 auto int a=0;//写成这样也太麻烦了,我们一般直接省略不写.
C语言中提供了存储auto,register,extern,static说明的四种存储类别。四种存储类别说明符有两种存储期:自动存储期和静态存储期。
自动存储期:
auto和register。自动变量指在局部创建该变量,然后出了局部的作用域,该变量的声明周期就结束了,还给操作系统了.
静态存储周期:
extern,static
C++赋予了auto新的“生命”:
C++11标准中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型
指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得.
🌰栗子
#include <stdlib.h> # include <iostream> using std::cout; using std::cin; using std::endl; int main() { int a = 3; double b = 4.1; auto c = a + b;//会自动推导出c的类型 cout << c << endl; cout << typeid(c).name() << endl;//typeid(c).name()会打印变量c的类型,后续会介绍,这里了解一下就行. return 0; }
这样看似乎auto关键字的作用不是很大,我们可以直接写double c,那是因为没有遇到复杂的场景,试着看一下下面这段代码(我们暂时不需要看懂代码的作用,只需要关注类型名即可).
#include <string> #include <map> int main() { std::map<std::string, std::string> m{ { "name", "名字" }, { "age", "年龄" }, {"sex","性别"} }; std::map<std::string, std::string>::iterator it = m.begin(); while (it != m.end()) { //.... } return 0; }
由于在工程中很多情况下,我们不能展开头文件,难免会遇到这样的类型:(上面代码的it的类型)
std::map<std::string, std::string>::iterator
使用宏替换可以吗?
可以是可以,但是宏替换的缺点你是否能接受?
- 没有安全检查,直接进行替换.
- 可读性很差,也不方便调试.
那是否有小伙伴想到使用typedef进行类型重定义呢?
其实typedef也是有缺点的.
例如:
typedef char* pchar; int main() { char arr[] = "cjn"; //const pchar p1;//此语句报错,这里是指向不可改变的常量指针,定义时需要初始化 const pchar p1=arr; *p1 = 'x';//指向不可以改变,但指向的内容可以. printf("%s\n", arr); char* parr =arr;//定义一个字符指针 const pchar* p2=&parr;//指向字符指针的地址 **p2 ='a';//解引用后的指针与p1类型一样,指向的内容可以改 //*p2 = NULL;//但是指针指向不能改 printf("%s\n", arr); return 0; }
const pchar等价于char* const表示指针的指向 不能改变.
因为此处类型pchar相当于是类型char* 的别名,const pchar表示const修饰的是char* ,即char*指针的的指向不能被修改,但是其指向的内容可以修改.
const pchar*的类型等价于 char * const* .
typedef有的场景很容易让我们误解类型.