【一天一个计算机知识】—— 【编程百度】翻译环境与运行环境
文章目录
- 一、什么是ANSI C?
- 二、翻译环境与运行环境
- 2.1)翻译环境
- 2.1.1)编译
- 2.1.1.1)预编译(预处理)
- 2.1.1.2)编译
- 2.1.1.3)汇编
- 2.1.1.4)链接
- 2.2)运行环境
一、什么是ANSI C?
ANSI C 在 C 语言从一个灵活但有点狂野的语言走向标准化、稳定、可移植的里程碑
- 内存管理 (
<stdlib.h>)
ANSI C 正式确立了动态内存分配的标准函数:
void* malloc(size_t size):分配指定大小的内存。
void* calloc(size_t num, size_t size):分配指定数量和大小的内存,并初始化为0。
void* realloc(void* ptr, size_t new_size):重新调整已分配内存的大小。
void free(void* ptr):释放之前分配的内存。
它还引入了 size_t 类型,用于表示内存大小。
- 文件操作 (
<stdio.h>)
ANSI C 对标准输入输出(Standard I/O)库进行了全面标准化。
文件指针:FILE*类型作为文件流的抽象。
文件打开/关闭:fopen(), fclose(), freopen()
格式化 I/O:printf(), scanf(), fprintf(), fscanf()
字符 I/O:fgetc(), fputc(), getc(), putc()
行 I/O:fgets(), fputs()
块 I/O:fread(), fwrite()
文件定位:fseek(), ftell(), rewind()
错误处理:feof(), ferror()
- 新的关键字和类型
void 关键字:
void func(void):明确表示函数不接受任何参数。
void func():在C中表示接受任意参数(为了兼容K&R C),但在C++中表示不接受参数。推荐使用 (void)。
void*:通用指针类型。它可以指向任何类型的数据,但不能直接解引用。malloc 的返回值就是void*
const 关键字:
用于声明一个变量为只读,const int x = 10;
volatile 关键字:
告诉编译器该变量可能随时被外部(如硬件、多线程)修改,禁止编译器对其进行优化(如缓存到寄存器中)。
signed 和 unsigned:
明确了 char 和 int 等类型的符号性。
enum (枚举类型):
提供了一种创建命名整数常量集的标准方法。
- 预处理器 (Preprocessor)
ANSI C 改进和标准化了C预处理器 (CPP)。
#if defined(...):比 #ifdef 更强大的条件编译。
#(Stringizing Operator): 字符串化操作符,将宏参数转换为字符串字面量。
##(Token-Pasting Operator): 标记连接操作符,将两个标记(token)粘合成一个新的标记。
__FILE__ 和 __LINE__: 标准化的预定义宏
ANSI C通过引入函数原型和标准化库 (如stdlib.h和stdio.h),极大地提高了C代码的可移植性、健壮性和安全性
二、翻译环境与运行环境
在
ANSI C的任意一种实现中,存在两种不同的编译环境:第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令(二进制指令)
第2种是执行环境,它用于实际执行代码
2.1)翻译环境
将
C源代码转换成可执行机器代码的整个过程
翻译环境由编译和链接两大部分组成。而编译过程通常又可细分为:预处理(有些资料中也称为预编译)、编译和汇编三个阶段
- 编译
- 链接
一个 C 语言项目中,如果有多个 .c 文件需要共同构建,那么它们是如何一步步生成可执行程序的呢?
- 首先,每个
.c文件都会单独经过编译器处理,编译成对应的目标文件。 - 注意:在
Windows环境下,目标文件的后缀是.obj;而在Linux环境下,则是.o - 接着,这些目标文件会与所需的链接库一起,交给链接器进行处理,最终生成可执行程序。
- 这里所说的“链接库”,主要包括运行时库(即支持程序运行的基本函数集合)以及任何可能使用的第三方库
如果再把编译器展开成3个过程,那就变成了下面的过程:

