C语言预处理预编译命令及宏定义详解
.c 源程序 ----- 编译 ----- 链接 ---- exe ----运行 -------->
程序翻译环境和执行环境
翻译环境:源代码被转换为可执行机器指令(二进制代码)。
执行环境:用于实际执行代码。
翻译环境:详解编译+链接
1.组成程序的每个源文件通过编译过程分别转换成目标代码。
2.每个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序。
3.链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且他可以搜索程序员个人的程序库,将其需要的函数也链接到程序。
extern
声明外部文件中的函数
1. 编译 ― 预处理/预编译 test.c ---- test.i
文本操作
#include 头文件的包含
注释删除:使用空格替换注释
#define 替换,所以宏无法进行调试。
……
2. 编译 ― 编译 test.i ---- test.s
把c语言代码翻译成汇编代码
语法分析
词法分析
语义分析
符号汇总
3. 编译 ― 汇编 test.s ---- test.obj
把汇编代码转换成二进制代码(指令)。
形成符号表。(符号+地址)
4. 链接 test.obj ---- test.exe
合并段表
符号表的合并和重定位
运行环境
1.程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2.程序的执行便开始。接着便调用main函数。
3.开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
4.终止程序。正常终止main函数;也有可能是意外终止
预处理/预编译详解
预定义符号
本来就有的符号
__FILE__ //进行编译的源文件 __LINE__ //文件当前的行号 __DATE__ //文件被编译的日期 __TIME__ //文件被编译的时间 __STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
应用
printf("data: %s\n time: %s" ,__DATE__,__TIME__);
输出
data: Jul 13 2021 time: 15:13:54
#define 定义标识符
宏
宏和define区别,宏是有参数的。
下面是宏的声明方式:
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中
参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuf的一部分。
例如
#define SQUARE(X) (X)*(X) int main() { int ret = SQUARE(5); return 0; }
宏的参数是替换的,不是传参的。
在定义宏的时候不要吝啬括号。
#和##
#的作用
使用#
,把一个宏参数变成对应的字符串。
把参数插入到字符串中
#define PRINT(X) printf("the value of "#X" is %d\n", X) int main() { int a = 10; int b = 20; PRINT(a); PRINT(b); return 0; }
输出
the value of a is 10
the value of b is 20
##的作用
##
可以把位于他两边的符号合成一个符号,允许宏定义从分离的文本片段创建创建标识符。
#define CAT(X,Y) X##Y int main() { int class84 = 2021; printf("%d\n", CAT(class, 84)); }
输出
2021
带副作用的宏参数
#define MAX(a, b) ( (a) > (b) ? (a) : (b) ) ... x = 5; y = 8; z = MAX(x++, y++); printf("x=%d y=%d z=%d\n", x, y, z); //输出的结果是什么?
这里我们得知道预处理器处理之后的结果是什么:
z = ( (x++) > (y++) ? (x++) : (y++));
输出结果
x=6 y=10 z=9
宏和函数的对比
对于上述的宏,也可以用函数实现其功能。
使用宏的优点:
1.用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作需要的时间更多,所以宏比函数在程序的规模和速度方面更胜一筹。
2.函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使用。反之,这个宏可以用于整型、长整型、浮点数等等,宏是类型无关的。
使用宏的缺点:
1.每次调用宏,一份宏定义的代码插入程序中,除非宏比较短,否则可能会大幅度增加代码的长度。
2.宏无法调试。在预编译(预处理)阶段,已经把 # define 给替换了,已经不再是宏了。
3.宏由于类型无关,也就不够严谨。
3.宏可能会带来运算符优先级的问题,更容易导致程序出错。
inline
内联函数
命名约定
函数和宏语法相似,语言本身没法帮我们区分二者。把宏名全部大写,函数名不要全部大写。
#undef 移除宏定义
这条指令用于移除宏定义。
如果现存的一个名字需要被重新定义,那么他的旧名字首先要被移除。
#undef NAME
命令行定义
许多C的编译器提供了一种能力,允许在命令行中定义符号,用于在启动编译过程。例如:当我们根据一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。假设某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写。
条件编译
#define DEBUG #ifdef DEBUG #endif
常见的条件编译指令
#if 常量表达式 //... #endif
举例子:为真参与编译,为假 (0)不参与编译。
#if 1 printf("balabala...."); #endif
二、多个分支的条件编译
#if 常量表达式 //... #elif 常量表达式 //... #else //.... #endif
举例子
#if 1==1 #elif 2==1 #else #endif
三、判断是否被定义
#if defined(symbol) #ifdef symbol #if !defined(symbol) #ifndef symbol
四、嵌套指令
#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
文件包含
我们已经知道,#include指令可以使另外一个文件被编译。就像它实际出现于#include指令的地方一样。这种替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容替换。这样一个源文件被包含10次,那就实际被编译10次。
头文件包含的方式:
1.本地文件包含:#include "Filename"
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误。
2.库文件包含:#include <Filename.h>
查找策略:查找头文件直接去标准路径下去查找,如果找不到就返回错误信息。
这样是不是可以说,对于库文件也可以使用“”的形式包含?
答案是肯定的,可以。但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。
wwww想到自己经常重复包含,留下了悔恨的泪水~~
出现嵌套文件包含解决方法 :条件编译
每个头文件开头这样写:
#ifndef __TEST__H__ #define __TEST__H__ //头文件的内容 #endif //__TEST__H__
或者
#pragma once
就可以避免头文件的重复引入。
总结一下:预处理阶段的预处理指令:条件编译指令 / #include / #define / #error /#pragma / ……
offsetof(宏类型,成员名字)偏移量模拟实现
#include <stdio.h> #include <stdlib.h> #include <stddef.h> struct S { char c1; int a; char c2; }; #define OFFSETOF(struct_name, member_name) (int)&(((struct_name*)0)->member_name) int main() { printf("%d\n", OFFSETOF(struct S, c1)); printf("%d\n", OFFSETOF(struct S, a)); printf("%d\n", OFFSETOF(struct S, c2)); return 0; }