一、翻译环境
(1)组成一个程序的每个源文件,通过翻译过程分布转换成对应的目标代码;
(2)每个目标文件由链接器链接到一起,形成一个单一而完整的可执行程序;
(3)链接器同时还会引入标准库中任何被该程序所使用到的函数,而且它还可以搜索程序员个人的程序库,将其所需要的函数也一同链接到程序中。
二、程序编译与运行
1. 程序编译的四个阶段
1.1 预处理
(1)引入头文件;
(2)去掉注释;
(3)宏替换;
……
1.2 编译
(1)语法分析;
(2)词法分析;
(3)符号汇总;
(4)语义分析;
……
把高级语言代码翻译成与之等价的汇编指令。
1.3 汇编
把汇编代码转换为与之等价的二进制机器指令。
1.4 链接
将机器指令与之所使用的库函数对应库文件中的机器指令打包到一起,组织成为可执行程序。
2. 运行环境
程序的执行过程:
(1)程序运行必须载入到内存中。在操作系统环境中:这个操作一般由操作系统完成;在独立环境中:程序的载入必须由手工完成,也可能通过可执行代码置入只读内存来完成。
(2)程序执行开始,紧接着调用main函数。
(3)开始执行程序代码:此时程序将使用一个运行时堆栈,存储函数的局部变量和返回地址。程序同时也可以使用静态内存,存储于静态内存中的变量在程序的整个执行过程中一直保持有效。
(4)程序终止:正常终止于main函数返回,也可能在程序运行过程中发生意外终止。
三、详解预处理
1. 预定义符号
__FILE__ // 进行编译的源文件
__LINE__ // 文件当前的行号
__DATE__ // 文件编译的日期
__TIME__ // 文件被编译的时间
__STDC__ // 若编译遵循ANSI C,值为1;否则未定义
示例:
2. #define
2.1 定义标识符
(1)语法
#define name stuff
(2)示例
#define MAX 100
#define name "zhangsan"
#define null NULL
#define llt long long int
2.2 定义宏
(1)定义
#define机制包括了一个规定:允许把参数替换到文本中,这种实现通常称为宏或定义宏。
宏的声明方式:
#define name(parament-list) stuff
parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中
(2)示例
(3)注意
参数列表左括号必须与name紧邻,否则参数列表会被解释为stuff的一部分。
2.3 宏替换规则
在程序中扩展#define定义符号和宏时,需要涉及以下几个步骤:
(1)在调用宏时,首先对参数进行检查,查看是否包含任何由#define定义的符号,若有则它们首先被替换。
(2)替换文本随后被插入到程序中原来文本的位置;对于宏,参数名被它们的值所替换。
(3)最后再次对结果文件进行扫描,查看是否包含任何由#define定义的符号,若有则重复上述处理过程。
注意:
(1)宏参数和#define定义中可用出现其他#define定义的变量,但是宏不能出现递归。
(2)当预处理器搜索#define定义的符号的时候,字符串常量的内容并不会被搜索(也就是字符串常量中的,不会被当作宏进行处理)。
2.4 # 和 ##
(1)#的作用
将宏参数转换为对应的字符串。
示例:
(2)##的作用
把位于它两边的符号合成为一个符号;它允许宏定义从分离的文本片段创建标识符。
注意:
这样的连接必须产生的是合法的标识符,否则其结果就是未定义。
示例:
2.5 宏与函数对比
宏通常被应用于执行简单的运算。
宏的优势:
(1)宏比函数在程序的规模和速度方面更优
用于调用函数和从函数返回的代码可能比实际执行这个小型计算所需的时间更多。
(2)宏是类型无关的
函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使用;宏可以适用于多种类型。
(3)宏的参数可以出现类型
宏的劣势:
(1)一份宏定义的代码将嵌入到程序中,若宏较长则可能大幅增加程序的长度。
(2)宏不方便调试。
(3)因为宏是类型无关的,所以存在不够严谨问题。
(4)宏可能带来运算符优先级问题,导致程序容易出错。
(5)宏不能递归。
3. #undef
(1)功能
移除一个宏定义。
(2)示例
4. 条件编译
4.1 单分支条件编译
#if 常量表达式
// ……
#endif
常量表达式由预处理器求值,为真则编译之间的内容,否则不参与编译。
4.2 多分支条件编译
#if 常量表达式
// ……
#elif 常量表达式
// ……
#else
// ……
#endif
4.3 判断是否被定义
// 判断symbol是否定义,定义则编译
#if defined(symbol)
// #ifdef symbol 等价
#endif
// 判断symbol是否定义,未定义则编译
#if !defined(symbol)
// #ifndef symbol 等价
#endif
4.4 嵌套指令
#if defined(OS_UNIX)
#ifdef OPTION1
// ...
#endif
#ifdef OPTION2
// ...
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
// ...
#endif
#endif
5. 文件包含
5.1 库文件包含
#include <filename>
查找策略:
直接在标准路径下(编译器提供的标准库目录中)去查找,若找不到则编译报错。
5.2 本地文件包含
#include "filename"
查找策略:
先在源文件所在目录下查找;若未找到,则编译器像查找库函数头文件一样在标准位置查找;若还是未找到,则编译报错。
5.3 嵌套文件包含
(1)问题引入
若头文件内包含了其他头文件,则可能引发嵌套包含,在预处理引入头文件时就会产生重复引入问题,导致程序臃肿。
(2)解决方法
①条件编译
#ifdef __TEST_H__
#define __TEST_H__
// 头文件内容
#endif
②#pragma once
在文件开头添加#pragma once指令。
四、模拟实现offsetof
1. offsetof功能介绍
offsetof(type, member)
offsetof是一个宏,功能是计算一个结构体变量相较于结构体起始位置的偏移量。
type:结构体类型
member:结构体成员
2. 模拟实现
#define OFFSETOF(type, name) ((size_t)&(((type*)0)->name))
将0地址强转为结构体类型的指针,则认为在0地址处存放的是一个该结构体,然后获取指定成员的地址,将地址值强转为整型,即它相对于0地址的偏移量,即该成员相对于结构体起始地址的偏移量。
测试: