【C语言:编译、预处理详解】
文章目录
1.编译
我们都知道,一个程序如果想运行起来要经过编译、链接然后才能生成.exe的文件。
编译?可以分解为三个过程:
- 预处理(有些书也叫预编译)、
- 编译
- 汇编
预处理阶段主要处理那些源文件中以#开始的预编译指令。比如:#include,#define,处理的规则如下:
- 删除所有的注释
(该步骤在宏替换之前)
- 将所有的 #define 删除,并替换所有的宏定义。
- 处理所有的条件编译指令,如: #if、#elif、#else、#ifdef、#ifndef、#endif 。
- 处理#include 预编译指令,将包含的头文件的内容插?到该预编译指令的位置。这个过程是递归进行的,也就是说被包含的头文件也可能包含其他文件。
- 添加行号和文件名标识,方便后续编译器生成调试信息等。
- 保留所有的#pragma的编译器指令,编译器后续会使?。
经过预处理后的.i?件中不再包含宏定义,因为宏已经被展开。并且包含的头?件都被插?到.i文件中。所以当我们无法知道宏定义或者头文件是否包含正确的时候,可以查看预处理后的.i文件来确认。
编译阶段:就是将预处理后的?件进行?系列的:词法分析、语法分析、语义分析及优化 ,?成相应的汇编代码?件。
汇编阶段:汇编器将汇编代码转转变成机器可执?的指令,每?个汇编语句?乎都对应?条机器指令。就是根据汇编指令和机器指令的对照表??的进?翻译,也不做指令优化。
2.预处理
C语言提供的预处理功能主要有以下三种:
- 宏定义
- 条件编译
- 文件包含
2.1宏定义
2.1.1预定义符号
C语言设置了?些预定义符号,可以直接使?,预定义符号也是在预处理期间处理的。
__FILE__ //进?编译的源?件
__LINE__ //?件当前的?号
__DATE__ //?件被编译的?期
__TIME__ //?件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
2.1.2#define定义常量
- 基本使用
- 巧妙使用
- 续航符
当需要替换的内容过长的时候,可以使用 \ 实现续航。
思考:在#define定义标识符的时候,要不要在最后加上 ;
建议不要加上 ; 这样容易导致问题
如果是加了分号的情况,等替换后,if和else之间就是2条语句,?没有?括号的时候,if后边只能有?条语句 。这?会出现语法错误。
2.1.3#define定义宏
#define机制包括了?个规定,允许把参数替换到?本中,这种实现通常称为宏。
宏的声明方式:
#define name( parament-list ) stuff
其中的 parament-list 是?个由逗号隔开的符号表,它们可能出现在stuff中。参数列表的左括号必须与name紧邻
,如果两者之间有任何空?存在,参数列表就会被解释为stuff的?部分。
对于上述案例,我们可以清楚的看到括号在宏中起着至关重要的作用,为了让表达式的结果是我们想要的结果,要尽量给宏带上括号。避免在使?宏时由于参数中的操作符或邻近操作符之间不可预料的相互作?。
要点:宏在使用的时候,只是原样替换,不进行任何的改变。
下面代码的输出结果是什么呢?
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main()
{
int x = 5;
int y = 8;
int z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);
return 0;
}
z = ( (x++) > (y++) ? (x++) : (y++));
x-->5-->6
y-->8-->9-->10
z-->9
所以输出的结果是:x=6 y=10 z=9
2.1.4do-while-zero
假如现在我有一个代码,我就想在if后面跟一条用宏定义多条语句的一条语句,我可以怎么做呢?
我们发现这样是可以实现目的,但是有点别扭,而且我们习惯在一条语句后面加分号,所以在预处理后的代码中,就会对应有两个分号。
那到底能怎么处理呢?
这样是不是就可以满足既是一条语句,还能写分号(没有空语句)的目的了。
2.1.5宏的注意事项
- 源文件的任何地方,宏都可以定义,与是否在函数内外,无关
- 宏替换的规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
- 替换?本随后被插?到程序中原来?本的位置。对于宏,参数名被他们的值所替换。
- 最后,再次对结果?件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
- 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
- 宏的作用范围
从定义处开始,往后都有效(没有出现
#undef
)
出现#undef
思考下面的代码:
2.1.6宏与函数的对比
宏通常被应用于执行简单的运算。
比如在两个数中找出较?的?个时,写成下?的宏,更有优势?些。
#define MAX(a, b) ((a)>(b)?(a):(b))
那为什么不?函数来完成这个任务?
原因有二:
- 用于调用函数和从函数返回的代码可能?实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度??更胜?筹。
- 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使?。反之这个宏怎可以适?于整形、?整型、浮点型等可以?于 > 来?较的类型。宏是类型?关的。
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到
。
#include<stdio.h>
#include<stdlib.h>
#define MALLOC(num, type) (type*)malloc((num) * sizeof(type))
int main()
{
int* p = (int*)malloc(40);
int* q = MALLOC(10, int);
//(int*)malloc((10) * sizeof(int))
return 0;
}
宏和函数的?个对比
属性 | #define定义宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 | 函数的代码只出现在一个地方,每次使用函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对慢一点 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号 | 函数的参数只在调用时求值一次,结果传递给函数。表达式的求值更容易预测 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,如果宏的参数被多次计算,带有副作用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一次,结果更容易控制 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。 | 函数的参数与类型有关,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是相同的。 |
调试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
2.2条件编译
一般情况下,源程序中所有行都参加编译。但有时候希望程序中的一部分内容只在满足一定条件时才进行编译
,也就是对这一部分内容指定编译的条件,这就是“条件编译”。
条件编译的指令有以下几种形式:
- #ifdef #else # endif
它的作用是:若指定的标识符以定义(不管真假)
,则让程序段1参与编译,否则让程序段2参与编译。
#endif必须要有,#else可以没有
- #ifndef #else # endif
它的作用是:若指定的标识符没有定义
,则让程序段1参与编译,否则让程序段2参与编译。这种形式与第一种恰恰相反。
- #if #else # endif
它的作用是:判断指定表达式的结果是否为真,为真就执行程序段1;否则执行程序段2.
多分支的条件编译
利用 #if 实现 #indef / #ifndef
2.3文件包含
所谓文件包含,是指一个源文件可以将另一个源文件的全部内容包含进来,即将另外的文件内容包含到本文件之中,插入到当前位置。
C语言有两种文件包含的方式:
- #include< >
- #include" "
这两种方式有什么区别呢?- - - - -二者的查找策略不同
- #include< > :查找头文件直接
去标准路径下查找
,如果找不到就提?编译错误。 - #include" ":
先在源?件所在?录下查找
,如果该头文件未找到,编译器就像查找库函数头?件?样,再去标准路径
下查找头?件。如果找不到就提示编译错误。
这样是不是可以说,对于库?件也可以使? " " 的形式包含?
是的,可以,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地?文件了。
文件包含就是将指定文件内容包含到本文件之中,插入到当前位置。那如果我将一个头文件重复包含会怎么样呢?
包含几次,就会将所包含文件的代码复制几次,如果工程比较大,有公共使用的头文件,被?家都能使用,又不做任何的处理,那么后果真的不堪设想。
如何解决头?件被重复引?的问题?
答案:条件编译
#ifndef __TEST_H__
#define __TEST_H__
//头?件的内容
#endif //__TEST_H__
或
#pragma once
3.offsetoff
这个其实是一个宏,它的作用就是返回结构体/共用体成员,在结构体/共用体中的偏移量。
既然我们已经学习了宏,那就来模拟实现以下吧!
我们都知道,偏移量是相较于起始位置的地址,那么是不是就可以将这个起始位置想象为0。
将0强转为一个结构体类型的指针,然后去访问结构体的成员,
取出该成员的地址,这时候该成员的地址是不是就是相较于0地址处的偏移量了呢?
代码如下:
#define MY_OFFSETOF(type,member_name) (int)&(((type*)0)->member_name)
struct stu
{
char c1;
int i;
char c2;
};
int main()
{
struct stu s = { 0 };
printf("%d\n", MY_OFFSETOF(struct stu, c1));
printf("%d\n", MY_OFFSETOF(struct stu, i));
printf("%d\n", MY_OFFSETOF(struct stu, c2));
return 0;
}
4.#与##
4.1. #号
平常当我们有?个变量 int a = 10; 的时候,我们想打印出: the value of a is 10 是不是会这样写:
这样写是不是不能是字符串中的变量名联动起来,一个变量就得写一条printf,挺麻烦的。
下面了解一下#:
#运算符是将宏的?个参数转换为字符串字?量
。它仅允许出现在带参数的宏的替换列表中。#运算符所执?的操作可以理解为”字符串化“。
有了#就可以这样写:
4.2 ##号
##可以把位于它两边的符号合成?个符号,它允许宏定义从分离的?本?段创建标识符,## 被称为记号粘合。
这样的连接必须产生?个合法的标识符,否则其结果就是未定义的。
右侧为预处理后的结果:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!