TIPS
- 在C++当中有一个东西可以打印类型?typeid(变量名).name()
- 数组一旦从参数进入到函数里面,它就已经是个指针了,再也不是一整个数组了
内联函数(正常函数定义前加个inline修饰)
- 在实际当中,有时候去调用函数的时候会有很多开销与消耗,调用函数的消耗就在于建立函数栈帧,栈帧就是最大的消耗。现在假设有一个函数,这个函数能够被频繁的调用,比如说某个函数被调用1万次,因此需要建立1万次的函数栈帧,太过分了。
- 因此如果要解决这个问题的话,在c当中就只能去写一个宏函数,因为宏他实际上并不是函数,它是一种替换,所以说是不需要去频繁的建立函数栈帧。所以说在写宏的时候也根本就不需要类型。在宏里面不需要类型,return与分号。
- 因为宏的本质就是替换与展开,在预处理阶段就会完成,因为他是无脑展开,所以说在很多时候运算符的优先级上就会发生问题,因此在写宏的时候,必须全部无死角加上括号,甚至包括每一个单独的变量,比如
#define MAX(a,b) ((a)>(b)?(a):(b))
- 宏函数的优势就在于不需要去建立函数栈帧,能够去提高调用效率,但它的缺点就在于很容易出错(必须无死角保险加上括号),有些宏函数很复杂会让代码的可读性变差,并且没有类型的检查,并且不能调试
- 在c++当中为了去解决c当中的这些缺陷,所以就有了内联函数。就先正常写一个函数,内联函数就是普通函数,然后在函数定义的时候最前面加上一个inline。如:
inline int add(int a, int b)
{
return a+b;
}
- 以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率,内联函数会在调用的地方展开,也就没有函数的调用,也就没有去建立函数栈帧,并且不复杂,不容易出错,可读性强,也可以进行调试。
- 内联函数与宏函数一样,都是展开(当然实际上内联函数并不是全部无脑展开),并且他们都有一个共同的特点,就是适用于短小,逻辑简单,并且需要频繁调用的函数,并不适合代码长的。
- 因为假设现在我有一个函数,它的汇编指令是50行,然后我需要去调用10000次,如果说这个函数不是内联函数,当然他需要去建立1万次函数栈帧,但是他的汇编指令总共相关的也就10050行(注意call指令);如果说这个函数是内联函数,虽然这1万次的调用都不需要去建立函数栈帧,但是程序相关的汇编指令总共有有500000行(假设内联函数是无脑展开,实际上不是),但汇编指令量级的差距可以很明显的看出来,所以说如果说对于一个代码很长的,逻辑复杂的函数把它当成一个内联函数来看的话,如果多次调用就会导致代码膨胀,所以汇编指令就会变多,所以机器二进制指令也会相应的变多,最终导致这个可执行程序变得更大,最终映射到生活当中的例子就是比方说一个安装包变的非常大 也是不好的。
- inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
- 因此内联函数inline用于那些短小,逻辑简单,并且需要频繁调用的函数。如果说一个函数它并不需要频繁调用,那么建立一些函数栈帧开销也无所谓。
- 一定要记住:短小,逻辑简单,频繁调用。
编译器角度看内联函数
- 但实际上当你把一个函数给它定义成一个内联函数的时候,因为inline对于编译器而言仅仅是一个建议,所以最终能否成为inline内联函数,是由编译器自己决定。
- 对于那些比较长的或者是递归函数,不要用内联函数。不然就很容易导致代码膨胀,然后实际上因为编译器对你也并不是很放心,就怕你乱搞事情,所以实际在代码编译过程当中,尽管你设置成内联函数了,但是编译器还是会自动去判断,如果情况不允许,他就会否决。
- 因为之前讲过内联函数相较于宏函数有一个好处就在于,他是可以支持调试的。但实际上是这样的:因为内联相当于是给他全部展开,所以实际上是不会跳到或进到函数里面去的而是直接这么顺着走下去,所以实际上是不能调试的。但是因为为了让程序员也能够进行调试一下,所以在默认debug模式下,inline是不会生效的,就是为了让程序员能够调试,
- 所以即使符合了内联的条件,编译器在默认debug模式下也不搞内联。但是改成release版本就可以了,这时候在底层就已经在搞内联了。
- 因为release版本是不能很好的去查看汇编代码,也不能够调试,所以如果说硬要去看在内联的情况之下汇编指令是怎么样进行的,但默认debug模式之下又不会给你搞成内联,因此需要设置一下
- 在debug下修改了一下设置后,就OK了,此时汇编代码就是按内联函数展开的逻辑走的,看一下果然没有call add了
汇编底层看内联函数
- 因为你从底层的汇编代码去简单的看一下的话,你会发现一个函数,它是否是内联函数,主要在于他调用的时候,如果说他不是内联函数,那么调用的时候就先会执行call指令,然后进入到函数里面的汇编指令当中,并且老老实实的从一开始建立函数栈帧push ebp…到具体的进行某一些操作等等;
- 但如果你是内联函数的话,因为不需要去建立函数栈帧且相当于是直接展开,所以在汇编指令当中就没有call,所以也就不需要进行跳转,直接往下执行接下来的汇编指令,在接下来的汇编指令当中,也不存在建立函数栈帧的相关指令,直接一些寄存器之间的的简单操作指令。
内联函数声明与定义不能分离的底层原因
- 因为内联函数的本质就是直接展开,也就是说汇编语句继续执行咵咵下去,所以内联函数的话是不会进入到符号表的,也是不会有地址的,
- 因为他以为他直接在调用的地方直接就展开了,所以为什么需要进到符号表里面去的?
- 所以内联函数比方说他的声明是在头文件,然后定义是在某个.c文件,这样子不行的,因为当你把头文件进行展开之后,然后到那个内联函数那边,由于是内联函数,所以说是直接展开,但此刻头文件展开部分只有内联函数的声明,所以说他也不知道具体的执行逻辑是什么,所以就不知道展开什么.
- 因为既然展开不了,所以说就去找就用非内联函数的方式去走,但因为内联函数是不进入到符号表当中也是没有地址的,所以在等会儿链接的过程当中,符号表那边又找不到他的函数名与地址,所以这时候就会发生链接错误,但是能够编译通过,就是链接不上
- 因为内联函数在汇编指令这个层面来看的话是不需要call的,所以说内联函数没有地址的,要啥地址?对于内联函数而言,他的声明与定义是不能够进行分离的,所以不要声明与定义分给直接咵一下全部写在头文件上面。这时候只要包含头文件,因为头文件的内容全部都在预处理阶段会全部替换展开,所以等会儿在内联函数那个部分进行直接展开的时候,就有对应的展开逻辑,不然只包含函数声明的话,内联函数的展开逻辑是不知道的,所以编译时候汇编代码生成不下去,所以那就只能通过call了,所以这时候你虽然编译通过了,到时候链接的时候又over了。如图:链接不上:
补充:宏的优缺点与C++替换方案
- 宏的优缺点
优点:
1.增强代码的复用性。
2.提高性能。
缺点:
1.不方便调试宏。(因为预编译阶段进行了替换)
2.导致代码可读性差,可维护性差,容易误用。
3.没有类型安全的检查 。 - C++有哪些技术替代宏?
1. 常量定义 换用const enum
2. 短小函数定义 换用内联函数
auto 变量名 = 右等式 (可以根据等号右边自动推导类型)
- 随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在:类型难于拼写,含义不明确导致容易出错。
- 在编程时,常常需要把表达式的值赋值给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而有时候要做到这点并非那么容易,因此C++11给auto赋予了新的含义
- auto是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得,也就说可以用auto作为类型来创建变量,数据类型由编译器自动推导
- 也就是说auto它可以在代码当中根据右边的表达式自动推导类型,但是他真的有很大的价值吗?其实也没有,但以后会有很大的用途,auto的话一般就是说在类型很长的时候去用比较划算一点,比如说以后要学的迭代器
- 具体例子:
auto 关键字注意点: - auto的话要自动识别类型,因此在等号右边必须得有一个推导才可以,auto就是长类型替换,auto可以通过右边表达式的值自动推导出类型。使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
- 用auto声明指针类型时,用auto和auto没有任何区别,对于这个auto而言,就是已经指定了,在等号的右边必须是一个指针或地址,不然报错
- auto不能作为函数参数
- auto不能用来声明数组
- auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用
for 语法糖
- 对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
- 与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环
- 只要是数组都可以这样(for循环迭代的范围必须是确定的),但不能修改数据,但可以去给auto后加个引用符号就ok
指针空值nullptr
- 在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。
- NULL实际是一个宏,在传统的***C头文件(stddef.h)**中,可以看到如下代码:NULL可能被定义为字面常量0,或者被定义为无类型指针(void)的常量
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
- 但在c++的项目里面默认都会定义 _cplusplus
5. nullptr就是 (void*)0 ,NULL就是0,这两个字面量不一样。NULL 与nullptr 本质上都表示空指针,他们的值都是0,所以指向的都是0位置的那个地址。
6.
1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。