C语言期末速成1:程序的骨架
从 main 入口到 .c→.obj→.exe,读懂 C 程序的结构骨架、编译流程、内存视角与注释规则。
写在前面:C 语言是贴近底层的经典程序设计语言,以高效著称。本文虽名“速成”,但程序设计绝非可“速成”之技。建议以本文为纲,善用网络资源深入钻研,并勤于动手实践——唯有亲自编写代码、反复练习,方能真正掌握,为未来工程应用奠定坚实基础。
本文是《C语言期末速成》系列第 1 篇。本文的目的是让你先知道程序长什么样、怎么从源码变成可执行文件、运行时发生了什么,后面每一章都是在这副骨架上添加血肉。
考点清单
- 程序由三种基本结构组成:顺序结构、选择结构、循环结构
- 程序有且仅有一个
main函数,它是运行的起点 - 源码经 预处理 → 编译 → 链接 三步变成可执行文件:
.c → .obj → .exe .c是源程序(文本文件),.obj是目标文件,只有.exe能直接运行- 计算机中所有数据以二进制存储;数据存放的位置叫地址
bit(位)是 0 或 1;byte(字节)= 8 个 bit- 算法可以没有输入,但一定要有输出
- 注释不是 C 语言,不占运行时间、不加分号、不可以嵌套
一、程序是什么:三种基本结构
再复杂的程序,从控制流的角度看,都只是下面三种结构的任意组合:
| 结构 | 特点 | C 语言对应 |
|---|---|---|
| 顺序结构 | 语句从上到下逐条执行 | 普通的 语句; |
| 选择结构 | 根据条件决定执行哪一段 | if / else、switch |
| 循环结构 | 满足条件时反复执行某段 | while、do-while、for |
这和你背书时”顺序背、偶尔判断哪页、反复背到记住”是同一回事:顺序是默认节奏,选择是岔路口,循环是兜圈子。整份 C 语言考试卷,无论题目多花哨,最后都能拆成这三种结构的嵌套组合。
#include <stdio.h>
int main(void) {
int n = 5;
/* 顺序:先算好总和 */
int sum = 0;
/* 循环:把 1 加到 n */
for (int i = 1; i <= n; i++)
sum += i;
/* 选择:看结果在哪个区间 */
if (sum > 10)
printf("sum = %d, 超过了10\n", sum);
else
printf("sum = %d\n", sum);
return 0;
}
顺序结构(两行赋值)→ 循环结构(for 求和)→ 选择结构(if 判断)——一个函数里三种结构各司其职,这就是最基本的骨架。
二、main:一切运行的起点
每个 C 程序有且仅有一个 main 函数。操作系统加载程序后,就是从 main 的第一条语句开始执行,然后顺着顺序结构往下走,碰到选择做选择,碰到循环进循环。
关于 main 有四个几乎必考的点:
main是入口:程序执行不看你写文件的先后,只看main的位置。程序是从main开始、按语句顺序执行,而不是从文件第一行开始。- 一个程序只能有一个
main:链接阶段如果发现两个main,链接器会直接报错——它不知道该从哪个入口进。 main之外不能再定义main(当然更常见的是:任何函数内都不能再定义函数,这点在第 6 章展开)。main的标准写法带返回类型和返回值:
int main(void) { /* void 表示不接受命令行参数 */
...
return 0; /* 0 表示"正常结束" */
}
标准写法是 int main()(返回 int,正常结束 return 0;)。早期教材里的 void main() 是过时写法,正规标准不接受,考试判题时以 int main() 为准。
TIP遇到改错题目时,若题干没有提示,将
void main()放到最后进行判断,如果你已经挑出符合题目要求数量的错误,则可以认为void main()写法没有问题。
某些嵌入式领域的 C 程序可能没有
int类型的main函数,这是因为编译套件已经在内部进行了相关处理。
三、从源码到可执行:.c → .obj → .exe
很多教材只让你背”先编译、后链接、最后运行”,但理解每一步到底做了什么,才能不靠死记硬背。
第 0 步:预处理
真正编译之前,编译器先调用预处理器,把以 # 开头的指令处理掉:
#include <stdio.h>→ 把头文件的内容原样插入到这个位置;#define PI 3.14→ 把后面代码里的PI逐字替换成3.14。
这一步是纯文本替换,不检查任何语法。所以预处理指令不是 C 语言语句,不占运行时间、不能加分号(#define PI 3.14; 里那个分号会真的被替换进代码里,常常制造诡异的错误)。
第 1 步:编译
预处理器处理完的文本才进入编译器。编译器做词法分析、语法分析、语义分析,最后把每个 .c 文件翻译成一个 目标文件(Windows 下是 .obj,Linux 下是 .o)。
目标文件里已经是机器能看懂的二进制指令了,但它还不能运行,原因见下一步。
第 2 步:链接
链接器把多个目标文件和你调用的库函数(比如 printf 的代码在库文件里)拼接成一个完整的可执行文件 .exe。同时它会做一件关键的事:确认 main 有且只有一个。这也是为什么”一个程序只能有一个 main”是常识而”头文件包含进来的文件可以没有 main”。
TIP最终生成的可执行文件的后缀因平台而异,Windows 下是
.exe,而类 Unix 系统(如 Linux、MacOS 等)不以文件后缀区分类型,因此其后缀是自由的,甚至可以没有后缀。本文中的说明均基于 Windows 下的可执行文件。
三步全貌
预处理 编译 链接 运行
.c ────→ .obj ────→ .exe ──→ 屏幕上出结果
文本源码 二进制目标 可执行文件
必考点:.c 和 .obj 都无法直接运行,只有最终链接出来的 .exe 才可以。原因很直白——.c 是给人看的文本,CPU 不认识;.obj 虽然已经是机器码,但还缺库函数、还没确定程序入口,是一个没拼装完的半成品。
/* 常见判断题 */
/* 1. ".c 文件可以直接运行" → 错,那是文本 */
/* 2. ".obj 文件可以直接运行" → 错,还没链接 */
/* 3. ".exe 文件可以直接运行" → 对 */
四、源程序为什么是文本文件
printf("hello"); 这行代码在磁盘上,本质就是一堆 ASCII 字符(p、r、i……各自对应一个 ASCII 码存进文件)。所以我们说 C 语言源程序以 ASCII 数值存放在文本文件中。
这也是为什么编辑器能打开 .c 文件、能直接看到代码,而打开 .exe 看到的是一堆乱码——因为 .exe 里存的是二进制机器指令,不是字符。
容易混淆的判断题:
- “编译预处理是 C 语言的一部分” → 错。预处理是编译过程中的一个阶段,不属于 C 语言语法本身。
- “预处理指令要加分号” → 错。
#define、#include都不是语句。 - “预处理占运行时间” → 错。它在编译前就做完了,程序运行的时候它早就不存在了。
五、注释与书写格式:给程序做”旁白”和”排版”
5.1 注释:考试的隐藏考点
原文把它列为”近几年考试的重点”,因为它有一堆容易记错的性质:
注释不是 C 语言的一部分,不占运行时间,没有分号,不可以嵌套。
注释有两种写法:
// 单行注释:从 // 到行尾结束(C99 引入)
/* 块注释:可以占一行 */
/*
多行注释:块注释可以跨多行
适合写函数说明、模块说明
*/
注释的性质决定了这些判断题的答案:
- “注释是 C 语言的一部分” → 错。它会被编译器忽略,不参与任何语法检查。
- “注释占运行时间” → 错。编译阶段就被剔除了。
- “注释要加分号” → 错。它不是语句。
- “注释可以嵌套” → 错。
/* /* */ */是非法的,外层注释在第一个*/处就结束了,后面的*/会变成语法错误。 - “字符串里的
//或/*是注释” → 错。printf("//hello\n");输出的就是//hello,引号内的一切都是字符串内容,不是注释。
/* 判断题速查 */
/* 1. 注释是 C 语言的一部分 → 错 */
/* 2. 注释不占运行时间 → 对 */
/* 3. 注释不可以嵌套 → 对 */
/* 4. 字符串内的 /* */ 也是注释 → 错 */
5.2 书写格式是自由的
C 语言书写格式自由:一行可以写多条语句,一条语句也可以拆成多行,空白(空格、Tab、换行)随意使用,只要不把单词拆开。编译器靠分号判断一条语句的结束,靠空格/换行区分不同的单词,其余空白一律忽略。
int a = 1; int b = 2; /* 一行两条语句,合法 */
printf(
"%d", /* 一条语句拆多行,合法 */
a
);
int c;c=3; /* 没有空格也能过(不推荐) */
- 分号是语句结束符,漏了分号是初学者最常见的编译错误;
- 空语句就是一个单独的分号
;(第 5 章会讲它造成的死循环陷阱);
TIP尽管 C 语言书写格式是自由的,但建议你保持一致的书写风格和整洁的缩进,比如 4 个空格或 1 个 Tab。这有助于代码的可读性和维护性。
六、数据与地址:二进制视角
6.1 数据以二进制存储
计算机里的一切——整数、小数、字符、图片——最终都以 0 和 1 两种状态存储。所以你给 int a = 5,内存里存放的其实是 0000 0000 0000 0101(16 位情形)。
由此引出两个基本单位:
- bit(位):一个二进制位,取值只能是 0 或 1。
- byte(字节):8 个 bit。
1 字节 = 8 位,这是换算关系的核心。
1 byte = 8 bit
char = 1 字节 ← 一个字符要 1 个字节
int = 4 字节 ← 一个整数通常要 4 个字节
(不同机器/编译器整型长度可能不同,但 char 永远是 1 字节,考试默认按 4/2/8 的常见配置出题。)
6.2 数据存放的位置:地址
“数据存哪了?“——这个问题在 C 里格外重要,因为地址(指针)是 C 语言的灵魂(第 8 章专讲)。
内存就像一排排编好号的储物柜。你把 5 存进去,就会得到一个”柜门编号”,这个编号就是地址。C 语言用取址符 & 拿到它:
#include <stdio.h>
int main(void) {
int a = 5;
printf("a 的值是 %d,存放在地址 %p\n", a, (void*)&a);
return 0;
}
&a 就是变量 a 所在的内存地址。普通变量放数值,后面你会学到:指针变量放的是地址。
七、程序运行时的内存视角(加深)
教材常把”运行”说得像黑盒,其实一个正在运行的程序,内存大致分成四块区域,理解它对你理解函数、递归、数组、指针都有帮助:
| 区域 | 存什么 | 生命周期 |
|---|---|---|
| 代码段 | 编译后的机器指令 | 程序运行期间 |
| 数据段 | 全局变量、静态变量 | 程序运行期间 |
| 栈(stack) | 函数调用时的局部变量、参数、返回地址 | 函数调用期间,自动分配/自动释放 |
| 堆(heap) | malloc 手动申请的动态内存 | 由程序员手动申请/释放 |
一个直观的比喻:栈是”进进出出的电梯”——函数调用时入栈(压入变量),函数返回时出栈(释放变量),先调用后返回,规矩严格;堆是”仓库自助取货”——要用多大取多大,用完了得自己还(free)。
现在你只需要在脑里挂上这张图,后面讲函数调用栈、递归、malloc 时都会回来找它。
八、算法:无输入可以,无输出不行
算法是”解决问题的步骤描述”。C 语言里常考算法的一个特征:
算法可以没有输入,但一定要有输出。
理由是:算法是拿来解决实际问题的,如果算完没有结果告诉外界,那这个步骤就没意义了。比如”求 1 到 n 的和”这个算法,n 是输入,和是输出;而”打印九九乘法表”可以不接收输入,但它一定要打印东西出来——那就是输出。
易错点 / 陷阱
#define后加分号:#define PI 3.1415926;是错误的。分号会连同数字一起被替换,后续代码莫名奇妙多出个分号。- 把
.c/.obj当可执行文件:它们都不能运行,考试常问”哪个能运行”,答案是.exe。 - 误以为多个
main合法:编译单个文件可能不报错,但链接阶段一定会因为入口冲突而失败。 - 1 字节当成 1 位:
1 字节 = 8 位,bit和byte别混。 - 认为预处理是 C 语句:
#include、#define不属于 C 语言,不占运行时间,不用加分号。 - 函数内定义函数:C 不允许函数嵌套定义(但可以嵌套调用,见第 6 章)。
- 注释嵌套:
/* /* */ */非法,注释不能套注释。 - 字符串里的
//当注释:printf("//x");里的是字符串内容,不是注释。 - 漏分号:语句必须以分号结束,这是初学者最常见的编译错误。
自测小练习
- 判断对错:
#include <stdio.h>是 C 语言的一条语句,所以要加分号。 - 一个程序能有两个
main函数吗?如果能,编译时会发生什么? 1 byte等于多少bit?char类型占几个字节?- 算法的哪个特征决定了”它必须告诉外界结果”?
- 源程序
.c文件、目标文件.obj、可执行文件.exe,哪一个可以直接双击运行? - 判断对错:
/* 外层 /* 内层 */ 外层 */这段注释是合法的。
参考答案
- 错。
#include是编译预处理指令,不是 C 语句,不能加分号。 - 不能。链接阶段会因为入口函数冲突而报错,程序无法生成可执行文件。
1 byte = 8 bit;char占 1 个字节。- 算法一定要有输出(可以没有输入)。
.exe可以直接运行,.c是文本、.obj尚未链接完成。- 错。注释不能嵌套,内层的
/* */会让外层在第一个*/处提前结束。