前言
之前我写过几篇关于C语言的相关文章(可以翻翻我的个人主页),参考的是那本经典的红皮书。
当时不仅是面向大一的同学分享了相关知识,还有不少大二的同学也来听讲,甚至大三的同学在阅读之后也表示受益匪浅。
原本计划继续讲解文件处理相关内容,但由于一直都有事情,后续的讲解被搁置了很长一段时间。
看了下大家的进度,我决定这次分享一些不一样的——让大家对软件工程有一个整体的把握,让大家知道现在是在“学些什么”。
引入
每一位学习C语言的新同学,最初接触的编程场景往往是在命令行中运行一个简单的hello_world.c程序,我也是一样。面对这个“黑框框”,我其实之前是有困惑的:如何将课堂上的小程序,变成像QQ、微信那样功能丰富的软件? 这次我们从一个简单的C语言源代码出发,与大家分享现代软件工程的构建流程。
从一个小程序说起
计算机程序本质上就是根据输入,经过计算,得到输出。
输入、输出本质上是程序的数据(变量),计算本质上是程序的业务逻辑代码/函数。
数据结构课上一定会学的内容:
程序=数据结构+算法逻辑
我们首先来看一个简单的C语言程序:
该程序实现一个简单的成绩等级评定系统。用户输入一个学生的成绩(0~100),程序根据规则输出等级。
#include <stdio.h>
// 使用 #define 定义常量
#define MAX_SCORE 100
// 函数声明
char getGrade(int score);
int main() {
int score;
// 输入部分
printf("请输入学生成绩(0-%d): ", MAX_SCORE);
scanf("%d", &score);
// 检查输入是否合法
if (score < 0 || score > MAX_SCORE) {
printf("输入无效!成绩应在 0 到 %d 之间。\n", MAX_SCORE);
return 1;
}
// 调用函数获取等级
char grade = getGrade(score);
// 输出结果
printf("成绩等级为:%c\n", grade);
return 0;
}
// 函数定义:根据分数返回等级
char getGrade(int score) {
if (score >= 90) return 'A';
else if (score >= 80) return 'B';
else if (score >= 70) return 'C';
else if (score >= 60) return 'D';
else return 'F';
}
如何让计算机执行这段代码呢?我们需要将代码“翻译”成机器能够识别和运行的语言。这一“翻译”过程依赖于一种称为“编译器”的软件工具。在C语言中,常用的编译器是GCC,下面我们来深入了解一下:
GCC(GNU Compiler Collection,GNU 编译器套件)是一套完整的编译工具链。我们常用的 gcc.exe 实际上只是编译流程的前端接口,它会依次调用 cpp、cc1、as 和 ld 等工具,分别完成从源代码到预处理程序、汇编代码、目标文件,最后生成可执行文件的整个过程。构建一个源代码文件为独立的应用程序,其详细流程如下:

