文章目錄
- 編寫Linux下的第一個小程序——進度條
- 進度條的樣式
- 前置知識
- 回車和換行
- 緩沖區
- 對回車、換行、緩沖區、輸出的測試代碼
- 簡單的測試樣例
- 倒計時程序
- 進度條程序
- 理論版本
- 基本框架
- 代碼實現
- 真實版本
- 基礎框架
- 代碼實現
編寫Linux下的第一個小程序——進度條
在前面的基礎開發工具的學習中,我們學習了幾個比較重要的開發工具。
當然,對于在開發過程中常用的工具是gcc/g++、vim、make,有了這三個工具的基礎后,我們就可以嘗試著寫一個在Linux系統下開發的小項目了。
而本篇文章將講解在Linux系統下開發的小程序——進度條。
進度條的樣式
在這里,我們得明確進度條的樣式。有了進度條的樣式之后,我們才好根據我們要求的樣式進行相對應的程序包編寫。
進度條在命令行上的樣式大致就是這個樣子,但是對于進度條而言,需要特別注意的是:
1.旋轉光標應當一直在旋轉,以表明當前進度仍在進行(只不過某段時間內進度為0)
2.百分比應該動態的增大
3.進度條展示的位置應該是有東西不斷地再填充中間的空位,從肉眼上看的效果就是進度條再動態的前進,其實背后是顯示屏在不斷地刷新和字符不斷地填充輸出而已。
基于上述的原理,我們就可與依次來編寫對應的程序。
前置知識
但是,當前我們還得再補充一些基礎的知識,這是針對于IO流的輸入和輸出的問題。
回車和換行
首先,我們來重新了解一下回車和換行。
在我們以往的認知中,我們會很自然地認為”回車“和”換行“是同一件事情。其實不是的。
就以我們寫作文的作文格子來舉例,如上圖所示
單純的換行是把筆尖直接置到同一列的下一行的位置。
單純的回車是把筆尖置到這一行的第一個開頭位置。
回車換行先把筆尖置于開頭后再進行換行。
這也很清楚的說明,換行和回車其實不是一樣的操作。但是為什么在c/c++程序里面,打印‘\n’確實出現了回車 + 換行的操作呢?
這是因為編譯器自行解析了,編譯器在碰到換行符的時候,會解析成“回車 + 換行”。
在c/c++程序中,換行符是‘\n’,回車符是‘\r’
c/c++碰到‘\n’會自動解析成‘\r’ + ‘\n’。
緩沖區
這個概念其實早在c語言學習的時候就講到過了,但是我們這里還是再進行一次復習,以便后面順利編寫進度條的代碼。
我們來看看下面這一個代碼:
#include<stdio.h>
#include<unistd.h>int main(){printf("Hello World");sleep(3);return 0;
}
我們來看看這個代碼的輸出結果:
暫停了三秒鐘…
我們會發現,顯示器上會暫停三秒鐘,也就是在這三秒鐘內是沒有任何的輸出的。但是三秒后,Hello World就被輸出在了顯示器上,但是沒有換行效果。
可是我們學過c/c++程序的代碼執行順序,是從上往下執行的。也就是說,printf函數必然是比sleep函數先執行的。那么printf執行后,打印的內容放在哪里呢?答案就是緩沖區。
其實我們之前講過,Linux下一切皆為文件。即使是我們的顯示器(FILE* stdout
)。printf是先把內容輸出到緩沖區的。緩沖區再輸出到顯示器上是執行行刷新原則,即碰到換行符才會把內容給輸出到標準輸出流這個顯示器文件上。
上面那一段代碼就是先把Hello World寫入緩沖區,但是又沒碰到換行符。所以就沒有辦法先把字符串輸出到顯示器上。
但是后面能打印出來的原因是——程序結束后會自動刷新緩沖區,所以內容就會被輸出了。
–
當然,如果我們想要自行刷新緩沖區從而輸出字符串也是可以的,可以使用函數fflush,我們可以對stdout進行刷新(fflush(stdout)):
此時我們就可以發現,字符串先打印出來到顯示器了,然后再進行休眠。
對回車、換行、緩沖區、輸出的測試代碼
在寫進度條項目之前,我們還是需要進行一些前置準備——測試代碼。
我們上面僅僅是理論上講解了回車、換行、緩沖區的知識,這些其實和系統層面是有關系的。我們以前很多時候寫的程序是停留在語法層面上的。所以對系統層面上的一些內容不是很清楚。在這里我們需要進行相關功能的測試,結合對輸出的理解,這樣子后面寫進度條這個小項目的時候寫起來就快很多了。
簡單的測試樣例
我們下面看第一個測試代碼:
#include<stdio.h>
#include<unistd.h>
int main(){printf("%d\r", 1235456);fflush(stdout);sleep(3);printf("%s\n", "xx123456");return 0;
}
我們來看看這個代碼,最后的輸出效果是怎么樣的:
我們會發現,這里會先打印出123456這個數字,停留三秒后,由于回車符的作用,光標會重新放在開頭處,然后再次打印字符串”xx123456“的時候就會把原先打印的字符給覆蓋掉。
所以這里的輸出效果是,先在一行中打印數字123456,停留三秒后,打印字符串”xx123456“,覆蓋掉原來的123456,最后輸出結束。
這個代碼結合了回車、換行、緩沖區的知識,能夠更好地幫助我們理解這些內容。
倒計時程序
上面的測試代碼已經初步地了解和掌握了換行、回車、緩沖區的一些相關的用法。但是還有一些問題是我們在寫進度條的代碼的時候會遇見的。
這些問題其實在這個簡單地倒計時程序里就可以體現出來,接下來我們一起來看一下:
初版代碼:
#include<stdio.h>int main(){int i = 9;while(i >= 0){printf("%d\n", i);--i;}return 0;
}
這個代碼費城非常簡單,在這里就不多說了。就是輸出倒計時9 ~ 0,只不過是會換行輸出。這里寫這個代碼的是為了和后面的改進版本進行對比:
但是我們想要的倒計時不是這個樣子的,我們希望的是,每次屏幕上閃動動一個數字,一秒后在相同的位置變成下一秒的數字,那這個時候該怎么辦?
版本二:
這個不久正好用到了上個部分測試樣例中用到的原理嗎?我們使用回車符就可以了,然后每次控制打印的數字的顯示時間即可:
#include<stdio.h>
#include<unistd.h>int main(){int i = 9;while(i >= 0){printf("%d\r", i);sleep(1);--i;}return 0;
}
但是我們發現一個問題,就是不打印。這個其實就是沒有刷新緩沖區的原因:
添加了刷新緩沖區的代碼后,我們發現,打印完最后一個數字0后被命令行覆蓋掉了。因為命令行的前面這一串內容也可以看作是字符串,但是我們剛剛輸出的數字倒計時沒有進行換行,所以最后命令行字符串打印的時候直接覆蓋掉了,所以我們可以自行添加一個換行符:
#include<stdio.h>
#include<unistd.h>int main(){int i = 9;while(i >= 0){printf("%d\r", i);fflush(stdout);sleep(1);--i;}printf("\n");return 0;
}
上面的代碼看似沒有問題,但其實還有一些小問題沒有解決,比如我們把i改成10看看:
我們會發現,打印完10之后,剩下的數字都會打印在開頭的位置,但是末尾會有一個0出現。
這個原因其實很簡單,只是我們得明白一個原理:
所謂的打印,其實就是打印字符!
我們將123456打印在顯示器上,其實并不是把數字打印在上面,而是把123456字符打印在了顯示器上而已。所以 這里也是一樣的道理。10是兩個字符,但是0 ~ 9是一個字符,所以10正常輸出后,剩下的0 ~ 9因為回車符\r的原因,會在行開頭位置打印,但是因為占位是1個字符,所以后面那個0沒辦法被覆蓋掉。
解決辦法很簡單,改一下printf函數的打印格式就好了:
打印的時候在占位符前面加一個數字2,表示一次性打印的字符位寬為2(這個數字取決于倒計數里面最大的那個的數位),不足的就填充。但是這樣子我們會發現,個位數打印的時候左邊是空的,這非常不美觀。
所以這就用需要用到我們在printf函數中學到的“左對齊”打印了:
最終版本:
#include<stdio.h>
#include<unistd.h>int main(){int i = 10;while(i >= 0){printf("%-2d\r", i);fflush(stdout);sleep(1);--i;}printf("\n");return 0;
}
printf函數默認是右對齊的,所以字符不夠的時候前面會被填充。所以這里我們可以在數字的前面加一個-,這樣子就會左對齊了:
至此,我們就完成了倒計時程序的編寫。
同時,我們還進一步地用實踐來驗證了一我們前面所講的一些前置知識,有了這些前置知識,我們就可以更好地去編寫我們對應的進度條程序了。
進度條程序
這個部分我們將一起來編寫進度條的程序,就按照我們在前面提及的樣式進行編寫。
在這里我們會給出兩個版本:理論版本和真實版本。
理論版本就是我們現在只負責寫進度條的變動邏輯。兩個版本的差異呢?這是因為理論版本只寫了進度條的變動邏輯,但是進度條這個東西一般都是配合一些任務進行的,進度跳的邏輯應該是穿插在某些程序的內部的。
所以由此得知,理論版本的進度條其實是沒辦法運行的,就只能作為進度條的跳動的理論展示而已,所以我們會分開兩個版本進行編寫。
但是寫出來理論版本就好辦多了,因為真實版本也就是把進度條的邏輯嵌套在其他的代碼中。
理論版本
我們先來看理論版本的代碼。
基本框架
我們需要四個文件:
Process.h 包含進度條代碼需要用到的頭文件、變量、聲名進度條展示函數
Process.c 定義進度條展示的函數
main.c 對編寫的進度條代碼進行測試
Makefile 進行自動化編譯
對于Makefile的編寫,其實我們早就學習過了。
而且我們在Makefile的學習中,我們把對應的Makefile的形式改的更加通用,所以我們只需要修改一下文件中的部分文件名即可:
//Makefile
BIN=Process.exe
SRC=$(wildcard *.c)
OBJ=$(SRC:.c=.o);
FFLAG=-o
FLAG=-c
RM=rm -rf$(BIN):$(OBJ)gcc $(FFLAG) $@ $^
%.o:%.cgcc $(FLAG) $<.PHONY:clean
clean:$(RM) $(BIN) $(OBJ)
其實最后我們發現也就是修改了一下生成的可執行文件而已。
接下來是項目的主體框架:
//Process.h
#pragma once
#include<stdio.h>
#include<unistd.h>
#include<string.h>#define Num 101
#define STYLE '='void Process();//Process.c
#include"Process.h"void Process(){}//main.c
#include"Process.h"int main(){Process();return 0;
}
代碼實現
首先,我們得知道進度條的變動的原理,其實就是打印一個字符數組。那是如何做到進度條的前進的呢?
其實就是不斷地打印這個進度條,把上一次的進度給覆蓋掉,每次打印的進度條里面需要填充更多的符號以表示進度跳動而已。
我們在這里先看代碼,然后再詳細解釋:
void Process(){char buffer[Num];memset(buffer, 0, sizeof(buffer));int i = 0;char lable[] = "/-|\\";int length = strlen(lable);while(i <= 100){printf("[%-100s][%d%%][%c]\r", buffer, i, lable[i % length]);fflush(stdout);buffer[i] = STYLE;usleep(10000);++i;}printf("\n");
}
首先,我們確定了進度條的最大百分比是100%,所以需要100個空間打印字符(表示進度),這些字符是放在一個char數組內的,本質就是字符串。打印的時候需要碰到\0才會停止,所以存儲進度條字符的數組是需要101個空間的,所以Num的值為101。
而且我們把buffer這個數組中的所有值初始化為\0,這樣子后面我們就不需要再處理字符串的\0問題了。
在這個理論版本,我們就假設進度條每次進度+1%,所以打印百分比很簡單,就是打印我們定義的計數器i。
當然,我們還需要設置一個旋轉光標,以表示當前進度還在繼續,只不過是進度很慢而已,所以需要一直旋轉光標。光標的旋轉其實也是字符的快速打印,把中間間隙去掉,在我們的肉眼來看其實就是旋轉的效果,所以定義了一個label數組,里面就是光標旋轉的時候可能會有圖案。但是這里要注意的,由于 \ 這個字符本身在c/c++程序中有轉義字符的意思,所以要想打印出 \ ,需要對其再加一層 \ 的轉義。
對于光標的打印,其實就是不斷地打印label數組中的字符,但是不能越界訪問label數組,所以需要對坐標進行取模運算。
然后每次打印完進度條后,就應該改動進度條,以便下一次的打印。在這個理論版本中,就是不斷地往這個數組內部填充字符。
還有一個點就是,由于在printf函數中%是被賦予了特殊意義,所以只寫一個%是打印不出來的,要想打印出來%就需要寫兩個。
當然上面還有一些細節我沒有講到,但其實都是在前面的前置知識中有說過的。所以在這里就不再過多的贅述了。
最后,我們來看幾張效果圖:
這里就隨意的選取幾張圖看看,感興趣的可以自行拷貝實驗一下。
真實版本
前面是理論版本,也就是我們完成了進度條的動態操作。但是這個程序一般都是放在一些下載/上傳等場景下配合使用的。所以這個部分我們就來模擬一下下載場景。
基礎框架
還是一樣,我們需要準備四個文件:
ProcessBar.h 包含進度條代碼需要用到的頭文件、變量、聲名進度條展示函數
ProcessBar.c 定義進度條展示的函數
main.c 定義下載函數和其相關變量
Makefile 進行自動化編譯
有了前面的基礎,這里就不過多的廢話了:
//Makefile
BIN=Load.exe
SRC=$(wildcard *.c)
OBJ=$(SRC:.c=.o);
FFLAG=-o
FLAG=-c
RM=rm -rf$(BIN):$(OBJ)gcc $(FFLAG) $@ $^
%.o:%.cgcc $(FLAG) $<.PHONY:clean
clean:$(RM) $(BIN) $(OBJ)
//main.c
//使用隨機數來模擬每次下載的量 需要用到下面兩個頭文件
#include<stdlib.h>
#include<time.h>
#include"ProcessBar.h"
#define TOTAL 1024.0//總的下載量void Download(){}int main(){Download();return 0;
}//ProcessBar.h
#pragma once #include<unistd.h>
#include<string.h>
#include<stdio.h>
#define Num 101
#define STYLE '='static int lable_move = 0;//用于光標數組的坐標void Process(int cur, double total);//進度條函數//ProcessBar.c
#include"ProcessBar.h"void Process(int cur, double total){}
但是這里需要稍微解釋一下,這里的進度條打印函數相比理論版本是多了兩個參數的。即當前下載的量cur和總共要下載的量total。
因為當前這里的進度條函數是嵌套在下載函數Download當中的,但是進度條最終的是什么?是進度。進度是需要計算的。這里的進度條需要展示下載的進度,所以需要獲取到當前下載的量和總共要下載的量,以便能夠在內部進行計算。所以這里需要傳入兩個參數。
代碼實現
接下來我們將對代碼的實現進行講解。
首先我們得明白,在真實版本下的進度條,我們是不需要想理論版本那樣,去進行屏幕沉睡(sleep函數)的控制,也不需要我們自己去計算下一次的進度。因為這些事情都是下載函數Download去操作的。
換而言之:Process函數只負責接收當前的進度和總任務量,進行進度的計算,然后根據計算量去顯示進度條即可!
這里我們也是先直接展示代碼:
//main.c
#include<stdlib.h>
#include<time.h>
#include"ProcessBar.h"
#define TOTAL 1024.0void Download(){srand((unsigned int)time(NULL));int speed = rand()%10;//0 ~ 9int cur_load = 0;while(cur_load <= TOTAL){//打印進度條Process(cur_load, TOTAL);usleep(20000);//sleep(1);// if(cur_load == TOTAL){printf("\n");return;}//重新計算下載量cur_load += speed;if(cur_load > TOTAL) cur_load = TOTAL;speed = rand()%10;}
}int main(){Download();return 0;
}//ProcessBar.h
#pragma once #include<unistd.h>
#include<string.h>
#include<stdio.h>
#define Num 101
#define STYLE '='static int lable_move = 0;void Process(int cur, double total);//ProcessBar.c
#include"ProcessBar.h"void Process(int cur, double total){char buffer[Num];memset(buffer, 0, sizeof(buffer));char lable[] = "/|-\\";int length = strlen(lable);double Precent = (cur * 100 / total);int i = 0;for(i = 0; i < (int)(Precent); ++i){buffer[i] = STYLE;} printf("[%-100s][%.2lf%%][%c]\r", buffer, Precent, lable[lable_move]);fflush(stdout);++lable_move;lable_move %= length;
}
我們下面來解釋一下上述代碼的邏輯:
首先對于main.c來說,其實就是寫一個Download的下載函數,然后main函數中調用一下即可。但是這里我采用了隨機數的方式來獲取每次的下載量(speed),讓下載量有所不同。
但是這里就需要注意一個問題了,很有可能某一次打印完進度條后,再接收下載量可能就超過TOTAL了,這樣子下一次就不會進入循環了。但其實還是有最后一小部分的進度是沒有顯現出來的。所以當下載量speed大于剩余可以下載的量時候,就需要調整speed大小。然后為了能夠進入循環,所以循環條件內,下載量cur_load是需要可以等于TOTAL的。
但是又因為我們對下載量進行特殊處理,如果我們打印完進度條(Process函數)后,不進行判斷,就會導致死循環(因為剩余下載量0 <= speed)。所以這里的循環其實是內部進行退出的。不這么寫會導致死循環的。
然后就是退出循環的時候,需要打印一個換行符,這樣子才能使得進度條單獨成一行。
然后就是Process函數的處理,其實就是修改一下理論版本的進度條函數就可以了。
只不過就是我們需要自行的計算進度,然后把進度對應的字符數填入數組后再打印數組。
但是這里還有一點是不一樣的,就是光標的旋轉。前面寫理論版本的時候我是偷了一個懶,直接拿計數器取余i就獲得了label數組的下標了。
但其實這樣子不對,光標應該是有一個它自己的獨立的坐標數的,即使有時候進度條進度增加為0%,但是它也應該在轉,這表示當前進度仍在進行。如果寫成理論版本那樣就是進度增加為0%的時候光標是不轉的。只不過理論版本的計數器i一直在變,所以光標也會一直轉。
所以這里需要一個計數器,每次打印進度條就獲取對應位置中label數組的元素。但是這里是需要使用一個靜態變量的。因為靜態變量存儲在靜態區,每次++后是會被保存下來這個狀態的。如果是使用棧區上的局部變量,那每次進來都是重新初始化的,這不符合要求。然后再注意這個坐標不要越界即可。
最后,我們還是來看幾張效果圖:
這里也是隨機截的幾張圖,我們發現確實是我們想要的結果。
至此,我們所有的關于進度條的內容就講完了。