1. 程序的翻译环境和执行环境
在ANSI C的任何一种实现中,存在两个不同的环境。
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第2种是执行环境,它用于实际执行代码
例如:#define 定义的标识符会发生替换…等操作
2. 详解编译+链接
2.1 翻译环境
组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中
2.2 编译的几个阶段
2.2.1预编译/预处理过程的操作:
2.2.2 编译、汇编、链接的操作:
- 预处理 选项 gcc -E test.c -o test.i
预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。 - 编译 选项 gcc -S test.c
编译完成之后就停下来,结果保存在test.s中。 - 汇编 gcc -c test.c
汇编完成之后就停下来,结果保存在test.o中。
2.3 运行环境
程序执行的过程:
- 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中(例如:单片机),程序
的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。 - 程序的执行便开始。接着便调用main函数。
- 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack) (函数栈帧),存储函数的局部变量和返回
地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。 - 终止程序。正常终止main函数;也有可能是意外终止。
3. 预处理详解
3.1 预定义符号
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
3.2 #define
3.2.1 #define 定义标识符
#define MAX 1000
#define reg register //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;) //用更形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ )
反斜杠(续行符)其实就是将Enter换行给转义了,转义Enter也就意味着没有换行是往后追加的意思
当一行代码太长,可以采用反斜杠(续行符)进行多行书写
提问:在define定义标识符的时候,要不要在最后加上 ;(分号) ??
3.2.2 #define 定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
下面是宏的申明方式:
#define name( parament-list ) stuff
其中的 parament-list(参数列表) 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:
参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
但是这样使用存在一定的问题:
只是在X的周围加上括号还是存在运算优先级的问题
例如:
所有用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。(总结:在#define 定义宏时,不要吝啬使用括号)
3.2.3 #define 替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先
被替换。 - 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
- 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上
述处理过程。
注意:
1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏, 不能出现递归。
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
例如:
#define DOUBLE(x) ((x) + (x))
// 字符串中带x 不会发生替换
"x";
3.2.4 #和##
如何把参数插入到字符串中?
先来观察下述代码:
通过使用 # ,可以把一个宏参数变成对应的字符串
##的作用
##可以把位于它两边的符号合成一个符号。
它允许宏定义从分离的文本片段创建标识符。
3.2.5 带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么在使用这个宏的时候就可能出现危险,导致不可预测的后果。
副作用就是表达式求值的时候出现的永久性效果 ,例如:
x+1;//不带副作用
x++;//带有副作用
MAX宏可以证明具有副作用的参数所引起的问题,如下所示:
这种副作用会导致分析起来很复杂,所以在替换时要注意。
3.2.6 宏和函数对比
宏通常被应用于执行简单的运算,比如在两个数中找出较大的一个
那为什么不用函数来完成这个任务?
原因有二:
- 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。
所以宏比函数在程序的规模和速度方面更胜一筹。 - 更为重要的是函数的参数必须声明为特定的类型。
所以函数只能在类型合适的表达式上使用。反之这个宏却可以适用于整形、长整型、浮点型等可以用于>来比较的类型。
宏是与类型无关的(具体可参考3.2.4中更为高阶的玩法:传递参数类型进行打印)
当然和函数相比宏也有劣势的地方:
- 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。 例如:宏的代码长度为20行时,每次替换都是20行代码插入文本当中
- 宏是没法调试的 宏定义完成之后会在预编译阶段发生替换,而调试是发生在运行阶段,无法有效的观察宏内部的操作
- 宏由于类型无关,也就不够严谨
- 宏可能会带来运算符优先级的问题,导致程容易出现错。
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到
宏和函数的对比,如下图所示:
3.2.7 命名约定
一般来讲函数和宏的使用语法很相似。所以语言本身无法区分二者。
那我们平时的一个习惯是:把宏名全部大写 函数名不要全部大写
// 宏
MAX
// 函数
Max
3.3 #undef
这条指令用于移除一个宏定义
3.4 命令行定义
许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程
但是可以通过 命令行定义 的形式更改:
3.5 条件编译
在编译一个程序的时候,如果要将一条语句(一组语句)编译或者放弃是很方便的,可以采用条件编译指令。
比如说:调试性的代码,删除可惜,保留又碍事
常见的条件编译形式:
1. 判断是否被定义
2. #if 常量表达式
if 0 代替注释
3. 多个分支 if / elif / else
4. 嵌套指令
3.6 文件包含
#include 指令可以使另外一个文件被编译 就像它实际出现于 #include 指令的地方一样
预处理器先删除这条指令,并用包含文件的内容替换。(发生在预处理阶段)
一个源文件被包含10次,那就实际被编译10次
3.6.1 头文件被包含的方式:
- 本地文件包含:
#include “filename” 双引号
查找策略:先在源文件所在目录下查找(当前文件路径下),如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。
如果找不到就提示编译错误
标准头文件的路径各不相同,根据自己的安装路径找
- 库文件包含:
#include <filename.h> 尖括号
查找策略:直接去标准路径下去查找,如果找不到就提示编译错误
那么对于库文件是不是也可以使用 " " (双引号) 的形式包含?
答案是肯定的,可以
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。
3.6.2 嵌套文件包含
如何解决嵌套包含头文件的问题:
条件编译
4. 使用宏模拟实现offsetof
offsetof 是用来计算结构体中的成员相较于起始位置的偏移量
偏移量的计算其实就是成员变量的地址减去起始位置的地址
如果将0强制类型转换成起始位置,那么成员变量的地址就是偏移量
通过宏定义的形式,实现offsetof计算偏移量