5.编译链接和宏**
1. 宏(考察很多)-要求轻松实现宏,很容易出错
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏或定义宏。
下面是宏的声明方式:
#define name(参数列表) 内容
参数列表的左括号必须与name紧邻,如果两者间存在空白,参数列表就会被解释成内容的一部分。
#define SQUARE(x) x*x
这个宏接收一个参数x,如果写SQUARE(5),预处理器就会用5*5这个表达式替换SQUARE(5)。
但是这个宏存在一个问题:
#define SQUARE(x) x*x
int main()
{
int a = 5;
printf("%d\n", SQUARE(a+1));
return 0;
}
我们想象中应该是6×6=36,但是实际结果居然是11,为什么呢?
SQUARE(a+1) 实际上被替换成了 a+1*a+1,并不是(a+1)*(a+1),所以结果是11。
应该在宏定义上加上两个括号:
#define SQUARE(x) (x)*(x)
如果是这样一个宏,我们吸取经验在每个x上加上括号:
#define DOUBLE(x) (x)+(x)
int main()
{
int a = 5;
// printf("%d\n", SQUARE(a+1));
printf("%d\n", 10*DOUBLE(a));
return 0;
}
我们想象中是10×10=100,但是实际上是55,我们展开替换DOUBLE,实际上是10*(5)+(5)
先算10*5 = 50, 最后再+5,等于55。
所以我们要在外面也加上括号。
#define DOUBLE(x) ((x)+(x))
所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
当宏参数在宏的定义中出现超过一次时,如果参数带有副作用,那么在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现永久性的效果。
x++就是带有副作用
#define MAX(x, y) ((x)>(y)?(x):(y))
int x = 5;
int y = 8;
int z = MAX(x++, y++);
printf("%d %d %d\n", x, y, z);
预处理器处理后的结果是:
z = ((x++) > (y++) ? (x++) : (y++))
(x++) > (y++) 都会走,走完x=6, y=9,然后走y++,z=9,y=10,最后的结果就是6,10,9。
某笔试题:
写一个宏,计算结构体中某变量相对于首地址的便宜,并给出说明
#define OFFSET_OF(type, member) ((size_t)&(((type *)0)->member))
说明:
-
type
: 结构体的类型。 -
member
: 结构体中的成员变量。 -
((type *)0)
: 将地址0
强制转换为指向type
类型的指针。这相当于假设结构体的首地址是0
。 -
&(((type *)0)->member)
: 获取成员变量member
的地址。由于结构体的首地址是0
,这个地址就是成员变量相对于结构体首地址的偏移量。 -
(size_t)
: 将偏移量转换为size_t
类型,通常用于表示内存地址或偏移量的大小。
struct example
{
int a;
char b;
double c;
};
int main()
{
printf("Offset of 'a': %zu\n", OFFSET_OF(struct example, a));
printf("Offset of 'b': %zu\n", OFFSET_OF(struct example, b));
printf("Offset of 'c': %zu\n", OFFSET_OF(struct example, c));
return 0;
}
Offset of 'a': 0
Offset of 'b': 4
Offset of 'c': 8
2. 编译链接的过程(考的不多)
1. 预处理(Preprocessing)
预处理是编译过程的第一步,主要处理源代码中的预处理指令(以 #
开头的指令)。
主要任务:
-
宏展开:将所有的宏定义展开。
-
头文件包含:将
#include
指定的头文件内容插入到源文件中。 -
条件编译:根据
#if
、#ifdef
等条件编译指令,选择性地包含或排除代码。 -
删除注释:删除源代码中的注释。
输入输出:
-
输入:
.c
源文件。 -
输出:
.i
预处理后的文件。
gcc -E main.c -o main.i
2. 编译(Compilation)
编译阶段将预处理后的代码转换为汇编代码。
主要任务:
-
词法分析:将源代码分解为 token(如关键字、标识符、运算符等)。
-
语法分析:根据语法规则构建抽象语法树(AST)。
-
语义分析:检查语义是否正确(如类型检查)。
-
代码优化:对代码进行优化。
-
生成汇编代码:将高级语言代码转换为目标机器的汇编代码。
输入输出:
-
输入:
.i
预处理后的文件。 -
输出:
.s
汇编文件。
gcc -S main.i -o main.s
3. 汇编(Assembly)
汇编阶段将汇编代码转换为机器代码(目标文件)。
主要任务:
-
将汇编代码翻译为机器指令。
-
生成目标文件(通常是
.o
或.obj
文件),包含机器代码和符号表。
输入输出:
-
输入:
.s
汇编文件。 -
输出:
.o
目标文件。
gcc -c main.s -o main.o
4. 链接(Linking)
链接阶段将多个目标文件和库文件合并为一个可执行文件。
主要任务:
-
符号解析:解析目标文件中的未定义符号(如函数和变量)。
-
地址分配:为代码和数据分配最终的内存地址。
-
重定位:根据最终的内存地址调整代码中的引用。
-
合并目标文件:将多个目标文件合并为一个可执行文件。
-
链接库文件:将静态库或动态库链接到可执行文件中。
输入输出:
-
输入:
.o
目标文件和库文件。 -
输出:可执行文件(如
a.out
或main.exe
)。
gcc main.o -o main