目录
引用的概念与性质:
引用 其实很好理解 通俗的来讲就是给一个东西取一个 别名 就像是我们生活中的 “外号” 一样我们叫一个人的名字他会答应 叫他的外号也会答应
引用不是新定义一个变量,而是给已存在的变量取了一个别名,编译器不会为引用变量额外开辟空间,他和他引用的变量共用一块空间
所以呢 一个变量的别名和他的地址是一样的 一方改变另一方也就跟着变了 所以我们的swap函数也就可以这样写了
void swap(int* p, int* q)//C语言
{
int tmp = *p;
*p = *q;
*q = tmp;
}
void swap(int& p, int& q)//C++
{
int tmp = p;
p = q;
q = tmp;
}
int main()
{
int a = 10, b = 20;
swap(&a, &b);
cout << " a = " << a << " b = " << b << endl;
int c = 10, d = 20;
swap(c, d);
cout << " c = " << c << " d = " << d << endl;
}
这里两个 swap重名 是没有问题的 这个是 c++ 中的函数的重载 两个函数函数名相同 参数的数量,类型,类型的顺序不同 那么他们在一个作用域中是可以同时存在的
还有几个要注意的点:
- 我们的引用在定义的时候必须初始化 不能先定义 后面再初始化
- 一个变量可以有多个引用 就像我们的第一张图片 b c d都是a的引用
- 引用一旦引用一个实体,就不能够引用其他的实体了
- 不能给常量取别名 除非加const修饰
关于第三个点 大家来看这张图片看看是什么意思?
大家 觉得这里的 c=d 是什么意思呢 是变更 c 引用的量 还是给 c 赋值呢?
其实第三个特性已经很显然了 我们引用完一个实体就不能再取引用其他的实体了(好像物品们不能拿 甲同学 的外号 去叫 乙同学) 所以这里的答案就必然是赋值了
注意:C语言中的指针是可以改变所指向的对象 初始化后 还可以更改
当然我们的引用也是可以用到 const 的 大家看一下这下面的几种情况
const int a = 10;
int& b = a;
const int a = 10;
const int& b = a;
int a = 10;
const int& b= a;
大家觉得这里面的代码 那些是不可行的呢? 其实很显然就是一个肯定是不行的 !!
因为 我们对一个变量取别名 别名相对于原引用变量的权限是只能 缩小 不能够 放大 的
像这里的const就是一个减少权限的操作 被const修饰下的变量就变成只读 不能修改了(如果是一个变量有多个指针或者是多个引用 其中一个被const修饰 也仅仅只能代表我们不能通过这个被修饰的指针或引用来改变这个变量 其他的指针或引用还是可以改变他的)
double a = 3.333;
int b = a;
double a = 3.333;
int& b = a;
double a = 3.333;
const int& b = a;
大家看这几个代码有问题嘛?
第一个: 我们知道C语言里面中 如果两个类型不同的变量赋值是会发生 隐形转换 的 此时的a 其实并不是直接就把值给过去的 首先 a 会把它的浮点数部分丢掉后 把剩余的部分给到一个临时变量中 再从这个临时变量把值赋给b
第二个: 我们的引用的初始化的时候 如果遇到这种情况一样也是会产生一个 临时变量 而不能直接把 b 作为 a 的引用是因为 临时变量是具有常性的(被const修饰) 我们一个权限小的怎么能赋值给一个权限更大的变量呢?
注意:这是一个很重要的点 临时变量是具有 常性 的就相当于是这个临时变量是被 const 修饰过的
所以我们的第三个代码就是正确的 那有的人就会问了“为什么引用的部分初始化遇到有临时变量 要用const修饰 可是我们的 int b=a 不用这样修饰呢?”
首先对于这种赋值的操作只有使用 引用和指针 时 一个变量的改变才会影响到另一个变量 而对于这种平常的变量赋值是不需要的 在这里 我们的 变量的值赋值给了b 那么b的改变会影响我们的临时变量嘛 他们是拷贝的关系阿 是不会有影响的
此时我们再来看我们的第三个代码 那么此时我们的 b 就是 临时变量 的 别名 了!!!那么此时我们这个临时变量的生命周期就跟着 b 走了
引用的一般使用场景:
分为:做 性参 和 返回值
做形参:
就好像是我们前面写的 那两个swap函数一样 再来就是:
大家都知道我们的C语言的 scanf("%d", &a ); 那这里的取地址操作就是因为我们的scanf函数回去我们缓冲区里面区提取数据 如果直接给给 a 这样是肯定不行的 这里的 &a 就是scanf函数的参数 我们传 a 的地址进去 在里面对a的地址解引用改变a 这样对a进行的改变才会影响到我们传进去的参数(实参)
那现在有了我们的引用操作还需要这样嘛
我们利用引用传参 的效率是比 我们直接传值 的效率要高的
因为 我们以值作为参数或者返回类型的时候,在开始的传参 和 返回期间 函数不会直接传递实参或者直接把返回值直接返回 而是会传递实参的临时拷贝或者是返回变量的临时拷贝 如果这样的话 那我们的效率可想而知是很低下的 并且如果我们参数或者返回值类型很大时 效率会更低 就好比是 结构体类型
做返回值:
大家看这样的结果是什么
int test()
{
static int n=0;
n++;
return n;
}
int main()
{
cout<<test()<<endl;
cout<<test()<<endl;
cout<<test()<<endl;
}
答案: 1 2 3 这里大家要注意一个点就是我们的 static 语句是只执行一次的 之所以提这个我们在了解引用做返回值之前 我们要理清楚值返回的特性
那有什么办法可以证明 临时变量 的存在呢? 大家看下面的代码:
int test()
{
int n=0;
n++;
return n;
}
int main()
{
int& a=test();
}
大家觉得上面这段代码 编译器能编译通过嘛? 大家试过应该知道这里是会报错的 正确的代码应该是这样的:
int test()
{
int n=0;
n++;
return n;
}
int main()
{
const int& a=test();
}
这样我们的编译器才不会报错 也就暗示我们传返回值回来的时候 产生的临时变量是常性的(也就是被const修饰的)
那如果是这样呢?
int& test()
{
int n=0;
n++;
return n;
}
int main()
{
int& a=test();
}
我们这里使用传引用返回又是什么意思呢 ?
我们既然返回的是一个引用 那这个引用是谁的引用呢 我们这里写得是 return n 不就是返回n的引用嘛 那既然我返回的是 n 的引用(返回n的别名) 我们用 int& a 来接收 这样我们不就得 a不就是 返回来的别名的别名了嘛 意思就是我们得 a 是n的别名的别名 那这样想我们的a不就是n的别名了嘛
我们来验证一下吧:
这样不就表明我们的 a是n的别名了嘛 那大家现在觉得我们的传引用返回和传值返回的区别是什么?
很显然 我们的传值返回 是会产生一个临时变量的 这个临时变量具有常性 而我们的传引用返回不会 它会直接返回返回的变量的别名
所以: 我们的传引用返回是不需要拷贝的
但是我们上面的代码还是有问题的 如果大家有了解过栈帧的创建与销毁的话 大家就知道我们的一个函数在执行它的功能后就会返回上一层函数 与此同时这个函数里面为临时变量分配的空间就会还给操作系统 那我们这里写的代码肯定也是会那样的 所以当我们返回引用后 a的地址 就不能再去访问了 因为这块空间因为 n 所在函数栈帧的销毁而返回给操作系统了 此时如果在访问 就是越界的问题了
我们第一次去调用 没问题 而后面两次出现了随机数的情况 是因为我们第一次使用 cout 从流里面读取数据时 我们首先是要先传参的 参数就是我们的 a 而这个时候我们去访问那片空间值还是正确的 因为这片空间还没有被清理 修改 或者 覆盖 那么此时访问这片空间得到的值就还时正确的 但是后面我们传完参之后 调用 cout (这个也是一个函数) 也会有一片自己的空间 此时这篇空间就继续使用test函数用完后的空间 这是这里的空间就被覆盖使用了 里面的数据也就跟随着修改了所以后面两次调用也就时随机的值了
总结:如果函数返回时,出了函数的作用域,如果返回对象还在(还没还给我们的操作系统),那么就可以使用引用返回,如果已经返回给系统了,那么就必须使用传值返回
内联函数:
内联函数 就是函数名前面以inline修饰的函数 编译时c++编译器会在调用内联函数的地方展开(篇幅小的函数才会展开 编译器会自动识别) 没有函数调用建立栈帧的开销。内联函数可以提升程序运行的效率 所以我们经常调用的小函数就适合
这个替换的机制是不是和C语言里面的 宏 很相似呢 但是他们两个是不相同的 就好像 我们的宏是不能够调试的 但是我们的内联函数可以调试的
大家觉得为什么c++要有一个这个内联函数呢? 首先我们的宏是替换 如果我们写得时候没有注意 优先级 那么是有可能出错的 大家学习宏的时候应该有所体会吧 但是我们自己写函数时容易出错嘛? 还有就是我们的内联函数的特性 是去把一个函数展开 不用开辟栈帧
默认的debug版本下不会展开 只有优化后的 或者 自己去调了设置的才会看到像下面这样的没有 call 的
大家看这里面就没有call指令了 不会去调用函数了
内联函数的特性:
inline是一种 空间换取时间 的做法,这样为我们省去了开辟栈帧的开销 当然也不是说我们内联函数哪里都可以适用,像对于我们的很长的代码或者你这个函数里面包含有递归的这些内联函数就不太适用了
其次,我们的inline对于编译器只是一个建议,编译器会自己判断、优化,像如果是上面那种不适用的情况,我们编译器就会忽略掉它的内联 大家来看:
篇幅小的会替换 大家来看篇幅大的:
大家看到篇幅大的它还会替换嘛 它是不是就是去调用call指令了
另外大家在使用inline的时候还一个要注意的点就是,我们的函数因为inline展开后,是没有函数地址的,所以大家在使用的时候,不要把 声明 定义 分离 因为这样的话后面程序在链接的时候就会出现链接不上的问题
我们的程序在执行的时候分为这么四个步骤 预处理->编译->汇编->链接 我们的汇编会利用编译期间实现过的符号汇总来进行一个 形成符号表的操作 如果有的符号没有地址的话 就是属于声明 和 定义分离了 只有声明是没有地址的 所以就会在链接的时候进行一个 符号表的合并和重定位(多个目标文件(.o)进行链接的时候会通过符号表查看来自外部的符号是否存在)
auto:
auto是一个新的类型指示符来指示编译器,auto声明的变量必须由该编译器在编译时期推导而得 意思就是它可以自动的去推导一个变量是什么类型
int a=10;
auto b=a;
auto c='d';
对于这个我们后面学习迭代器的时候用起来还是很方便的 就像那些类型名很长的这个就挺适用的
还有这样的场景:auto& auto* 这两个得意思是 我们表明了我们传过去得变量得是一个引用或者是指针
int a = 10;
auto& = a;
auto* = &a;
auto* = a;
最下面得那种写法就是错误得 因为我们指定了传过去的得是一个指针
如果说在同一行声明多个变量时,这些必须得是相同得类型,因为这时我们得auto是先自动推导第一个变量的类型再拿推导出来的类型去定义以后的变量
auto a=1,b=2.0;
这样就是不对的
auto 还有一个应用场景: 就是我们的范围for
我们以前的for循环:for(int i=0;i<size;i++) 就好像是这样 我们的范围for是可以这样写的:
for(auto ch:arr)
{
//依次自动取arr中的数据,赋值给ch,会自动判断结束
}
虽说我们是依次把arr中的值赋值给ch 但ch始终是一个拷贝 我们是不能通过ch直接改变arr数组中的内容的
for(auto& ch:arr)
{
//取别名后 就可以直接改变arr数组里面的值了
}
有一个点大家要注意我们使用的数组的范围必须是固定的,不能是不确定的 大家看下面的代码我们传进来的参数是一个数组首元素的地址 哪怕是常规的for循环也是不能直接使用arr的
void test(int arr[])
{
for(auto ch:arr)
{
;
}
}
auto不适用的场景:
① auto不能作为函数的参数 因为在编译阶段会报错 编译器那时无法推导具体类型
auto也是不能做返回值的哦
② auto也不能用来声明一个数组
nullptr:
再c++11中 我们的NULL 和 0 是等价的 大家可以到stddef.h文件去看一下 而我们的nullptr就是为了弥补这个缺陷,好像我们平常同时使用 NULL 和 0 时 系统就会使用最前面的那个了那我写在后面的 0 或 NULL 不就没有用了嘛,所以有了nullptr
所以我们在c++中表示一个空指针都是用的 nullptr
谢谢大家能看到这里!祝大家都能收到自己心仪大厂的offer!!!