从Makefile到进度条:Linux开发工具与项目实战
在 Linux 环境下写 C 语言程序,除了掌握语言本身,还需要熟悉一系列开发工具。这篇文章将带你了解 make/makefile 构建工具、缓冲区与回车换行的概念,并最终完成一个命令行彩色进度条的项目实战,还会简单聊聊版本控制工具 Git 。
1. 认识 make 与 makefile
1.1 make 和 makefile 是什么?
在 Linux 中,make 是一个命令,而 Makefile(或 makefile)是一个文件。make 会根据 makefile 中定义的内容,自动完成编译、清理等工作。
一个最简单的 makefile 可能是这样:
mytest: test.c
gcc -o mytest test.c
.PHONY: clean
clean:
rm -f mytest
这里解释几个核心概念:
- 目标文件:冒号左边的
mytest,是我们希望生成的文件。 - 依赖文件列表:冒号右边的
test.c,以空格分隔。可以有多个,比如test.c test1.c test2.c。 - 依赖关系:目标和依赖之间的映射关系。
- 依赖方法:下一行缩进(必须是 Tab)的
gcc -o mytest test.c,表示如何从依赖生成目标。
make 默认会形成第一个目标文件。所以当我们直接执行 make 时,它就会尝试构建 mytest。
1.2 .PHONY 伪目标
.PHONY: clean 声明 clean 是一个伪目标,并不是真的要生成一个名为 clean 的文件。伪目标对应的方法,总是会被执行。如果没有 .PHONY,当恰好有一个文件叫 clean 时,make clean 会因为目标已存在而拒绝执行。
1.3 make 如何判断是否需要重新编译?
问题:为什么 make 对最新的可执行程序默认不想重新形成?
答案:通过比较文件修改时间。
make 会对比可执行文件的最近修改时间和源文件的最近修改时间,谁更新。
- 如果可执行文件比源文件新,说明源码没改过,不需要重新编译。
- 如果源文件比可执行文件新,说明源码更新了,需要重新编译生成新的可执行文件。
这就是 make 能够实现“增量编译”的原理,也是大型项目构建速度的重要保障。
1.4 自动推导与变量
看一个稍微复杂点的 makefile:
code.exe: code.o
gcc code.o -o code.exe
code.o: code.s
gcc -c code.s -o code.o
code.s: code.i
gcc -S code.i -o code.s
code.i: code.c
gcc -E code.c -o code.i
虽然代码是从上往下写的,但执行时却会从最后的 code.c 开始,一步步向上推导:因为一开始没有 code.o,make 会继续向后找依赖关系,直到找到实际存在的 code.c,再一层层往上构建。
make/makefile 会根据依赖关系自动推导,帮助我们执行所有相关的依赖方法,类似于递归。 甚至乱序写也没问题,不过一般要求最终目标放在第一个。
日常使用我们不需要写这么复杂,为了更方便,makefile 还提供了变量和自动变量:
bin = code.exe
src = code.c
$(bin): $(src)
gcc -o $@ $^
.PHONY: clean
clean:
rm -f $(bin)
- 变量:
bin、src就是变量,用$(变量名)来提取内容,类似于 C 语言的宏。 - 自动变量:
$@代表目标文件$^代表所有依赖文件
这样如果以后要改文件名,只需要修改变量定义即可,不用一个一个去改。
1.5 抑制命令回显
默认 make 会打印正在执行的命令。如果在命令前加上 @,就不会显示命令本身,只显示命令的结果:
$(bin): $(src)
@gcc -o $@ $^
@echo "compiled $(src) to $(bin)..."
这样输出更干净,也可以自定义提示信息。
2. 缓冲区与回车的秘密
2.1 回车和换行
- 换行
\n:光标移动到下一行开头(在 Linux 中\n包含换行+回车)。 - 回车
\r:光标回到当前行的最左端。
老式打字机需要手动把滚筒推回到最右边,这就是“回车”动作。
当有\r时,\n就只表示换行。
2.2 缓冲区的存在
观察一段代码:
#include <stdio.h>
#include <unistd.h>
int main() {
printf("hello bit, hello world...");
sleep(3);
return 0;
}
如果 printf 没有 \n,你会发现先等 3 秒,然后才看到字符串。加了 \n 后,字符串会立刻显示,然后程序再等 3 秒结束。
sleep 期间打印的字符去哪了?——它们暂存在“缓冲区”里。
缓冲区是一段内存空间,用于暂时存放数据。程序结束时自动冲刷缓冲区,遇到 \n 也会立刻冲刷(但只冲刷当前内容)。
强制刷新缓冲区:使用 fflush(stdout)。
Linux 一切皆文件,显示器也是文件(stdout)。fflush(FILE *stream) 可以将缓冲区的数据强制写到对应的文件(设备)中。
显示器为什么要行刷新?
缓冲区是为了提高效率,但显示器是给人看的,如果一行半天不刷新,人会着急。所以显示器通常采用“行刷新”策略,在效率与用户体验之间取得平衡。
一个小知识:Linux 下输入密码不回显,是因为没有把 stdin 的数据拷贝显示到 stdout,如果拷贝了就可见了。
3. 进度条项目实战
有了上述知识,就能实现一个命令行进度条了。
3.1 简单倒计时
利用 \r 回车,可以在同一行不断覆盖刷新:
#include <stdio.h>
#include <unistd.h>
int main() {
int i = 10;
while (i >= 0) {
printf("%2d\r", i); // %2d 保证两位数宽度,避免残留字符
fflush(stdout); // 强制刷新
sleep(1);
i--;
}
printf("\n");
return 0;
}
不加 fflush(stdout),数据留在缓冲区,显示器不会有任何变化;不加 %2d,数字位数变化时会发生覆盖不完整。
3.2 第一版进度条
加上旋转光标和百分比显示:
const char *label = "|/-\\";
#define Style '#'
#define Length 101
void ProcBar() {
char bar[Length];
memset(bar, '\0', sizeof(bar));
int len = strlen(label);
int cnt = 0;
while (cnt <= 100) {
printf("[%-100s][%d%%][%c]\r", bar, cnt, label[cnt % len]);
fflush(stdout);
bar[cnt++] = Style;
usleep(20000); // 微秒,20000us = 20ms
}
printf("\n");
}
这里用 %-100s 让进度条左对齐,%d%% 输出百分号,label[] 循环产生旋转效果。
3.3 与下载场景结合
进度条通常需要反映实际进度,把它设计成可接受总大小和当前值的函数:
void ProcBar(double total, double current) {
char bar[Length];
memset(bar, '\0', sizeof(bar));
int len = strlen(label);
double rate = (current * 100.0) / total;
int loop_count = (int)rate;
int i = 0;
for (; i < loop_count; i++) {
bar[i] = Style;
}
printf("[%-100s][%.1f%%][%c]\r", bar, rate, label[i % len]);
fflush(stdout);
}
在下载函数中使用:
void download() {
double filesize = 100 * 1024 * 1024 * 1.0; // 100MB
double current = 0.0;
double bandwidth = 1024 * 1024 * 1.0; // 1MB/s
printf("download begin, filesize: %.1lf\n", filesize);
while (current <= filesize) {
ProcBar(filesize, current);
current += bandwidth;
sleep(1);
}
printf("\ndownload done.\n");
}
这样就能看到每秒钟进度条的一次更新,进度随下载量动态增长。
3.4 使用函数指针提高灵活性
如果想对不同下载任务使用不同进度条样式,可以传递函数指针:
typedef void (*callback_t)(double, double);
void download(callback_t cb) {
double filesize = 100 * 1024 * 1024 * 1.0;
double current = 0.0;
double bandwidth = 1024 * 1024 * 1.0;
printf("download begin, current: %lf\n", current);
while (current <= filesize) {
cb(filesize, current);
current += bandwidth;
sleep(1);
}
printf("\ndownload done.\n");
}
// 调用
download(ProcBar);
更进一步,我们还可以给进度条加上彩色输出、色块等,做出更好看的效果。
4. Git 版本控制:从手动保存到分布式协作
老师布置实验报告,你舍友交上去被要求反复修改,改到最后连第二次的版本都找不到了。而你每次修改前都拷贝一份:V1、V2、V3……老师突然想要 V2,你立刻就能拿出来。这就是最原始的版本控制。
如果把这个能力做成一个工具:
- 给每个人分配一个“仓库”,可以提交、删除、查看历史版本。
- 提供网络功能,让不同地方的人也能访问。
- 做一个可视化网站,方便网页查看代码。
这个小工具就叫 Git,而网站就像 GitHub 或 Gitee。
Git 的作用:对源代码的历史修改进行维护,保留历史的所有修改痕迹。
它的作者正是 Linux 之父 Linus Torvalds,因为当时很多版本控制工具需要收费,Linus 就自己写了一个,用来管理 Linux 内核的开发。
有了 Git,你可以:
- 查看每次改了什么
- 版本回退到任意历史状态
- 多分支并行开发
- 多人协作,合并代码
安装与首次配置
在 Linux(如 Ubuntu)上通过包管理器即可安装:
sudo apt install git
git --version # 查看是否安装成功
安装后必须配置用户名和邮箱,它们会写入每一次提交记录,用来溯源是谁做了什么修改:
git config --global user.name "Your Name"
git config --global user.email "your_email@example.com"
注意:这里的用户名和邮箱建议与 Gitee/GitHub 账号保持一致,这样在平台上才能正确显示提交头像和“小绿点”。
Git 的三段式提交流程(三板斧)
Git 的核心工作流可以概括为“三板斧”:git add → git commit → git push。理解这个过程的关键是搞清楚三个区域:工作区、暂存区、本地仓库,以及最后的远程仓库。
工作区(写代码) ──git add──▶ 暂存区 ──git commit──▶ 本地仓库 ──git push──▶ 远程仓库
1. git add —— 将修改加入暂存区
当你在文件夹里增、删、改了文件后,这些变动最初只发生在工作区。执行:
git add . # 添加当前目录所有变化
# 或
git add 文件名
这些变动就被提交到了暂存区(也称为索引)。暂存区相当于一个“购物车”,你可以在正式结算前自由选择哪些修改需要提交。
git status 可以随时查看当前状态:文件是否处于暂存区,是否有新文件未被跟踪等。
2. git commit —— 确认提交到本地仓库
暂存区的修改确认无误后,用 commit 提交到本地仓库:
git commit -m "本次提交的日志说明"
-m 后的字符串就是提交日志,一定要认真写清楚这次改了什么,方便以后回溯。此时修改已经进入本地仓库,但远端(GitHub/Gitee)还不会收到任何更新。
3. git push —— 推送到远程仓库
想把本地仓库的内容同步到远端,让队友看到,就需要:
git push
至此,你的代码在远端可见了。真正意义上的“协作”由此开始。
如果只想本地做版本管理,完全可以在
commit后就享受所有历史记录,不需要push—— 只是别人看不到你的仓库罢了。
理解隐藏文件夹 .git 与 .gitignore
执行 git init 或 git clone 后,项目目录下会生成一个隐藏文件夹 .git。它就是 Git 的本地仓库实体,存放了所有版本历史、暂存区内容、配置信息…… 非特殊情况下不要手动修改这个文件夹,否则仓库可能损坏。
还有一个重要的文件:.gitignore。它的作用是指定哪些文件忽略不跟踪。原因很简单:仓库通常只管理源代码,像编译中间文件、可执行程序、临时文件、日志等不应该上传。例如,在 .gitignore 中添加一行 *.txt,那么所有以 .txt 结尾的文件就不会被 Git 纳入版本管理。忽略规则可以按后缀、目录名、单文件灵活配置。
查看历史与版本回退
git log 可以查看所有的提交记录,包含作者、日期、邮箱和日志内容。如果某段代码出了问题,可以快速定位到是谁、在什么时间引入的。
想回到过去的某个版本?可以使用:
git reset --hard <commit_id>
这将把工作区恢复为指定提交时的状态。需要注意的是,reset 后的历史并不会真正消失,只是在提交链上移动指针,之前的 commit 依然可通过 reflog 找回。
此外,git rm 可以删除文件,但删除动作也会作为一条提交记录被保存下来,历史永远不会丢失(除非你刻意清理),这就是“去中心化版本控制器”的魅力——每个克隆都有自己的完整历史,即使远程仓库被删,本地依然保有全部记录。
分支管理的简单概念
实际项目中,常常会有多分支协作。通常:
- master / main 分支:稳定的、可发布的主分支。
- develop / feature 分支:开发新功能或修复 bug 的分支,完成后合并回主分支。
小公司或个人项目可能一个 master 分支就足够,但随着团队规模增大,合理的分支策略能避免互相干扰,保证主线随时可部署。
暂时不需要深入分支操作,只要知道 Git 支持这种并行的开发模式,以后遇到复杂协作时再学不迟。
&spm=1001.2101.3001.5002&articleId=160983699&d=1&t=3&u=c50e9e3550e64d1e9ac137e82da0b468)
726

被折叠的 条评论
为什么被折叠?



