Makefile_进度条(Git)

从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)
  • 变量binsrc 就是变量,用 $(变量名) 来提取内容,类似于 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 版本控制:从手动保存到分布式协作

老师布置实验报告,你舍友交上去被要求反复修改,改到最后连第二次的版本都找不到了。而你每次修改前都拷贝一份:V1V2V3……老师突然想要 V2,你立刻就能拿出来。这就是最原始的版本控制

如果把这个能力做成一个工具:

  • 给每个人分配一个“仓库”,可以提交、删除、查看历史版本。
  • 提供网络功能,让不同地方的人也能访问。
  • 做一个可视化网站,方便网页查看代码。

这个小工具就叫 Git,而网站就像 GitHubGitee

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 addgit commitgit 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 initgit 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 支持这种并行的开发模式,以后遇到复杂协作时再学不迟。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值