Linux 第一个系统程序 - 进度条

发布于:2025-07-09 ⋅ 阅读:(21) ⋅ 点赞:(0)

目录

一、回车与换行概念

1、基本概念

2、历史背景

3、现代系统中的区别

二、行缓冲区实验引入

实验1:带换行符的输出

实验2:不带换行符的输出

三、标准I/O缓冲区机制

1、显示器刷新策略

2、相关概念

3、强制刷新缓冲区

fflush函数

实验3:手动刷新缓冲区

原理说明

四、倒计时程序

1. %-2d 格式化输出

2. \r 回车符

3. 为什么需要 %-2d?

五、进度条实现

文件结构

process.h(头文件)

process.c(进度条实现)

main.c(测试程序)

Makefile(构建文件)

功能说明


一、回车与换行概念

1、基本概念

  • 回车(Carriage Return): 将光标移动到当前行的行首,对应转义字符 \r

  • 换行(Line Feed): 将光标移动到下一行,对应转义字符 \n

2、历史背景

在老式打字机中:回车:将打印头移回行首        换行:将纸张向上移动一行

在老式键盘中的Enter键形象地表示了:

光标先换到下一行再回到行首位置,实际上就等价于\n+\r。

3、现代系统中的区别

  • Windows系统:使用 \r\n 表示换行

  • Unix/Linux系统:使用 \n 表示换行

  • Mac OS(早期):使用 \r 表示换行


二、行缓冲区实验引入

实验1:带换行符的输出

#include <stdio.h>
int main()
{
    printf("hello bite!\n");
    sleep(3);
    return 0;
}

现象:立即输出"hello bite!",然后等待3秒后程序结束

实验2:不带换行符的输出

#include <stdio.h>
int main()
{
    printf("hello bite!");
    sleep(3);
    return 0;
}

现象:先等待3秒,然后输出"hello bite!"并立即结束程序

        这段代码仅删除了字符串末尾的'\n',但运行结果与之前有所不同:程序会先休眠3秒,然后才打印"hello world"并结束运行。这一现象证实了行缓冲区的存在。

        显示器采用行刷新机制,只有当缓冲区遇到'\n'或被写满时才会输出内容。在修改后的代码中,由于缺少'\n',"hello world"首先被写入缓冲区。经过3秒休眠后,直到程序结束时才将该字符串输出到显示器上。

所以这里存在一个问题:我们如何在不使用'\n'的情况下将缓冲区中的数据输出?


三、标准I/O缓冲区机制

1、显示器刷新策略

  • 行缓冲(Line Buffering):标准输出(stdout)默认使用行缓冲模式

    • 遇到换行符\n时自动刷新

    • 缓冲区(在内存中)满时自动刷新

    • 程序正常结束时自动刷新

2、相关概念

  1. 标准流

    • stdin:标准输入(通常为键盘)

    • stdout:标准输出(通常为显示器)

    • stderr:标准错误(无缓冲,立即输出)

  2. 缓冲区类型

    • 全缓冲:文件I/O通常使用

    • 行缓冲:终端I/O通常使用

    • 无缓冲:如stderr

3、强制刷新缓冲区

fflush函数

功能:强制刷新指定流的输出缓冲区

        然后回到上面的问题中,我们想到的解决方法是通过调用fflush函数强制刷新缓冲区,将数据立即显示在屏幕上:

实验3:手动刷新缓冲区

#include <stdio.h>
int main()
{
    printf("hello bite!");
    fflush(stdout);  // 手动刷新输出缓冲区
    sleep(3);
    return 0;
}

现象:立即输出"hello bite!",然后等待3秒后程序结束

原理说明

  • 标准输出通常是行缓冲的,遇到\n或缓冲区满时会自动刷新

  • 使用fflush(stdout)可以强制刷新输出缓冲区


四、倒计时程序

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

int main()
{
    int i = 10;
    while(i >= 0)
    {
        printf("%-2d\r", i);  // 使用\r回车不换行
        fflush(stdout);       // 刷新输出缓冲区
        i--;
        sleep(1);             // 暂停1秒
    }
    printf("\n");            // 最后换行
    return 0;
}

对此上面的结论,如上,我们可以根据其内容编写一个倒计时的程序。

