写在前面:
上一篇文章我介绍了缺省参数和函数重载,
探究了C++为什么能够支持函数重载而C语言不能,
这里是传送门,有兴趣可以去看看:http://t.csdn.cn/29ycJ
这篇我们继续来学习C++的基础知识。
目录
写在前面:
1. 引用
2. 引用的底层
3. auto 关键字
4. 范围for(语法糖)
总结:
写在最后:
1. 引用
引用就是起别名。
举一个经典的例子:周树人给自己起了一个笔名叫鲁迅,
那鲁迅和周树人是同一个人吗?答案是肯定的。(你找鲁迅跟我周树人有什么关系。。。)
那引用的语法是怎么样的呢:
#include using namespace std; int main() { int a = 10; int& b = a; //b就是a的别名 cout << a << endl; cout << b << endl; return 0; }
引用的符号是&,在C语言中这个符号是取地址,
引用的符号也是这个,他共用了这个符号。
上面这段代码其实我们就能直接理解成 a 是周树人,b 是他的别名鲁迅。
所以他们实际上是一样的,来看输出:
10
10
所以 b 和 a 其实就是一样的,所以 a = 10,b 当然也等于10。
所以我们再看:
#include using namespace std; int main() { int a = 10; int& b = a; //b就是a的别名 cout << &a << endl; cout << &b << endl; return 0; }
输出:
005EFE28
005EFE28
他们的地址也是一样的。
再来看:
#include using namespace std; int main() { int a = 10; int& b = a; //b就是a的别名 int& c = b; int& d = c; cout << &a << endl; cout << &b << endl; cout << &c << endl; cout << &d << endl; return 0; }
输出:
00AFF8B4
00AFF8B4
00AFF8B4
00AFF8B4
这样子当然也是一样的。
现在你大概就知道引用是什么样子了。
另外,引用是不能这样写的:
#include using namespace std; int main() { int& a; return 0; }
使用引用的时候你一定要告诉编译器,你是谁的别名。
这里我们马上来一个场景,
当我们学了引用之后,指针一下子就不香了,很多地方我们就直接使用引用了:
比如说经典的Swap函数:
#include using namespace std; // 实际上这里x就是a的引用,y就是b的引用 void Swap(int& x, int& y) { int tmp = x; x = y; y = tmp; } int main() { int a = 10; int b = 20; Swap(a, b); cout << a << endl; cout << b << endl; return 0; }
我们就能根据引用的特性实现,不需要用繁杂的指针操作,
让 x 是 a 的引用,y 是 b 的引用,
这样我们在函数里面操作 x 和 y 的时候其实就是在操作 a 和 b 。
这里我总结了引用的特性作为补充:
1. 一个变量可以有多个引用(就好像一个人可以有多个别名)
2. 引用必须在定义时初始化(前面演示过了)
3. 引用一旦引用了一个实体,就不能再引用其他实体
(说人话就是如果你是 a 的引用,你就不能改成是 b 的引用,只能一直是 a 的引用)
这里给出例子:
#include using namespace std; int main() { //一个变量可以有多个引用 int a = 0; int& b = a; int& c = a; //引用在定义时必须初始化 //int& d; int x = 10; c = x; //这里是赋值操作,c依旧是a的引用(别名) return 0; }
这是引用的基本特性,一定要熟悉好。
这里我继续介绍引用的作用:
1. 引用作为函数参数(输出型参数)(前面举了Swap函数的例子)
引用作为函数参数还能提高效率(之后学深浅拷贝的时候会再介绍)
2. 引用做返回值
我们来看这样一个例子:
当一个有返回值的函数返回值的时候,
他会将返回值拷贝生成一个临时变量,再将临时变量赋值给 ret 。
学过C语言,我们都知道,当这个函数结束的时候他的函数栈帧就销毁了,
所以他才需要生成一个临时变量,这样就能将返回值成功赋给主函数中的 ret 。
而且无论返回值是个什么变量,就算是静态变量,在返回的时候也会生成临时变量,
这个时候该引用出场了,
使用引用作为返回值,就不会生成临时变量:
#include using namespace std; int& Count() { static int n = 0; n++; return n; } int main() { int ret = Count(); return 0; }
所以这个时候我们又能理解前面所说的引用的作用,
使用引用能够提高效率,减少拷贝。
但是要注意,我们上面那段代码用引用返回没有问题,
那如果变量 n 不是一个静态变量呢?
#include using namespace std; int& Count() { int n = 0; n++; return n; } int main() { int ret = Count(); return 0; }
这样就出问题了,
ret 的值是不确定的,因为出了作用域 n 就不在了。
如果Count函数结束,栈帧销毁,没有清理栈帧,那么ret的结果侥幸是正确的,
如果Count函数结束,栈帧销毁,清理了栈帧,那么ret的结果就是随机值。
总结:
1. 基本任何场景都可以用引用传参,
2. 谨慎使用引用返回,避免出现上面的情况。
这里还有一种情况,常引用:
比如说这段代码:(这是一段错误代码)
#include using namespace std; int main() { //引用的过程中,权限不能放大 const int a = 0; int& b = a; return 0; }
再来看这一段代码:
#include using namespace std; int main() { //引用的过程中,权限不能放大 //const int a = 0; //int& b = a; //引用的过程中,权限可以平移或者缩小 int a = 0; int& b = a; const int& c = b; return 0; }
这个时候问题来了,
我们能不能修改 a 的值呢?
#include using namespace std; int main() { //引用的过程中,权限不能放大 //const int a = 0; //int& b = a; //引用的过程中,权限可以平移或者缩小 int a = 0; int& b = a; const int& c = b; a++; return 0; }
答案是可以的,const int& c 修改的是这个别名的权限,
而 a 是不受影响的。
我们再来看一个例子:
#include using namespace std; int main() { double a = 1.1; int b = a; //隐式类型转换 int& c = a;// ? return 0; }
我们都知道,第二个语句是隐式类型转换,
那 int& c = a ,能编译通过吗? 答案是不能:
难道是因为不同类型的变量不能用引用吗?
我们再来看:
加了一个 const 在前面然后就编译成功了。。。
这又是为什么?
实际上,在学习C语言阶段我们曾经学习过,
在进行类型转换的时候,会生成一个临时变量,如图:
而临时变量具有常性,具有常性是什么意思呢?其实就类似用 const 修饰过一样。
这样就会出现权限的放大,所以导致报错。
这个时候我们结合前面的例子来看:
#include using namespace std; int cnt() { static int n = 0; return n; } int main() { int& a = cnt(); //这里会出错 return 0; }
为什么这段代码会出错?
其实这也是一个道理,函数返回值的时候会创建一个临时变量,
而临时变量具有常性,所以这里也会出现权限放大导致的错误。
只要在 int& a 前面加上一个const就行了,这样就达成了权限的平移。、
2. 引用的底层
那么引用又是怎么实现的呢?他的底层是什么样的?
我们还是得从汇编的角度来观察,
先来看这段代码:
#include using namespace std; int main() { int a = 0; int& ra = a; int* pa = &a; return 0; }
这段代码里面分别使用了引用和指针来对 a 进行操作,
来看看他的汇编代码是怎么样的:
哦~,看看我们发现了什么,引用和指针的底层怎么一模一样?
在语法的层面:
引用不开空间,是对 a 取别名,
而指针开空间,是取 a 的地址,
但是,从底层汇编的角度来看,引用是以类似指针的方式实现的。
不过我们平时牢记引用在语法层的效果就行,底层就简单了解一下。
补充:引用之后还会有许多的场景,这些我会之后遇到具体的场景在做总结和分析。
3. auto 关键字
auto 能根据右边的表达式自动推导类型,
来看一个例子:
#include using namespace std; int main() { int a = 0; auto c = a; //这个操作能够查看变量的类型 cout << typeid(c).name() << endl; return 0; }
输出:
int
当然,右边不一定要是变量,也可以使表达式:
#include using namespace std; int main() { auto c = 1 + 1; //这个操作能够查看变量的类型 cout << typeid(c).name() << endl; return 0; }
输出:
int
现在这样看来,auto好像价值不大,
但是如果以后遇到非常复杂的类型的时候,直接使用auto会非常方便。
这里补充一点:auto是不能作为函数参数或者用来声明数组的。
4. 范围for(语法糖)
这里再基于我们刚刚学的auto,介绍一下范围for。
C语言阶段,我们平时遍历一个数组都是这样遍历的:
#include using namespace std; int main() { int arr[] = { 1, 2, 3, 4, 5 }; for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) { cout << arr[i] << " "; } cout << endl; //使用范围for for (auto e : arr) { cout << e << " "; } return 0; }
以上面的代码为例:
范围for 其实就是依次取数组中的数据赋值给e,
自动迭代,自动判断结束。
你就说范围for 方不方便,甜不甜?不甜又怎么会被叫做语法糖呢。
这里补充一点,修改e 是不会修改到数组的,因为e 的值是取数组的数据赋值过来的,
如果想要通过修改e 来修改数组,需要加个引用:
#include using namespace std; int main() { int arr[] = { 1, 2, 3, 4, 5 }; //使用范围for for (auto& e : arr) { e++; cout << e << " "; } return 0; }
输出:
2 3 4 5 6
当然,不止int 类型的数组,其他什么类型的数组都可以用范围for,
还有一些我们之后要学的容器,因为auto 会自动推导类型。
总结:
C++入门铺垫的知识学的差不多了,准备要开始类和对象了。
写在最后:
以上就是本篇文章的内容了,感谢你的阅读。
如果感到有所收获的话可以给博主点一个赞哦。
如果文章内容有遗漏或者错误的地方欢迎私信博主或者在评论区指出~