C 语言拾遗

从源代码到可执行文件

以如下代码为例:

hello.c
1
2
3
4
5
6
#include <stdio.h>

int main(void) {
printf("Hello, World!\n");
return 0;
}
预处理

可以使用

BASH
来让 gcc 只进行预处理操作。

预处理过程主要处理以 # 开头的预处理指令:

  • 展开宏定义 #define

  • 处理条件编译指令 #if #ifdef

  • 包含文件 #include

编译

产生汇编代码。
可以使用

BASH
来让 gcc 只进行编译操作。

汇编

将汇编代码转为机器码。
可以使用

BASH
来让 gcc 只进行汇编操作。

此时生成的 hello.o 称为目标文件object file

链接

简而言之,链接就是把多个目标文件的代码段放在一起、数据段放在一起,然后和一些库函数放在一起共同形成可执行文件。

变量的存储类别

存储类别storage class用来描述 C 中变量或函数的一些特征,包括存储期、作用域、链接属性。

存储期

存储期storage duration描述一个对象[1]在内存中的生命周期。

在 C 中,对象一共有四种存储期:静态存储期、自动存储期、动态分配存储期和线程存储期。在这里介绍静态存储期和自动存储期。

  • 静态存储期:使用 static 关键字定义的变量,或者在函数外定义的变量,它们具有静态存储期。

    • 如果对象具有静态存储期,那么它在程序执行期间会一直存在,并且起始地址和占用的内存空间大小不会发生改变。如果不对它初始化,那么它自动初始化为 0
  • 自动存储期:在函数中的、不使用 static 关键字定义的变量,它们具有自动存储期。

  • 程序执行到该变量声明的语句的时候,会创建变量对应的对象。在执行到该变量作用域结束的时候会释放掉该对象,其占据的内存空间可作他用。如果不对它初始化,那么它自动初始化为 随机值

  • 可以使用 auto 关键字显示声明这个变量对应的对象具有自动存储期。

作用域

作用域scope描述一个标识符在程序中可以被使用的区域。一个 C 变量常见的作用域包括块作用域、函数作用域或文件作用域。

  • 块作用域block scopeblock 是用一对花括号括起来的代码区域。块作用域变量的可见范围是从定义处到包含该定义的块结尾。函数的形式参数声明也具有块作用域,属于函数体这个块。

block_scope.c
1
2
3
4
5
6
7
8
9
#include <stdio.h>
int foo(int a, int b) { // a, b 作用域开始
int c; // c 作用域开始
for(c = 0; c < 10; c++) {
int d = c; // d 作用域开始
printf("%d\n", d);
} // d 作用域结束
return a + b;
} // a, b, c 作用域结束
  • 文件作用域file scope:如果变量定义在函数的外部,那么这个变量具有文件作用域。具有文件作用域的变量,从定义处到该定义所在的文件末尾都可见,因此它又被称为全局变量。

file_scope.c
1
2
3
4
5
6
7
8
int gv = 1; // gv 作用域开始,它具有文件作用域
void foo(void) {
gv++;
}
int main(void) {
foo();
gv++;
}

链接

C 中,一个变量的链接linkage描述了这个变量是否可以在别的文件中被使用。变量有三种链接属性:内部链接、外部链接和无链接。

  • 内部链接:内部链接的变量只能在定义它的文件中使用。使用 static 关键字声明的全局变量是内部链接的。

  • 外部链接:外部链接的变量可以在所有的文件中使用。全局变量默认是外部链接的

  • 无链接:无链接的变量没有链接属性,它们不能通过任何方式变为内部链接或者外部链接。在函数中定义的变量都是无链接的。

内部链接的变量去掉 static 关键字就是外部链接的。外部链接的变量加上 static 关键字就变成了内部链接的。

linkage.c
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int a = 1; // a 外部链接
static int b = 2; // b 内部链接
void foo(int c) { // c 无链接
int d; // d 无链接
for (d = 0; d < 10; d++) {
printf("%d\n", d);
}
}

如果要使用其它文件定义的变量,需要先引用式声明。引用式声明就是对一个变量的声明加上 extern 关键字。引用式声明让编译器假设这个变量已经定义在了别的地方,而定义式声明(不加 external)会创造一个新的对象[1:1]

foo.c
1
int val; // 定义式声明
main.c
1
2
3
4
5
6
7
8
9
int num; // 定义式声明

int main(void) {
extern int val; // 引用式声明,引用自 foo.c 的 val 定义
extern int num; // 引用式声明,引用自该文件的 num 定义
val = 1;
num = 2;
return val + num;
}

函数的存储类别

函数的存储类别主要体现的是函数的链接属性。函数具有外部链接或者内部链接两种属性。

  • 外部链接:函数默认是外部链接的。

  • 内部链接:如果函数定义前加上了 static 关键字,那么它是内部链接的。

