预处理(十二-终)
翻译环境和执行环境
翻译环境
在这个环境中源代码被转换为可执行的机器指令,就是把(.c)文件翻译成(.exe)文件。
一个C语言程序需要经过的四个步骤:编辑(.c)、编译(.obj)、链接(.exe)、运行。
而其中编译又分为:预编译(预处理)、编译、汇编。
预编译(预处理)
源码中的所有预处理语句(#号开头的语句便是预处理语句,例如:#include)得到处理并删除注释。
编译
就是把C语言代码翻译成汇编代码。
汇编
把汇编代码翻译成二进制代码。
链接
但机器代码还是不能直接运行。所以链接器将处理合并目标代码,生成一个可执行目标文件,可以被加载到内存中,由系统执行。
执行环境
这个环境用于实际执行代码。
- 程序必须载入内存中。在有操作系统的环境中,一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
- 程序的执行开始。接着便调用main函数。
- 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储在静态内存中的变量在程序的整个执行过程一直保留他们的值。
- 终止程序。正常终止main函数;也有可能是意外终止。
预编译(预处理)详解
预定义符号
1 | __FILE__//源文件的绝对路径 |
例
1 | #include <stdio.h> |
预处理指令
1 | #开头的就是预处理指令例如: |
#define定义标识符
#define name stuff
例:
1 | #include <stdio.h> |
注:在define定义标识符时,最后不要加;
#define定义宏
#define可以把参数替换到文本中,这种实现称为宏(macro)或定义宏(define macro)。
宏的申明方式:
#define name(parament-list) stuff
parament-list是一个由逗号隔开的符号表,这些符号可能出现在stuff中。
参数列表的左括号必须与name紧邻
例:
1 | #include <stdio.h> |
注:宏的参数不是传参,而是替换
例:
1 | #include <stdio.h> |
所以为了避免这个问题,我们要给stuff加上括号(stuff)以及stuff中每个参数也加上括号。
1 | #include <stdio.h> |
注:
- 宏参数和#define定义中可以出现其他#define的变量,但宏不能出现递归。
- 预处理器搜索#define定义的符号时,字符串常量的内容并不被搜索,通俗点讲,就是字符串常量的内容不会被替换。
#和##的区别
#是将其后面的宏参数进行字符串化操作,并不替换。
例:
1 | #include <stdio.h> |
##是将它两边的符号合成一个符号。
例:
1 | #include <stdio.h> |
宏和函数
区别
- 宏的参数与类型是无关的,只要对参数的操作合法,就可以使用于任何类型;函数的参数类型是固定的,如果参数的类型不同,就需要使用不同的函数,即使他们执行的任务是相同的。
- 宏可能会带来运算优先级的问题,导致运算结果出错;
- 宏的参数替换是直接替换的;而函数调用时会将实参的值传给形参;
- 宏是不方便调试的,因为宏是在编译之前进行(先用宏体替换宏名,再进行编译);而函数是可以逐语句调试;
- 宏的参数是不占内存空间的,因为只做字符串的替换;而函数调用时参数之间的传递,所以占用内存;
- 宏的速度比函数速度快,因为函数有调用和返回时间的开销;
- 宏在传参时可以传类型,但是函数不能传类型;
- 宏不能递归,函数可以递归;
- 宏的代码长度很长(除去非常小的宏),每次使用时,宏代码都被插入到程序中,使得程序的长度大幅度增加;
命名约定
宏名全部大写,函数名不要全部大写。
#undef
用于移除一个宏定义。
#undef name
例:
1 | #include <stdio.h> |
条件编译
选择性的按照某种条件编译代码。
常见的条件编译指令:
1 | //1 |
文件包含
#include指令可以使另外一个文件被编译。就像它实际出现于#include指令的地方一样。
替换方式:预处理器先删除这条指令,并用包含文件的内容替换。这样一个源文件被包含10次,那就实际被编译10次。
头文件被包含方式
本地文件包含#include "filename.h"
先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误。
库文件包含#include <filename.h>
编译器直接在标准位置查找头文件。如果找不到就提示编译错误。