持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第20天,点击查看活动详情
前言
本文就来分享一波笔者对于C++内联函数和指针空值的学习经验和心得。
笔者水平有限,难免存在纰漏,欢迎指正交流。
内联函数
C的宏与函数
先复习一波C的宏与函数,作为铺垫。
宏的优势&函数的不足
(1)宏生成内联代码,即直接在程序中生成语句,而函数调用需要跳入函数内,执行完后又得跳回主调函数,相比起来,实现同样的功能语句时宏耗时更短,运行速度更快。
(2)宏可以不用担心变量类型,因为宏处理的是字符串而不是实际的值,不管是什么类型,直接替换。
(3)增强代码的复用性。
//两数比大小
//使用宏
#define MAX (x, y) ((x) > (y) ? (x) : (y)) //int,float,char...都行
//使用函数
int MAX(int x, int y)//只能传int
{
return x > y ? x : y;
}
(3)宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。
#define MALLOC(num, type)\
(type *)malloc(num * sizeof(type))
...
//使用
MALLOC(10, int);//类型作为参数
//预处理器替换之后:
(int *)malloc(10 * sizeof(int));
宏的不足&函数的优势
(1)每次使用宏替换都会向程序中插入一段代码,插得越多占用空间越多,明显增加占用空间,而函数只有一份副本,每次调用时进入函数中执行语句,可能会传入一些参数值,但是函数调用完后栈帧释放,等到下次调用时再创立,不会明显增加占用的空间,更节省空间。
(2)宏无法调试,而函数可以逐句调试。
(3)由于宏与变量类型无关,也就不够严谨,没有类型安全检查,可能会出现难以察觉的问题。
(4)宏可能会带来运算符优先级的问题,写代码时不注意括号容易出错。
(5)宏参数可能被替换到替换体中的多个位置,带有副作用的宏参数求值可能会产生不可预料的后果,而函数参数只在传参的时候求值一次,结果更加可控。
(6)宏不可递归,而函数可以递归。
(7)容易写错,代码可读性差,可维护性差,容易误用。
如何选择
对于简单的逻辑(如两数比大小之类的),通常考虑用宏,写起来方便,并且代价更低。
#define MAX(a, b) ((a)>(b)?(a):(b))
对于复杂的逻辑,考虑用函数,占用空间更少,在耗时上与宏差别不大。
C语言中可以用宏替代函数来减少栈帧带来的消耗,但宏本身又具有一些缺点,可谓是“鱼与熊掌不可兼得”了,不过C++中根据宏和函数的特性规定了一种新的东西同时带有宏和函数的特性,想要克服宏的缺陷。
内联函数概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,提升了程序运行的效率。
如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。
查看方式(VS下):
- 在release模式下,查看编译器生成的汇编代码中是否存在call Add
- 在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不会对代码进行优化,以下给出VS下的设置方式
特性
-
inline是一种以空间换时间(空间指的是编译出来的可执行程序大小)的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用。
缺陷:可能会使目标文件变大,规模较大流程复杂的函数不宜展开,递归函数也不宜展开(展开无法递归)。
优势:对于代码量较少且调用较多的函数,少了调用函数的栈帧开销,提高程序运行效率。
-
inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。下图为《C++ primer》关于inline的建议:
- inline不建议声明和定义分离,分离会导致链接错误。
链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用
因为inline修饰后函数不会进符号表(不论函数是否真的展开了),符号表就没有函数地址,链接时就会找不到函数定义。 解决办法就是inline函数直接定义在源文件或者被包含的头文件内,不需要链接即可。
为什么函数长了以后不展开
因为展开会引起代码膨胀,也就是代码量会很大,其中有好一些重复代码,导致可执行程序很大。举个例子:
C++有哪些技术替代宏
- 常量宏定义换用const或 enum
- 短小函数定义换用内联函数
指针空值nullptr(C++11)
在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:
int* p1 = NULL;
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:
void f(int)
{
cout<<"f(int)"<<endl;
}
void f(int*)
{
cout<<"f(int*)"<<endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
return 0;
}
程序本意是想通过f(NULL)调用指针版本的f(int)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void *)
常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void*)0
。
而在C++11中就新增一个关键字nullptr来解决这个问题,nullptr用来表示指针空值,而且仅指针类型变量可以使用。
有人可能会有这么一个疑问:为什么C++11不直接把NULL改掉甚至丢弃掉?
因为存在历史包袱:以前的很多程序都是基于对NULL的使用的,直接修改很有可能牵一发而动全身,造成的影响难以估量,所以就用打补丁似的手段补充解决方案。其实各种语言的标准修订都不太敢直接改动某些内容,一般都是进行“补救”,原因主要就是上面讲的。
注意:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的 。
- 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
- 为了提高代码的健壮性,在表示指针空值时建议最好使用nullptr。
以上就是本文全部内容,感谢观看,你的支持就是对我最大的鼓励~