本文主要是介绍预处理--详细介绍,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
预处理这部分知识,对于我们理解程序的运行及其重要,老铁们一起来学习把,但这仅仅是对预处理的详细介绍,对于其他的过程只是简单的介绍,了解即可~
那么先来了解一个可执行程序是如何生成的吧~~
目录
1、前言
1-1 翻译环境
1-1-1 预处理(预编译)
1-1-2 编译
1-1-2-1 词法分析
1-1-2-2 语法分析
1-1-2-3语义分析
1-1-3 汇编
1-1-4 链接
1-2 运行环境
2、预定义符号
3、#define定义常量
4、#define定义宏
4-1 总结
5、带有副作用的宏参数
6、宏替换的规则
7、宏与函数的对比
7-1 宏与函数简要对比
7-2 宏与函数详细对比
8、#和##
8-1 #运算符
8-2 ##运算符
9、命令约定
10、#undef
11、命令行编译
12、条件编译
12-1-1 指令1
12-1-2 指令2
12-1-3 指令3
13、头文件的包含
13-1 头文件被包含的方式
13-1-1 本地文件包含
13-1-2 库文件包含
13-2嵌套文件包含
14、笔试题
14-1 试题1---指令问题
14-2 试题2---头文件包含问题
14-3 其他预处理指令
14-1-1 #error指令
14-1-2 #line指令
14-1-3 #progma指令
15、总结
15-1 总结
15-2 警告的总结
15-3 编程提示的总结
1、前言
在ANSIC的任何一种实现中,存在两个不同的环境
一个是翻译环境,在这个环境中源代码被转换为可执行的机器指令
一个是执行环境 ,它用于实际执行代码
主要学习翻译环境
1-1 翻译环境
翻译环境包括:预编译,编译,汇编,链接
其实翻译环境是由编译和链接两个大过程组成的,而编译又可以分为:预处理,编译,汇编三个过程
一个C语言项目可能由多个 .c文件一起构建,那么多个.c文件是如何生成可执行程序呢?
- 多个 .c文件单独经过编译器,编译处理生成对应的目标文件
- 注意:在Windows环境下的目标文件的后缀是 .obj,Linux环境下的目标文件的后缀是 .o
- 多个目标文件和链接库一起经过链接器处理生成最终的可执行程序
- 链接库是指运行时库(它是支持程序运行的基本函数集合)或第三方库
如果再把编译器展开成3个过程,那就是如下的过程:
1-1-1 预处理(预编译)
在预处理阶段,源文件和头文件会被处理成 .i为后缀的文件。
具体内容请看下文~
1-1-2 编译
编译过程就是将预处理后的文件进行一系列的:词法分析,语法分析,语义分析及优化,生成相应的汇编代码文件。
1-1-2-1 词法分析
源代码程序被输入扫描器,扫描器的任务就是简单的进行词法分析,把代码中的字符分割成一系列的记号(关键字、标识符、字面量、特殊字符等)。
1-1-2-2 语法分析
就下来语法分析器对扫描产生的记号进行语法分析,从而产生语法树。这些语法树是以表达式为节点的树。
比如:
array[index] = (index+4)*(2+6);
该段代码产生的语法数如下:
1-1-2-3语义分析
语义分析器来完成语义分析,即对表达式的语法层面进行分析。编译器所能做的分析是语义的静态分析。静态语义分析包括:声明和类型的匹配,类型的转换等。这个阶段会报告错误的语法信息。
1-1-3 汇编
汇编器将汇编代码转变成机器可执行的指令,每一条汇编指令几乎都对应一条机器指令。就是根据汇编指令和机器指令的对照表一 一的进行翻译,也不做指令优化。
1-1-4 链接
- 链接是一个复杂的过程,链接的时候需要把一堆文件链接在一起才能生成可执行程序
- 链接过程主要包括:地址和空间分配,符号决议和重定位这些步骤。
- 链接解决的是一个项目中多个文件、多个模块之间互相调用的问题。
比如一个C语言项目中有test.c和add.c文件
我们知道每个源文件都是单独经过编译器处理生成对应的目标文件。
test.c 经过编译器处理生成 test.o
add.c 经过编译器处理生成add.o
重定位的引入~
我们在test.c 的文件中使用了add.c文件中的 Add函数和 g_val 变量。
我们在 test.c 文件中每一次使用Add 函数和 g_val 的时候必须确切的知道Add和g_val 的地址,但是由于每个文件是单独编译的,在编译器编译test.c 的时候并不知道Add函数和 g_val变量的地址,所以暂时把调用Add 的指令的目标地址和g_val 的地址搁置。等待最后链接的时候由链接器根据引用的符号Add 在其他模块中查找Add 函数的地址,然后将test.c 中所有引用到 Add 的指令重新修正,让他们的目标地址为真正的Add函数的地址,对于全局变量g_val 也是类似的方法来修正地址。这个地址修正的过程也被叫做:重定位。
1-2 运行环境
简单介绍运行环境把,了解即可
2、预定义符号
C语言设置了一些预定义符号可以直接使用,预定义符号也是在预处理期间处理的~
比如:
预处理的符号
#include<stdio.h>
int main()
{__FILE__ 进行编译的源文件__LINE__ 文件当前的行号__DATE__ 文件被编译的日期__TIME__ 文件被编译的时间__STDC__ 如果编译器遵循ANSIC,那么值是1,否则未定义printf("file:%s,line:%d\n", __FILE__, __LINE__);printf("date:%s,time:%s\n", __DATE__, __TIME__);return 0;
}
输出结果
file:D:\C--knowladge\c-primary\预处理\预处理\test.c,line:13
date:May 8 2024,time:21:37:01
3、#define定义常量
语法:
#define name stuff
比如:
#define reg register
//为register这个关键字创建一个简短的名字
#define do_favour() for(;;)
//用更形象的符号代替一种实现
#define CASE break;case
//在写case语句时,自动把break语句写上
#define PRINT printf("date:%s\t,\time:%s\n",\__DATE__, __TIME__)
//如果定义过长,可以分成几行写,除最后一行外,其余每行的后面都有加上一个反斜杠('\'),也称为续行符。
那么有人就会存在这样的疑问,#define在定义标识符的时候,要不要再最后加上;
代码如下:
#define定义常量
#define MAX 100;
答案是最好不要加上;这样容易导致问题
比如下面的这个场景
#define MAX 100;
#include<stdio.h>
int main()
{while (1){int m = MAX;//#define 定义标识符时最后,最好不要添加';'int m = MAX;}return 0;
}
分析:如果加上;在实际应用宏的时候就会存在问题
4、#define定义宏
#define机制包括了一个规定,允许把参数替换到文本中,这种实现称为宏或者定义宏。
如下是宏的声明:
#define name(parament_list) stuff
其中parament_list是一个由逗号隔开的符号表,它们可能出现在stuff中
注:参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
比如:
#define SQUARE( x ) x * x
这个宏接收参数,如果在上述声明之后,传参得到SQUARE( 5) 置于程序中,就会用下面的表达式替换上面的表达式 5*5
实际上这个宏是有问题的,看下面的代码是你想的那样吗?
#define SQUARE(x) x*x
#include<stdio.h>
int main()
{int a = 10;printf("result is:%d\n", SQUARE(a+1));//解析:printf("result is:%d\n", a+1*a+1);return 0;
}
输出结果:
result is:21
解决方案:
在宏定义上加上两个括号,这就可以解决!
#define SQUARE( x ) (x) * (x)
这就可以达到预期效果
代码如下:
#define SQUARE(x) (x)*(x)#include<stdio.h>
int main()
{int a = 10;printf("result is:%d\n", SQUARE(a+1));return 0;
}
输出结果
121
这里还有一个宏定义
#define DOUBLE(x) (x) + (x)
定义中使用括号想避免之前的问题,但这个宏可能会出现新的问题~
代码如下:
#define DOUBLE(x) (x)+(x)
#include<stdio.h>
int main()
{int a = 10;printf("result is:%d\n", 10*DOUBLE(a));return 0;
}
输出结果
110
原因:乘法运算先于宏定义的加法运算
解决方案:
在宏定义表达式两边加上一对括号~
#define DOUBLE(x) ((x) + (x))
代码如下:
//解决方案
#define DOUBLE(x) ((x)+(x))#include<stdio.h>
int main()
{int a = 10;printf("result is:%d\n", 10*DOUBLE(a));//文本替换为://之前:printf("result is:%d\n", 10 * (10)+(10));//乘法运算先于宏定义的加法运算return 0;
}
输出结果:
200
4-1 总结
总结:所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间产生不可预料的相互作用。
5、带有副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能存在危险,导致不可预料的后果。副作用是表达式求值的时候出现的永久性效果
比如:
i+1;//不带副作用
i++;//带副作用
MAX可以证明具有副作用的参数所引起的问题
代码如下:
#define MAX(a,b) ((a)>(b)?(a):(b))
#include<stdio.h>
int main()
{int a = 6, b = 7;int z = MAX(a++, b++);printf("a=%d b=%d z=%d\n", a,b,z);//文本替换为:z = ((a++) > (b++) ? (a++) : (b++))return 0;
}
输出结果:
a=7 b=9 z=8
6、宏替换的规则
在程序中扩展#define定义符号和宏时,需要涉及以下步骤
- 在调用宏时,首先对参数进行检查,看是否包含任何由#define定义的符号。如果是它们首先被替换。
- 其次,替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被它们的值所替换。
- 最后,再次对结果文件进行扫描,看是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
实际就是文本替换的过程!!
注意:
- 宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归!
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索!
7、宏与函数的对比
宏通常用于执行简单的运算
比如:
//宏参数的对比
#define MAX(a,b) ((a)>(b)?(a):(b))
为什么不使用函数完成?
原因如下:
- 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需的时间更多。所以宏比函数在程序规模和速度方面更优
- 更重要的是函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使用,反之这个宏可以适用于整型,长整型,浮点型等可以用>比较的类型,宏的参数是与类型无关的
和函数相比宏的劣势
- 宏是没法调试的
- 宏与类型无关,也就不够严谨
- 宏可能会带来运算符优先级的问题,容易出现错误
- 每次使用宏时,一份宏定义的代码将插入到程序中,除非宏比较短,否则可能大幅度增加程序的长度
宏有时可以做到函数做不到的事情,例如:宏的参数可以出现类型,但函数做不到
下面这段代码恰好能说明宏的优势
//宏的参数可以出现类型
#define MALLOC(num,type)\
(type*)malloc(num*sizeof(type))
int main()
{int*ptr=MALLOC(10, int);//等价于int* ptr = (int*)malloc(10 * sizeof(int));return 0;
}
7-1 宏与函数简要对比
属性 | #define定义宏 | 函数 |
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增加 | 函数代码只出现于一个地方,每次使用函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对慢一些 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多写括号 | 函数参数只在函数调用的时候求值一次。表达式的结果较容易控制 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,如果宏的参数被多次计算,带有副作用的参数求值可能产生不可预料的结果 | 函数参数只在传参的时候求值一次,结果容易控制 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用任何类型的参数 | 函数的参数是与类型有关的,如果参数类型不同,就需要不同的函数,即使它们的任务不同 |
调试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
7-2 宏与函数详细对比
属性 | 宏 | 函数 |
使用方式 | 宏是通过预处理指令#define定义的,它做的是简单的文本替换,不涉及类型检查 | 函数是具有特定返回类型和参数类型的代码块,需要通过函数声明来定义 |
执行速度 | 由于宏在预处理阶段就进行了替换,因此它的执行速度通常比函数快 | 函数调用涉及到现场保存、参数传递和返回地址等额外的开销。所以速度慢一些 |
参数求值 | 在宏中,参数每次用于宏定义时都将重新求值 | 在函数中,参数只在函数调用时求值一次,并将结果传递给函数 |
作用时机 | 宏的替换在编译之前进行 | 函数是在编译之后执行时才调用的 |
内存消耗 | 宏的参数不占用额外的内存空间,因为它们只是在预处理阶段被替换 | 函数调用则需要为参数和局部变量分配内存空间 |
类型安全 | 宏则没有类型要求,可以处理不同类型的参数,但这也可能导致类型错误或精度丢失的问题 | 函数的参数必须声明特定的类型,这提供了类型安全的保证 |
调试与递归 | 宏则不能进行调试,也不支持递归,因为每次展开都会生成一份副本,可能导致代码膨胀。 | 函数可以被调试,支持递归调用 |
总的来说,宏在执行速度上可能优于函数,但在类型安全、调试和代码管理方面,函数更为强大和灵活。在实际开发中,选择使用宏还是函数应根据具体的应用场景和需求来决定。
8、#和##
8-1 #运算符
#运算符将宏的一个参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中。
#运算符所执行的操作可以理解为“字符串化”
比如:我们想打印出the value of a is 100,就可以使用下面这个宏~
//#运算符---字符串化
#define PRINT(n) printf("the value of "#n" is %d",n);
int main()
{int n = 100;PRINT(n);return 0;
}
输出结果:
the value of n is 100
#define PRINT(n,format) printf("the value of "#n " is "format"\n",n)int main()
{/*int a = 100;PRINT(a,"%d");*/float b = 3.14f;PRINT(b, "%f");return 0;
}
输出结果:
the value of b is 3.140000
8-2 ##运算符
##运算符可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。##被称为记号标记
这样的连接必须产生一个合法的标识符,否则其结果就是未定义的。
比如:我们想实现比较数据的大小的函数,正常的书写一个个函数比较繁琐,此时就可以使用##运算符
//##运算符---记号粘合
#define GENERIC_MAX(type)\
type type##_max(type a,type b)\
{\return (a>b?a:b);\
}
//使用宏定义不同的函数
GENERIC_MAX(int);
GENERIC_MAX(float);int main()
{int a = int_max(10, 100);printf("%d\n", a);float b = float_max(3.1f, 2.1f);printf("%f\n", b);return 0;
}
分析:相当于调用int_max和float_max函数,书写更加简便~
输出结果:
100
3.100000
9、命令约定
一般来讲函数与宏的使用语法很相似。所以语言无法帮助我们区分二者。
那么我们平时的一个习惯是:
把宏名全部大写
函数名不要全部大写
10、#undef
#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除
如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除
11、命令行编译
许多C语言编译器提供了一种能力,允许在命令行中定义符号,用于启动编译过程。
比如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。
假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大些。
比如:
#define ARRAY_SIZE 10
#include <stdio.h>
int main()
{int array[ARRAY_SIZE];int i = 0;for (i = 0; i < ARRAY_SIZE; i++){array[i] = i;}for (i = 0; i < ARRAY_SIZE; i++){printf("%d ", array[i]);}printf("\n");return 0;
}
12、条件编译
在编译一个程序的时候,我们如果要将一条语句/一组语句编译或放弃是很方便的。因为我们有条件编译指令
比如说:
#define __DEBUG__
int main()
{int i = 0;int arr[10] = { 0 };for (i = 0; i < 10; i++){arr[i] = i;
#ifdef __DEBUG__printf("%d\n", arr[i]);//观察数组arr是否赋值成功
#endif //__DEBUG__}return 0;
}
调试性的代码,删除可惜,保留又碍事。所以我们可以选择性的编译。
常见的条件编译的指令
12-1-1 指令1
//常见的条件编译指令
#if 常量表达式
...
#endif#define __DEBUG__ 1……
#if __DEBUG__//..
#endif
比如:
#define M 2
int main()
{
#if M==0printf("hehe\n");
#elif M==1printf("haha\n");
#elif M==2printf("hello world!\n");
#elseprintf("no\n");
#endifreturn 0;
}
输出结果:
hello world!
12-1-2 指令2
多个分支的条件编译
#if 常量表达式//...
#elif 常量表达式
//...
#else
//...
#endif
判断是否被定义
#if defined(symbol)
#ifdef symbol#if !defined(symbol)
#ifndef symbol
比如:
#define MAX 100
int main()
{
//如果是定义的话
//#ifdef MAX
// printf("hehe!\n");
//#endif
//#if defined(MAX)
// printf("hehe!\n");
//#endif
//如果没有定义的话
//#ifndef MAX
// printf("haha!\n");
//#endif
#if !defined(MAX)printf("haha!\n");
#endifreturn 0;
}
解析:如果MAX未定义,就输出haha,否则输出空
12-1-3 指令3
嵌套指令
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
13、头文件的包含
13-1 头文件被包含的方式
13-1-1 本地文件包含
#include "filename"
查找方式:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。
若找不到就提示编译错误。
比如VS环境的标准头文件的路径
C:\Users\华为\Desktop\kil5-project\5-1模块化编程
13-1-2 库文件包含
#include <filename.h>
查找方式:查找头文件直接去标准路径查找,如果找不到就提示编译错误!
那么对于库文件也可以使用“ ”的形式包含,但是这样做查找的效率就低些,当然这样也不容易区分该文件是库文件还是本地文件。
13-2嵌套文件包含
我们知道,#include指令可以使另外一个文件被编译。就像它实际出现于#include指令的地方一样。
这种替换方式很简单:预处理器先删除这条指令,并用包含文件的内容替换。
如果一个头文件被包含了10次,那就实际被编译10次,如果重复包含,对编译的压力就比较大。
比如:如果test.c文件中将test.h包含5次,那么test.h文件的内容将会被拷贝5份在test.c中,
如果test.h文件比较大,这样预处理后代码量会剧增。如果工程比较大,有公共使用的头文件,被大家使用,又不做任何处理,那么后果不堪设想!
那么如何解决头文件被重复引入的问题?
答案就是条件编译。
每个头文件的开头写:
//避免头文件重复引用
#ifndef __NAME__
#define __NAME__
//头文件的内容#endif
或者
#pragma once
就可以避免头文件的重复引入
在实际中应用如下:
14、笔试题
14-1 试题1---指令问题
1. 头文件中的ifndef/define/endif是干什么用的?
这是一个防止头文件重复包含的问题。在C/C++中,如果一个头文件被多次包含,可能会导致编译器报告变量或函数重复定义的错误。为了避免这种情况,我们使用#ifndef, #define, 和 #endif预处理指令。
- #ifndef(如果未定义): 检查某个特定的宏是否未被定义。
- #define(定义): 定义一个宏。
- #endif(结束 if): 结束前面的#ifndef。这种结构通常被称为“include guards”,其目的是确保头文件只被包含一次。
14-2 试题2---头文件包含问题
2. #include <filename.h> 和 #include "filename.h"有什么区别?
这两种形式都是用来包含头文件的,但是搜索头文件的方式不同:
- #include <filename.h>: 编译器首先在系统头文件目录(例如 /usr/include)中查找指定的头文件。
- #include "filename.h": 编译器首先在当前目录下查找指定的头文件,如果没有找到,再去系统头文件目录中查找。<> 语法通常用于标准或系统提供的头文件, 而 "" 通常用于程序自己的头文件 。因此,通常来说, 如果你要包含的是标准库头文件,你应该使用尖括号< >;如果你要包含的是你自己编写的头文件或者第三方库的头文件,你应该使用双引号" "。
14-3 其他预处理指令
#error
#pragma
#line
#pragma pack()//结构体部分内容
14-1-1 #error指令
#error是C/C++预处理器指令,用于在编译时生成一个错误消息。当编译器遇到 #error指令时,它会停止编译并显示指定的错误消息。这通常用于检查编译器、平台或特定条件是否满足要求。
使用方法如下:
#error "错误信息"
例如,如果你想确保编译器支持C++11标准,可以使用以下代码:
#if __cplusplus < 201103L
#error "需要支持C++11标准的编译器"
#endif
14-1-2 #line指令
#line是C/C++预处理器指令,用于改变编译器产生的错误和警告消息中的行号和文件名。这通常用于将源代码中的一部分代码映射到另一个文件中,以便在编译时生成正确的错误消息。
使用方法如下:
#line number "filename"
其中,number是要设置的行号,filename是要设置的文件名。如果不指定文件名,则只更改行号。
例如,如果你想将一段代码映射到另一个文件中,可以使用以下代码:
// 原始文件:main.cpp
#include <iostream>int main() {std::cout << "Hello, World!" << std::endl;return 0;
}// 映射文件:mapped.cpp
#line 1 "main.cpp"
#include <iostream>int main() {std::cout << "Hello, World!" << std::endl;return 0;
}
在这个例子中,我们将 main.cpp中的代码映射到 mapped.cpp中,并使用 #line 指令将行号设置为1。这样,当编译器在 mapped.cpp 中遇到错误时,它会显示正确的行号和文件名。
14-1-3 #progma指令
#progma是C/C++预处理器指令,用于在编译时设置编译器的特定选项。这通常用于控制编译器的行为,例如启用或禁用某些特性、优化级别等。
使用方法如下:
#progma directive
其中,directive 是要设置的编译器选项。不同的编译器可能支持不同的选项,具体取决于编译器的实现。
例如,如果你想设置GCC编译器的优化级别为3,可以使用以下代码:
#progma GCC optimize("O3")
在这个例子中,我们使用 #progma指令将GCC编译器的优化级别设置为3。这将告诉编译器使用最高级别的优化来生成代码。
15、总结
15-1 总结
编译一个C程序的第1个步骤就是对它进行预处理。预处理器共支持5个符号,它们在表中#define 指令把一个符号名与一个任意的字符序列联系在一起。例如,这些字符可能是一个字面值常量、表达式或者程序语句。这个序列到该行的末尾结束。如果该序列较长,可以把它分开数行,但在最后一行之外的每一行末尾加一个反斜杠。宏就是一个被定义的序列,它的参数值将被替换。当一个宏被调用时,它的每个参数都被一个具体的值替换。为了防止可能出现于表达式中的与宏有关的错误,在宏完整定义的两边应该加上括号。同样,在宏定义中每个参数的两边也要加上括号。#define指令可以用于"重写"C语言,使它看上去像是其他语言。
#argument 结构由预处理器转换为字符串常量"argument"。##操作符用于把它两边的文本粘贴成同一个标识符。
有些任务既可以用宏也可以用函数实现。但是,宏与类型无关,这是一个优点。宏的执行速度快于函数,因为它不存在函数调用/返回的开销。但是,使用宏通常会增加程序的长度,但函数却不会。同样,具有副作用的参数可能在宏的使用过程中产生不可预料的结果,而函数参数的行为更容易预测。由于这些区别,使用一种命名约定,让程序员很容易地判断一个标识符是函数还是宏是非常重要的。
在许多编译器中,符号可以从命令行定义。#undef 指令将导致一个名字的原来定义被忽略。
使用条件编译,你可以从一组单一的源文件创建程序的不同版本。#if 指令根据编译时测试的结果,包含或忽略一个序列的代码。当同时使用#elif 和#else 指令时,你可以从几个序列的代码中选择其中之一进行编译。除了测试常量表达式之外,这些指令还可以测试某个符号是否已被定义。#ifdef 和#ifndef 指令也可以执行这个任务。
#include指令用于实现文件包含。它具有两种形式。如果文件名位于一对尖括号中,编译器将在由编译器定义的标准位置查找这个文件。这种形式通常用于包含函数库头文件时。另一种形式,文件名出现在一对双引号内。不同的编译器可以用不同的方式处理这种形式。但是,如果用于处理本地头文件的任何特殊处理方法无法找到这个头文件,那么编译器接下来就使用标准查找过程来寻找它。这种形式通常用于包含你自己编写的头文件。文件包含可以嵌套,但很少需要进行超过一层或两层的文件包含嵌套。嵌套的包含文件将会增加多次包含同一个文件的危险,而且使我们更难以确定某个特定的源文件依赖的究竟是哪个头文件。
#error 指令在编译时产生一条错误信息,信息中包含的是你所选择的文本。#line 指令允许你告诉编译器下一行输入的行号,如果它加上了可选内容,它还将告诉编译器输入源文件的名字。因编译器而异的#progma 指令允许编译器提供不标准的处理过程,比如向一个函数插入内联的汇编代码。
15-2 警告的总结
1.不要在一个宏定义的末尾加上分号,使其成为一条完整的语句。
2.在宏定义中使用参数,但忘了在它们周围加上括号。
3.忘了在整个宏定义的两边加上括号。
15-3 编程提示的总结
1.避免用#define指令定义可以用函数实现的很长序列的代码。
2.在那些对表达式求值的宏中,每个宏参数出现的地方都应该加上括号,并且在整个宏定义的两边也加上括号。
3.避免使用#define 宏创建一种新语言。
4.采用命名约定,使程序员很容易看出某个标识符是否为#define 宏。
5.只要合适就应该使用文件包含,不必担心它的额外开销。
6.头文件只应该包含一组函数和(或)数据的声明。
7.把不同集合的声明分离到不同的头文件中可以改善信息隐藏。
8.嵌套的#include 文件使我们很难判断源文件之间的依赖关系。
一键三联吧,老铁们~~
这篇关于预处理--详细介绍的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!