# 斯坦福编程范式 CS107_12

# 编译过程的三个阶段(接着上节课)

# 预处理阶段

#define 对全局变量进行简单的替换。

#define 对于宏定义的处理

#define MAX(a,b) ((a)>(b))?(a):(b)

#define 这种简单的替换在汇编语言中一般就两三行,但如果采用函数的形式,则会需要更多汇编语言,因为函数中的申请变量、返回等操作占用了大量的开销。

这串代码在预处理阶段不会报错。在后续的编译中会报错。

MAX(40.2,"HELLO")

assert 的宏定义。是的,assert 在 C 语言中其实是一种宏定义。

这条宏定义的意义就是,如果语句 cond 中的内容为真,就什么也不做,(void) 0 表示将 0 转换为 void 类型,不进行返回值,在汇编中这里不会产生汇编代码。如果为假,就输出正在编译的文件名,并退出程序。

#define assert(cond) (cond)?((void) 0):fprintf(stderr,"正在编译的文件名"),exit(0)

如果采用了 NO DEBUG 模式,就会将所有的 assert 部分替换为 空。

#ifdef NDEBUG
	#define assert(cond) (void)0
#else
	#define assert(cond) (cond)?((void) 0):fprintf(stderr,"正在编译的文件名"),exit(0)
#endif

# 预处理器的一些不足

对于下述式子,如果斐波那契 100 比较大,那么最终会运行两遍的求斐波那契 100 的函数,编译器不会进行中间值的保留,因为它认为你的式子就是这样写的,计算两次斐波那契数或者 factorial (4000)。

int max = MAX(fib(100),fact(4000));
// 对于上述宏定义进行展开后,将会是:
int max = (fib(100)>fact(4000))?fib(100):fact(4000);

这样的式子更加明确的表明,在实际程序中,可能会出现的冗余计算或错误。

最终的结果如果是 m 比较大的话,m 会自增 2,n 则会自增 1。

int max = MAX(m++,n++);

预处理器的这些不足会可能让你很难找出你代码的问题出在了哪里,尤其是对于一些工程量巨大的项目,除非你能保证你能一次就写出正确的 C++ 代码,那几乎是不太可能的。C 语言在它诞生的 60 年代时程序还都非常的小巧,所以能很容易的找到问题,现在就不太行了。

# #include

尖括号和双引号的区别。

当使用尖括号来包括.h 文件时,对于预处理器这意味着这是个系统头文件,因此这个头文件应该是编译器提供的,因此可以通过默认路径找到这些文件;如果使用的是双引号,那么编译器会假设这是个用户编写的.h 文件,因此会默认从当前的工作目录查找该头文件。

对于 #include 头文件,在预处理过程中会在目录中进行查找,并将查找到的头文件中的内容对这一行进行替换。#include 操作是递归的,意思就是如果你的头文件中还含有 #include 内容,那么系统就会继续查找下去,直到到达最底层。最终要交给编译器的文件是不会包括任何 include 或 define 符号的。

预处理器对每一个已经处理过的头文件,都会加入 Hash 表中,以避免重复的替换某一个头文件。

#include <stdio.h>
#include <assert.h>
#include "vector.h"

# 编译阶段 和 链接阶段

基于 GCC 的。

# main.c

#include <stdio.h>	//printf
#include <stdlib.h>	//malloc,free
#include <assert.h>
int main(int argc,char * argv[]){
    void *memory = malloc(400);
    assert(memory != NULL);
    printf("Yeah!");
    free(memory);
    return 0;
}

预处理后,将上述代码传递给 GCC,根据 makefile 编译器会对其进行编译,编译生成 .o 文件。编译文件中显然的会有以下内容。

...
CALL <malloc>
...
CALL <fprintf>
...
CALL <printf>
...
CALL <free>
...
RV = 0;
RET;

为什么没有 assert 函数的调用呢?因为经过预处理阶段,#include <assert.h> 就已经对所有的 assert 函数进行替换了,就没有 assert 函数了,对应的会有 fprintf 函数。

随后 GCC 会对 o 文件进行链接操作,并最终生成可执行文件 .out 文件。如果只是想让编译器进行编译后就停止,需要使用 gcc -c 命令。如果想设置生成的可执行文件的名称,就使用 gcc -o