预编译处理

预编译

C代码的执行流程大致可以分为:

1、编译:.c编译为目标代码(.obj);
2、链接:将目标代码与C函数库链接合并,形成最终的可执行文件(包含了汇编阶段);
3、执行;

而预编译,也称为预处理,发生于编译之前,目的是为编译做准备工作,完成头文件与对应函数代码
的替换;

比如,include指令,用来在预编译阶段将对应的头文件内容引入到指定位置;

定义myhead.txt到头文件夹下:

1
printf("i am a little boy");

直接在函数中引入:

1
2
3
4
void main(){
#include "myhead.txt"
getchar();
}

编译可以正常运行,并打印出:i am a little boy

可见,头文件在预编译时期被替换成了其中对应的内容,此处等价于:

1
2
3
4
void main(){
printf("i am a little boy");
getchar();
}

头文件的作用

头文件定义了函数的声明,能够告诉编译器存在这样的函数,预编译时,头文件中的所有声明会被替
换到对应位置;当链接时,链接器负责找到头文件对应的函数实现;连接器是从所有的obj文件或者
函数链接库中(静态.a 动态.so)进行查找的。系统提供了标准的系统函数库;

对于自定义的函数,即使没有具体实现,在编译时期也不会报错误,而是在生成obj文件后,进行
链接时,找不到对应函数,才会报错,这点和Java是不同的。

1
2
3
4
5
6
7
void main(){

// myprintf函数是不存在的,但此处并不会报错
myprintf("hihi");

getchar();
}

宏定义

宏定义又称为宏替换或预编译指令;

宏定义通过define指令进行定义,其实质就是字符串的替换,在预编译时期,引用宏定义的地方将会
被替换成对应的定义内容;

作用1

宏定义可以用来作为头文件的引用标识,也能够防止头文件被重复引用;

比如在某些库中,通过宏定义来标识运行环境支持C++语法;

1
#define __cplusplus

另一个重要作用是防止头文件被重复引用:

a.h

1
2
3
4
5
6
7
8
9
// 通过define定义标识AH,当该头文件被第一次引用时,标识会进行定义
// 在进行第二次引用时,检测到AH标识已经定义,则标识和其间的代码将不会被执行;
#ifndef AH
#define AH
#include "b.h"

void printfA();

#endif

b.h

1
2
3
4
5
6
7
8
// 通过define定义标识BH,当该头文件被第一次引用时,标识会进行定义
// 在进行第二次引用时,检测到BH标识已经定义,则标识和其间的代码将不会被执行;
#ifndef BH
#define BH
#include "a.h"

void printfB();
#endif

other.c

1
2
3
4
5
6
7
8
9
#include <stdio.h>

void printfA(){
printf("a\n");
}

void printfB(){
printf("b\n");
}

main.c

1
2
3
4
5
6
7
8
#include <stdlib.h>
#include <stdio.h>
#include "a.h"

void main(){
printfA();
getchar();
}

可以看到在main.c引入了头文件a.h,而a.h与b.h之间又存在相互引用,如果不通过宏定义进行限制,
则a.h与b.h之间将形成循环引用,预编译都无法通过。

通过宏定义,为两个头文件都增加标识,第一次引用头文件时,标识没有定义,则会定义标识,宏定
义之间的函数声明也会被成功引入main.c中;第二次引用时,发现标识已被定义,则不会引入定义
内容;

在高版本C编译器中,通过使用#progma once指令就可防止头文件被重复引入:

a.h

1
2
3
4
5
6
// 高版本的编译器,只需要使用pragma标识,就能实现头文件不被重复引用的效果
#pragma once

#include "b.h"

void printfA();

b.h

1
2
3
4
5
6
// 高版本的编译器,只需要使用pragma标识,就能实现头文件不被重复引用的效果
#pragma once

#include "a.h"

void printfB();

作用2

宏定义的另一个作用是可以定义常量,这样在引用之后能够方便全局修改,同时能够提高可读性;

1
2
#define MAX 100
#define MIN 1
作用3

宏定义另一个重要的作用就是能够用来定义宏函数;

所谓宏函数,实际也就是函数名和参数的替换,利用这种替换,可以实现类似多态的效果;

定义两个函数名较长的函数:

1
2
3
4
5
6
7
void my_jni_fun_A(char* a){
printf("A\n");
}

void my_jni_fun_B(char* b){
printf("B\n");
}

通过提取函数名的共同点,我们可以把函数名共同部分(my_jni_fun_)提取出来,定义:

1
2
3
// ## 的作用是用来区分和指定被替换的字符串模板变量,模板变量为NAME
// N 用来表示函数的一个参数
#define jni(NAME,N) my_jni_fun_##NAME(N)

以后调用:

1
2
3
4
5
void main(){
// 等价于调用my_jni_fun_B("a");
jni(B, "a");
getchar();
}

另一种简化函数调用:

C 中是不允许函数重载的,所以通过宏函数也无法定义出重载的效果;

1
2
3
4
5
// 除了可变参数,其他的参数名字都是任意的
// __VA_ARGS__ 用来表示对可变参数的引用,即前面的 "..."参数
#define LOG(TYPE, FORMAT, ...) printf(TYPE); printf(FORMAT, __VA_ARGS__);
// 类似重载的效果,但是函数名是不允许相同的,因为C并不支持重载
#define LOG_I(FORMAT, ...) LOG("INFO: ", FORMAT, __VA_ARGS__)

调用:

1
2
3
4
5
void main(){
// 实际间接调用了LOG,接着调用printf
LOG_I("%s\n", "hihi")
getchar();
}