C预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。简言之,C预处理器只不过是一个文本替换工具而已,他们不会指示编译器在实际编译之前所需的预处理。
头文件和源文件
我们在开始编写一份程序的时候,从键盘录入的第一行代码:
#include<stdio.h>//这里就使用了预处理,引入头文件。
- 头文件和源文件在本质上没有任何区别。
后缀为.h的文件是头文件,内含函数声明,宏定义,结构体定义等内容。
后缀为.c的文件是源文件,内含函数实现,定义变量等内容。
而且是什么后缀也没有关系,只不过编译器会默认对某些后缀的文件采取某些动作。这样分开写成两个文件是一个良好的编程风格。
简单的说其实要理解C文件与头文件(即.h)有什么不同之处,首先需要弄明白编译器的工作过程
一般说来编译器会做以下几个过程:①预处理阶段;②词法与语法分析阶段;③ 编译阶段,首先编译成纯汇编语句,再将之汇编成跟CPU相关的二进制码,生成各个目标文件 (.obj文件);④连接阶段,将各个目标文件中的各段代码进行绝对地址定位,生成跟特定平台相关的可执行文件,当然,最后还可以用objcopy生成纯二进制码,也就是去掉了文件格式信息。(生成.exe文件)
比方说,在aaa.h里定义了一个函数的声明,然后在aaa.h的同一个目录下建立aaa.c,aaa.c里定义了这个函数的实现,然后是在main函数所在.c文件里#include这个aaa.h,然后我就可以使用这个函数了。
main在运行时就会找到这个定义了这个函数的aaa.h文件。这是因为:main函数为标准C/C++的程序入口,编译器会先找到该函数所在的文件。假定编译程序编译myproj.c(其中含main())时,发现它include了mylib.h(其中声明了函数void test()),那么此时编译器将按照事先设定的路径(Include路径列表及代码文件所在的路径)查找与之同名的实现文件(扩展名为.cpp或.c,此例中为mylib.c),如果找到该文件,并在其中找到该函数(此例中为void test())的实现代码,则继续编译;如果在指定目录找不到实现文件,或者在该文件及后续的各include文件中未找到实现代码,则返回一个编译错误.
其实include的过程完全可以“看成”是一个文件拼接的过程,将声明和实现分别写在头文件及C文件中,或者将二者同时写在头文件中,理论上没有本质的区别。以上是所谓动态方式。对于静态方式,基本所有的C/C++编译器都支持一种链接方式被称为Static Link,即所谓静态链接。在这种方式下,我们所要做的,就是写出包含函数,类等等声明的头文件(a.h,b.h,…),以及他们对应的实现文件(a.cpp,b.cpp,…),编译程序会将其编译为静态的库文件(a.lib,b.lib,…)。在随后的代码重用过程中,我们只需要提供相应的头文件(.h)和相应的库文件(.lib),就可以使用过去的代码了。
相对动态方式而言,静态方式的好处是实现代码的隐蔽性,即C++中提倡的“接口对外,实现代码不可见”。有利于库文件的转发。
-
在有多个 .h 文件和多个 .c 文件的时候,往往我们会用一个 global.h 的头文件来包括所有的 .h 文件,然后在除 global.h 文件外的头文件中 包含 global.h 就可以实现所有头文件的包含,同时不会乱。方便在各个文件里面调用其他文件的函数或者变量。
#ifndef _GLOBAL_H #define _GLOBAL_H #include<fstream> #include<iostream> #include<math.h> #include<Config.h>
预处理
预处理是将源文件的文本作为翻译的第一阶段操作的文本处理步骤。 预处理不会分析源文本,但会为了查找宏调用而将源文本细分为标记。 主要包括了下面三个方面:
- 预处理指令
- 预处理运算符
- 预定义宏,这个有很多了,比如__FILE__、__LINE__和__DATA__等。
一.预处理命令
指令 | 描述 |
---|---|
#define | 定义宏 |
#include | 包含一个源代码文件 |
#undef | 取消已定义的宏 |
#ifdef | 如果宏已经定义,则返回真 |
#ifndef | 如果宏没有定义,则返回真 |
#if | 如果给定条件为真,则编译下面的代码 |
#else | #if的替代方案 |
#elif | 如果前面的#if给定条件不为真,当前条件为真,则编译下面的代码 |
#endif | 结束一个#if……#else条件编译块 |
#error | 当遇到标准错误时,输出错误消息 |
#pragma | 使用标准化方法,向编译器发布特殊的命令到编译器中 |
示例:
- 使用 #define 定义常量来增强可读性。
#define MAX_ARRAY_LENGTH 20//这个指令告诉CPP把所有的MAX_ARRAY_LENGTH 定义为 20
#include<>
和#include" "
#include<stdio.h>//这个指令告诉CPP从 系统库 中获取stdio.h,并添加文本到当前的源文件中
#include"myheader.h"//告诉CPP从 本地目录 中获取myheader.h,并添加内容到当前的源文件中
#include< >引用的是编译器的类库路径里面的头文件。
#include" "引用的是你程序目录的相对路径中的头文件,如果在程序目录没有找到引用的头文件则到编译器的类库路径的目录下找到该头文件。
- #undef
#undef FILE_SIZE
#define FILE_SIZE 42//这个指令告诉CPP取消已定义的FILE_SIZE,并定义它为42
- #ifndef
#ifndef MESSAGE
#define MESSAGE "You wish!"
#endef//这个指令告诉CPP只有当MESSAGE未定义时,才定义MESSAGE
- #ifdef
#ifdef DEBUG
/*Your debugging ststements here*/
#endif//这个指令告诉CPP如果定义了DEBUG,则执行处理语句。
在编译时,如果您向 gcc 编译器传递了 -DDEBUG 开关量,这个指令就非常有用。它定义了 DEBUG,您可以在编译期间随时开启或关闭调试。
- #error命令是C/C++语言的预处理命令之一,当预处理器预处理到#error命令时将停止编译并输出用户自定义的错误消息。如下所示:
#include<stdio.h>
#define SHOW_INFO//当删去这一行语句就会输出错误消息
int main(void){
#ifndef SHOW_INFO
#error you must define SHOW_INFO marco.
#endif
printf("hello world!\n");
return 0;
}
- #pragma参考#pragma的常用方法讲解-CSDN博客
二.预处理运算符
C预处理器提供了下列的运算符来帮助您创建宏:
1.宏延续运算符(\)
一个宏通常写在一个单行上。但是如果宏太长,一个单行容纳不下,则使用宏延续运算符(\)。例如:
#define message_for(a,b)\
printf(#a "and" #b ":We love you!\n")
2.字符串常量化运算符(#)
在宏定义中,当需要把一个宏的参数转换为字符串常量时,则使用字符串常量化运算符(#)。在宏中使用的该运算符有一个特定的参数或参数列表。例如:
#include <stdio.h>
#define message_for(a, b) \
printf(#a " and " #b ": We love you!\n")
int main()
{
message_for(Carole, Debra);
return 0;
}
//输出:Carole and Debra: We love you!
3.标记粘贴运算符(##)
宏定义内的标记粘贴运算符(##)会合并两个参数。它允许在宏定义中两个独立的标记被合并为一个标记。例如:
#include <stdio.h>
#define tokenpaster(n) printf ("token" #n " = %d", token##n)
int main()
{
int token34 = 40;
tokenpaster(34);
return 0;
}
//输出:token34 = 40
这是怎么发生的?因为这个实例会从编译器产生下列的实际输出:
printf("token34=%d",token34);
这个实例演示了token##n会连接到token34中,在这里,我们使用了字符串常量化运算符(#)和标记粘贴运算符(##)。
4.define()运算符
预处理器defined运算符是在常量表达式中的,用来确定一个标识符是否已经使用#define
定义过。如果指定的标识符已定义,则值为真(非零)。如果指定的标识符未定义,则值为假(零)。下面的实例演示了defined()运算符的用法:
#include<stdio.h>
#if !defined(MESSAGE)
#define MESSAGE "You wish!"
#endif
int main(){
printf("Here is the message:%a\n",MESSAGE);
return 0;
}
//输出:Here is the message: You wish!
三.预定义宏
ANSI C定义了许多宏。在编程中您可以使用这些宏,但是不能直接修改这些预定义的宏。
宏 | 描述 |
---|---|
_DATE_ | 当前日期,一个以“MMM DD YYYY”格式表示的字符常量 |
_TIME_ | 当前时间,一个以“HH:MM:SS"格式表示的字符常量 |
_FILE_ | 这会包含当前文件名,一个字符串常量 |
_LINE_ | 这会包含当前行号,一个十进制常量 |
_STDC_ | 当前编译器以ANSI标准编译时,则定义为1 |
实例:
#include <stdio.h>
int main()
{
printf("File :%s\n", __FILE__ );
printf("Date :%s\n", __DATE__ );
printf("Time :%s\n", __TIME__ );
printf("Line :%d\n", __LINE__ );
printf("ANSI :%d\n", __STDC__ );
return 0;
}
参数化的宏
CPP一个强大的功能是可以使用参数化的宏来模拟函数。例如,下面的代码是计算一个数的平方:
int square(int x){
return x*x;
}
我们可以使用宏重写上面的代码,如下:
#define square(x) ((x)*(x))
在使用带参数的宏之前,必须使用**#define**指令定义。参数列表是括在圆括号内,且必须紧跟在宏名称的后边。宏名称和左圆括号之间不允许有空格。例如:
#include <stdio.h>
#define MAX(x,y) ((x) > (y) ? (x) : (y))//三目运算符
int main(void)
{
printf("Max between 20 and 10 is %d\n", MAX(10, 20));
return 0;
}
//输出:Max between 20 and 10 is 20