Linux 系统篇(八):开发工具 Chapter4 —— 手把手实现终端进度条,从回车换行到多文件工程化

在这里插入图片描述

观众老爷们大家好 这里是邪修KING的独家频道
本文属于系列Linux系统篇 ——操作指令
一起学Linux的小伙伴可订阅专栏: Linux系统篇

前面我们学习了 vim 编辑、gcc 编译、Makefile 构建,掌握了 Linux 下 C 程序开发的完整工具链。本篇我们用一个非常经典的实战项目「终端进度条」,把前面的知识全部串起来用一遍。
平时下载软件、安装包时,我们总能看到一行不断前进的进度条、百分比、还有转圈的加载动画。这么一个小小的进度条是怎么在终端里实现的?背后涉及两个非常重要的底层概念:回车与换行、行缓冲区。
本篇我们从底层原理讲起,一步步写出两个版本的进度条,最后做成标准的多文件工程,配合 Makefile 一键构建,带你完整体验从原理到工程落地的全过程。

一、前置知识一:回车与换行,傻傻分不清楚?

1.1 从老式打字机说起

「回车」和「换行」是两个从老式打字机时代延续下来的概念:
回车(Carriage Return):把打字的滑架(光标)推回到一行的最开头
换行(Line Feed):把纸张往上推一行,光标移到下一行
两个动作合在一起,才是我们平时说的「换到下一行开头」。

1.2 C 语言中的两个转义字符

对应到 C 语言里:

转义字符含义作用
\r回车光标回到当前行的行首,不换行
\n换行光标移到下一行,同时回到行首(现代系统一般自带回车效果)
关键区别:
\n 会换行,新内容出现在下一行
\r 不换行,新内容会覆盖当前行原来的内容
我们要实现「同一行不断更新的进度条」,核心靠的就是 \r:每次打印都回到行首,用新的进度覆盖旧的进度,视觉上就形成了动画效果。

二、前置知识二:行缓冲区 —— 为什么我的 printf 没有立刻输出?

2.1 一个反直觉的现象

先看三段代码,猜一猜各自的输出现象。
代码 1:带换行符

#include <stdio.h>
#include <unistd.h>

int main() {
    printf("hello bite!\n");
    sleep(3);
    return 0;
}

现象:先打印 hello bite!,然后程序休眠 3 秒,最后退出。符合直觉。
代码 2:不带换行符

#include <stdio.h>
#include <unistd.h>

int main() {
    printf("hello bite!");
    sleep(3);
    return 0;
}

现象:先休眠 3 秒,然后才打印 hello bite!,程序退出。和直觉完全相反。

2.2 行缓冲区机制

为什么会这样?因为 printf 输出的内容并不会直接写到显示器上,而是先放到行缓冲区里。
满足以下条件之一,缓冲区才会被「刷新」,内容才真正显示到屏幕上:

遇到换行符 \n
程序正常结束(main 返回)
缓冲区被写满了
手动强制刷新

代码 2 里没有 \n,程序又在 sleep,缓冲区没满也没结束,所以内容一直卡在缓冲区里看不到。等 sleep 完程序退出了,才一次性刷新出来。

2.3 手动刷新:fflush

我们可以用 fflush(stdout) 强制刷新标准输出缓冲区,让内容立刻显示。
代码 3:手动刷新

#include <stdio.h>
#include <unistd.h>

int main() {
    printf("hello bite!");
    fflush(stdout); // 强制刷新缓冲区,立刻显示
    sleep(3);
    return 0;
}

现象:先打印 hello bite!,再休眠 3 秒。和代码 1 效果一致。

进度条为什么需要 fflush?

进度条用 \r 回到行首覆盖打印,全程没有 \n。如果不手动刷新,内容会一直卡在缓冲区里,直到程序结束才一股脑显示出来,根本看不到动画效果。所以每次打印完进度都要 fflush(stdout) 一下。

三、热身练习:10 秒倒计时

搞懂了 \r 和 fflush,我们先做个简单的热身:在同一行显示倒计时,从 10 倒数到 0。
代码实现

#include <stdio.h>
#include <unistd.h>

int main() {
    int i = 10;
    while (i >= 0) {
        // \r 回到行首,覆盖打印;%-2d 左对齐占2位,防止数字变短后残留字符
        printf("%-2d\r", i);
        fflush(stdout);  // 强制刷新,立刻显示
        i--;
        sleep(1);        // 每秒减1
    }
    printf("\n"); // 结束后换个行,别让提示符粘在同一行
    return 0;
}

几个细节说明
为什么用 %-2d 左对齐?
数字从 10(两位)变成 9(一位)时,如果不占固定宽度,行尾会残留上一次的 0,变成 90。左对齐占两位,短数字补空格,就不会有残留。
为什么最后加 printf(“\n”)?
程序结束时光标还在进度条那一行,shell 提示符会直接接在后面,非常难看。最后打印一个换行,让提示符出现在下一行。
四、进度条 Version1:基础完整版
热身完毕,我们正式写第一个完整进度条。

4.1 实现思路

准备一个字符数组当「进度条本体」,初始全是 \0
循环从 0 到 100,每次往数组里填一个 = 字符
用 \r 回到行首,重新打印整条进度条、百分比、旋转光标
每次打印后 fflush 刷新,休眠一小段时间
循环结束后换行

4.2 旋转光标是怎么实现的?

很简单,用一个字符数组存四个状态 | / - \,然后用计数器对 4 取模,依次循环取字符,视觉上就是在转圈。

4.3 完整代码

// process.c  v1版本
#include "process.h"
#include <string.h>
#include <unistd.h>

#define NUM 101     // 进度条总长度+1,留位置放'\0'
#define STYLE '='   // 进度条填充字符