| 步骤 | 指令 | 执行程序 | 作用 | 简记 | 输入 | 输出 |
| 预处理 | gcc -E | cpp.exe | 处理源代码中的宏定义、条件编译指令(带“#”的语句*) | Expand | source.c(你写的源代码) | source.i(也是个源代码) |
| 编译** | gcc -S | cc1.exe | 将预处理后的代码编译成汇编语言代码 | Source to Assembly | source.i | source.s(汇编代码) |
| 汇编 | gcc -c | as.exe | 将汇编语言代码汇编成目标文件 | Compile and Assemble | source.s | source.o(机器码) |
| 链接 | gcc | ld.exe | 将目标文件和其他必要的库文件链接在一起,生成最终的可执行文件 | – | source.o | source.exe/.ELF |
注:
*这里带“#”的语句包括的类型如下(也包括#include):
- 文件包含 (
#include):预处理器会将指定的头文件内容插入到源代码中#include指令所在的位置。这允许你将代码分散在多个文件中,并根据需要包含其他文件的内容。 - 条件编译 (
#if,#ifdef,#ifndef,#else,#elif,#endif):这些指令允许根据某些条件(如宏是否定义)有条件地编译代码部分。这对于编写跨平台代码特别有用,可以根据不同的系统特性选择性地包含特定代码段。 - 宏定义 (
#define):除了简单的宏替换,还可以定义带参数的宏,它们可以像函数一样使用但会在编译前被替换为相应的代码片段。 - 取消宏定义 (
#undef):这个指令用于取消先前定义的宏,使得之后该宏不再生效。 - 行控制 (
#line):更改编译器内部记录的当前行号和文件名,主要用于调试或生成错误消息时提供更准确的信息。 - 错误生成 (
#error):当预处理器遇到#error指令时,它会停止编译并输出一条错误消息。这通常用来强制检查某些编译时条件是否满足。 - pragma指令 (
#pragma):这是一种特殊类型的指令,用于向编译器传递具体的编译选项或行为指示。不同编译器可能支持不同的#pragma指令,因此它不是可移植的。
**我们常说的“编译”指的是这整个从源代码到可执行文件的过程,只不过在GCC中进行了细化,此处的“编译”是指编译到目标汇编代码的过程。
可以看出,一个基本的编译流程包括:编译和链接,这也是后续大型工程构建的基础。
下面我们手把手进行操作:
首先,执行下列代码,对代码进行预处理(-o表示输出路径):
gcc -E source.c -o source.i
无论是打开source.c还是source.i,大家都可以发现,其实这俩都是源代码。只不过source.i中新增了很多东西。注意关注两点:1.头文件消失了,变成了具体实现 2.MAX_SCORE同时也消失了,变为了具体数值。

如果我们删掉头文件试试呢?你会发现,代码突然少了许多:

接下来,我们将这个预处理文件编译为汇编语言,执行下列命令:
gcc -S source.i -o source.s
可以看到汇编代码,接下来我们分析一下这个汇编代码:
.file "source.c"
.text
.section .rdata,"dr"
.align 8
.LC0:
.ascii "\350\257\267\350\276\223\345\205\245\345\255\246\347\224\237\346\210\220\347\273\251\357\274\210"
.ascii "0-%d\357\274\211: \0"
.LC1:
.ascii "%d\0"
.align 8
.LC2:
.ascii "\350\276\223\345\205\245\346\227\240\346\225\210\357\274\201\346\210\220\347\273\251\345\272\224\345\234\250 0 \345\210\260 %d \344\271\213\351\227\264\343\200\202\12\0"
.LC3:
.ascii "\346\210\220\347\273\251\347\255\211\347\272\247\344\270\272\357\274\232%c\12\0"
.text
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
pushq %rbp
.seh_pushreg %rbp
movq %rsp, %rbp
.seh_setframe %rbp, 0
subq $48, %rsp
.seh_stackalloc 48
.seh_endprologue
call __main
movl $100, %edx
leaq .LC0(%rip), %rax
movq %rax, %rcx
call printf
leaq -8(%rbp), %rax
movq %rax, %rdx
leaq .LC1(%rip), %rax
movq %rax, %rcx
call scanf
movl -8(%rbp), %eax
testl %eax, %eax
js .L2
movl -8(%rbp), %eax
cmpl $100, %eax
jle .L3
.L2:
movl $100, %edx
leaq .LC2(%rip), %rax
movq %rax, %rcx
call printf
movl $1, %eax
jmp .L5
.L3:
movl -8(%rbp), %eax
movl %eax, %ecx
call getGrade
movb %al, -1(%rbp)
movsbl -1(%rbp), %eax
movl %eax, %edx
leaq .LC3(%rip), %rax
movq %rax, %rcx
call printf
movl $0, %eax
.L5:
addq $48, %rsp
popq %rbp
ret
.seh_endproc
.globl getGrade
.def getGrade; .scl 2; .type 32; .endef
.seh_proc getGrade
getGrade:
pushq %rbp
.seh_pushreg %rbp
movq %rsp, %rbp
.seh_setframe %rbp, 0
.seh_endprologue
movl %ecx, 16(%rbp)
cmpl $89, 16(%rbp)
jle .L7
movl $65, %eax
jmp .L8
.L7:
cmpl $79, 16(%rbp)
jle .L9
movl $66, %eax
jmp .L8
.L9:
cmpl $69, 16(%rbp)
jle .L10
movl $67, %eax
jmp .L8
.L10:
cmpl $59, 16(%rbp)
jle .L11
movl $68, %eax
jmp .L8
.L11:
movl $70, %eax
.L8:
popq %rbp
ret
.seh_endproc
.def __main; .scl 2; .type 32; .endef
.ident "GCC: (x86_64-posix-seh-rev0, Built by MinGW-Builds project) 14.2.0"
.def printf; .scl 2; .type 32; .endef
.def scanf; .scl 2; .type 32; .endef
我们继续执行下面的语句,将汇编码编译为机器码:
gcc -c source.s -o sources.o
这样我们的代码就已经编译完成了,但是我们如果想运行,还需要进行链接,执行下面的操作进行链接:
gcc source.o -o source.exe
注意,如果使用Linux,这里建议设置chmod 777(读4写2执行1,本用户、用户组、所有人(从小到大))
从一个文件到多个文件
这是一个看似简单的代码示例。然而根据我们的观察,现实中我们所使用的软件功能往往非常复杂。小到一个 QQ 或微信,大到一个完整的 Linux 操作系统,其背后的代码量往往是数以万计甚至更多。如果将如此庞大的代码全部写在一个文件中,不仅用文本编辑器打开可能都需要几分钟时间,效率低下,而且在协作开发时也极不现实——因为一个文件只能由一个人编写和修改,多人协作将难以高效合并各自的改动。
更严重的问题在于编译过程。如果每次修改都必须重新编译整个项目,那么对于一个拥有数万行代码的大型项目来说,这样的做法几乎不可行。
为了解决这些问题,聪明的工程师们提出了一种更高效的方案:将代码拆分成多个文件,每个文件由不同的开发者负责。这样不仅可以并行开发,提高效率,也无需每次都对全部代码进行重新编译,只需编译发生变更的部分即可。
那么,如何实现这种跨文件的代码调用呢?这就引出了“头文件”的概念。我们需要一种方式来告诉编译器:“我在其他文件中定义了一些函数或变量,如果你在当前文件中使用到了它们,请去正确的地方找到它们。”
头文件正是用来提供这些声明信息的工具,它帮助编译器理解代码结构,并顺利地完成整个编译过程。
模块化拆分实践
接下来,我们手把手对上面的代码进行拆解,我们先建立如下目录结构:

步骤1:创建头文件
// grade.h
#ifndef GRADE_H
#define GRADE_H
#define MAX_SCORE 100
// 函数声明
char getGrade(int score);
#endif // GRADE_H
步骤2:分离功能实现
// grade.c
#include "../inc/grade.h"
// 函数实现
char getGrade(int score) {
if (score >= 90) return 'A';
else if (score >= 80) return 'B';
else if (score >= 70) return 'C';
else if (score >= 60) return 'D';
else return 'F';
}
步骤3:主程序调用
// main.c
#include <stdio.h>
#include "../inc/grade.h" // 引入自定义头文件
int main() {
int score;
printf("请输入学生成绩(0-%d): ", MAX_SCORE);
scanf("%d", &score);
if (score < 0 || score > MAX_SCORE) {
printf("输入无效!成绩应在 0 到 %d 之间。\n", MAX_SCORE);
return 1;
}
char grade = getGrade(score);
printf("成绩等级为:%c\n", grade);
return 0;
}
依次执行下面的内容,编译并链接:
cd src
gcc -c main.c -o main.o
gcc -c grade.c -o grade.o
gcc main.o grade.o -o main.exe
这个时候GCC的编译流程:

这里注意:ar 是一个用于创建、修改和提取归档文件的命令行工具。在 C/C++ 开发中,尤其是在使用 GCC 编译器时,ar 常被用来管理和构建静态库。
那么,程序应该从哪里开始执行呢?对于一个独立的可执行程序来说,必须存在一个包含 main 函数的源文件。该源文件会被编译为目标文件,并在最终链接阶段与其他目标文件或库链接在一起,形成完整的可执行程序,而 main 函数则作为整个应用程序的入口点。
上文说过了,编译归根到底,还是编译和链接。多个文件,本质上就是使用多次gcc,分别编译多个文件,然后打包,最后链接到一起。
自动化构建工具——Make与Makefile
现在大家应该已经看出来了,我们想搭建的是一个多文件组成的代码工程。理论上来说,只要手动敲几条 gcc 命令,把每个文件一个个编译出来,最后再打包、链接一下,就能完成整个工程的构建流程。
不过话说回来,真让你一个命令一个命令地敲,估计谁都不太乐意吧?别说乐意了,当项目文件数量一多,比如超过十个,纯靠手动输入命令来编译和链接,根本不现实,不仅麻烦,还容易出错。
那有没有更聪明的办法呢?当然有!既然我们写程序的目的之一就是让计算机来替我们做重复劳动,那把这些编译命令写成一个程序不就好了?没错,这就是“构建工具”诞生的初衷。其实你可能已经接触过类似的东西,比如 Windows 下的批处理脚本(.bat 文件)——构建工具本质上就是帮我们自动执行一系列编译任务的程序。
它的出现,大大简化了多文件工程的构建流程,让我们可以把精力更多地放在写代码上,而不是手动拼命敲命令。
Make与Makefile介绍
聊到构建工具,就不得不提一个老牌又经典的工具——Make,它几乎是 C/C++ 开发者的“启蒙级”构建助手。配合使用的还有一个关键角色:Makefile。
什么是 Make?
Make 是一个自动化构建工具,它的核心功能就是:根据文件之间的依赖关系,自动决定哪些文件需要重新编译,并执行相应的命令。简单来说,它就是一个“聪明的命令执行器”。
你只需要告诉它:哪些文件依赖哪些源文件,该用什么命令来编译,一切交给它搞定。
什么是 Makefile?
Makefile 是 Make 工具用来读取指令的“剧本”。你可以把它看成一个配置文件,里面写明了各种编译规则、依赖关系和执行命令。
下面是一个最简单的 Makefile 示例):
# Makefile
# 默认目标,生成可执行文件 grade_program
grade_program: src/main.o src/grade.o
gcc -o grade_program src/main.o src/grade.o
# 从 src/main.c 生成 src/main.o
src/main.o: src/main.c
gcc -c src/main.c -o src/main.o
# 从 src/grade.c 生成 src/grade.o
src/grade.o: src/grade.c
gcc -c src/grade.c -o src/grade.o
# 清理生成的目标文件和可执行文件
clean:
rm -f src/*.o grade_program
.PHONY: clean
观察代码可以发现,Makefile的语法非常简单,目标(文件):依赖(文件),为了方便更改部分内容,我们通过定义变量的方式,替换命令中的部分内容,修改后的代码如下:
# Makefile
# 定义编译器
CC=gcc
# 设置头文件包含路径
CFLAGS=-Iinc
# 定义目标可执行文件名称
TARGET=grade_program
# 指定所有需要编译的目标文件(.o)
OBJ=src/main.o src/grade.o
# 默认规则:生成目标可执行文件
$(TARGET): $(OBJ)
$(CC) -o $@ $^ $(CFLAGS)
# 规则:从 .c 文件生成 .o 文件
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
# 清理编译产物
clean:
rm -f src/*.o $(TARGET)
.PHONY: clean
这个 Makefile 的意思是:
- 变量定义:
CC: 编译器命令,默认使用 GCC。CFLAGS: 编译选项,这里我们通过-Iinc来指定头文件目录。TARGET: 最终生成的可执行文件名。OBJ: 所有需要编译成目标文件的源文件对应的对象文件列表。
- 默认规则 (
$(TARGET): $(OBJ)):- 这是主规则,用来生成最终的可执行文件。它依赖于所有的对象文件(
$(OBJ))。一旦这些对象文件被更新,这个规则就会被执行来链接这些对象文件生成最终的可执行文件。
- 这是主规则,用来生成最终的可执行文件。它依赖于所有的对象文件(
- 模式规则 (
%.o: %.c):- 这个规则告诉 make 如何从
.c文件生成.o文件。这里的$@是目标的名字(即.o文件),而$<是第一个依赖项(即.c文件)。
- 这个规则告诉 make 如何从
- 清理规则 (
clean:):- 这个规则用于清理生成的对象文件和最终的可执行文件,方便重新编译整个项目。
Make的工作流程是:从上到下进行解析,对于每一条命令,生成target的时候会检测各种依赖文件是否存在,如果不存在,则搜索下方是否有target与需要的依赖文件相同,如果有,则开始编译,通过这种方式,完成递归编译。
同时,如果你做过测试可以发现,make工具会监控文件的变化,如果文件没有变化,其中一部分文件的编译是不会执行的,这样极大的提升了编译速度。
为什么要用 Make 和 Makefile?
- 高效:只编译改动过的文件,节省大量等待时间;
- 自动化:一次配置,终身受益,避免手动敲命令出错;
- 可维护:工程越大,Makefile 的价值越明显,结构清晰可扩展;
- 跨平台:在类 Unix 系统中普遍支持,配合一些技巧也能用于 Windows。
使用 Make 和 Makefile,你就能轻松管理多文件编译项目,哪怕是几百个源文件也不在话下。它是构建系统的“入门款”,也是理解更高级构建工具(比如 CMake、Ninja)的基础。
CMake:更强大的构建工具
虽然 Make 和 Makefile 很好用,但随着项目越来越复杂,你可能会遇到不少痛点:
- 不同平台上的构建方式不一样,Makefile 写起来麻烦;
- 想用 IDE(比如 Visual Studio、CLion)打开工程,还得手动配置;
- Makefile 文件一多,维护起来非常混乱……
这时候,就轮到 CMake 登场了。
什么是 CMake?
CMake 是一个跨平台的构建系统生成工具,它并不直接编译代码,而是生成本地平台上的构建系统文件——比如 Makefile、Visual Studio 的工程文件、Ninja 脚本等等。你只需要写一次 CMake 配置,CMake 会帮你适配各种平台和构建工具。
一句话总结就是:
👉 CMake 生成构建系统,Make 执行构建系统。
CMake 工作流程长啥样?
简单来说,CMake 的使用流程是(Linux):
mkdir build
cd build
cmake ..
make
解释一下:
mkdir build && cd build:创建一个构建目录,干净又整洁;cmake ..:调用 CMake,读取上级目录的CMakeLists.txt文件,生成 Makefile 或其他构建脚本;make:执行生成的构建脚本,开始编译!
注意,如果是windows就比较复杂一点,要有到minGW(请在powershell中执行):
mkdir build
cd build
cmake .. -G "MinGW Makefiles" # 这里如果不设置,默认生成的Visual Studio工程
mingw32-make
CMakeLists.txt 是啥?
就像 Make 依赖 Makefile,CMake 需要你提供一个配置文件,名字叫做 CMakeLists.txt。里面写明了项目名、源码路径、依赖库、编译选项等信息。
一个最小的示例长这样:
# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(GradeProgram C)
include_directories(inc)
add_executable(grade_program src/main.c src/grade.c)
是不是很清爽?甚至比 Makefile 还简单些。通常,我们需要创建一个build文件夹用于存放cmake的文件,因此执行下列命令:
mkdir build
cd build
cmake .. # CMakeLists.txt在上级目录
make # cmake生成make文件,还需要执行make进行编译
CMake 的优势在哪?
- 跨平台:支持 Windows、Linux、macOS;
- 支持多种构建系统:不仅是 Make,还支持 Ninja、Xcode、Visual Studio 等;
- 模块化更强:大型工程中可以轻松拆分多个子模块,便于管理;
- IDE 友好:CMake 配置的项目可以直接在 Visual Studio、CLion 中打开并编译;
- 支持现代 C++:支持编译选项检测、头文件检查、链接依赖处理等,智能化程度更高;
总之,如果你只是写几个 C 文件,Make 已经够用了。但一旦项目规模扩大、平台复杂、或者你想使用更现代的开发工具链,那 CMake 几乎是必备技能。
常见的构建工具
当然,目前我们的开发早已不再是单独的C语言,还存在C++、C#、Java等语言,每个语言,都有一套相似的逻辑构建自己的可执行文件,考虑到大家还没学习Java,Java、Python等设计理念和相关工程性质的内容我就放在后面补充讲解,这里对比几个构建工具,方便大家学习:
| 工具名称 | 适用语言 | 适用平台 | 配置文件 | 目标文件 | 用途 |
| make | 主要用于 C/C++ 等编译型语言 | Linux | Makefile | 可执行文件(如 .out 或 .exe) | Linux 自带的构建工具,用于编译和链接源代码 |
| CMake | C/C++语言 | 跨平台 | CMakeLists.txt | Makefile, Visual Studio, Xcode 等 | 构建成目标工程,跨平台配置和管理项目依赖 |
| Maven | Java | 跨平台 | pom.xml | *.jar, *.war, *.ear 等 | 构建 Java 工程,依赖管理和自动化构建过程 |
| Gradle | Java | 跨平台 | bulid.gradle | *.jar, *.war, *.ear 等 | 构建 Java 工程,依赖管理和自动化构建过程 |
C语言标准项目结构
下面是一个C语言工程的项目结构:
my_project/
├── CMakeLists.txt # 构建配置
├── include/ # 公共头文件
│ └── audio.h
├── src/ # 源代码
│ ├── main.c
│ └── audio.c
├── tests/ # 测试代码
├── docs/ # 文档
└── third_party/ # 第三方库
如果你使用IDE,可以看到下面的工程结构(C语言常用于嵌入式工程开发,这里以我的一个STM32工程SmartAgriculture为例):

GDB:你的程序调试好帮手
当我们把代码写好并成功编译之后,接下来往往还会遇到一个问题:程序运行不正常!
有可能是段错误(Segmentation Fault)、无限循环、输出不对……这时候,光看代码很难定位问题。怎么办?调试工具登场!
在 C/C++ 开发中,最常用的调试工具之一就是:GDB(GNU Debugger)。
GDB 是什么?
GDB 是 GNU 提供的一个功能强大的命令行调试器,可以帮助你:
- 单步执行代码,观察每一步的运行情况;
- 查看和修改变量的值;
- 设置断点,跳过某些部分;
- 分析程序崩溃的原因(比如段错误);
- 调用栈回溯,排查函数调用流程。
一句话总结:GDB 就像是一个程序“显微镜”,让你能清楚看到程序在运行时的真实状态。
使用 GDB 前的准备
要使用 GDB 进行调试,编译的时候必须加上 -g 选项,告诉编译器保留调试信息:
gcc -g -o main main.c
这样编译出来的 main 可执行文件才可以被 GDB 调试。
GDB 基本用法
运行 GDB:
gdb ./main
进入 GDB 后,可以使用以下常用命令:
| 命令 | 作用 |
|---|---|
break <函数名> 或 b <行号> | 设置断点 |
run 或 r | 启动程序 |
next 或 n | 单步执行(不进入函数内部) |
step 或 s | 单步执行(会进入函数) |
continue 或 c | 继续运行直到下一个断点 |
print <变量> 或 p | 打印变量的值 |
backtrace 或 bt | 打印调用栈 |
quit 或 q | 退出调试 |
示例流程
假设我们在 main.c 中写了个有 bug 的程序:
#include <stdio.h>
int divide(int a, int b) {
return a / b;
}
int main() {
int x = 10;
int y = 0;
int z = divide(x, y);
printf("Result: %d\n", z);
return 0;
}
我们编译并运行:
gcc -g -o main main.c
gdb ./main
然后在 GDB 里可以这样做:
(gdb) break divide # 在 divide 函数设置断点
(gdb) run # 启动程序
(gdb) print a # 查看变量 a 的值
(gdb) print b # 查看变量 b 的值(是 0)
(gdb) backtrace # 查看调用栈
可以很快发现,问题就是除以了 0。

其他的一些功能:
watch <变量>:监视变量的值,只要变了就暂停程序;info locals:查看当前函数的所有局部变量;set var x = 10:修改变量值,动态尝试修复错误;layout src:进入文本 UI 模式(适合在终端中浏览代码);
相比打印调试(printf 调试),GDB 更专业、更高效、更强大。虽然命令行界面看起来有点“硬核”,但只要掌握了基本命令,就能在实际开发中事半功倍。GDB 是 C/C++ 程序员的必备技能之一,也是理解程序运行机制的利器。
什么是开发框架?写程序真的不用从零开始!
当我们写程序写多了,你可能会突然思考:
“每次都从
main函数开始写,IO、界面、业务逻辑……是不是太原始了?”
没错,重复造轮子并不高效。
这时候,我们引入一个非常重要的概念:开发框架(Development Framework)。
开发框架到底是啥?
简单来说,开发框架就是一个为开发者提供好“地基”的工程模板,它帮你提前准备好常见的结构、功能、工具,让你只需要专注于业务逻辑的开发。
就像搭积木,开发框架已经帮你垒好了底座、立柱、墙体,剩下的只是盖个屋顶,涂个颜色。它们往往还提供:
- 封装好的库和 API(比如 UI 控件、网络通信);
- 统一的项目结构(比如 MVC 架构);
- 自动化工具(热重载、构建脚本等);
- 甚至还帮你做好了跨平台适配!
说的直白一点,这里,就是一般我们常见的应用的开发模式——通过框架进行构建。举个简单的例子,在Windows中,页面的渲染一般会有两个区域,显示区域和缓存区域,当显示区域A时候,区域B开始绘制,具体绘制什么,怎么绘制,各个组件大小等,都是用代码实现的。通过直接使用框架,这一类细节均由框架管理,因此我们无需处理非常复杂的逻辑。也就是工程上的“不要重复造轮子”的说法。
一些常见的开发框架如下:
| 分类 | 框架名称 | 使用语言 | 应用类型 | 特点与用途说明 |
|---|---|---|---|---|
| UI(前端)开发 | WPF | C# | 桌面应用(Windows) | 属于 .NET 平台,支持丰富控件、数据绑定与动画,适合构建现代化 Windows 界面应用 |
| .NET Framework | C# | 桌面 & Web 应用 | 微软全家桶平台,包含 WPF、WinForms、ASP.NET 等,用于开发 Windows 软件与服务 | |
| QT | C++ | 跨平台应用 | 一种跨平台框架,被广泛用于各种展台 | |
| 后端开发 | Tomcat | Java | Web 服务容器 | Java Web 项目的运行平台,支持 JSP/Servlet,配合 Spring 系列框架更常用 |
| Spring Boot | Java | 后端服务 | 简化配置、开箱即用,构建 REST API 快捷稳定,适合中大型企业级项目 | |
| Flutter | Dart | 跨平台 UI + 逻辑 | 主打前端,但具备一定后端逻辑处理能力,可实现客户端 + 服务一体的轻量应用 | |
| 游戏开发 | Unity | C# | 2D/3D 跨平台游戏 | 使用简单、社区丰富,适合从小游戏到商业项目,支持多平台部署(PC、移动、Web、主机) |
| Unreal Engine (UE) | C++ | 高端 3D 游戏 | AAA 游戏引擎,图形渲染能力强,支持蓝图和 C++ 混编,适合大型商业游戏开发 | |
| 嵌入式开发 | HAL | C | STM32的HAL库开发工程 | 嵌入式必备 |
重新认识IDE
据我所知,大家进入大学以来,肯定是懵懵懂懂地装上了十几个G的Visual Studio,但是不知道是要干什么………
在前面的学习中,你可能已经习惯了在命令行中用 gcc 编译代码、用 gdb 调试程序、用文本编辑器写代码……
但你有没有想过:有没有一种工具,可以把这些操作都集成起来,让开发体验更流畅?
答案就是:IDE(集成开发环境)。
IDE 全称是 Integrated Development Environment(集成开发环境),它是为程序员打造的“全能工具箱”,把代码编辑、编译构建、调试运行、项目管理等功能集成在一个界面中,大大提高开发效率。
以下是IDE包含的功能(前三个是核心):
| 功能模块 | 说明 |
|---|---|
| 代码编辑器 | 提供语法高亮、代码补全、错误提示、自动缩进等功能 |
| 构建系统 | 支持一键编译、链接、打包,集成如 Make、CMake、Gradle 等 |
| 调试器 | 可视化调试界面,支持断点、单步执行、变量观察、堆栈查看等 |
| 版本控制集成 | 集成 Git/SVN 等工具,可直接进行代码提交、拉取、查看改动 |
| 插件系统 | 支持扩展功能,如代码格式化器、AI 助手、数据库浏览器等 |
| 项目导航 | 清晰展示文件结构、类/函数列表,便于快速定位和跳转 |
下面是常见的IDE:
| IDE 名称 | 主要语言/平台 | 特点与适用场景 |
|---|---|---|
| Visual Studio | C/C++、C#、.NET | 微软官方出品,功能强大,适合 Windows 平台桌面、Web 和游戏开发 |
| Visual Studio Code | 多语言(轻量编辑器) | 虽不算传统 IDE,但通过插件能胜任前后端、Python、C++ 等多种开发任务 |
| CLion | C/C++ | JetBrains 出品,专为 C/C++ 打造,内建 CMake 支持,调试体验优秀 |
| IntelliJ IDEA | Java、Kotlin | JetBrains 旗舰产品,支持 Spring、Maven、Gradle,Java 开发首选 |
| PyCharm | Python | 专业 Python IDE,适合数据分析、Web 开发、机器学习项目 |
| Rider | C#/.NET | JetBrains 的跨平台 .NET IDE,适合用来替代 Visual Studio |
| Android Studio | Kotlin、Java | Google 官方 Android 开发 IDE,基于 IntelliJ IDEA 构建 |
| Xcode | Swift、Objective-C | 苹果官方 IDE,开发 iOS/macOS 应用必备 |
| Unity Editor | C# | 游戏开发专用 IDE,集成场景编辑器、脚本工具、调试器等 |
无论是哪个IDE,代码的构建和调试,其本质上都是通过执行命令行来完成对应操作。
*软件测试
我们常说:“程序能跑不代表它没问题。”
软件测试的目的就是找出那些“能跑但不对”的情况,确保产品上线后不会出问题。
软件测试 是对程序进行系统性的验证与检查,确保其功能、性能、界面等符合预期要求,避免 BUG 和异常情况流入生产环境。
那么如何去测试呢?大多数说的软件测试,指的是自动化测试。
这里总结单元测试的一个口诀:构建测试环境、执行测试单元、检查返回结果是否符合预期。限于篇幅,感兴趣的同学可以自行查询资料学习。
*一个完整的软件工程(B/S架构)
| 模块名称 | 职责说明 | 常见技术/工具 |
|---|---|---|
| 🖥 前端 | 提供用户界面,负责用户交互与页面展示 | HTML、CSS、JavaScript、Vue、React、Flutter |
| 🖧 后端 | 处理业务逻辑,接口编写,连接数据库,响应前端请求 | Java(SpringBoot)、Python(Django/Flask)、Node.js |
| 🗄 数据库 | 存储和管理数据,支持数据持久化与查询 | MySQL(关系型)、Redis(缓存)、MongoDB(文档型) |
下集预告
原本计划介绍一个Java Web项目的构建过程,但考虑到大家目前尚未学习Java,讲解过早可能导致理解困难。
鉴于大家已经在学习模拟电子电路并开始接触单片机,下节课我们将转向探讨单片机开发的相关内容。这也是工程开发领域的一个重要组成部分。
当然,为了简化讲解,同时由于计算机知识博大精深,部分内容存在一定的错误,请大家随时为我指出,大家一起共同成长,谢谢!
小结
高等数学与大学物理等基础理论课程的学习,为电路构建提供了坚实的理论支撑。
模拟电路的发展,使我们能够理解二极管、三极管以及各类逻辑门电路的底层实现原理。
数字电路构建了基本的与门、或门、非门等逻辑电路,赋予电路逻辑运算能力,奠定了现代计算机体系的硬件基础。
冯·诺依曼计算机架构的提出,为通用计算机系统的设计与执行提供了统一的框架,极大推动了计算技术的发展。
汇编语言的诞生,使得对寄存器的操作和中断处理更加直观和高效,拉近了人类与机器的沟通距离。
从C语言到C++,再到Java和Python,编程语言不断进化,从底层向高级抽象发展,大幅提升了开发效率与表达能力。
软件工程的兴起,使编程从单一源文件走向多模块协作,面向对象、封装、继承与多态等理念引领了新一代编程范式。
现代编程语言构建的各种开发框架,进一步简化了逻辑设计流程,使软件开发更加高效、系统化。
在你学习的时候,请记住,你的软件渲染展示的每一个按钮,你的游戏渲染的每一帧画面,你与电脑的每一次流畅交互,都建立在无数的工程之上,从一个小小的单品机点灯程序,到大语言模型服务的并行推理、分布式计算架构,正是每一个工程与模块的稳定运行,才造就了现在高速发展的计算机系统。
参考文献
拓展阅读:算法竞赛与入门经典,GDB、调用栈、可执行文件结构:P65-P79(纸质);编译器、调试器、IDE:P460-464
如何用C语言构建一个大型项目(c代码,gcc编译器gdb调试器及makefile常用知识的应用)_c语言项目怎么写-CSDN博客







