C语言的预处理
程序的翻译环境和运行环境
翻译环境
在该环境中源代码被转换为可执行的机器指令(二进制指令)
执行环境
用与实际执行代码
翻译环境
编译:
使用编译器,对所有源文件都进行编译,获得目标文件,后缀为.obj
链接:
每个目标文件会由链接器捆绑在一起,形成单一而完整的可执行程序(.exe),于此同时,链接器会引入标准C函数库中的任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库将其需要的函数也链接到程序中
编译:(gcc test.c)->一步到位
预编译/预处理(详解见后文):
gcc -E test.c(Linux命令)
生成**.i**文件
文本操作:
#incude – 头文件的包含
删除注释 --使用空格来替换注释
#define --宏的替换
编译:
gcc -S test.i (Linux命令)
生成**.s**文件
把C代码翻译成汇编代码:
语法分析
词法分析(编译原理,语法树)
语义分析
符号汇总 (函数名,全局变量等)
汇编:
gcc -c test.s(Linux命令)
生成**.o(Linux) 文件.obj(Windows)**文件
把汇编代码转换成二进制代码(机器指令):
形成符号表(符号名+地址)
链接:
合并段表:
.o文件有固定的格式(elf文件格式),会分段
对应段的合并
符号表的合并和重定位:
有效地址替换无效地址
生成.txt文件(可执行程序)
运行环境
1.程序必须载入内存中,若环境中有操作系统,则该操作一般由操作系统来完成,若在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成
2.程序的执行便开始,接着调用main函数
3.开始执行程序代码,这个时候程序将使用一个运行时堆栈(stack),储存函数的局部变量与返回地址,程序同时也可以使用静态(static),储存与静态内存中的变量在程序的整个执行过程中一直保留他们的值
4.终止程序,正常终止main函数,也有可能是意外终止
预编译
预定义符号
__ FILE __ ->文件所在的路径
__ LINE __ ->代码所在的行数
__ DATE __ ->获取日期
__ TIME __ ->获取时间
__ STDC __ -> 如果编译器遵循ANS C 其值为1,否则未定义
#define
定义标识符:(文本替换)
带参数的宏
#define name( parament-list ) stuff //parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中 //参数列表的左括号必须紧邻name,若俩者直接存在任何空白,参数列表就会被解释为stuff一部分 //若宏的参数是表达式的时候,最好在宏定义的时候,加括号,不要吝啬括号!!
#define的替换规则
1.调用宏时,首先对参数进行检查,检测是否包含任何由#define定义的符号,如果是,它们首先被替换
2.替换文本随后被插入到程序中原来文本的位置,对与宏,参数名被他们的值所替换
3.最后,再次对结果文件进行扫描,检查它是否包含任何有#define定义的符号,如果是,则重复上述操作过程
注意:
1.宏参数和#define定义中可以出现其他#define定义的变量,但是对于宏,不能出现递归
2.当预处理器搜索#define定义中的符号时,字符串常量的内容并不能被搜索
#和##
#可以把参数插入到字符串中
##可以把位于它俩边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符
带有副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么在使用这个宏的时候就可能出现危险,导致不可测的后果,副作用就是表达式求值的时候出现永久性效果
宏和函数的对比
1.函数在调用的时候,会有函数调用和返回的开销,而宏定义在预处理阶段就完成了替换,没有函数调用和返回的开销,所以宏比函数在程序的规模和速度方面更胜一筹,同时,宏是与类型无关的
2.每次使用宏的时候,一份宏定义的代码将插入到程序中,除非宏比较短,否则可能大幅度增加程序的长度,宏是无法调试的,宏由于与类型无关,所以也不够严谨,宏可能会带来运算优先级的问题,导致程序容易出错
3.宏有时候可以做到函数做不到的事情,比如:宏的参数可以出现类型命名约定
宏名全部大写,函数名不要大写
#undef
该指令可以用于移除一个宏定义
命令行定义(gcc test.c -D)
C的编译器提供了一种能力,允许在命令行中定义符号,用于启动编译过程,在预编译阶段进行
条件编译
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的,因为有条件编译
比如说:调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译
#include <stdio.h> #define DEBUG int main() { int arr[10]={1,2,3,4,5,6,7,8,9,0}; int i=0; for (i=0;i<10;i++) { arr[i]=0; #ifdef DEBUG printf("%d ",arr[i]); #endif } return 0; }
#ifdef{…}#endif 之间的内容即为条件编译的内容,当DEBUG不被定义时,条件编译的内容不被执行,当DEBUG被定义时,可以不赋值,条件编译内容执行,该过程仍发生在预编译阶段
常见的条件编译指令:
//1. //#if 常量表达式 //条边编译的内容 //#endif //常量表达式由预处理器求值,若常量表达式非0,则执行条件编译内容,返回0,不执行条件编译的内容 //2.多个分支的条件编译 //#if 常量表达式 //…… //#elif 常量表达式 //…… //#else //…… //#endif //3.判断是否被定义 //#if defined(symbol) //#ifdef symbol //#if !defined(symbol) //#ifndef symbo //4.嵌套指令 //#if defined(OS_UNIX) // #ifdef OPTION1 // unix_version_option1(); // #endif // #ifdef OPTION2 // unix_version_option2(); //#elif defined(OS_MADOS) // #ifdef OPTION2 // msdos_version_option2(); // #endif //#endif //
文件包含(#include)
头文件被包含的方式:
1.本地文件包含:#include “filename”
查找策略:先在源文件所在的目录下查找,若该头文件未被找到,编译器就像查找库函数头文件一样在标准位置查找头文件,如果找不到就提示编译错误
2库文件包含: #include <filename.h>
查找策略;查找头文件直接区去标准路径下区查找,若找不到就提示编译错误
3.避免文件嵌套的方式
在头文件(.h文件)下
#ifndef __FILENAME.H__ #define __FILENAME.H__ //头文件中所包含的文件 #endif //或 #pragma once//防止头文件被多次重复引用 //头文件中所包含的文件