六:引用
c++中的引用非常好用,可以避免我们在c语言中使用一级指针二级指针等等,引用不是重新定义一个变量,而是给这个变量起一个别名,他和他引用的变量在同一个空间地址。
如图所示k是a的引用,在我们查看a和k的地址的时候发现他们俩的地址相同,说明他们在同一块空间。在这里要说明一点,引用类型一定是和引用实体是同种类型的。
在a自加后k也自加了也能证明k就是a。
以前需要传地址才能交换两个数现在直接用引用就能解决。
引用的特性:
1.引用在定义时必须初始化
2.一个变量可以有多个引用
3.引用一旦引用一个实体,再不能引用其他实体
因为b已经是a的引用了,然后b又去当c的引用所以就报错了。
引用使用都有什么场景呢?第一个场景就是刚刚swap函数中用引用做参数,第二个场景就是用引用做返回值。
用引用做返回值有什么好处呢?我们学过C语言的都知道当函数结束需要返回的时候会创建一个临时变量去接收返回值然后销毁函数栈帧,那么在创建临时变量的过程中无疑会浪费空间,我们发现当一个变量是静态的或者是函数结束不被销毁的,那么我们就可以用引用返回这样就避免了空间的浪费。
#include <assert.h> #define N 10 typedef struct Array { int a[N]; int size; }AY; int& PosAt(AY& ay, int i) { assert(i < N); return ay.a[i]; } int main() { Array ay; for (int i = 0; i < N; i++) { PosAt(ay, i) = 10 * i; } for (int i = 0; i < N; i++) { cout << PosAt(ay, i) << " "; } cout << endl; return 0; }
从上图中我们可以看到ay是我们创建的结构体对象,这个对象在函数结束才会销毁,那么像以前C语言那样每次函数返回一个值就需要开一个临时变量去接收返回值很浪费空间,既然这个变量在函数调用后没有被销毁那么就可以直接返回自己,所以在上图中我们使用了引用做返回值。
用引用返回有两个特点:
1.减少拷贝 2.调用者可以修改返回对象
int& Add(int a, int b) { int c = a + b; return c; } int main() { int& ret = Add(1, 2); Add(3, 4); cout << "Add(1, 2) is :"<< ret <<endl; return 0; }
上面这个代码有什么问题呢?这个代码是正确的吗?很明显这个代码是错误的,我们已经说过引用返回仅限于函数栈帧销毁后还存在的变量,add的返回值c作用域仅限于add函数当add函数返回就被销毁了,那么这个时候c的空间是不能被访问的。
如果函数返回时,出了函数作用域,如果返回对象还在 ( 还没还给系统 ) ,则可以使用
引用返回,如果已经还给系统了,则必须使用传值返回。
常引用:
如图所示我们发现好像不能去引用const修饰的变量,这是为什么呢?
这是因为指针和引用,在赋值/初始化的时候权限只能缩小,不能放大,本来a的权限仅仅是只读,结果在引用的时候给了aa可读可写的权限,这当然是不可以的。
int main() { int c = 1; int& cc = c; //权限的放大 //const int a = 10; //int& aa = a; //权限的放大会报错 // const int*ptr = NULL // int* pptr = ptr //权限的缩小 int a = 10; const int& aa = a; //权限的缩小没有问题 int* p = NULL; const int* p1 = p; return 0; }
上图中为什么会报错呢?我们可以看到count函数的返回类型为传值返回,传值返回返回的是临时变量,由于临时变量具有常性,所以我们必须加上const
int count() { int n = 0; n++; return n; } int main() { const int& ret = count(); return 0; }
上图中的代码怎么修改才是正确的呢?因为从int转换成double需要隐式转换,而类型转换会产生临时变量,还是刚刚的问题临时变量具有常性所以加上const就可以了
int main() { int i = 0; const double& rb = i; return 0; }
引用和指针的区别:
在语法概念上引用就是变量的别名,没有独立空间,和其引用实体共用一块空间。
而在底层实现上实际上是有空间的,因为引用是按照指针的方式实现的。
int main() { int a = 10; int& ra = a; ra = 20; int* pa = &a; *pa = 20; return 0; }
从上图我们可以看到int& ra = a 和 int* pa = &a 的反汇编实现是一样的,这也可以证明引用是按照指针的方式实现的。
引用和指针的不同点:
1.引用概念上是定义一个变量的别名,指针存储一个变量的地址
2.引用在定义时必须初始化,而指针可以不初始化。
3.引用在初始化引用一个实体后就不能再去引用其他的实体,而指针可以在任何时候指向任何一个同类型实体。
4.没有空引用,但是有NULL指针。
5.在sizeof中的含义不同,引用的大小是其引用实体类型的大小,而指针永远是32位下4字节,64位下8字节
6.引用自加就是其引用实体自加,而指针自加是指针往后偏移一个类型的大小。
7.有多级指针,但是没有多级引用。
8.访问实体的方式不同,指针需要显示解引用,引用由编译器自动处理。
9.引用比指针使用起来相对更安全
七:内联函数
在c++中一般不在用宏了,一般都用const和enum去替代宏常量,用inline去替代宏函数。
那么为什么c++中不使用宏了呢?因为宏的缺点很明显,第一:不能调试。第二:没有类型安全的检查。第三:有些场景下非常复杂。
为什么说非常复杂呢?大家可以现在用宏写一个ADD函数
#define Add(x,y) ((x)+(y)) int main() { //如果将宏定义成这样#define Add(x,y) (x)+(y) int ret = Add(10, 15) * 3; //结果为(10)+(15)*3 与我们想的(10+15)*3就不一样了 int a = 1, b = 3; //如果将宏定义成#define Add(x,y) x + y int ad = Add(a & b, a | b); //结果为 a & (b+a) | b,因为+的优先级高于&和|所以会先进行+ return 0; }
以inline修饰的函数叫内联函数,编译时c++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。看到这里大家也发现了这不就是宏的优点吗,既然宏的优点被替代了那么自然就很少再使用宏了。
下面是内联函数的反汇编:
inline int Add(int x, int y) { return x + y; } int main() { int ret = Add(1, 2); cout << "ret:" << ret << endl; return 0; }
下面是普通函数的反汇编:
int Add(int x, int y) { return x + y; } int main() { int ret = Add(1, 2); cout << "ret:" << ret << endl; return 0; }
通过对比我们发现内联函数在汇编中直接展开,不会再像普通函数那样开一个函数栈帧进入这个函数。
内联函数的特性:
1.inline是一种以空间换时间的做法,如果编译器将函数当做内联函数处理,在编译阶段,会用函数体替换函数调用。缺陷:可能会使目标文件变大。优势:少了调用开销,提高程序运行效率。
2.inline对于编译器而言只是一个建议,不同编译器关于inline的实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现),不是递归且频繁调用的函数采用inline修饰,否则编译器会忽视inline特性
3.inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
八:auto关键字
使用auto关键字可以让编译器自动推导其类型。
int main() { int a = 0; auto b = a; auto c = &a; cout << typeid(b).name() << endl; cout << typeid(c).name() << endl; return 0; }
用typeid().name()可以查看auto推导的是什么类型。
auto的实际价值:简化代码,当类型很长的时候,可以考虑自动推导。
在这里就会有人说了,typedef不是也可以起到简化代码的作用吗?可以是可以,但是typedef在一些场景下会有很大的缺点,比如:
typedef char* pstring; int main() { const pstring p1; const pstring* p2; return 0; }
大家可以看一下上面的代码哪条会报错呢?
答案是p1,这就让人很疑惑了,为什么const char* p1会出错呢?出错的原因在于使用typedef重命名char*后,p1实际上变成了char* const p1,const去修饰p1很明显p1变成了一个常量,常量的定义必须初始化。这就是typedef的缺点。
我们在使用auto的时候,可以强制类型,比如:
int main() { int a = 10; auto* aa = &a; //强制aa是指针类型,当然不加*编译器也能自己推导出来aa的类型 //auto aa = &a; char c = 'a'; auto& d = c; //auto后想要其是另一个变量的引用必须加上引用符号 cout << typeid(aa).name() << endl; cout << typeid(d).name() << endl; return 0; }
注意:使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种类型的声明,而是一个类型声明时的“占位符”,编译器会在编译期会将auto替换为变量实际的类型。
用auto在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。如下图所示:
正确的应该是:
int main() { auto a = 10, b = 20; auto c = 2.33, d = 2.20; return 0; }
auto不能推导的场景:
1.auto不能作为函数的参数
2.auto不能直接用来声明数组
九:基于范围的for循环
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误,因此c++11中引入了基于范围的for循环。for循环后的括号由冒号“ : ”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,如下图所示:
int main() { int array[] = { 1,6,7,4,2,9,5 }; //自动依次取数组中数据赋值给e对象,自动判断结束 for (auto e : array) { cout << e << " "; } cout << endl; return 0; }
如果赋值给数组中的元素直接用e赋值即可
int main() { int array[] = { 1,6,7,4,2,9,5 }; for (auto e : array) { e *= 2; cout << e << " "; } cout << endl; for (auto e : array) { cout << e << " "; } cout << endl; return 0; }
通过上图我们可以发现赋值后好像并没有改变原数组,这该怎么办呢?其实很简单,我们定义迭代的变量的使用用引用即可。
范围for的使用条件:
for循环迭代的范围必须是确定的,对于数组而言,就是数组中第一个元素和最后一个元素的范围。
如下图所示,这样的代码就不能使用范围for:
我们在学习C语言的时候就知道,数组传参只是数组首元素地址,是不知道数组有多少个元素的,需要将数组内的元素大小也传过来。
十:指针空值nullptr
c++中的nullptr实际上是打的一个补丁,因为c++中的NULL出了bug,如下图所示:
void f(int) { cout << "f(int)" << endl; } void f(int*) { cout << "f(int*)" << endl; } int main() { f(0); f(NULL); return 0; }
按照我们所想f(0)应该调用的第一个f函数,f(NULL)调用的应该是传指针的那个函数,但是事实却并不是这样。
我们可以看到都调用了f(int)这个函数,这是因为在C中NULL实际上是一个宏,如下图:
我们可以看到在c++中NULL被定义为0.在c++11中打了一个补丁加了一个关键字nullptr,nullptr是能正确使用的。
void f(int) { cout << "f(int)" << endl; } void f(int*) { cout << "f(int*)" << endl; } int main() { f(0); f(NULL); f(nullptr); return 0; }
如上图所示,nullptr正确匹配了f函数。
注意:
1.在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是c++11作为新关键字引入的。
2.在c++11中,sizeof(nullptr)与sizeof((void*)0)所占的字节数相同。
3.为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
总结
本次所讲解的都是从c过渡到c++所改进的一些东西,这些东西更偏向于语法,需要大家动手去练习才能更好地记住。