欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C语言预处理与宏定义以及内联函数

程序员文章站 2024-03-21 10:08:04
...

1.C语言预处理理论

1.1 由源码到可执行程序的过程

当写了一个test.c文件,对它进行编译似乎是直接生成了可执行文件a.out

[email protected]:/mnt/hgfs/winshare/pointer$ gcc test.c 
[email protected]:/mnt/hgfs/winshare/pointer$ ls
a.out  test.c

但是上面的说法其实是一个很粗略的过程,一个相对比较完整的源代码编译过程包括:源码.c文件,经过预处理器得到预处理过的.i源文件;.i源文件在经过编译器编译得到汇编文件.S;然后.S文件经过汇编器汇编成为目标文件.o;最后目标文件.o经过链接器链接生成elf可执行程序。

预处理用预处理器,编译用编译器,汇编用汇编器,链接用链接器,这几个工具再加上其他一些额外的会用到的可用工具,合起来叫编译工具链。Linux下的gcc就是一个编译工具链。

1.2 预处理的意义

编译器本身的主要目的是编译源代码,将C的源代码转化成.S的汇编代码。编译器聚焦核心功能后,就剥离出了一些非核心的功能到预处理器去了。预处理器可以帮助编译器做一些编译前的杂事。

1.3 编程中常见的预处理

源文件中常见的需要被预处理的有:

  • 1.头文件处理:#include<>#include" "
  • 2.注释处理。
  • 3.#if#elif#endif#ifdef等处理。
  • 4.#define宏定义处理。

gcc可以给一些参数来做一些设置,比如:

  • gcc xx.c -o xx可以指定生成可执行程序的名称为xx,如不指定则默认为上面的a.out
  • gcc xx.c -c -o xx.o可以指定只编译不链接,也可以生成.o的目标文件。
  • gcc -E xx.c -o xx.i可以实现只预处理不编译。一般情况下没必要只预处理不编译,但有时候这种技巧可以用来帮助研究源文件的预处理过程,帮助调试程序。

举个栗子:
a.c:

//#include<stdio.h>

#define MAX_SIZE 100
typedef int* pInt

int main(int argc,char**argv)
{
	int a[MAX_SIZE];
	pInt p = a;
	
	return 0;
}

只预处理不编译命令:

gcc -E a.c -o a.i

生成的a.i文件内容:

# 1 "a.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "a.c"

typedef int* pInt

int main(int argc,char**argv)
{
 int a[100];
 pInt p = a;

 return 0;
}

可以看出,宏定义被预处理时的现象有:

  • 第一,宏定义语句本身不见了,在编译之前就被预处理掉(替换)了,可见编译器根本就不认识#define,根本不知道还有个宏定义。
  • 第二,typedef重命名语言还在,说明它和宏定义是有本质区别的,说明typedef是由编译器来处理而不是预处理器处理的。

2.C语言预处理代码实践

2.1 #include头文件包含

#include <>#include" "的区别:

  • <>专门用来包含系统提供的头文件,就是系统自带的,不是程序员自己写的)。更深层次来说:<>的话C语言编译器只会到系统指定目录,可以在编译器中配置的或者操作系统配置的寻找目录,比如在Ubuntu/usr/include目录,编译器还允许用 -I参数来附加指定其他的包含路径去寻找这个头文件,隐含意思就是不会找当前目录下,如果找不到就会提示这个头文件不存在。
  • " "用来包含自己写的头文件。" "包含的头文件,编译器默认会先在当前目录下寻找相应的头文件,如果没找到然后再到系统指定目录去寻找,如果还没找到则提示文件不存在。

值得注意的是:

  • 规则虽然允许用双引号来包含系统指定目录,但是一般的使用原则是:如果是系统指定的自带的用<>,如果是自己写的在当前目录下放着用" ",如果是自己写的但是集中放在了一起专门存放头文件的目录下将来在编译器中用-I参数来寻找,这种情况下用<>
  • 头文件包含的真实含义就是:在#include<xx.h>的那一行,将xx.h这个头文件的内容原地展开替换这一行#include语句,这个过程在预处理中进行。

