预编译
C代码的执行流程大致可以分为:
1、编译:.c编译为目标代码(.obj);
2、链接:将目标代码与C函数库链接合并,形成最终的可执行文件(包含了汇编阶段);
3、执行;
而预编译,也称为预处理,发生于编译之前,目的是为编译做准备工作,完成头文件与对应函数代码
的替换;
比如,include指令,用来在预编译阶段将对应的头文件内容引入到指定位置;
定义myhead.txt到头文件夹下:1
printf("i am a little boy");
直接在函数中引入:1
2
3
4void main(){
#include "myhead.txt"
getchar();
}
编译可以正常运行,并打印出:i am a little boy
可见,头文件在预编译时期被替换成了其中对应的内容,此处等价于:1
2
3
4void main(){
printf("i am a little boy");
getchar();
}
头文件的作用
头文件定义了函数的声明,能够告诉编译器存在这样的函数,预编译时,头文件中的所有声明会被替
换到对应位置;当链接时,链接器负责找到头文件对应的函数实现;连接器是从所有的obj文件或者
函数链接库中(静态.a 动态.so)进行查找的。系统提供了标准的系统函数库;
对于自定义的函数,即使没有具体实现,在编译时期也不会报错误,而是在生成obj文件后,进行
链接时,找不到对应函数,才会报错,这点和Java是不同的。
1 | void main(){ |
宏定义
宏定义又称为宏替换或预编译指令;
宏定义通过define指令进行定义,其实质就是字符串的替换,在预编译时期,引用宏定义的地方将会
被替换成对应的定义内容;
作用1
宏定义可以用来作为头文件的引用标识,也能够防止头文件被重复引用;
比如在某些库中,通过宏定义来标识运行环境支持C++语法;
1 | #define __cplusplus |
另一个重要作用是防止头文件被重复引用:
a.h1
2
3
4
5
6
7
8
9// 通过define定义标识AH,当该头文件被第一次引用时,标识会进行定义
// 在进行第二次引用时,检测到AH标识已经定义,则标识和其间的代码将不会被执行;
#ifndef AH
#define AH
#include "b.h"
void printfA();
#endif
b.h1
2
3
4
5
6
7
8// 通过define定义标识BH,当该头文件被第一次引用时,标识会进行定义
// 在进行第二次引用时,检测到BH标识已经定义,则标识和其间的代码将不会被执行;
#ifndef BH
#define BH
#include "a.h"
void printfB();
#endif
other.c1
2
3
4
5
6
7
8
9#include <stdio.h>
void printfA(){
printf("a\n");
}
void printfB(){
printf("b\n");
}
main.c1
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.h1
2
3
4
5
6// 高版本的编译器,只需要使用pragma标识,就能实现头文件不被重复引用的效果
#pragma once
#include "b.h"
void printfA();
b.h1
2
3
4
5
6// 高版本的编译器,只需要使用pragma标识,就能实现头文件不被重复引用的效果
#pragma once
#include "a.h"
void printfB();
作用2
宏定义的另一个作用是可以定义常量,这样在引用之后能够方便全局修改,同时能够提高可读性;
1 | #define MAX 100 |
作用3
宏定义另一个重要的作用就是能够用来定义宏函数;
所谓宏函数,实际也就是函数名和参数的替换,利用这种替换,可以实现类似多态的效果;
定义两个函数名较长的函数:1
2
3
4
5
6
7void 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
5void main(){
// 等价于调用my_jni_fun_B("a");
jni(B, "a");
getchar();
}
另一种简化函数调用:
C 中是不允许函数重载的,所以通过宏函数也无法定义出重载的效果;
1 | // 除了可变参数,其他的参数名字都是任意的 |
调用:1
2
3
4
5void main(){
// 实际间接调用了LOG,接着调用printf
LOG_I("%s\n", "hihi")
getchar();
}