
🫧 励志不掉头发的内向程序员 :个人主页
✨️ 个人专栏: 《C++语言》《Linux学习》
🌅偶尔悲伤,偶尔被幸福所完善
👓️博主简介:

文章目录
前言
我们已经了解了我们的 vim 编辑器,还明白了 gcc/g++ 编译器,同时了解了 Makefile 的自动化编译,本章节我们凭借这几个基本的开发工具来实现一个小程序,也是我们常见的进度条。我们一起来看看吧。

一、回车与换行
很多人可能认为回车与换行是一个概念,那就是跳转到下一行的开始,然后再去进行我们的操作,其实并不是这样的。
- 回车:回到这一行的开头。
- 换行:从这一行的当前位置换到下一行的当前位置。
所以我们之前理解的回车或者说换行其实本质上是 回车+换行。

在计算机语言中,换行符一般为 " \n ",回车符一般为 " \r "。我们往往用 " \r\n " 来表示回车 + 换行。
二、行缓冲区
缓冲区的内容我们在这里就不过多的去了解,等我们知识储备量多一点的时候再去具体说明,此时我们只要把它当成一个特殊一点的内存块即可。
我们先来看看这一串代码:
c
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("hello world!\n");
sleep(3);
return 0;
}
此时输出 hello world 的时候我们让程序休眠 3s。
注意:
sleep 函数存在 unistd.h 的头文件中。它的作用是让程序休眠一定时间,参数为要休眠的时间,单位为秒。
运行时可以看见,先打印 hello world 后等待 3s 程序退出。这很简单,没有必要去讲。此时我们把 " \n " 去掉的现象是什么呢?
我们发现当我们运行程序时,没有第一时间打印我们的内容,而是等待 3s 后程序停止时一起显示出来。看上去好像是先执行 sleep 再执行 printf,但是我们的常识告诉我们这不太可能。程序在执行时永远都是从前往后执行的。也就是说我们把 printf 执行完了,但是显示器上却没有显示内容。在计算机休眠的时候,hello world 在哪里呢?其实它们刚开始时数据是存储在计算机的缓冲区中的。之前有 " \n "就会先显示出来是因为我们 " \n " 是我们缓冲区的一个刷新条件,也叫行刷新。
我们刷新缓冲区的条件一共有两中:
- 行刷新:编译器在识别到 " \n " 时会刷新缓冲区。
- 程序退出:当我们程序退出时会自动刷新我们的缓冲区。
这就是为什么我们结束的时候 hello world 会和命令行一起出现。如果想要我们的缓冲区立刻刷新,我们可以使用 fflush 函数来让其立刻刷新。其实我们每次打开程序时都会默认打开 3 个文件,分别是 stdin、stdout、stderr, stdin 默认对应的是我们键盘文件,stdout、stderr 默认对应的是我们显示器文件。
注意:
fflush 函数的参数是你要刷新的文件,我们想要刷新显示器就是以 stdout 文件为参数去刷新。fflush 在 stdio.h 的头文件中。
c
int main()
{
printf("hello world!");
fflush(stdout);
sleep(3);
return 0;
}
此时我们再去运行就会发现 hello world 会先打印,然后程序再休眠。
三、倒计时
我们在实现进度条之前先来写一个倒计时的程序,通过这个倒计时进而去了解一些进度条需要用到的方法。
我们就来简单的写一个 10 ~ 1 的倒计时。
c
int main()
{
int i = 9;
while(i >= 0)
{
printf("%d\n", i);
i--;
}
return 0;
}
这个代码非常简单,没啥可说的。
powershell
zxl@iv-ye423qlwxsqc6ikwbogx:~/lesson1$ ./code
9
8
7
6
5
4
3
2
1
0
但是它输出出来是这样的一串,我们不希望输出一串,而是在同一个位置去打印。
我们可以发现,其实我们的输入的字符的位置在哪里本质上是跟着我的光标位置走的,我的光标哪里,我输入的字符也就会出现在那个位置,所以我们只需要让我们的光标在输入的时候位置一直不变,此时就能不停的覆盖我们之前输入的位置。这样不就实现了一个倒计时吗。此时我们用到上面的知识。只需要让我们的内容输入完,我们的光标回车即可,也就是 " \r "。
c
int main()
{
int i = 9;
while(i >= 0)
{
printf("%d\r", i);
sleep(1);
i--;
}
return 0;
}
此时运行时,由于没有满足缓冲区刷新条件,所以一直到程序结束都不会有数字出现。
手动刷新一下就行了。
c
printf("%d\r", i);
fflush(stdout);
sleep(1);
i--;

如果我们想要变成从 10 倒计时会怎样呢?
我们发现它从 10 变得 90,然后是 80...。这样打印肯定是错误的,我们解决方法也很简单,把我们输出的位宽改成 2d 即可。
c
printf("%2d\r", i);

如果想要让 9、8、7... 靠左边显示,再在前面加一个 " - " 即可。
c
printf("%-2d\r", i);

四、进度条
在有了我们倒计时的前置内容后,我们想要实现一个进度条也就变得简单了许多。
powershell
zxl@iv-ye423qlwxsqc6ikwbogx:~/lesson2$ touch process.c
zxl@iv-ye423qlwxsqc6ikwbogx:~/lesson2$ touch process.h
zxl@iv-ye423qlwxsqc6ikwbogx:~/lesson2$ touch main.c
zxl@iv-ye423qlwxsqc6ikwbogx:~/lesson2$ touch Makefile
main.c Makefile process.c process.h
#Makefile
SRC=$(shell ls *.c)
OBJ=$(SRC:.c=.o)
BIN=processbar
$(BIN):$(OBJ)
gcc -o $@ $^
%.o:%.c
gcc -c $<
.PHONY:clean
clean:
rm -f $(BIN) $(OBJ)
以上便是我们的准备工作,此时我们变开始写一个进度条。先给我们进度条声明。
c
// process.h
#pragma once
#include <stdio.h>
void process_v1();
// process.c
#include "process.h"
void process_v1()
{
//....
}
// main.c
#include "process.h"
int main()
{
process_v1();
return 0;
}
以上便是我们实现进度条的基本框架,此时我们即可以来实现一个进度条了。
我们进度条的样式:[########### ][ 50.0% ][ \ ]
我们得创建一个101大小的字符串用来存储我们的 100 个进度(#),最后一个是 " \0 " 的位置。
c
#define NUM 101
#define STYLE '#'
void process_v1()
{
char buffer[NUM];
memset(buffer, 0, sizeof(buffer));
int cnt = 0;
while(cnt <= 100)
{
printf("[%s]\r", buffer);
fflush(stdout);
buffer[cnt] = STYLE;
cnt++;
sleep(1);
}
printf("\n");
}
此时我们就实现了一个简易的进度条主体部分。达到的效果就是随着时间增加,我们的进度条长度也会增加。
当然,像这样增长的进度条看上去就不是很好看,我们可以给他预留出一定的空间,这样它只是进度在增长,边框就不会增长。
c
printf("[%-100s]\r", buffer);

当然,我们的进度条还有百分比,此时我们在 printf 后面继续输出即可。
c
printf("[%-100s][%d%%]\r", buffer, cnt);

我们在加载的时候会碰到各种情况,就比如说我们的进度不变。此时我们如果发现进度条一直没有变化,那到底是卡了还是没有只是加载慢呢?所以我们还得加一个光标。
c
#define NUM 101
#define STYLE '#'
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(65000);
}
printf("\n");
}
我们直接创建一个字符串,记录光标的四种形态,此时我们在运行时让我们的光标一直在这四种形态中循环即可。当然,这个进度条有点太慢了,所以这里用了一个 usleep() 函数,它和 sleep 的区别在于 sleep 的单位是秒,usleep 的单位是微秒。

进度条的实现就完成了,但是这个进度条是没法使用的,因为加入我们加入一个下载任务,这个进度条的加载没有什么意义。
一个进度条一定要结合具体的场景,边下载,边更新进度条才行。所以我们不能把进度条只写在一个文件中,而是要结合具体的情况来使用。
假设我们要下载一串数据,数据大小为 1024,网速为 1。
c
// main.c
double total = 1024.0;
double speed = 1.0;
void DownLoad()
{
double current = 0;
while(current <= total)
{
FlushProcess(total, current) // 刷新进度
// 下载代码
usleep(3000); // 充当下载数
current += speed;
}
printf("download %lfMB Done\n", current);
}
int main()
{
DownLoad();
return 0;
}
此时我们模拟了一个下载的任务,此时我们尝试运行就会发现,没有进度条,所以我们什么都不会显示出来。
此时我们得再去实现一个进度条 FiushProcess,这个进度条得有已下载数据的大小和数据大小两个参数,这样才能够实现进度条和下载的数据量同步的效果。
c
//process.h
void FlushProcess(double total, double current);
//process.c
void FlushProcess(double total, double current)
{
}
我们的进度条就没有必要去循环了,只需要对我们下载过来的数据进行计算百分比后,看看要输出多少进度即可。
c
void FlushProcess(double total, double current)
{
char buffer[NUM];
memset(buffer, 0, sizeof(buffer));
const char *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][%lf][%c]\r", buffer, rate * 100, lable[cnt]);
cnt++;
fflush(stdout);
}
我们的光标应该怎么设计呢,我们不能像之前一样设计,因为我们网络下载有可能下载的很慢,此时如果和之前一样用比值,就有可能会像卡着那样一直不变,所以我们可以写一个静态成员变量,使我们每次进入刷新进度条时都 ++,这样我们就能保证我们的光标一直在转了。
总结
以上便是我们实现的第一个小程序,进度条啦,虽然有很多新的知识点,但是都不是特别的难,主要就是熟悉熟悉我们的 vim 和 gcc/g++ 以及 Makefile 的使用。
🎇坚持到这里已经很厉害啦,辛苦啦🎇 ʕ • ᴥ • ʔ づ♡ど