14 KiB
title, subtitle, version, category, order, tags, prev, next
| title | subtitle | version | category | order | tags | prev | next | |||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 编程基础 | Programming Philosophy - Basic Concepts | 1.0 | 哲学篇 · 基础 | 0.2 |
|
./0.1%20Philosophy%20-%20Intro.md | null |
0.2 编程基础
本节导读:掌握编程开发的规范体系,深入理解编译器的工作原理与完整编译流程。
一、开发规范体系
规范是团队协作的基石,良好的编码习惯是程序员的基本素养。
1.1 项目规范
- 目录结构规范
- 配置管理规范
- 版本控制规范
示例 — 推荐的 C 项目目录结构:
my_project/
|-- include/ # 头文件 (.h)
| +-- utils.h
|-- src/ # 源文件 (.c)
| +-- main.c
| +-- utils.c
|-- lib/ # 第三方库
|-- build/ # 编译输出(不纳入版本控制)
|-- docs/ # 项目文档
+-- README.md # 项目说明
1.2 命名规范
- 变量命名规则
- 函数命名规则
- 文件命名规则
示例 — 常见命名风格对比:
| 风格 | 示例 | 适用场景 |
|---|---|---|
snake_case |
user_name, file_path |
C 语言变量 / 函数 |
UPPER_SNAKE |
MAX_BUFFER, PI |
宏常量 / 枚举 |
camelCase |
userName, filePath |
Java / JavaScript 变量 |
PascalCase |
ClassName, StructName |
类型 / 结构体名称 |
// Good: 符合 C 语言命名惯例
int user_count = 0;
#define MAX_LENGTH 256
void calculate_sum(int a, int b);
// Bad: 混用风格、含义模糊
int x = 0; // 含义不明
#define ML 256 // 缩写无法理解
void CS(int a, int B); // 函数名无意义
1.3 代码规范
- 缩进与格式
- 注释规范
- 错误处理规范
示例 — 良好 vs 不良代码对比:
// --- Bad: 无缩进、无注释、魔法数字 ---
int f(int r){return 3.14*r*r;}
// ---
// Good: 规范缩进、有意义的命名、清晰注释
// Calculate the area of a circle with given radius.
// @param radius The radius of the circle (must be >= 0)
// @return The area of the circle
double calculate_circle_area(double radius)
{
const double PI = 3.14159265;
if (radius < 0) {
return 0.0; // Invalid input guard
}
return PI * radius * radius;
}
二、编译器概述
编译器是将人类可读的 源代码 转换为机器可执行的 目标代码 的程序。完整的编译过程包含四个主要阶段:
| 阶段 | 工具 | 输入 | 输出 | 核心任务 |
|---|---|---|---|---|
| ① 预处理 | Preprocessor | .c / .h |
.i |
宏展开、头文件包含、条件编译 |
| ② 编译 | Compiler | .i |
.s |
语法分析、语义分析、代码生成 |
| ③ 汇编 | Assembler | .s |
.o |
汇编代码转机器码 |
| ④ 链接 | Linker | .o |
.exe |
符号解析、地址重定位 |
三、编译流程详解
3.1 预处理阶段 (Preprocessing)
预处理器的任务是对源文件进行文本层面的替换和处理,生成 预处理后的源代码。
操作命令
# 查看预处理结果
g++ -E main.c -o main.i
预处理步骤详解
| 步序 | 操作 | 说明 | 示例 |
|---|---|---|---|
| 1 | 处理 #include |
包含指定的头文件内容 | #include <stdio.h> |
| 2 | 展开 #define |
进行宏替换 | #define PI 3.14 |
| 3 | 条件编译 | 根据 #ifdef/#ifndef 等决定是否编译某段代码 | #ifndef HEADER_H |
| 4 | 删除注释 | 移除所有 // 和 /* */ 注释 |
— |
Tip
:预处理后的
.i文件通常非常大,因为包含了所有头文件的展开内容。
预处理前后对比
输入 (main.c):
#include <stdio.h>
#define MAX_SIZE 100
#define SQUARE(x) ((x) * (x))
int main()
{
int arr[MAX_SIZE];
int val = SQUARE(5);
printf("Value: %d\n", val);
return 0;
}
输出 (main.i — 部分截取):
// #include <stdio.h> 被展开为上千行 stdio 的实际内容...
// 以下仅为宏展开部分:
int main()
{
int arr[100]; // MAX_SIZE -> 100
int val = ((5) * (5)); // SQUARE(5) 宏展开
printf("Value: %d\n", val); // 注释保留(未删除前)
return 0;
}
Note
:观察
SQUARE(5)如何被替换为((5)*(5))—— 这就是 纯文本替换,不做任何计算。
3.2 编译阶段 (Compilation)
编译器将预处理后的代码进行词法、语法、语义分析,最终生成 汇编代码。
操作命令
# 生成汇编代码
g++ -S main.i -o main.s
编译步骤详解
┌──────────────────────────┐
│ Source Code (.i) │
│ │ │
│ ▼ │
│ ┌────────────────────┐ │
│ │ (1) Lexical │ ←│ Convert character stream into Token stream
│ │ Analysis │ │ int main() -> [int] [main] [(] [)] [{] ...
│ └────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────┐ │
│ │ (2) Syntax │ ←│ Build AST from Token stream
│ │ Analysis │ │ Check grammatical correctness
│ └────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────┐ │
│ │ (3) Semantic │ ←│ Type checking, scope analysis
│ │ Analysis │ │ Ensure semantic legality
│ └────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────┐ │
│ │ (4) IR Generation │ ←│ Generate platform-independent IR
│ │ │ │ e.g., Three-address code, SSA form
│ └────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────┐ │
│ │ (5) Code Optimizer │ ←│ Improve execution efficiency
│ │ │ │ e.g., Constant folding, DCE
│ └────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────┐ │
│ │ (6) Target Code │ ←│ Generate assembly code (.s)
│ │ Generation │ │ Platform-specific instructions
│ └────────┬───────────┘ │
│ │ │
│ ▼ │
│ Assembly Code (.s) │
└──────────────────────────┘
词法分析示例
源代码片段:
int result = a + b * 2;
词法分析输出 (Token 流):
| Token | 类型 | 词面值 |
|---|---|---|
| 1 | KEYWORD | int |
| 2 | IDENTIFIER | result |
| 3 | OPERATOR | = |
| 4 | IDENTIFIER | a |
| 5 | OPERATOR | + |
| 6 | IDENTIFIER | b |
| 7 | OPERATOR | * |
| 8 | LITERAL | 2 |
| 9 | PUNCTUATION | ; |
Note
:词法分析器不关心语法是否正确,只负责将字符流切割为有意义的 Token。语法正确性由下一阶段的 Parser 判断。
汇编代码示例
以下是一段 C 代码及其对应的 x86-64 汇编输出:
C 源码:
int add(int x, int y)
{
return x + y;
}
生成的汇编代码 (main.s 截选):
add:
pushq %rbp # Save frame pointer
movq %rsp, %rbp # Set up new stack frame
movl %edi, -4(%rbp) # Store param x at [rbp-4]
movl %esi, -8(%rbp) # Store param y at [rbp-8]
movl -4(%rbp), %eax # Load x into eax
movl -8(%rbp), %edx # Load y into edx
addl %edx, %eax # eax = eax + edx (core addition)
popq %rbp # Restore frame pointer
ret # Return (result in eax)
Note
:每一行汇编对应一条 机器指令,汇编器将人类可读的助记符(如
movl,addl)翻译为二进制机器码。
3.3 汇编阶段 (Assembly)
汇编器将汇编代码翻译为 机器码,生成 可重定位目标文件。
操作命令
# 生成目标文件
g++ -c main.s -o main.o
目标文件结构
目标文件 (.o) 采用 PE/COFF(Portable Executable / Common Object File Format)格式,主要包含以下节区:
| 节区 | 名称 | 存储内容 |
|---|---|---|
.text |
代码段 | 程序的机器指令 |
.data |
数据段 | 已初始化的全局变量和静态变量 |
.bss |
BSS 段 | 未初始化的全局变量和静态变量(启动时清零) |
.rodata |
只读数据段 | 字面常量(如字符串字面量) |
.symtab |
符号表 | 函数和全局变量的符号信息 |
.strtab |
字符串表 | 符号名称字符串 |
.rel.text / .rel.data |
重定位表 | 需要重定位的信息 |
3.4 链接阶段 (Linking)
链接器将多个目标文件及库文件合并,生成最终的 可执行文件。
操作命令
# 链接单个目标文件
g++ main.o -o main.exe
# 链接多个目标文件
g++ main.o utils.o -o app.exe
链接器核心任务
① 符号解析 (Symbol Resolution)
将每个符号引用与其定义进行关联。未找到定义的符号称为 未定义符号 (Undefined Symbol),将导致链接错误。
main.o calls printf()
|
v
Linker looks up printf in libstdc++.a / libmingw32.a (MinGW-w64 runtime)
|
v
Found! -> Symbol resolved
② 重定位 (Relocation)
将目标文件中的相对地址转换为最终的可执行内存地址。
重定位前 (目标文件中):
call printf @ 偏移量 0x10
↓ 重定位
重定位后 (可执行文件中):
call printf @ 绝对地址 0x7ffc12345678
③ 节区合并 (Section Merging)
将来自不同目标文件的同类节区合并为一个。
┌────────────────────────────────────────────┐
│ main.o .text ──┐ │
│ ├→ Merge → Executable .text │
│ utils.o .text ─┘ │
│ │
│ main.o .data ──┐ │
│ ├→ Merge → Executable .data │
│ utils.o .data ─┘ │
└────────────────────────────────────────────┘
完整链接示例
假设有以下多文件项目结构:
project/
|-- main.c # 主程序入口
|-- utils.c # 工具函数
+-- math.c # 数学函数
步骤演示:
# Step 1: 分别编译每个 .c 文件为 .o 目标文件(此时还未链接)
g++ -c main.c -o main.o # main.o 中引用了 add() 和 printf(),但不知道它们在哪里
g++ -c utils.c -o utils.o # utils.o 定义了 add()
g++ -c math.c -o math.o # math.o 定义了 multiply()
# Step 2: 链接器将所有 .o 合并 + 关联 C 标准库
# 此时链接器会发现:
# - add() 在 utils.o 中定义 --> 符号解析成功
# - multiply() 在 math.o 中定义 --> 符号解析成功
# - printf() 在 msvcrt.dll (Windows 系统运行时) 中定义 --> 符号解析成功
g++ main.o utils.o math.o -o app.exe
# Step 3: 运行最终的可执行文件
app.exe
如果链接失败(符号未找到):
g++ main.o -o app.exe
undefined reference to 'add' # 链接错误:缺少 utils.o
undefined reference to 'printf' # 链接错误:未链接运行时库
collect2.exe: error: ld returned 1 exit status
Note
:编译错误 (Compile Error) 和 链接错误 (Link Error) 是不同的:
- 编译错误:语法问题,如缺少分号、类型不匹配(编译阶段报错)
- 链接错误:符号缺失,如函数声明了但未定义、忘记链接库文件(链接阶段报错)
四、完整编译流程速查
graph LR
A["源文件<br/>(.c/.h)"] --> B["预处理器<br/>Preprocessor"]
B --> C[".i 文件"]
C --> D["编译器<br/>Compiler"]
D --> E[".s 文件<br/>(汇编代码)"]
E --> F["汇编器<br/>Assembler"]
F --> G[".o 文件<br/>(目标文件)"]
G --> H["链接器<br/>Linker"]
H --> I[".exe 文件<br/>(可执行文件)"]
一键编译命令
# 完整编译过程(一步完成)
g++ main.c -o main.exe
# 分步编译(便于调试)
g++ -E main.c -o main.i # 预处理
g++ -S main.i -o main.s # 编译
g++ -c main.s -o main.o # 汇编
g++ main.o -o main.exe # 链接
本节小结
| 阶段 | 输入文件 | 输出文件 | 关键操作 |
|---|---|---|---|
| 预处理 | .c |
.i |
宏展开、头文件包含、去注释 |
| 编译 | .i |
.s |
词法/语法/语义分析、代码优化 |
| 汇编 | .s |
.o |
生成机器码和目标文件 |
| 链接 | .o |
.exe |
符号解析、重定位、合并 |
上一节:← 0.1 编程哲学导论 下一节:待续