C语言修行之基础篇 (二十四) 详解C语言预处理

文章目录

    • 源代码到可执行程序的过程
    • 编程中常见的预处理
    • gcc几种编译方式
    • 常见的预处理1 —— 头文件包含
    • 常见的预处理2 —— 注释
    • 常见的预处理3 —— 条件编译
    • 常见的预处理4 —— 宏定义


源代码到可执行程序的过程

源代码.c ->(编译)-> elf可执行程序
源代码.c ->(编译)-> 目标文件.o -> (链接)-> elf可执行程序
源代码.c ->(编译)-> 汇编文件.S ->(汇编)-> 目标文件.o -> (链接)-> elf可执行程序
源代码.c ->(预处理)-> 预处理过的.i源文件 ->(编译)-> 汇编文件.S ->(汇编)-> 目标文件.o -> (链接)-> elf可执行程序

  • 预处理用的预处理器;
  • 编译用的编译器;
  • 汇编用的汇编器;
  • 链接用的链接器,
    这些工具加上一些其他可用到的工具形成了编译工具链。 所以平时所说的编译工具链不仅仅只有一个工具,是这些工具的集合。

编程中常见的预处理

gcc就是一个编译工具链,当将源码通过gcc编译生成可执行文件的过程并不是一步到位的,而是这些工具作用生成的。
编译器本身的主要目的是 编译源代码,将C的源代码转化成.S的汇编代码。编译器聚焦核心功能后,就剥离出一些非核心功能到预处理去了。

编程中常见的预处理:
1、#include(#include <> 和#include “”区别)
2、注释
3、条件编译:#if #elif #endif
4、宏定义


gcc几种编译方式

gcc编译时可以给一些参数来做一些设置。
如:gcc xx.c -o xx 可以指定可执行程序名称
如:gcc xx.c -c -o xx.o 可以指定只编译不链接,也可以生成.o目标文件。
gcc -E xx.c -o xx.i 可以实现只预处理不编译。一般情况下没必要只预处理不编译,这主要是用来帮助我们研究预处理过程。

宏定义被预处理的现象:
1、宏定义不见了(编译器不知道宏定义,宏定义在预处理阶段就被替换了)
2、typdef还存在。(typdef是编译器处理的,而不是预处理器处理的)


常见的预处理1 —— 头文件包含

#include <> 和#include “”的区别
文件类型 编译器查找文件方式
#include <> 专门用来包含系统提供的头文件(就是系统自带的,不是程序员自己写的头文件) C语言编译器只会在系统指定目录(编译器中配置的或者操作系统配置的寻找目录,如Ubuntu中是/usr/include目录,编译器还允许用-I来附加指定其他的包含路径)去寻找这个头文件,如果找不到就会提示这个头文件不存在
#include “” 用来包含自己写的头文件 编译器默认会先在当前目录下寻找相应的头文件,如果没有找到然后再到系统指定目录去寻找,如果还没找到则提示文件不存在。

注意:规则虽然允许双引号来包含系统指定目录,但是一般的使用原则是:
1、如果是系统指定的自带的用<>
2、如果自己写的在当前目录下放着用“”
3、如果是自己写的但是集中放在一个专门存放头文件的目录下将来在编译器中用-I参数来寻找,这种情况下用<>

包含头文件包含的真实含义就是:在include 的那一行,将xx.h这个头文件的内容原地展开替换这一行#include语句,过程在预处理中。


常见的预处理2 —— 注释

注释是给程序员看的,不是给编译器看的。
由于编译器不需要看注释,则在预处理阶段,预处理器会将程序中所有的注释语句。到编译器编译阶段程序中已经没有注释。


常见的预处理3 —— 条件编译

// 宏定义
#define LINUX

// 条件编译
#ifdef LINUX
xxxxx
#elif
XXXXX
#endif

程序中希望支持多种配置的切换,可以使用条件编译做配置开关,在源代码预处理阶段根据条件编译选择需要的配置。
条件编译中用两种条件判定方式:#ifdef 和 #if
#ifdef XXX 判断条件成立与否主要看XXX这个符号在本语句之前定义。只要有定义就是判定成立的
#if (条件表达式)判定标准是()中的条件表达式是否成立。这个用法与C中if的用法是一样的。只是if是程序中正式语句,#if是预处理语句


常见的预处理4 —— 宏定义

宏定义的规则和使用解析
宏定义的解析规则就是: 在预处理阶段由预处理器进行替换,这个替换是原封不动的替换。宏定义替换会递归进行,直到替换到不是一个宏的为止
一个正确的宏定义式子分为三部分:
1、#define
2、宏名
3、宏值或表达式

宏可以带参数,称为带参宏。带参宏的使用和带参的函数非常像,但是使用上有些差异。在定义带参宏时,每个参数在宏体中引用时都必须添加括号,最后整体加括号。
当一个数字直接出现在程序中时,默认类型是int型。

若宏定义的值超出int范围应该如何定义呢?
#define SEC_PER_YEAR (365246060)UL // 不可行
#define SEC_PER_YEAR (365246060UL) // 可行

带参宏和带参函数的区别
带参宏:#define MAX(a,b) ((a)>(b))?(a):(b)
1、宏定义是在预处理期间处理的,而函数是在编译期间处理的。预处理是原地展开,没有调用开销;函数是跳转执行再返回,因此函数有比较大的调用开销。
当函数体比较短可以用宏定义来替代,这样效率高
2、宏定义不会检查参数类型,返回值也不会附带类型。函数有明确的参数类型和返回值类型。当函数调用时编译器会帮我们做参数的静态类型检查。如果编译器发现实际传参类型和参数声明不同时会报警告或错误。

总结: 如果代码比较多用函数适合而且不影响效率,但是对于那些只有一两句的函数开销就太大了,适合用带参宏,但是用带参宏有缺点:不检查参数类型。

内联函数和inline关键字
1、内联函数通过在函数定义前添加inline关键字实现。
2、内联函数本质上是函数,所以有函数的优点(内联函数是编译器负责处理的,编译器可以帮我们做参数的静态类型检查);同时它也有带参宏的优点(不用调用开销,而是原地展开)

总结: 内联函数就是带了参数静态类型检查的宏。
当函数很短的时候,又希望利用编译器的参数类型检查来排错,还希望没有调用开销时,最适合使用内联函数。

你可能感兴趣的:(C语言修行,c语言,linux,开发语言)