Linux下的C语言系统小程序——进度条(附函数指针对代码解耦解析)
中的 缓冲区刷新函数,核心作用是 强制将标准 I/O 流(如 stdout、文件流)的缓冲区数据立即写入实际设备(终端、文件等),避免数据因缓冲区机制延迟输出 / 写入。函数指针的核心价值是「解耦代码、动态扩展」,适合需要灵活切换函数实现、隐藏底层细节、统一接口的场景 —— 这些场景下,函数指针能大幅降低代码修改成本,提升扩展性。Linux下一切皆文件,你的显示设备也可以理解是文件一种 属于IO流
文章目录
1.背景知识的补充
\r or \n
回车和换行是两个动作,只是在C语言中将\n解释成了回车换行 其实本质含义是换行 而回车是\r:让光标回到最开始。

这两段代码的执行效果是不同的,第一段代码是先刷出hello world等两秒后程序执行完毕。
而第二段代码确是两秒后才刷出hello world 众所周知C语言程序是顺序进行的 也就是说这两段代码都一定是先执行printf的 那第二个hello world呢?
“hello world”被缓存起来,存储于内存之中,当程序结束后,缓存器内部数据会自动刷新。
而缓存区的刷新,是按照行为单位的,\n or \r\n 就会自动以行为单位刷新
sleep&usleep
sleep 和 usleep 都是 延时 / 休眠命令,核心作用是让程序 / 脚本暂停指定时间 只不过前者默认单位为秒 后者为毫秒。头文件为unistd.h
( sleep man手册)
fflush
fflush 是 C 语言标准库(<stdio.h>)中的 缓冲区刷新函数,核心作用是 强制将标准 I/O 流(如 stdout、文件流)的缓冲区数据立即写入实际设备(终端、文件等),避免数据因缓冲区机制延迟输出 / 写入
Linux下一切皆文件,你的显示设备也可以理解是文件一种 属于IO流中的输出流,缓冲区是有归属的,很显然 这片缓冲区里的数据就归属于显示器(stdout)
练手 倒计时程序
当你往显示器输入数字 123 他不是以一百二十三的方式进行显示的 而是以 一二三字符的形式显示的,显示器输出的全部都是字符。
#include<stdio.h>
#include<unistd.h>
int main()
{
int cnt =10;
while(cnt)
{
printf("count is:%-2d\r",cnt);
fflush(stdout);
sleep(1);
cnt--;
}
return 0;
}
最终呈现出倒计时效果。
2. 进度条程序
2.1 Makefile的实现
关于make详细知识 请转跳
BIN=process
Cc=gcc
SRC=$(wildcard *.c)
OBJ=$(SRC:.c=.o)
Rm=rm -f
$(BIN):$(OBJ)
$(Cc) -o $@ $^
%.o:%.c
$(Cc) -c $<
.PHONY:clean
clean:
$(Rm) $(BIN) $(OBJ)
2.2 进度条的打印
这里的C语言代码很简单 小编不仔细讲了 把一些重要的地方加了注释。
#include "processbar.h"
#include<string.h>
#include<unistd.h>
#define SIZE 101
#define STYLE '='
void Process()
{
char processbuff[SIZE];
memset(processbuff,'\0',sizeof(processbuff));//初始化
int cnt=0;
while(cnt<=100)
{
/*
* 打印进度条核心逻辑:
* 1. \r:光标回到当前行首,实现覆盖式更新(不刷屏)
* 2. [%-100s]:格式化输出,%-100s 表示左对齐占 100 个字符宽度的字符串
* - 左对齐确保进度条从左到右填充
* - 100 个字符宽度对应 100% 进度(每个字符代表 1%)
* 3. processbuff:缓存的进度条内容(填充 STYLE 字符,未填充部分为 '\0',printf 会忽略)
*/
printf("[%-100s]\r",processbuff);
fflush(stdout);//强制刷新标准输出缓冲区:printf 无 \n 时默认缓存,fflush 确保实时显示进度
processbuff[cnt++]=STYLE;//延时 50 毫秒(50000 微秒):模拟任务执行耗时,控制进度条刷新速度(数值越小刷新越快)
usleep(50000);
}
printf("\n");// 进度完成后换行:避免后续终端输出与进度条同行覆盖
}
效果:
2.3进度条的数字进度和旋转光标
进度条光有=填充也不行啊 他得有进度啊 而且现实中的进度条 是不是还有个小圈圈在那里转圈圈
补充的代码块:
const char* lable ="|/-\\";// 动态旋转光标字符集:循环显示 '|' '/' '-' '\',模拟加载动画(\ 需转义为 \\)
int len = strlen(lable);// 计算旋转光标字符集长度(用于循环取模,实现光标循环切换)
//[%d%%]:显示当前进度百分比,%% 转义为单个 % 符号(如 cnt=50 时显示 50%)
// [%c]:显示动态旋转光标,通过 cnt%len 循环取 lable 中的字符(实现旋转效果)
printf("[%-100s] [%d%%][%c]\r",processbuff,cnt,lable[cnt%len]);
完整代码和注释:
#include "processbar.h"
#include<string.h>
#include<unistd.h>
#define SIZE 101
#define STYLE '='
void Process()
{
const char* lable ="|/-\\";// 动态旋转光标字符集:循环显示 '|' '/' '-' '\',模拟加载动画(\ 需转义为 \\)
int len = strlen(lable);// 计算旋转光标字符集长度(用于循环取模,实现光标循环切换)
char processbuff[SIZE];
memset(processbuff,'\0',sizeof(processbuff));//初始化
int cnt=0;
while(cnt<=100)
{
/*
* 打印进度条核心逻辑:
* 1. \r:光标回到当前行首,实现覆盖式更新(不刷屏)
* 2. [%-100s]:格式化输出,%-100s 表示左对齐占 100 个字符宽度的字符串
* - 左对齐确保进度条从左到右填充
* - 100 个字符宽度对应 100% 进度(每个字符代表 1%)
* 3. processbuff:缓存的进度条内容(填充 STYLE 字符,未填充部分为 '\0',printf 会忽略)
* 4. [%d%%]:显示当前进度百分比,%% 转义为单个 % 符号(如 cnt=50 时显示 50%)
* 5. [%c]:显示动态旋转光标,通过 cnt%len 循环取 lable 中的字符(实现旋转效果)
*/
*/
printf("[%-100s] [%d%%][%c]\r",processbuff,cnt,lable[cnt%len]);
fflush(stdout);//强制刷新标准输出缓冲区:printf 无 \n 时默认缓存,fflush 确保实时显示进度
processbuff[cnt++]=STYLE;//延时 50 毫秒(50000 微秒):模拟任务执行耗时,控制进度条刷新速度(数值越小刷新越快)
usleep(50000);
}
printf("\n");// 进度完成后换行:避免后续终端输出与进度条同行覆盖
}
效果:
2.4 进度条的场景模拟
在现实中,进度条肯定不是按照一样的速度在加载 他是根据我们的下载速度进行加载的 这里我们就要模拟现实中的加载情况
老样子 代码不难所以 依然以注释的方式讲代码呈现出来
文件:main.c
#include "processbar.h"
#include <unistd.h>
#include <string.h>
// 宏定义:进度条相关配置(与 FlashProcess 函数中 100 字符宽度匹配)
#define SIZE 101 // 进度条缓存数组大小:100个填充字符 + 1个字符串结束符 '\0'
#define STYLE '=' // 进度条填充字符(可改为 '#'、'█' 等,自定义视觉样式)
// 全局变量:模拟下载场景的核心参数(方便后续修改,无需改动函数逻辑)
double total = 1024.0; // 文件总大小(单位可自定义,如 KB/MB,仅用于进度计算)
double speed = 1.0; // 下载速度(单位与 total 一致,此处为 1.0 单位/次循环)
// 下载模拟函数:模拟文件下载过程,循环更新已下载大小并触发进度条刷新
void DownLoad()
{
double curr = 0.0; // 当前已下载大小(初始为 0,逐步累加至 total)
// 循环下载:直到已下载大小 >= 总大小,结束下载
while (curr <= total)
{
// 刷新进度条:传入总大小和当前已下载大小,实时更新进度显示
FlashProcess(total, curr);
// 模拟下载行为:已下载大小累加下载速度(每次循环推进一点进度)
curr += speed;
// 延时 30 毫秒(30000 微秒):控制下载进度更新频率,避免刷新过快
usleep(30000);
}
}
// 主函数:程序入口,仅调用下载模拟函数
int main()
{
DownLoad(); // 启动下载模拟(含进度条显示)
return 0;
}
***********************************************************************************
文件:processbar.c
// 进度条刷新函数:根据总大小和当前进度,动态生成并打印进度条
// 参数:total - 任务总大小(如文件总大小);curr - 当前已完成大小(如已下载大小)
void FlashProcess(double total, double curr)
{
// 边界处理:防止当前进度超出总大小(避免进度百分比超过 100%)
if (curr > total)
curr = total;
// 计算进度百分比:(已完成大小 / 总大小) * 100,保留小数精度(如 50.2%)
double rate = curr / total * 100;
// 进度条填充计数:将百分比转为整数(如 50.8% → 50,对应 50 个填充字符)
int cnt = (int)rate;
// 进度条缓存数组:存储已填充的进度字符,未填充部分用 '\0' 占位
char processbuff[SIZE];
// 初始化缓存数组:将所有元素设为 '\0'(字符串结束符),避免内存垃圾值干扰输出
memset(processbuff, '\0', sizeof(processbuff));
// 填充进度条:根据计数 cnt,在缓存数组中写入 cnt 个 STYLE 字符(如 '=')
for (int i = 0; i < cnt; i++)
processbuff[i] = STYLE;
// 动态旋转光标:循环显示 '|' '/' '-' '\',模拟加载动画(提升视觉体验)
static const char *lable = "|/-\\"; // static 确保只初始化一次,优化性能
static int index = 0; // 光标索引(static 保持状态,循环递增)
/* 格式化打印进度条:实现覆盖式实时更新 */
printf(
"[%-100s][%.1lf%%][%c]\r", // 格式说明:
processbuff, // 1. 进度条主体:左对齐(-)占 100 字符,填充 STYLE
rate, // 2. 进度百分比:保留 1 位小数(如 50.2%),%% 转义为 %
lable[index++] // 3. 动态光标:取当前索引对应的光标字符,索引后移
);
index %= strlen(lable); // 光标索引循环:取模字符集长度(4),实现 '|/-\' 循环切换
// 强制刷新标准输出缓冲区:
// printf 无 \n 时默认行缓冲,fflush 确保进度条即时更新,避免缓存导致的显示延迟
fflush(stdout);
// 进度完成处理:当已完成大小 >= 总大小时,换行(避免后续输出与进度条同行覆盖)
if (curr >= total)
{
printf("\n");
}
}
效果:
2.5 网络浮动模拟
正常的网络存在波动,这里我们就用随机数种子进行网络模拟
//start为网速基础值,range 为网速浮动
double SpeedFloat(double start,double range)
{
int int_range=(int)range;
return start + rand()%int_range + (range - int_range);
}
void DownLoad()
{
srand(time(NULL));//随机数种子
double curr=0.0;
while(curr<=total)
{
FlashProcess(total,curr);//更新进度,按照下载进度,进行更新进度条
curr+=SpeedFloat(speed,4.3);//模拟下载行为 修改部分
usleep(30000);
}
}
随机网速的实现原理:
目标:生成 [start, start+range] 区间内的随机数(如 start=1.0、range=4.3 时,网速 1.0~5.3);
拆分处理:rand() 只能生成整数随机数,因此将 range 拆分为 “整数部分 + 小数部分”,既保证整数浮动,又保留小数精度(如 4.3 拆为 4 + 0.3);
示例:当 rand()%4 生成 2 时,网速 = 1.0 + 2 + 0.3 = 3.3;生成 4 时(rand()%4 最大为 3),网速 = 1.0 + 3 + 0.3 = 4.3。
但是这么做会出现一种情况
他有的时候加载到99.几就突然停止加载了。
bug出现在这里
while(curr<=total)
curr+=SpeedFloat(speed,4.3);
有的时候生成的随机数加上原本的curr,导致curr大于了total 从而不进入循环,导致进度条无法到达100%。
修改:
void DownLoad()
{
srand(time(NULL));//随机数种子
double curr=0.0;
while(1)
{
if(curr >= total)
{
curr=total;
FlashProcess(total,curr);
break;
}
FlashProcess(total,curr);//更新进度,按照下载进度,进行更新进度条
curr+=SpeedFloat(speed,4.3);//模拟下载行为
usleep(30000);
}
}
将while循环条件始终为真,当curr>=total时候主动将curr赋值为total 然后主动进行刷新到100%,然后break跳出循环即可。
下载文件的大小模拟
你网速有快有慢 文件大小是不是也是有大有小
void DownLoad(int total)//加个函数参数
test
DownLoad(20.0);
DownLoad(200.0);
DownLoad(2000.0);
2.6 代码的解耦(函数指针)
函数指针的核心价值是「解耦代码、动态扩展」,适合需要灵活切换函数实现、隐藏底层细节、统一接口的场景 —— 这些场景下,函数指针能大幅降低代码修改成本,提升扩展性。
比如说:多实现切换(如进度条需要支持 “旋转光标”“百分比”“动画进度条” 等多种风格) 我们可以通过函数指针自动调用不同风格,调用处与具体函数解耦,依赖 “函数类型” 而非 “函数名”。
我们最终通过函数里面加函数指针的方式(回掉函数)
2.6.1函数指针的语法规则(结合示例)
核心概念:什么是函数指针?
- 函数在内存中会占据一块连续的存储空间,其入口地址(函数第一条指令的地址)就是函数指针指向的值。
- 函数指针的本质是“指针”,但专门指向函数(而非普通变量),其类型由函数的返回值类型和参数列表决定(与函数名无关)。
比如之前的 FlashProcess 函数,它的入口地址可以用一个函数指针存储,之后通过这个指针就能调用 FlashProcess。
1. 函数指针的定义格式
返回值类型 (*指针变量名)(参数类型1, 参数类型2, ...);
- 关键括号:
(*指针变量名)必须加括号,否则会被解析为“返回值为指针的函数”(而非函数指针)。 - 类型匹配:函数指针的返回值类型、参数类型/个数,必须与它指向的函数完全一致。
语法示例(结合 FlashProcess 函数)FlashProcess 函数原型如下(之前的代码):
void FlashProcess(double total, double curr); // 返回值void,2个double参数
对应的函数指针定义:
// 定义一个名为 pFlash 的函数指针,指向“返回值void、参数为(double, double)”的函数
void (*pFlash)(double, double);
2. 函数指针的赋值与调用
赋值:让函数指针指向目标函数
函数名本身就是函数的入口地址,因此赋值时直接用函数名(无需加 &,加了也可以,效果相同):
pFlash = FlashProcess; // 推荐:函数名即地址
// 或 pFlash = &FlashProcess; // 等价,&可省略
调用:通过函数指针调用函数
有两种调用方式,效果完全一致:
// 方式1:指针变量名 + 参数列表(最常用)
(*pFlash)(total, curr); // 等价于 FlashProcess(total, curr)
// 方式2:简化写法(编译器自动解析)
pFlash(total, curr); // 与方式1等价,更简洁
2.6.2 函数指针的常见误区
- 语法错误:忘记加括号
错误写法:
void *pFlash(double, double); // 错误:这是“返回值为void*的函数”,而非函数指针
正确写法:
void (*pFlash)(double, double); // 必须加 (*pFlash) 括号
- 类型不匹配
函数指针的返回值、参数类型/个数必须与目标函数完全一致,否则编译报错或运行异常:
// 错误示例:FlashProcess返回值是void,函数指针返回值是int
int (*pFlash)(double, double) = FlashProcess; // 编译报错:类型不兼容
- 未初始化函数指针
未赋值的函数指针是野指针,调用会导致程序崩溃:
void (*pFlash)(double, double); // 未初始化(野指针)
pFlash(1024.0, 512.0); // 危险:野指针调用,程序崩溃
- 解决:使用前必须赋值(指向合法函数)。
2.6.3 代码(函数指针的别名&&回调函数)
// 给“返回值void、参数(double, double)的函数指针类型”起别名 pFlash
typedef void (*pFlash)(double, double);
- 此时 FlashFunc 不再是 “函数指针变量”,而是 “函数指针类型” 的别名;
- 后续可像使用 int、double 等基础类型一样,用 FlashFunc 定义函数指针变量。
//pf:回调函数
void DownLoad(int total,pFlash pf)
{
srand(time(NULL));//随机数种子
double curr=0.0;
while(1)
{
if(curr >= total)
{
curr=total;
pf(total,curr);
break;
}
pf(total,curr);//更新进度,按照下载进度,进行更新进度条
curr+=SpeedFloat(speed,4.3);//模拟下载行为
usleep(30000);
}
}
int main()
{
DownLoad(20.0,FlashProcess);
DownLoad(200.0,FlashProcess);
DownLoad(2000.0,FlashProcess);
DownLoad(20000.0,FlashProcess);
return 0;
}
这里可以有FlashProcess 1 2 3 4 .....多种不同的进度条版本 从而造成函数的解耦。
3.成品代码和git引言
main.c:
#include"processbar.h"
#include<unistd.h>
#include<time.h>
#include<stdlib.h>
double gtotal=1024.0;//文件总大小
double speed=1.0;//下载速度
typedef void(*pFlash)(double,double);
//start为网速基础值,range 为网速浮动
double SpeedFloat(double start,double range)
{
int int_range=(int)range;
return start + rand()%int_range + (range - int_range);
}
//pf:回调函数
void DownLoad(int total,pFlash pf)
{
srand(time(NULL));//随机数种子
double curr=0.0;
while(1)
{
if(curr >= total)
{
curr=total;
pf(total,curr);
break;
}
pf(total,curr);//更新进度,按照下载进度,进行更新进度条
curr+=SpeedFloat(speed,4.3);//模拟下载行为
usleep(30000);
}
}
int main()
{
DownLoad(20.0,FlashProcess);
DownLoad(200.0,FlashProcess);
DownLoad(2000.0,FlashProcess);
DownLoad(20000.0,FlashProcess);
return 0;
}
processbar.h:
#pragma once
#include<stdio.h>
//version1
void Process();
void FlashProcess(double total,double curr);
processbar.c:
#include "processbar.h"
#include<string.h>
#include<unistd.h>
#define SIZE 101
#define STYLE '='
void FlashProcess(double total,double curr)
{
if(curr>total)
curr=total;
double rate=curr/total*100;//1024.0/512.0 -> 0.5*100=50.0
int cnt=(int)rate;//50.8, 49.9 -> 50,49
char processbuff[SIZE];
memset(processbuff,'\0',sizeof(processbuff));
for(int i=0;i<cnt;i++)
processbuff[i]=STYLE;
static const char *lable="|/-\\";
static int index=0;
//刷新
printf("[%-100s][%.1lf%%][%c]\r",processbuff,rate,lable[index++]);
index%=strlen(lable);
fflush(stdout);
if(curr>=total)
{
printf("\n");
}
}
void Process()
{
const char* lable ="|/-\\";// 动态旋转光标字符集:循环显示 '|' '/' '-' '\',模拟加载动画(\ 需转义为 \\)
int len = strlen(lable);// 计算旋转光标字符集长度(用于循环取模,实现光标循环切换)
char processbuff[SIZE];
memset(processbuff,'\0',sizeof(processbuff));//初始化
int cnt=0;
while(cnt<=100)
{
printf("[%-100s] [%d%%][%c]\r",processbuff,cnt,lable[cnt%len]);
fflush(stdout);//强制刷新标准输出缓冲区:printf 无 \n 时默认缓存,fflush 确保实时显示进度
processbuff[cnt++]=STYLE;//延时 50 毫秒(50000 微秒):模拟任务执行耗时,控制进度条刷新速度(数值越小刷新越快)
usleep(50000);
}
printf("\n");// 进度完成后换行:避免后续终端输出与进度条同行覆盖
}
当我们把代码写完是不是要上传码云,那么该怎么做? 请移步我写的git板块。
求三
更多推荐


所有评论(0)