foo.c
1
2
3
int bar(int num) { // bar 具有外部链接属性
return num * num;
}
main.c
1
2
3
4
5
6
7
extern int bar(int num); // ANSI 函数原型,声明 bar 是一个接受 int 类型参数,返回 int 类型值的外部函数
int main(void) {
int foo = 2;
int num;
num = bar(foo);
return 0;
}

预处理指令

宏定义 #define

预处理器发现程序中的宏时,会用宏等价的文本进行替换。如果替换的文本还有宏,就继续替换。

  • 变量式宏定义:如 #define N 65535

  • 函数式宏定义:如 #define MAX(a, b) ((a) > (b) ? (a) : (b))

注意

  • 由于预处理器只会做文本的替换,因此函数式宏定义的参数没有类型声明,也不做参数检查。如果有问题,会在编译期间报错(见)。

  • 定义函数式宏的时候需要小心小心再小心,尤其是多加括号,以免替换后因为运算优先级的问题导致结果与预期不符。

  • 写人写得出来的代码,不要写诸如 MAX(++a, ++b) 的东西,这种东西一旦展开就是谭浩强来了都看不懂 ab 各加了几次。

# 运算符

# 运算符用来创建一个字符串,比如:

1
2
3
#define STR(s) #s

printf(STR(hello world));

在替换后就是 printf("hello world");

可以看到 # 自动把传入的参数用双引号括起来变成一个字符串,并且连续的空格会替换成一个空格

## 运算符

这个运算可以把两个预处理符号连接成一个,比如:

1
2
3
4
5
6
7
8
#define XNAME(a, b) a ## b

#include <stdio.h>

int main(void) {
printf("%d\n", XNAME(4, 5));
return 0;
}

输出结果是 45

变参宏:...__VA_ARGS__

在参数列表中用 ... 表示可变参数,在宏定义中用 __VA_ARGS__ 表示可变参数。... 代表的参数会看作一个参数替换到 __VA_ARGS__ 中。

1
2
#define showstr(...) printf(#__VA_AGRS__)
showstr(Hello, World);

观察到输出 Hello, World

解释

showstr 接受了两个参数,被当成一个整体 Hello, World 传递给 __VA_ARGS__
其前面有 # 运算,让这个参数被当成字符串看待,于是宏展开就变成了:
printf("Hello, World");

#undef

使用 #undef <macro> 来取消对 <macro> 的定义。如果之前没有定义过,这条语句会被忽略。

文件包含 #include

把被包含的文件全部替换到 #include 的位置。遵守以下规则:

  • #include <filename>:预处理器会先在标准包含目录中寻找该文件。在 Linux 中,这个目录是 /usr/include。可以使用 gcc -I dirdir 加入标准包含目录。

  • #include "filename":预处理器会先查找包含头文件的 .c 文件所在的目录,然后再查找标准包含目录。

条件预处理指令 #ifndefendif

这是一对组合在一起用的指令。比如在 stdio.h 中有这样一段:

stdio.h
1
2
3
4
#ifndef _STDIO_H
#define _STDIO_H 1
......
#endif

这表示如果之前没有定义过 _STDIO_H 这个宏,那么从 #ifndef#endif 之间的全部内容就包含在预处理的输出结果中,否则忽略。这样就可以避免头文件的内容被重复包含。

typedef#define 的区别

#define 是单纯的文本替换,在预处理阶段完成,没有作用域。

typedef 一般来说是给类型取别名,在编译阶段完成,有作用域。

MIPS 拾遗

我草,CO 还在追我。

访存流程

目前,只需要大家牢记,在 MIPS32 指令集中,我们的访存指令不再直接操作物理地址,而是使用虚拟地址以及地址翻译机制,间接管理物理内存。

栈帧

栈帧示意图
栈帧示意图

先看黄色部分:$a0$a3 这四个寄存器用于传参,在设计栈帧的时候,需要为它们预留栈帧空间,但不用手动压入栈中。如果参数超过 4 个的子函数,那么就要将多出来的 4 到 n-1 个参数手动压入栈中。

再看深绿色部分,即返回地址部分:这部分用于存储 $ra 的值。该值在执行当前函数开始时被复制到栈中,在当前函数返回之前复制回 $ra 中,详参此处

MIPS 伪指令和宏

.align

这条指令的作用是“使下面的数据进行地址对齐”,.align x 的意思是以 2x2^x 字节对齐。

如果写 .align 0,就表示覆盖掉 .word 等的自动对齐特性,比如:

1
2
3
.half 3
.align 0
.word 100

这样的话 .word 这个字的内容会紧紧跟着 .half 这个半字的内容,而不会从下一个字开始存入信息。


  1. C 中的对象与面向对象编程中的对象不是一个概念。C 中,对象指的是连续的一片内存空间,具有起始地址和大小这两个属性。使用标识符可以访问、修改对象。 ↩︎ ↩︎


本站由 Samustach Floresein 使用 Stellar 1.33.1 主题创建。
本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
本站总访问量 次。

载入天数…载入时分秒…

Static Badge
Static Badge