功能:实现从10到0的倒计时显示,在输出下一个数之前都让光标先回到本行行首,数字在同一位置更新,就得到了倒计时的效果。

1. %-2d 格式化输出

  • %d:以十进制形式输出整数 i

  • %2d至少占用 2 字符宽度,如果数字不足 2 位,左侧补空格(右对齐)。

    • 例如:

      • 10 → "10"(刚好 2 位,不变)

      • 5 → " 5"(左侧补 1 空格)

  • %-2d左对齐,不足 2 位时右侧补空格。

    • 例如:

      • 10 → "10"

      • 5 → "5 "(右侧补 1 空格)

2. \r 回车符

  • \r(Carriage Return):将光标移动到当前行的行首但不换行

  • 效果:下一次输出会覆盖当前行的内容(实现倒计时的动态更新)。

3. 为什么需要 %-2d

  • 如果直接用 %d\r

    • 当数字从 10 变为 9 时,"10" 会被 "9" 覆盖,但第二个字符 0 可能残留,显示为 "90"(错误)。

  • 使用 %-2d

    • 强制每个数字占 2 位,右侧补空格,确保完全覆盖前一次输出。

    • 例如:

      • 10 → "10"

      • 9 → "9 "(覆盖 "10",不会残留 0


五、进度条实现

文件结构

process.h(头文件)

#pragma once
#include <stdio.h>

// 简单进度条版本
void process_v1();

// 可配置进度条版本
void FlushProcess(double total, double current);

process.c(进度条实现)

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

#define NUM 101     // 进度条长度+1
#define STYLE '='    // 进度条填充字符

// 版本1: 简单进度条
void process_v1()
{
    char buffer[NUM];
    memset(buffer, 0, sizeof(buffer));
    const char *lable = "|/-\\";  // 旋转指示器
    int len = strlen(lable);
    int cnt = 0;
    
    while(cnt <= 100)
    {
        printf("[%-100s][%d%%][%c]\r", buffer, cnt, lable[cnt%len]);
        fflush(stdout);
        buffer[cnt] = STYLE;
        cnt++;
        usleep(50000);  // 50ms延迟
    }
    printf("\n");
}

// 版本2: 可配置进度条
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;
    cnt %= len;
    printf("[%-100s][%.1f%%][%c]\r", buffer, rate*100, lable[cnt]);
    cnt++;
    fflush(stdout);
}

当我们想make时,会发现下面的错误: 

如果我们选择把定义放外面的话,for会不美观,但能解决错误:

int i = 0;

for(; i < num; i++)
{
        buffer[i] = STYLE;
}

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);        // 3ms延迟
        current += speed;    // 增加下载量
    }
    printf("\ndownload %.2lfMB Done\n", current);
}

int main()
{
    // 模拟多次下载
    for(int i = 0; i < 8; i++)
    {
        DownLoad();
    }
    return 0;
}

Makefile(构建文件)

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

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

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

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

结合上面的错误,我们还可以再Makefile中修改,直接按照报错要求来: 

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

$(BIN): $(OBJ)
	gcc -o $@ $^ -std=c99  # 修正:添加 -std=c99

%.o: %.c
	gcc -c $< -std=c99      # 可选:如果其他 .c 文件也需要 C99

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

        但是我们按照要求来修改后,我们发现还是会出现有warning,但是能够成功构建可执行文件,我们不用管它: 

  1. 在某些系统(如较新的 Linux)中,usleep() 被标记为 废弃(obsolete),默认可能不暴露它的声明。

  2. 现代 Linux 推荐使用 nanosleep()usleep() 只是为了兼容性保留。

  3. usleep() 确实被调用了,但编译器认为它的声明是“隐式”的(即没有找到正式的函数原型)。

  4. 程序能运行,是因为在链接阶段,usleep() 的实现(在 libc 或 librt 中)被正确找到,但编译阶段仍然有警告。

功能说明

  1. process_v1():简单的100步进度条,自带循环

  2. FlushProcess():更灵活的进度条,根据传入的总量和当前值计算进度

  3. DownLoad():模拟下载过程,展示进度条的实际应用

进度条包含以下元素:

  • 进度条本身(用=填充)

  • 完成百分比

  • 旋转指示器(|/-\)表示程序正在运行


网站公告

今日签到

点亮在社区的每一天
去签到