写在前面:C 语言是贴近底层的经典程序设计语言,以高效著称。本文虽名“速成”,但程序设计绝非可“速成”之技。建议以本文为纲,善用网络资源深入钻研,并勤于动手实践——唯有亲自编写代码、反复练习,方能真正掌握,为未来工程应用奠定坚实基础。

本文是《C语言期末速成》系列第 1 篇。本文的目的是让你先知道程序长什么样、怎么从源码变成可执行文件、运行时发生了什么,后面每一章都是在这副骨架上添加血肉。


考点清单

  • 程序由三种基本结构组成:顺序结构、选择结构、循环结构
  • 程序有且仅有一个 main 函数,它是运行的起点
  • 源码经 预处理 → 编译 → 链接 三步变成可执行文件:.c → .obj → .exe
  • .c 是源程序(文本文件),.obj 是目标文件,只有 .exe 能直接运行
  • 计算机中所有数据以二进制存储;数据存放的位置叫地址
  • bit(位)是 0 或 1;byte(字节)= 8 个 bit
  • 算法可以没有输入,但一定要有输出
  • 注释不是 C 语言,不占运行时间、不加分号、不可以嵌套

一、程序是什么:三种基本结构

再复杂的程序,从控制流的角度看,都只是下面三种结构的任意组合:

结构特点C 语言对应
顺序结构语句从上到下逐条执行普通的 语句;
选择结构根据条件决定执行哪一段if / elseswitch
循环结构满足条件时反复执行某段whiledo-whilefor

这和你背书时”顺序背、偶尔判断哪页、反复背到记住”是同一回事:顺序是默认节奏,选择是岔路口,循环是兜圈子。整份 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 有四个几乎必考的点:

  1. main 是入口:程序执行不看你写文件的先后,只看 main 的位置。程序是main 开始、按语句顺序执行,而不是从文件第一行开始。
  2. 一个程序只能有一个 main:链接阶段如果发现两个 main,链接器会直接报错——它不知道该从哪个入口进。
  3. main 之外不能再定义 main(当然更常见的是:任何函数内都不能再定义函数,这点在第 6 章展开)。
  4. 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 字符(pri……各自对应一个 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 是输入,和是输出;而”打印九九乘法表”可以不接收输入,但它一定要打印东西出来——那就是输出。


易错点 / 陷阱

  1. #define 后加分号#define PI 3.1415926; 是错误的。分号会连同数字一起被替换,后续代码莫名奇妙多出个分号。
  2. .c/.obj 当可执行文件:它们都不能运行,考试常问”哪个能运行”,答案是 .exe
  3. 误以为多个 main 合法:编译单个文件可能不报错,但链接阶段一定会因为入口冲突而失败。
  4. 1 字节当成 1 位1 字节 = 8 位bitbyte 别混。
  5. 认为预处理是 C 语句#include#define 不属于 C 语言,不占运行时间,不用加分号。
  6. 函数内定义函数:C 不允许函数嵌套定义(但可以嵌套调用,见第 6 章)。
  7. 注释嵌套/* /* */ */ 非法,注释不能套注释。
  8. 字符串里的 // 当注释printf("//x"); 里的是字符串内容,不是注释。
  9. 漏分号:语句必须以分号结束,这是初学者最常见的编译错误。

自测小练习

  1. 判断对错:#include <stdio.h> 是 C 语言的一条语句,所以要加分号。
  2. 一个程序能有两个 main 函数吗?如果能,编译时会发生什么?
  3. 1 byte 等于多少 bitchar 类型占几个字节?
  4. 算法的哪个特征决定了”它必须告诉外界结果”?
  5. 源程序 .c 文件、目标文件 .obj、可执行文件 .exe,哪一个可以直接双击运行?
  6. 判断对错:/* 外层 /* 内层 */ 外层 */ 这段注释是合法的。
参考答案
  1. 错。#include 是编译预处理指令,不是 C 语句,不能加分号。
  2. 不能。链接阶段会因为入口函数冲突而报错,程序无法生成可执行文件。
  3. 1 byte = 8 bitchar 占 1 个字节。
  4. 算法一定要有输出(可以没有输入)。
  5. .exe 可以直接运行,.c 是文本、.obj 尚未链接完成。
  6. 错。注释不能嵌套,内层的 /* */ 会让外层在第一个 */ 处提前结束。

下一篇C语言期末速成 2:数据类型与进制

评论区