引用
什么是引用?
引用是给一个已经存在的变量取一个别名,在语法上并不会给这个别名开一个空间,它和她引用的变量共用一个空间。但是实际上引用也是开了一块空间的,用来存放引用名。引用是按照指针的方式来实现的。
引用语法:
类型& 引用变量名(对象名/别称) = 引用实体;注意引用类型必须和被引用类型相同。
int main() { int a = 10; int& b = a; printf("%p\n", &a); printf("%p\n", &b); return 0; }
- 引用特性:
引用在定义时必须初始化,引用必然是有被引用对象的,不可能说先取个别名,但是不知道这个别名是谁;
一个变量可以有多个引用,相当于一个人可以有很多个外号一样;
引用一旦引用一个实体,就不能再引用其他实体了。
int main() { int a = 10; //int& b;这个会直接报错 int& c = a; int& d = a; int e = 20; c = e; printf("%p\n%p\n%p\n%p\n", &a, &c, &d, &e); return 0; }
如果被引用对象是一个常数,则需要添加const修饰。
int main() { const int a = 10; const int& b = a; const int& c = 20; return 0; }
- 使用场景:
做参数
void swap(int* a, int* b) { int x = *a; *a = *b; *b = x; } void swap(int& a, int& b) { int x = a; a = b; b = x; } int main() { int a = 1; int b = 2; swap(&a, &b); cout << a << " " << b << endl; swap(a, b); cout << a << " " << b << endl; return 0; }
做返回值,可以减少拷贝;调用者可以修改返回值。
需要提前知道一个点,一个函数的返回值实际上是这个值的拷贝,而不是这个值本身。
如果函数返回时,出了函数作用域,如果返回对象还没有还给系统,则可以使用引用返回,如果已经还给系统了,则不能使用引用返回,如果使用结果是返回值是未定义的。
int& Count() { static int n = 0; n++; return n; } int main() { int ret = Count(); return 0; }
- 权限问题:
指针和引用可以进行权限平移和缩小,但是不能进行权限放大。
int main() { int a = 1; const int b = 2; //权限平移 int& ra = a; const int& rb = b; //权限缩小 const int& c = a; //权限放大 int& d = b; return 0; }
与指针的区别
引用在概念上是一个变量的别名,指针存储一个变量地址;
引用必须初始化,指针没有规定;
引用在初始化后就不能改变了,指针可以在任何时候改变指向;
在sizeof中含义不同,引用的结果是引用类型的大小,但是指针始终是地址空间所占用的字节个数;
引用自加是被引用的实体加一,指针自加是指针向后偏移一个类型的大小;
指针可以有多级,但引用没有;
访问实体时,指针需要显式的解引用,引用是编译器自己处理;
引用比指针更加安全。
内联函数:
概念:
用inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,这样就没有 函数调用建立栈帧的开销,内联函数可以提升程序运行的效率。
int Add(int left, int right) { return left + right; } inline int Sub(int left, int right) { return left - right; } int main() { int ret = 0; ret = Add(1, 2); int tmp = 0; tmp = Sub(4, 3); return 0; }
有call说明进行了跳转,是开辟了栈帧空间的。
我们默认的调试模式是在Debug模式,Debug是不会进行代码优化的,如果我们想要看到内联函数的展开就需要修改一些配置。Release又对代码优化的太厉害了,我们不太容易看懂。
特性:
内联函数是一种空间换时间的做法,如果编译器将函数当做内联函数处理,在编译阶段,会用函数体替换函数调用。这是就会出现一个问题,如果内联函数太大,就会使目标文件变大,所以内联函数常被用来写一些短小的函数。实际上,如果内联函数过长,编译器也会忽视这个内联,把它当做普通函数处理;
内联函数对于编译器只是一个建议,不同的编译器关于内联函数的实现机制可能存在不同。在《C++ prime》中明确指出,内联说明只是想编译器发出一个请求,编译器可以选择忽略这个请求;
内联函数的声明和定义一般情况下是不分离的,因为分离之后可能会导致链接错误,内联函数是会被展开的,展开之后就不存在函数地址了,那这个时候去链接地址就会找不到。
使用场景:
内联函数常被用来替代宏函数。这时候就有个问题,既然有宏函数,为什么又要搞出内联函数这个东西?因为宏是较大缺陷的。
宏的优缺点:
优点:增强代码的复用性;提高性能。
缺点:不方便调试(因为在预编译阶段就进行了宏替换);导致代码可读性差,可维护性差,容易误用;没有类型安全检查。
所以在C++中,常使用const enum代替宏的常量定义,用内联函数代替宏函数。
auto关键字(C++11):
在写代码的时候,可能会遇到一个类型名非常长的情况,这种类型名写一两个还行,但是如果多了就非常容易写错。有些人会用typedef来解决这一问题,但是typedef有一个缺陷。
早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但是并没有人使用它。之后在C++11中,auto就具有新的含义了,auto不再是一个存储类型的指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
auto定义变量时必须初始化,auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器编译时会将auto替换为变量实际的类型。
用autto声明指针类型时,用auto和auto*没有任何区别,但是用auto声明引用类型时必须要加&。
int main() { int x = 10; auto a = &x; auto* b = &x; auto& c = x; auto d = x; //typeid().name()可以返回变量的类型名 cout << typeid(a).name() << endl; cout << typeid(b).name() << endl; printf("%p\n", &x); printf("%p\n", &c); printf("%p\n", &d); return 0; }
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
auto不能作为函数参数;
auto不能直接用来声明数组;
为了避免与C++98中的auto发生混淆,C++11值保留了auto作为类型指示符的用法;
基于范围的for循环(C++):
C++11中引入了基于范围的for循环。for循环后的括号由冒号":"分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
int main() { int a[] = { 1,2,3,4,5 }; //自动依次取数组中的数据赋值给e(e只是个名字,可以改变),自动判断结束 for (auto e : a) e *= 2; for (auto e : a) cout << e << " "; cout << endl; //这里相当于e是a[0]、a[1]等的别名,就可以达到改变数组的效果 for (auto& e : a) e *= 2; for (auto e : a) cout << e << " "; cout << endl; return 0; }
与普通循环类似,也可以使用continue和break;
范围for的循环范围必须是确定的;
指针空值nullptr(C++11):
NULL实际是一个宏定义的,NULL可能被定义为字面常量0,或者被定义为无类型指针(void *)的常量。也不知道出于什么原因被定义成了字面常量0,这样在程序中就会造成一些不可预料的问题。
于是C++11就打了一个补丁,引入nullptr作为关键字,代表指针空值。在C++11中,sizeof(nullptr)与sizeof((void*)0)所占用的字节数相同。