void process_v1() {
    char buffer[NUM];
    memset(buffer, 0, sizeof(buffer)); // 初始化为全0
    
    const char* lable = "|/-\\";  // 旋转光标的四个状态
    int len = strlen(lable);

    int cnt = 0;
    while (cnt <= 100) {
        // [%-100s] 左对齐占100位的进度条
        // [%d%%] 百分比,两个%输出一个%
        // [%c] 旋转光标
        // \r 回到行首覆盖
        printf("[%-100s][%d%%][%c]\r", buffer, cnt, lable[cnt % len]);
        fflush(stdout);

        buffer[cnt] = STYLE; // 进度前进一格
        cnt++;
        usleep(50000);       // 休眠50毫秒,控制速度
    }

    printf("\n"); // 结束换行
}

五、进度条 Version2:工程实用升级版

5.1 v1 有什么问题?

v1 版本是自己在函数内部循环、自己控制速度,只能用来演示动画,没法用在真实项目里。
真实场景是什么样的?
比如下载文件:下载逻辑自己在跑,下了多少字节是业务逻辑算出来的。进度条函数只负责「你告诉我当前下了多少、总共多少,我帮你渲染成进度条」。

5.2 v2 升级点

参数化:函数接收 total(总量)和 current(当前量),由调用方控制进度
小数百分比:支持显示一位小数,更精确
状态保持:旋转光标的计数用 static 静态变量,每次调用自动往后走一格
解耦:渲染和业务逻辑分离,任何需要进度展示的场景都能调用

5.3 完整代码

// process.c  v2版本
void FlushProcess(double total, double current) {
    char buffer[NUM];
    memset(buffer, 0, sizeof(buffer));
    
    const char* lable = "|/-\\";
    int len = strlen(lable);
    static int cnt = 0; // 静态变量,记录旋转光标位置,函数调用间保持值

    // 计算当前进度对应的格数
    int num = (int)(current * 100 / total);
    
    // 填充进度条
    for (int i = 0; i < num; i++) {
        buffer[i] = STYLE;
    }

    // 计算百分比,保留一位小数
    double rate = current / total;

    // 打印:进度条 + 百分比 + 旋转光标
    printf("[%-100s][%.1f%%][%c]\r", buffer, rate * 100, lable[cnt % len]);
    fflush(stdout);

    cnt++;
}

5.4 真实场景模拟:下载进度

我们写一个模拟下载的函数,循环调用 v2 版本进度条:

// main.c
#include "process.h"
#include <stdio.h>
#include <unistd.h>

double total = 1024.0; // 总大小 1024MB
double speed = 1.0;    // 下载速度 1MB/次

void DownLoad() {
    double current = 0;
    while (current <= total) {
        FlushProcess(total, current); // 更新进度条
        usleep(3000);  // 模拟下载耗时
        current += speed;
    }
    printf("\ndownload %.2fMB Done\n", current);
}

int main() {
    DownLoad();
    DownLoad();
    DownLoad();
    return 0;
}

每次调用 DownLoad 都会跑一遍完整的下载进度,非常接近真实软件下载的效果。

六、多文件工程组织

我们把项目拆成标准的三分结构:头文件 + 实现文件 + 主函数,配合 Makefile 一键构建。

6.1 头文件 process.h

放函数声明,防止重复包含。

#pragma once

#include <stdio.h>

// v1 演示版:自动循环播放进度条
void process_v1();

// v2 实用版:传入总量和当前量,由调用方控制
void FlushProcess(double total, double current);

6.2 实现文件 process.c

放两个函数的具体实现,前面已经写过完整代码。

#include "process.h"
#include <string.h>
#include <unistd.h>

#define NUM 101
#define STYLE '='

void process_v1() {
    // ... v1 实现代码 ...
}

void FlushProcess(double total, double current) {
    // ... v2 实现代码 ...
}

6.3 主函数 main.c

程序入口,调用演示。

#include "process.h"
#include <stdio.h>
#include <unistd.h>

double total = 1024.0;
double speed = 1.0;

void DownLoad() {
    double current = 0;
    while (current <= total) {
        FlushProcess(total, current);
        usleep(3000);
        current += speed;
    }
    printf("\ndownload %.2fMB Done\n", current);
}

int main() {
    // 可以先跑 v1 演示
    // process_v1();

    // 再跑 v2 真实场景
    DownLoad();
    DownLoad();
    DownLoad();
    return 0;
}

6.4 Makefile

自动化构建,一个 make 命令全部搞定。

SRC=$(wildcard *.c)
OBJ=$(SRC:.c=.o)
BIN=processbar

$(BIN):$(OBJ)
	gcc -o $@ $^

%.o:%.c
	gcc -c $<

.PHONY: clean
clean:
	rm -f $(OBJ) $(BIN)

6.5 编译运行

# 编译
make

# 运行
./processbar

运行效果:终端同一行上,进度条不断前进,百分比不断增加,右侧光标不断旋转,完成一行后提示下载完成,继续下一个。

七、本篇总结

一个看似简单的终端进度条,背后藏着不少底层知识:

\r 回车:回到行首覆盖打印,是实现单行动画的核心
行缓冲区:没有 \n 时内容不会立刻显示,需要 fflush(stdout) 强制刷新
格式化输出:左对齐固定宽度、百分号转义、百分比计算
工程化思想:接口与实现分离,头文件放声明、源文件放实现,主函数调用
版本迭代:从演示版到实用版,参数化、解耦,让代码能融入真实业务

到这里,Linux 基础开发工具的内容就全部落地了。从 vim 写代码、gcc 编译四阶段、Makefile 自动化构建,到今天的完整项目实战,整套开发流程你已经全部跑通了。
在这里插入图片描述

评论 41
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值