2.2 注释

注释是给人看的,不是给编译器看的。编译器既然不看注释,那么编译时最好没有注释的。实际上在预处理阶段,预处理器会拿掉程序中所有的注释语句,到了编译器编译阶段程序中其实已经没有注释了。

2.3 条件编译

有时候我们希望程序有多种配置,于是在源代码编写时写好了各种配置的代码,然后给个配置开关,在源代码级别去修改配置开关来让程序编译出不同的效果。

条件编译中用的两种条件判定方法分别是#ifdef#if,它们的区别是:

  • #ifdef XXX判定条件成立与否主要是看XXX这个符号在本语句之前有没有被定义,只要定义了这个条件就是成立的。
  • #if的完整格式是:#if (条件表达式),它的判定标准是()中的表达式是否为true(非0)还是flase(0),跟C中的if语句有点像。

3.宏定义的使用

3.1 宏定义的规则和使用解析

宏定义的解析规则就是:在预处理阶段由预处理器进行替换,这个替换是原封不动的替换。宏定义替换会递归进行,直到替换出来的值本身不再是一个宏为止。

一个正确的宏定义语句本身分为3部分:第一部分是#dedine,第二部分是宏名 ,剩下的所有为第三部分。宏可以带参数,称为带参宏。带参宏的使用和带参函数非常像,但是使用上有一些差异。在定义带参宏时,每一个参数在宏体中引用时都必须加括号,最后整体再加括号,括号缺一不可。

3.2 宏定义示例

①定义MAX宏求2个数中较大的一个:

#define MAX(a, b) (((a)>(b)) ? (a) : (b))

注意:要想到使用三目运算符来完成,同时也要注意括号的使用。


②用宏定义表示一年中有多少秒SEC_PER_YEAR

#define SEC_PER_YEAR (365*24*60*60UL)

注意:当一个数字直接出现在程序中时,它的是类型默认是int,而一年有多少秒这个数字刚好超过了int类型存储的范围。


③宏定义来实现条件编译:

程序有DEBUG版本和RELEASE版本,区别就是编译时有无定义DEBUG宏。

3.3 带参宏和带参函数的区别

宏定义是在预处理期间处理的,而函数是在编译期间处理的。这个区别带来的实质差异是:宏定义最终是在调用宏的地方把宏体原地展开,而函数是在调用函数处跳转到函数中去执行,执行完后再跳转回来。因此宏定义没有调用开销。而函数有比较大的调用开销,所以当函数体很短,尤其是只有一句话时,可以用宏定义来替代,这样效率高。

带参宏和带参函数的另一个重要差别就是:宏定义不会检查参数的类型,返回值也不会附带类型;而函数有明确的参数类型和返回值类型。当我们调用函数时编译器会帮我们做参数的静态类型检查,如果编译器发现我们实际传参和参数声明不同时会报警告或错误。因此使用函数的时候程序员不太用操心类型不匹配因为编译器会检查,如果不匹配编译器会报错。而用宏的时候程序员必须很注意实际传参和宏所希望的参数类型一致,否则可能编译不报错但是运行有误。

总结:宏和函数各有千秋,各有优劣。总的来说,如果代码比较多用函数适合而且不影响效率。但是对于那些只有一两句话的函数开销就太大了,适合用带参宏。但是用带参宏又有缺点:不检查参数类型,这个需要写代码的人处理好。

4.内联函数和inline关键字

内联函数通过在函数定义前加inline关键字实现。

内联函数本质上是函数,所以有函数的优点:内联函数是编译器负责处理的,编译器可以帮我们做参数的静态类型检查。但是它同时也有带参宏的优点:不用调用开销,而是原地展开。所以几乎可以这样认为:内联函数就是带了参数静态类型检查的宏。

当我们的函数内函数体很短,比如只有一两句话的时候,我们又希望利用编译器的参数类型检查来排错还希望没有调用开销时,最适合使用内联函数。