2.1.1)编译
2.1.1.1)预编译(预处理)
输入:
hello.c(C源代码文件)输出:
hello.i(一个临时的、经过处理的、但仍然是C语言代码的文本文件)
- 预编译的四大主要任务
预处理器的工作主要就是处理 C 代码中那些以#开头的指令
任务一: 文件包含 (
#include)
任务二: 宏定义与替换 (#define)时光卷轴:#define宏定义
任务三: 条件编译 (
#if,#ifdef,#ifndef等)时光卷轴:预处理指令
任务四: 移除注释
任务五: 保留所有的#pragma的编译器指令,编译器后续会使⽤时光卷轴:预处理指令
经过预处理后生成的 .i 文件将不再包含任何宏定义,因为所有宏都已被展开。同时,所有包含的头文件内容也已被插入到 .i 文件中。因此,当我们无法确定宏定义或头文件是否包含正确时,查看预处理后的 .i 文件是一个有效的确认方法。
2.1.1.2)编译
编译器将语言代码一步步转换到机器可执行的指令,其中词法、语法、语义分析确保了代码的正确性,而优化步骤则致力于提升执行效率
array[index] = (index+4)*(2+6);
- 词法分析
作用: 将源代码字符流分解成有意义的词素
-
语法分析
作用: 接下来,语法分析器将对扫描器产生的记号进行语法分析,从而产生语法树。这些语法树是以表达式为节点树

-
语义分析
作用: 语义分析器负责进行语义分析,其任务是在语法分析的基础上,进一步检查程序结构的正确性。编译器进行的是语义的静态分析,主要包括声明与类型的匹配、类型转换等。此阶段会报告程序中存在的语义错误

2.1.1.3)汇编
汇编器负责将汇编代码转换为机器可以执行的指令(二进制指令)。每一个汇编语句几乎都对应一条机器指令。其转换过程基本上是按照汇编指令与机器指令的对照表逐一翻译,通常不进行指令优化
2.1.1.4)链接
链接是计算机程序开发过程中的一个关键步骤,它发生在编译之后、程序运行之前
简单来说,它的任务是将程序的不同部分组合在一起,形成一个单一的、可执行的文件
链接解决的是⼀个项目中多文件、多模块之间互相调用的问题
我们通过一个简单的 C 语言程序来具体演示链接的过程
我们已经知道,每个源文件会通过单独通过一个链接器生成对应的目标文件我们有两个目标文件:
add.o和main.o,它们都是独立的机器码文件
链接器会执行以下关键任务:符号解析
- 链接器检查
main.o,它发现 main.o 有两个未解决的引用:add和printf- 链接器查找
add.o,发现其中定义了add函数。它成功匹配了main.o中对add的引用。- 链接器查找
C标准库(libc.so或libc.a,gcc默认会链接它)。它在标准库中找到printf函数的定义,成功匹配了main.o中对printf的引用重定位
main.o认为自己从地址0开始,add.o也认为自己从地址0开始。但它们在最终的myprogram文件中将会有不同的实际内存地址- 链接器会决定
add函数的代码在myprogram中最终位于哪个虚拟内存地址(例如0x400500)- 链接器会决定
main函数的代码在myprogram中最终位于哪个虚拟内存地址(例如0x400600)- 然后,它会回到
main.o中调用add函数的指令处,将原来的占位符替换为add函数的实际地址 (0x400500)- 同样,
printf函数的调用也会被修正为printf在标准库中的实际地址- 所有相对跳转和数据引用都会被修正为最终程序中的绝对地址或正确偏移
合并代码和数据
add.o的.text段(代码)和main.o的.text段会被合并到myprogram的.text段中。add.o和main.o中的任何.data或.bss段也会被合并到 myprogram` 相应的段中。- 最终生成一个完整的、可执行的
myprogram文件
感兴趣的学者,可以考虑购买《程序员的自我修养》这本书学习学习
2.2)运行环境
-
程序载入内存: 任何程序都必须先加载到内存中才能运行。在通用操作系统的管理下,这项工作由系统自动完成。而在独立的嵌入式环境中,则需要开发者手动安排载入过程,或者直接将可执行代码写入只读内存。
-
控制权移交: 程序执行起点通常是
main函数,操作系统将控制权移交至此,程序开始运行。 -
运行时内存模型: 程序运行时依赖两种主要内存区域:栈 负责管理函数调用时的局部变量和返回地址;静态存储区 则用于存放全局和静态变量,它们的生命周期与程序相同。
-
执行结束: 程序的结束分为两种方式:正常终止(
main函数返回)和意外终止(如发生严重错误)





