C 语言拾遗
从源代码到可执行文件
以如下代码为例:
1 |
|
可以使用
预处理过程主要处理以 # 开头的预处理指令:
展开宏定义
#define处理条件编译指令
#if#ifdef等包含文件
#include
产生汇编代码。
可以使用
将汇编代码转为机器码。
可以使用
此时生成的 hello.o 称为目标文件。
简而言之,链接就是把多个目标文件的代码段放在一起、数据段放在一起,然后和一些库函数放在一起共同形成可执行文件。
变量的存储类别
存储类别用来描述 C 中变量或函数的一些特征,包括存储期、作用域、链接属性。
存储期
存储期描述一个对象[1]在内存中的生命周期。
在 C 中,对象一共有四种存储期:静态存储期、自动存储期、动态分配存储期和线程存储期。在这里介绍静态存储期和自动存储期。
-
静态存储期:使用
static关键字定义的变量,或者在函数外定义的变量,它们具有静态存储期。- 如果对象具有静态存储期,那么它在程序执行期间会一直存在,并且起始地址和占用的内存空间大小不会发生改变。如果不对它初始化,那么它自动初始化为
0。
- 如果对象具有静态存储期,那么它在程序执行期间会一直存在,并且起始地址和占用的内存空间大小不会发生改变。如果不对它初始化,那么它自动初始化为
-
自动存储期:在函数中的、不使用
static关键字定义的变量,它们具有自动存储期。 -
程序执行到该变量声明的语句的时候,会创建变量对应的对象。在执行到该变量作用域结束的时候会释放掉该对象,其占据的内存空间可作他用。如果不对它初始化,那么它自动初始化为 随机值。
-
可以使用
auto关键字显示声明这个变量对应的对象具有自动存储期。
作用域
作用域描述一个标识符在程序中可以被使用的区域。一个 C 变量常见的作用域包括块作用域、函数作用域或文件作用域。
-
块作用域:块 是用一对花括号括起来的代码区域。块作用域变量的可见范围是从定义处到包含该定义的块结尾。函数的形式参数声明也具有块作用域,属于函数体这个块。
1 |
|
-
文件作用域:如果变量定义在函数的外部,那么这个变量具有文件作用域。具有文件作用域的变量,从定义处到该定义所在的文件末尾都可见,因此它又被称为全局变量。
1 | int gv = 1; // gv 作用域开始,它具有文件作用域 |
链接
C 中,一个变量的链接描述了这个变量是否可以在别的文件中被使用。变量有三种链接属性:内部链接、外部链接和无链接。
-
内部链接:内部链接的变量只能在定义它的文件中使用。使用
static关键字声明的全局变量是内部链接的。 -
外部链接:外部链接的变量可以在所有的文件中使用。全局变量默认是外部链接的。
-
无链接:无链接的变量没有链接属性,它们不能通过任何方式变为内部链接或者外部链接。在函数中定义的变量都是无链接的。
内部链接的变量去掉 static 关键字就是外部链接的。外部链接的变量加上 static 关键字就变成了内部链接的。
1 |
|
如果要使用其它文件定义的变量,需要先引用式声明。引用式声明就是对一个变量的声明加上 extern 关键字。引用式声明让编译器假设这个变量已经定义在了别的地方,而定义式声明(不加 external)会创造一个新的对象[1:1]。
1 | int val; // 定义式声明 |
1 | int num; // 定义式声明 |
函数的存储类别
函数的存储类别主要体现的是函数的链接属性。函数具有外部链接或者内部链接两种属性。
-
外部链接:函数默认是外部链接的。
-
内部链接:如果函数定义前加上了
static关键字,那么它是内部链接的。
1 | int bar(int num) { // bar 具有外部链接属性 |
1 | extern int bar(int num); // ANSI 函数原型,声明 bar 是一个接受 int 类型参数,返回 int 类型值的外部函数 |
预处理指令
宏定义 #define
预处理器发现程序中的宏时,会用宏等价的文本进行替换。如果替换的文本还有宏,就继续替换。
-
变量式宏定义:如
#define N 65535 -
函数式宏定义:如
#define MAX(a, b) ((a) > (b) ? (a) : (b))
注意
由于预处理器只会做文本的替换,因此函数式宏定义的参数没有类型声明,也不做参数检查。如果有问题,会在编译期间报错(见上)。
定义函数式宏的时候需要小心小心再小心,尤其是多加括号,以免替换后因为运算优先级的问题导致结果与预期不符。
写人写得出来的代码,不要写诸如
MAX(++a, ++b)的东西,这种东西一旦展开就是谭浩强来了都看不懂a和b各加了几次。
# 运算符
# 运算符用来创建一个字符串,比如:
1 |
|
在替换后就是 printf("hello world");。
可以看到 # 自动把传入的参数用双引号括起来变成一个字符串,并且连续的空格会替换成一个空格。
## 运算符
这个运算可以把两个预处理符号连接成一个,比如:
1 |
|
输出结果是 45。
变参宏:... 和 __VA_ARGS__
在参数列表中用 ... 表示可变参数,在宏定义中用 __VA_ARGS__ 表示可变参数。... 代表的参数会看作一个参数替换到 __VA_ARGS__ 中。
1 |
|
观察到输出 Hello, World。
解释
showstr 接受了两个参数,被当成一个整体 Hello, World 传递给 __VA_ARGS__。
其前面有 # 运算,让这个参数被当成字符串看待,于是宏展开就变成了:printf("Hello, World");
#undef
使用 #undef <macro> 来取消对 <macro> 的定义。如果之前没有定义过,这条语句会被忽略。
文件包含 #include
把被包含的文件全部替换到 #include 的位置。遵守以下规则:
-
#include <filename>:预处理器会先在标准包含目录中寻找该文件。在 Linux 中,这个目录是/usr/include。可以使用gcc -I dir将dir加入标准包含目录。 -
#include "filename":预处理器会先查找包含头文件的.c文件所在的目录,然后再查找标准包含目录。
条件预处理指令 #ifndef 和 endif
这是一对组合在一起用的指令。比如在 stdio.h 中有这样一段:
1 |
|
这表示如果之前没有定义过 _STDIO_H 这个宏,那么从 #ifndef 到 #endif 之间的全部内容就包含在预处理的输出结果中,否则忽略。这样就可以避免头文件的内容被重复包含。
typedef 和 #define 的区别
#define 是单纯的文本替换,在预处理阶段完成,没有作用域。
typedef 一般来说是给类型取别名,在编译阶段完成,有作用域。
MIPS 拾遗
我草,CO 还在追我。
访存流程
目前,只需要大家牢记,在 MIPS32 指令集中,我们的访存指令不再直接操作物理地址,而是使用虚拟地址以及地址翻译机制,间接管理物理内存。
栈帧

先看黄色部分:$a0 到 $a3 这四个寄存器用于传参,在设计栈帧的时候,需要为它们预留栈帧空间,但不用手动压入栈中。如果参数超过 4 个的子函数,那么就要将多出来的 4 到 n-1 个参数手动压入栈中。
再看深绿色部分,即返回地址部分:这部分用于存储 $ra 的值。该值在执行当前函数开始时被复制到栈中,在当前函数返回之前复制回 $ra 中,详参此处。
MIPS 伪指令和宏
.align
这条指令的作用是“使下面的数据进行地址对齐”,.align x 的意思是以 字节对齐。
如果写 .align 0,就表示覆盖掉 .word 等的自动对齐特性,比如:
1 | .half 3 |
这样的话 .word 这个字的内容会紧紧跟着 .half 这个半字的内容,而不会从下一个字开始存入信息。