从代码到工程——我们的软件是如何诞生的
本文最后更新于272 天前,其中的信息可能已经过时,如有错误请发送邮件到blue16@email.swu.edu.cn

前言

之前我写过几篇关于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 实际上只是编译流程的前端接口,它会依次调用 cppcc1asld 等工具,分别完成从源代码到预处理程序、汇编代码、目标文件,最后生成可执行文件的整个过程。构建一个源代码文件为独立的应用程序,其详细流程如下:


步骤指令执行程序作用简记输入输出
预处理gcc -Ecpp.exe处理源代码中的宏定义、条件编译指令(带“#”的语句*)Expandsource.c(你写的源代码)source.i(也是个源代码)
编译**gcc -Scc1.exe将预处理后的代码编译成汇编语言代码Source to Assemblysource.isource.s(汇编代码)
汇编gcc -cas.exe将汇编语言代码汇编成目标文件Compile and Assemblesource.ssource.o(机器码)
链接gccld.exe将目标文件和其他必要的库文件链接在一起,生成最终的可执行文件source.osource.exe/.ELF

注:

*这里带“#”的语句包括的类型如下(也包括#include):

  1. 文件包含 (#include):预处理器会将指定的头文件内容插入到源代码中#include指令所在的位置。这允许你将代码分散在多个文件中,并根据需要包含其他文件的内容。
  2. 条件编译 (#if, #ifdef, #ifndef, #else, #elif, #endif):这些指令允许根据某些条件(如宏是否定义)有条件地编译代码部分。这对于编写跨平台代码特别有用,可以根据不同的系统特性选择性地包含特定代码段。
  3. 宏定义 (#define):除了简单的宏替换,还可以定义带参数的宏,它们可以像函数一样使用但会在编译前被替换为相应的代码片段。
  4. 取消宏定义 (#undef):这个指令用于取消先前定义的宏,使得之后该宏不再生效。
  5. 行控制 (#line):更改编译器内部记录的当前行号和文件名,主要用于调试或生成错误消息时提供更准确的信息。
  6. 错误生成 (#error):当预处理器遇到#error指令时,它会停止编译并输出一条错误消息。这通常用来强制检查某些编译时条件是否满足。
  7. 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文件)。
  • 清理规则 (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

解释一下:

  1. mkdir build && cd build:创建一个构建目录,干净又整洁;
  2. cmake ..:调用 CMake,读取上级目录的 CMakeLists.txt 文件,生成 Makefile 或其他构建脚本;
  3. 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++ 等编译型语言LinuxMakefile可执行文件(如 .out 或 .exe)Linux 自带的构建工具,用于编译和链接源代码
CMakeC/C++语言跨平台CMakeLists.txtMakefile, Visual Studio, Xcode 等构建成目标工程,跨平台配置和管理项目依赖
MavenJava跨平台pom.xml*.jar, *.war, *.ear 等构建 Java 工程,依赖管理和自动化构建过程
GradleJava跨平台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 <行号>设置断点
runr启动程序
nextn单步执行(不进入函数内部)
steps单步执行(会进入函数)
continuec继续运行直到下一个断点
print <变量>p打印变量的值
backtracebt打印调用栈
quitq退出调试

示例流程

假设我们在 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(前端)开发WPFC#桌面应用(Windows)属于 .NET 平台,支持丰富控件、数据绑定与动画,适合构建现代化 Windows 界面应用
.NET FrameworkC#桌面 & Web 应用微软全家桶平台,包含 WPF、WinForms、ASP.NET 等,用于开发 Windows 软件与服务
QTC++跨平台应用一种跨平台框架,被广泛用于各种展台
后端开发TomcatJavaWeb 服务容器Java Web 项目的运行平台,支持 JSP/Servlet,配合 Spring 系列框架更常用
Spring BootJava后端服务简化配置、开箱即用,构建 REST API 快捷稳定,适合中大型企业级项目
FlutterDart跨平台 UI + 逻辑主打前端,但具备一定后端逻辑处理能力,可实现客户端 + 服务一体的轻量应用
游戏开发UnityC#2D/3D 跨平台游戏使用简单、社区丰富,适合从小游戏到商业项目,支持多平台部署(PC、移动、Web、主机)
Unreal Engine (UE)C++高端 3D 游戏AAA 游戏引擎,图形渲染能力强,支持蓝图和 C++ 混编,适合大型商业游戏开发
嵌入式开发HALCSTM32的HAL库开发工程嵌入式必备

重新认识IDE

据我所知,大家进入大学以来,肯定是懵懵懂懂地装上了十几个G的Visual Studio,但是不知道是要干什么………

在前面的学习中,你可能已经习惯了在命令行中用 gcc 编译代码、用 gdb 调试程序、用文本编辑器写代码……
但你有没有想过:有没有一种工具,可以把这些操作都集成起来,让开发体验更流畅?

答案就是:IDE(集成开发环境)

IDE 全称是 Integrated Development Environment(集成开发环境),它是为程序员打造的“全能工具箱”,把代码编辑、编译构建、调试运行、项目管理等功能集成在一个界面中,大大提高开发效率。

以下是IDE包含的功能(前三个是核心):

功能模块说明
代码编辑器提供语法高亮、代码补全、错误提示、自动缩进等功能
构建系统支持一键编译、链接、打包,集成如 Make、CMake、Gradle 等
调试器可视化调试界面,支持断点、单步执行、变量观察、堆栈查看等
版本控制集成集成 Git/SVN 等工具,可直接进行代码提交、拉取、查看改动
插件系统支持扩展功能,如代码格式化器、AI 助手、数据库浏览器等
项目导航清晰展示文件结构、类/函数列表,便于快速定位和跳转

下面是常见的IDE:

IDE 名称主要语言/平台特点与适用场景
Visual StudioC/C++、C#、.NET微软官方出品,功能强大,适合 Windows 平台桌面、Web 和游戏开发
Visual Studio Code多语言(轻量编辑器)虽不算传统 IDE,但通过插件能胜任前后端、Python、C++ 等多种开发任务
CLionC/C++JetBrains 出品,专为 C/C++ 打造,内建 CMake 支持,调试体验优秀
IntelliJ IDEAJava、KotlinJetBrains 旗舰产品,支持 Spring、Maven、Gradle,Java 开发首选
PyCharmPython专业 Python IDE,适合数据分析、Web 开发、机器学习项目
RiderC#/.NETJetBrains 的跨平台 .NET IDE,适合用来替代 Visual Studio
Android StudioKotlin、JavaGoogle 官方 Android 开发 IDE,基于 IntelliJ IDEA 构建
XcodeSwift、Objective-C苹果官方 IDE,开发 iOS/macOS 应用必备
Unity EditorC#游戏开发专用 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++编译和链接原理 – 知乎

如何用C语言构建一个大型项目(c代码,gcc编译器gdb调试器及makefile常用知识的应用)_c语言项目怎么写-CSDN博客

CMake基础使用和实战详解 – 知乎

GCC与G++https://stackoverflow.com/a/172592/16953647

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