4.常引用(带有const的引用)
4.1 指针/引用在赋值中,权限可以缩小,但是不能放大
权限的缩小和放大,针对的是从引用实体到引用变量的过程中,权限的变化
int main() { int a = 0; int& ra = a;//ra既可以读到a,也可以修改a,权限的平移 const int& rra = a;//rra只能读到a,并不可以修改a,这里是权限的缩小 rra++;//rra没有修改a的权限,因为他是const引用 a++;//a本身是int修饰,没有const,可以修改 const int b = 1;//变量b只能被读取,不能被修改 int& rb = b;//rb没有const修饰,可以读写b,这就是典型的权限放大,编译器会报错 int& rd = 10;//常量不可以被修改,典型的权限放大。 const int& rb = b;//rb有了const修饰,只能读b,不能写b,权限的平移 return 0; }
4.2 常引用做参数
a.一般引用做参数都是用常引用,也就是const+引用,如果不用const会有可能产生权限放大的问题,而常引用既可以接收只读的权限,又可以接收可读可写的权限。
b.常引用做参数并不是为了修改参数,而是为了减少拷贝提高效率。
4.3 缺省参数如何引用?
缺省参数如果想做为引用的话,必须用常引用,因为缺省参数是一个常量,是不允许被修改的,只可以读。
void func(const int& N = 10) { }
4.4 临时变量具有常性不能修改(传值返回,隐式/强制类型转换)
a.常引用接收传值返回
传值返回我们前面就提到过,他返回时需要依靠一个临时变量,而临时变量具有常性不能修改,所以如果想要用引用接收那就必须用常引用,必须带上const。
int Count() { static int n = 0; n++; // ... return n; } int main() { int& ret = Count(); const int& ret = Count(); }
b.常引用接收临时变量
int main() { const int& b = 10; double d = 12.34; cout << (int)d << endl; //强制类型转换,并不是改变了变量d,而是产生临时变量,输出的值也是临时变量的值。 int i = d; //隐式类型转换,也是产生了临时变量。 const int& ri = d;//这里引用的实体其实就是从double d 到int类型转换中间的临时变量 cout << ri << endl;//这里输出的引用实际上就是double到int中间的临时变量的别名。 return 0; }
5.引用和指针的区别
a.语法概念上引用变量就是一个别名,不开空间,和引用实体共用一个空间。
底层实现上引用变量其实是要开空间的,因为引用在底层上是按照指针来实现的
b. 引用概念上定义一个变量的别名,指针存储一个变量地址。
c. 引用在定义时必须初始化,指针没有要求
d. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
e. 没有NULL引用,但有NULL指针
f. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
g. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
h. 有多级指针,但是没有多级引用
i. 访问实体方式不同,指针需要显式解引用,引用直接使用就好,具体细节编译器会自动处理
j. 引用比指针使用起来相对更安全
六、内联函数(不建立函数栈帧的函数,已经不是正常的函数了)
1.替代C语言中的宏
C语言中的宏在书写时,由于宏是单纯的替换,所以导致很容易出问题,例如下面,我们写一个实现两数之和的宏,大概能写出4种形式,可是这四种形式都是错的。
因为在不同的使用宏的场景下,对于宏的书写要求都是很高的。
a. 如果加分号,那么在分支语句的判断部分,会出语法错误。
b. 如果不加外层括号,可能由于运算符优先级的问题,无法得到我们想要的答案。
c. 如果内层不加括号,仅仅是加减这样的符号,都要比位操作符优先级高,这时候也无法得到我们想要的答案。
这时候,在C++中就提出了内联函数,内联函数在 ( 编译 ) 期间,编译器会用函数体来替换内联函数的调用,而不是宏那样的单纯替换
#define ADD(x,y) x+y #define ADD(x,y) (x+y) #define ADD(x,y) (x)+(y) #define ADD(x,y) ((x)+(y)); int main() { //不能加分号 if (ADD(1, 2)) { } //外层括号 ADD(1, 2) * 3; //内层括号 int a = 1, b = 2; ADD(a | b, a & b);//+运算符优先级高于|& }
2.编译器根据函数体大小来决定是否展开(代码膨胀)
内联函数一般适用于频繁调用的小函数。
如果不是内联函数还频繁调用的话,就会频繁的开辟函数栈帧,这会对程序产生不小的开销,影响程序运行时的效率,内联函数不害怕这一点,因为它根本就不建立函数栈帧
同时如果内联函数体过大,编译器也会将主动权掌握在自己手里,他会决定是否在内联函数调用的地方展开函数体。
如果函数体过大,将不会展开,如果较小,就会展开,这个结论我们可以通过汇编指令来查看。
inline int Add(int x, int y)//频繁调用的小函数,推荐搞成内联函数。 { return x + y; } inline int func(int x, int y)//编译期间不会展开 { int ret = x + y; ret = x + y; ret += x + y; ret = x * y; ret = x + y; ret *= x - y; ret = x + y; ret = x / y; ret += x + y; ret /= x + y; ret *= x + y; ret = x + y; return ret; } int main() { int ret = Add(1, 3); int ret2 = func(1, 2); return 0; }
由于debug版本下我们要对代码进行调试,所以代码中不会展开内联函数体,我们需要先将工程属性设置成这样子,然后打开调试中的反汇编查看底层的汇编指令,看看编译器对于内联函数体展开的情况。
下面的汇编指令就可以验证我们之前的结论,内联函数体过大,编译器不展开内联函数调用的地方,函数体较小的时候编译器会在内联函数调用的地方展开。
函数体较长时,编译器不会展开是因为代码膨胀,假设函数体中的指令有30行,程序中内联函数调用的地方有10000处,一旦编译器展开函数体,程序就会瞬间出现30w行指令,这会疯狂增加可执行程序文件的大小,也就是安装包的大小,所以编译器不会让这样的事情发生,即使你对编译器发出了内联的请求,编译器也不会管你,说了句 ‘’ 莫挨劳资,走远点 ‘’
3.声明和定义分离(本质:内联函数无论是否被编译器当作内联处理,他的函数名和有效地址都不进符号表,与static修饰的全局函数一样,都不进符号表)
如果下面这部分知识不太清楚的话,可以看看下面这篇博文,补一下基础,因为接下来讲的东西需要用到下面的知识。
程序运行原理和预编译
如果内联函数的声明和定义分开的话,程序就会报链接错误,为什么呢?我们前面说过内联函数只是有可能将函数体展开,并不会建立函数栈帧,所以stack.obj文件的符号表就不会存放Add函数和它的地址,那在链接阶段,test.obj会根据Add的函数名字到stack.obj文件的符号表中寻找Add函数的有效地址,但可惜符号表中别说地址了,连函数名都没有,自然目标文件之间的链接就无法成功,编译器就无法识别test.cpp中的Add到底是什么,光有个函数声明,没有函数定义编译器也就会报错:无法解析的外部符号。
结论:内联函数在定义时不要搞到.c文件里定义了,直接在.h文件里面定义就好,不要把定义和声明分开,这样在展开.h文件之后,函数体就在那里,链接阶段就不会在去找函数的地址了,因为函数就在他自身的目标文件里面。
七、auto用法
1.补一下C语言芝士
第一行const直接修饰的是指针变量p1,所以指针变量p1本身不能修改,它指向的内容还是可以修改的,但p1现在被你搞成const修饰了,所以它必须被初始化,因为它只有一次赋值的机会,就是在初始化的那个地方,不能说你后面在去给p1赋值,这样不可以。
第二行const修饰的不是二级指针p2,修饰的是二级指针p2所指向的内容,那么指针变量p2是没有被const修饰的,所以p2可以不初始化,但p2所指向的内容是不可以发生改变的,因为const修饰的是p2指向的内容。
注意:语法检查的时候,是不会先替换typedef内容的,他会先直接分析你的代码是否在语法上存在问题,比如第一行代码,编译器是不会把pstring替换为char的,如果替换为char当然这句语句就没有问题了,不初始化也OK,但是编译器看的不是替换之后的,他在预编译之前就发现你这段代码语法有问题,所以编译器就直接会报错了,因为他认为p1就是个变量,你用const修饰了,那就必须给初始值,第二行代码编译器认为p2是个指针,因为它看到*的存在了,所以它认为const修饰的是p2指向的内容,不是p2本身
出现分析问题错误的原因,其实就是我们思考的是替换之后的结果,编译器在分析语法时,只会看到代码本身,根本不存在替换不替换这么一说。
typedef char* pstring; int main() { const pstring p1; // 编译成功还是失败? const pstring* p2; // 编译成功还是失败? pstring* const p2;//如果这样写,const修饰的才是p2指针变量本身 return 0; }
2. auto用于自动推导类型
3.auto利用逗号运算符,一行定义多个变量时,这些变量必须是相同的类型。
因为编译器实际上只对第一个类型进行推导,然后用推导出来的类型来定义其他变量,所以你定义的多个变量就必须是同一类型的。
void TestAuto() { auto a = 1, b = 2; //必须是相同的类型 auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同 }
4.auto在推导类型时,如果想推导出引用类型则必须在auto后面加&,在推导指针类型时,auto后面加不加*都可以
int main() { int x = 10; auto a = &x; auto* b = &x;//加不加*无所谓 auto& c = x;//必须加& cout << typeid(a).name() << endl;//typeid().name()可以拿到类型的字符串 cout << typeid(b).name() << endl; cout << typeid(c).name() << endl; return 0; }
5. auto不能作为函数参数,因为无法事先确定需要开辟函数栈帧的大小
void TestAuto(auto a)//编译器无法推导a的类型,开辟栈帧时也就不知道开多大。 {}
6. auto不能用来声明数组
void TestAuto() { int a[] = {1,2,3}; auto b[] = {4,5,6};//这是错误的声明方式 }
八、基于范围的for循环
a. C++11中引入了基于范围的for循环,for后面的括号中有两部分组成,第一部分是在范围内用于迭代的变量,第二部分表示迭代的范围。
注意:for循环与普通循环类似,既可以用continue来结束本次循环,也可以用break来跳出整个循环。
void TestFor() { int array[] = { 1, 2, 3, 4, 5 }; for(auto& e : array)//将迭代变量搞成引用,这样可以直接操作数组中的数据。 e *= 2; for(auto e : array) cout << e << " "; return 0; }
b. for循环迭代的范围必须是确定的。
对于数组而言,就是数组中第一个元素和最后一个元素的范围;
对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
//以下代码是有问题的,因为for的范围是不确定的。 void func(int array[])//传过来的array不是数组,而是指针。 { for(auto& e : array) cout<< e <<endl; }
九、指针空值nullptr ==> (void*)0
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void *)0。
下面是stddef.h头文件的部分源码,所以C++98对于指针空值是没有确定的值的。
#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endif
C++11为了避免这样的情况发生,定义了关键字nullptr来表示指针空值,弥补C++98中有关NULL空指针的bug。
void f(int) { cout << "1" << endl; } void f(int*) { cout << "2" << endl; } int main() { f(0); f(NULL);//这里原本想调用输出2的结果,但NULL被编译器默认为0,就调用了输出为1的函数,所以我们要想调用输出2的函数,就用nullptr关键字。 f(nullptr); //nullptr就是(void*)0 return 0; }
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
- 在C++11中,sizeof(nullptr) 与 sizeof((void * )0)所占的字节数相同
- 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。