題外話

前言
1.本文主要圍繞 如何 在 控制台上 下起 一場 只有自己能看見的雪
2.是個簡易跨平台的,主要是C語言
3.動畫 采用 1s 40幀, 雪花具有 x軸速度和y軸速度
4.比較簡單,可以給學生作為C語言結課作業吧.
正文
1.1 先簡單處理跨平台
本文寫作動機,還是感謝一下大學的啟蒙老師,讓我知道了有條路叫做程序員,可以作為工作生存下去.那就上代碼了.
首先代碼定位 是 面向 簡單跨平台,至少讓 gcc 和 vs 能夠跑起來.
其實跨平台都是嚼頭, 說白了就是一些丑陋的宏. 真希望所有系統合二為一,采用統一的標准api 設計,但這是不可能的,就相當於很早之前的電視制式一樣.
那麼我們先看 圍繞跨平台的宏
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
/*
* 時間 : 2015年12月26日11:43:22
* 描述 : 應該算過節吧,今天,寫了個雪花特效 代碼,
* 送給 大學啟蒙 蘆老師
* 學生王志 祝福上
* */
/*
* 清除屏幕的shell 命令/控制台命令,還有一些依賴平台的實現
* 如果定義了 __GNUC__ 就假定是 使用gcc 編譯器,為Linux平台
* 否則 認為是 Window 平台
*/
#if defined(__GNUC__)
//下面是依賴 Linux 實現
#include <unistd.h>
#define sleep_ms(m) \
usleep(m * 1000)
//向上移動光標函數 Linux
static void __curup(int height)
{
int i = -1;
while (++i<height)
printf("\033[1A"); //先回到上一行
}
#else
// 創建等待函數 1s 60 幀 相當於 16.7ms => 1幀, 我們取16ms
// 咱麼的這屏幕 推薦 1s 25幀吧 40ms
// 這裡創建等待函數 以毫秒為單位 , 需要依賴操作系統實現
#include <Windows.h>
#define sleep_ms(m) \
Sleep(m)
//向上移動光標
static void __curup(int height)
{
COORD cr = {0,0};
// GetStdHandle(STD_OUTPUT_HANDLE) 獲取屏幕對象, 設置光標
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), cr);
}
#endif /*__GNUC__ 跨平台的代碼都很丑陋 */
首先是 sleep_ms 這個宏, 傳入一個毫秒數,讓操作系統等待.
對於__curup 實現的不好. 功能是 讓 控制台當前光標移動到 上面的 height 位置,對於 window直接移動到第一行(0,0)位置.
上面一共用了 5個頭文件 還是容易的代碼. string.h 主要用的是 memset 函數, 讓一段內存初始化,用0填充.
對於time.h 主要是為了 初始化時間種子,方便每次運行都不一樣.
// 初始化隨機數種子,改變雪花軌跡
srand((unsigned)time(NULL));
1.2 再說主業務代碼
這裡程序員運行的主業務,先說一說這裡用的數據結構 如下
// 定義初始屏幕的寬高像素宏
#define _INT_WIDTH (100)
#define _INT_HEIGHT (50)
// 屏幕刷新幀的速率
#define _INT_FRATE (40)
// 雪花飄落的速率,相對於 屏幕刷新幀 的倍數
#define _INT_VSNOW (10)
/*
* 錯誤處理宏,msg必須是""括起來的字符串常量
* __FILE__ : 文件全路徑
* __func__ : 函數名
* __LINE__ : 行數行
* __VA_ARGS__ : 可變參數宏,
* ##表示直接連接, 例如 a##b <=> ab
*/
#define cerr(msg,...) \
fprintf(stderr, "[%s:%s:%d]" msg "\n",__FILE__,__func__,__LINE__,##__VA_ARGS__);
/*
* 屏幕結構體, 具有 寬高
* frate : 繪制一幀的周期, 單位是 毫秒
* width : 屏幕的寬,基於窗口的左上角(0,0)
* height : 屏幕的高
* pix : 用一維模擬二維 主要結構如下
* 0 0 0 1 0 0 1 0 1 0
* 0 1 0 1 0 1 0 1 2 0
* . . .
* => 0表示沒像素, 1表示1個像素,2表示2個像素....
*/
struct screen {
int frate; // 也可以用 unsigned 結構
int width;
int height;
char *pix;
};
創建了一個繪圖對象 struct screen 這裡 構建這個結構體的時候用了下面一個技巧
//後面是 為 scr->pix 分配的內存 width*height
scr = malloc(sizeof(struct screen) + sizeof(char)*width*height);
一次分配兩個內存空間.下面是主要實現的api 對象
/* * 創建一個 屏幕結構指針 返回 * * int frate : 繪制一幀的周期 * int width : 屏幕寬度 * int height : 屏幕高度 * return : 指向屏幕結構的指針 * */ struct screen* screen_create(int frate, int width, int height); /* * 銷毀一個 屏幕結構指針, 並為其置空 * struct screen** : 指向 屏幕結構指針的指針, 二級銷毀一級的 * */ void screen_destory(struct screen** pscr); /** * 屏幕繪制函數,主要生成一個雪花效果 * * struct screen* : 屏幕數據 * return : 0表示可以繪制了,1表示圖案不變 */ int screen_draw_snow(struct screen* scr); /** * 屏幕繪制動畫效果, 繪制雪花動畫 * * struct screen* : 屏幕結構指針 */ void screen_flash_snow(struct screen* scr);
創建銷毀, 繪制一個雪花界面, 繪制雪花動畫效果的api. 其實都很相似,用opengl 庫, 主要讓我們省略了需要單獨和操作系統顯示層打交道工作.
這裡介紹一下,個人 一個 簡單避免 野指針的 的方法, 具體看下面實現
/*
* 銷毀一個 屏幕結構指針, 並為其置空
* struct screen** : 指向 屏幕結構指針的指針, 二級銷毀一級的
* */
void
screen_destory(struct screen** pscr)
{
if (NULL == pscr || NULL == *pscr)
return;
free(*pscr);
// 避免野指針
*pscr = NULL;
}
在執行之後置空,因為C程序員對NULL一定要敏感,形成條件反射. 和大家開個玩笑 ,
請問 :
C 語言中, NULL , 0 , '\0' , "0", false 有什麼異同 ?
歡迎同行,在招聘的時候問問,應聘初級開發工作者. 為什麼C需要扣的那麼細. 因為其它語言.你不明白是什麼,
你可以用的很好. 但是C你寫的代碼,如果不知道會有怎樣的結果,那麼 線上就一大片服務器直接崩掉.而且還很難找出
問題所在. 因為C很簡單,越簡單就是越復雜.就越需要專業的維護人員.導致它成了'玩具'.
最後看一下 主業務
// 主函數,主業務在此運行
int main(int argc, char *argv[])
{
struct screen* scr = NULL;
//創建一個屏幕對象
scr = screen_create(_INT_FRATE, _INT_WIDTH, _INT_HEIGHT);
if (NULL == scr)
exit(EXIT_FAILURE);
//繪制雪花動畫
screen_flash_snow(scr);
//銷毀這個屏幕對象
screen_destory(&scr);
return 0;
}
還是非常容易看懂的, 創建一個屏幕對象,繪制雪花效果.銷毀屏幕對象.
1.3 說一寫 接口的實現細節
先看幾個簡單的api 實現,創建和銷魂代碼如下,很直白.
/*
* 創建一個 屏幕結構指針 返回
*
* int frate : 繪制一幀的周期
* int width : 屏幕寬度
* int height : 屏幕高度
* return : 指向屏幕結構的指針
* */
struct screen*
screen_create(int frate, int width, int height)
{
struct screen *scr = NULL;
if (frate<0 || width <= 0 || height <= 0) {
cerr("[WARNING]check is frate<0 || width<=0 || height<=0 err!");
return NULL;
}
//後面是 為 scr->pix 分配的內存 width*height
scr = malloc(sizeof(struct screen) + sizeof(char)*width*height);
if (NULL == scr) {
cerr("[FATALG]Out of memory!");
return NULL;
}
scr->frate = frate;
scr->width = width;
scr->height = height;
//減少malloc次數,malloc消耗很大,內存洩露呀,內存碎片呀
scr->pix = ((char *)scr) + sizeof(struct screen);
return scr;
}
/*
* 銷毀一個 屏幕結構指針, 並為其置空
* struct screen** : 指向 屏幕結構指針的指針, 二級銷毀一級的
* */
void
screen_destory(struct screen** pscr)
{
if (NULL == pscr || NULL == *pscr)
return;
free(*pscr);
// 避免野指針
*pscr = NULL;
}
後面說一下 如何 繪制 屏幕中雪花
主要算法 是
a.有個屏幕 w x h
b.屏幕從上面第一行 出雪花 , 出雪花 位置是隨機的[0,w], 但是有個距離,這個距離內只有一個雪花
c.下一行 雪花 依賴上一行雪花的生成, 每個雪花在可以飄動的時候, 只能 在[-1,1] 范圍內
d.實現動畫 效果 就是 每畫一幀就等待 一段時間
下面看具體一點的 a
//創建一個屏幕對象
scr = screen_create(_INT_FRATE, _INT_WIDTH, _INT_HEIGHT);
scr對象就是我們的創建屏幕. _INT_WIDTH 和 _INT_HEIGHT 就是屏幕大小. 對於_INT_FRATE 表示繪制一幀時間.
b實現 代碼如下:
//構建開頭 的雪花,下面宏表示每 _INT_SHEAD 個步長,一個雪花,需要是2的冪
//static 可以理解為 private, 宏,位操作代碼多了確實難讀
#define _INT_SHEAD (1<<2)
static void __snow_head(char* snow, int len)
{
int r = 0;
//數據需要清空
memset(snow, 0, len);
for (;;) {
//取余一個技巧 2^3 - 1 = 7 => 111 , 並就是取余數
int t = rand() & (_INT_SHEAD - 1);
if (r + t >= len)
break;
snow[r + t] = 1;
r += _INT_SHEAD;
}
}
#undef _INT_SHEAD
技巧如上,可以看說明. 這裡 科普一下, 對於 for(;;) {} 和 while(true) {} 異同.
for(;;) {} 和 while(true) {} 這兩段代碼轉成匯編是一樣的, 不一樣 的是 強加的意願. 第一個 希望 跳過 檢測步驟 速度更快一點.
再擴展一點.
//另一種 循環語句, goto 還是 很強大實用的
__for_loop:
if(false)
goto __for_break;
goto __for_loop;
__for_break:
可以再擴展深一點, 還有一種 api 比 這個goto 還NB. 有機會分享. 特別強大, 是異常處理程序本質.
對於c.
//通過 上一個 scr->pix[scr->width*(idx-1)] => scr->pix[scr->width*idx]
//下面的宏 規定 雪花左右搖擺 0 向左一個像素, 1 表示 不變, 2表示向右一個像素
#define _INT_SWING (3)
static void __snow_next(struct screen* scr, int idx)
{
int width = scr->width;
char* psnow = scr->pix + width*(idx - 1);
char* snow = psnow + width;
int i, j, t; // i索引, j保存下一個瞬間雪花的位置,t 臨時補得,解決雪花重疊問題
//為當前行重置
memset(snow, 0, width);
//通過上一次雪花位置 計算下一次雪花位置
for (i = 0; i<width; ++i) {
for (t = psnow[i]; t>0; --t) { // 雪花可以重疊
// rand()%_INT_SWING - 1 表示 雪花 橫軸的偏移量,相對上一次位置
j = i + rand() % _INT_SWING - 1;
j = j<0 ? width - 1 : j >= width ? 0 : j; // j如果越界了,左邊越界讓它到右邊,右邊越界到左邊
++snow[j];
}
}
}
下一行雪花 依賴 上一行雪花, 這裡 有點像插入排序.
整體的繪制代碼 如下
/**
* 屏幕繪制函數,主要生成一個雪花效果
*
* struct screen* : 屏幕數據
* return : 0表示可以繪制了,1表示圖案不變
*/
int
screen_draw_snow(struct screen* scr)
{
// 靜態變量,默認初始化為0,每次都共用
static int __speed = 0;
int idx;
if (++__speed != _INT_VSNOW)
return 1;
//下面 就是 到了雪花飄落的時刻了 既 __speed == _INT_VSNOW
__speed = 0;
//這裡重新構建雪花界面,先構建頭部,再從尾部開始構建
for (idx = scr->height - 1; idx > 0; --idx)
__snow_next(scr, idx);
//構建頭部
__snow_head(scr->pix, scr->width);
return 0;
}
繪制了一個屏幕對象的雪花. __speed 記錄 繪制次數, _INT_VSNOW 控制繪制速率
d 的實現代碼 如下
首先實現一個 銷毀屏幕代碼和 繪制代碼
//buf 保存scr 中pix 數據,構建後為 (width+1)*height, 後面宏是雪花圖案
#define _CHAR_SNOW '*'
static void __flash_snow_buffer(struct screen* scr, char* buf)
{
int i, j, rt;
int height = scr->height, width = scr->width;
int frate = scr->frate; //刷新的幀頻率
//每次都等一下
for (;;sleep_ms(frate)) {
//開始繪制屏幕
rt = screen_draw_snow(scr);
if (rt)
continue;
for (i = 0;i<height; ++i) {
char* snow = scr->pix + i*width;
for (j = 0; j<width; ++j)
buf[rt++] = snow[j] ? _CHAR_SNOW : ' ';
buf[rt++] = '\n';
}
buf[rt - 1] = '\0';
//正式繪制到屏幕上
puts(buf);
//清空老屏幕,屏幕光標回到最上面
__curup(height);
}
}
#undef _CHAR_SNOW
這裡 sleep_ms(frate); 是等待時間,否則太快, 人眼看不見.
繪制原理是 讓屏幕轉成控制台能夠認識的字符. 塞入到buf 中.
__curup(height); 讓繪制光標回到開頭.
後面還有一段 代碼實現
/**
* 屏幕繪制動畫效果, 繪制雪花動畫
*
* struct screen* : 屏幕結構指針
*/
void
screen_flash_snow(struct screen* scr)
{
char* buf = NULL;
// 初始化隨機數種子,改變雪花軌跡
srand((unsigned)time(NULL));
buf = malloc(sizeof(char)*(scr->width + 1)*scr->height);
if (NULL == buf) {
cerr("[FATAL]Out of memory!");
exit(EXIT_FAILURE);
}
__flash_snow_buffer(scr, buf);
//1.這裡理論上不會執行到這,沒加控制器. 2.對於buf=NULL,這種代碼 可以省掉,看編程習慣
free(buf);
buf = NULL;
}
這種雙函數實現一個功能技巧用的也很多. 例如寫快速排序代碼, 就是這樣.
到這裡 我們 設計和實現都完成了.
2.代碼效果展示
2.1 window 上展示
使用VS新建一個控制台項目,F5就可以了效果如下

是動態的.
2.2 對於Linux
直接使用
gcc -g -Wall snow.c -o snow.out ./snow.out
運行效果如下

到這裡 , C語言實現雪花效果就如上了.
2.3 完整的代碼展示. 感謝 有你,一路同行.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
/*
* 時間 : 2015年12月26日11:43:22
* 描述 : 應該算過節吧,今天,寫了個雪花特效 代碼,
* 送給 大學啟蒙 蘆老師
* 學生王志 祝福上
* */
/*
* 清除屏幕的shell 命令/控制台命令,還有一些依賴平台的實現
* 如果定義了 __GNUC__ 就假定是 使用gcc 編譯器,為Linux平台
* 否則 認為是 Window 平台
*/
#if defined(__GNUC__)
//下面是依賴 Linux 實現
#include <unistd.h>
#define sleep_ms(m) \
usleep(m * 1000)
//向上移動光標函數 Linux
static void __curup(int height)
{
int i = -1;
while (++i<height)
printf("\033[1A"); //先回到上一行
}
#else
// 創建等待函數 1s 60 幀 相當於 16.7ms => 1幀, 我們取16ms
// 咱麼的這屏幕 推薦 1s 25幀吧 40ms
// 這裡創建等待函數 以毫秒為單位 , 需要依賴操作系統實現
#include <Windows.h>
#define sleep_ms(m) \
Sleep(m)
//向上移動光標
static void __curup(int height)
{
COORD cr = {0,0};
// GetStdHandle(STD_OUTPUT_HANDLE) 獲取屏幕對象, 設置光標
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), cr);
}
#endif /*__GNUC__ 跨平台的代碼都很丑陋 */
// 定義初始屏幕的寬高像素宏
#define _INT_WIDTH (100)
#define _INT_HEIGHT (50)
// 屏幕刷新幀的速率
#define _INT_FRATE (40)
// 雪花飄落的速率,相對於 屏幕刷新幀 的倍數
#define _INT_VSNOW (10)
/*
* 錯誤處理宏,msg必須是""括起來的字符串常量
* __FILE__ : 文件全路徑
* __func__ : 函數名
* __LINE__ : 行數行
* __VA_ARGS__ : 可變參數宏,
* ##表示直接連接, 例如 a##b <=> ab
*/
#define cerr(msg,...) \
fprintf(stderr, "[%s:%s:%d]" msg "\n",__FILE__,__func__,__LINE__,##__VA_ARGS__);
/*
* 屏幕結構體, 具有 寬高
* frate : 繪制一幀的周期, 單位是 毫秒
* width : 屏幕的寬,基於窗口的左上角(0,0)
* height : 屏幕的高
* pix : 用一維模擬二維 主要結構如下
* 0 0 0 1 0 0 1 0 1 0
* 0 1 0 1 0 1 0 1 2 0
* . . .
* => 0表示沒像素, 1表示1個像素,2表示2個像素....
*/
struct screen {
int frate; // 也可以用 unsigned 結構
int width;
int height;
char *pix;
};
/*
* 創建一個 屏幕結構指針 返回
*
* int frate : 繪制一幀的周期
* int width : 屏幕寬度
* int height : 屏幕高度
* return : 指向屏幕結構的指針
* */
struct screen* screen_create(int frate, int width, int height);
/*
* 銷毀一個 屏幕結構指針, 並為其置空
* struct screen** : 指向 屏幕結構指針的指針, 二級銷毀一級的
* */
void screen_destory(struct screen** pscr);
/**
* 屏幕繪制函數,主要生成一個雪花效果
*
* struct screen* : 屏幕數據
* return : 0表示可以繪制了,1表示圖案不變
*/
int screen_draw_snow(struct screen* scr);
/**
* 屏幕繪制動畫效果, 繪制雪花動畫
*
* struct screen* : 屏幕結構指針
*/
void screen_flash_snow(struct screen* scr);
// 主函數,主業務在此運行
int main(int argc, char *argv[])
{
struct screen* scr = NULL;
//創建一個屏幕對象
scr = screen_create(_INT_FRATE, _INT_WIDTH, _INT_HEIGHT);
if (NULL == scr)
exit(EXIT_FAILURE);
//繪制雪花動畫
screen_flash_snow(scr);
//銷毀這個屏幕對象
screen_destory(&scr);
return 0;
}
/*
* 創建一個 屏幕結構指針 返回
*
* int frate : 繪制一幀的周期
* int width : 屏幕寬度
* int height : 屏幕高度
* return : 指向屏幕結構的指針
* */
struct screen*
screen_create(int frate, int width, int height)
{
struct screen *scr = NULL;
if (frate<0 || width <= 0 || height <= 0) {
cerr("[WARNING]check is frate<0 || width<=0 || height<=0 err!");
return NULL;
}
//後面是 為 scr->pix 分配的內存 width*height
scr = malloc(sizeof(struct screen) + sizeof(char)*width*height);
if (NULL == scr) {
cerr("[FATALG]Out of memory!");
return NULL;
}
scr->frate = frate;
scr->width = width;
scr->height = height;
//減少malloc次數,malloc消耗很大,內存洩露呀,內存碎片呀
scr->pix = ((char *)scr) + sizeof(struct screen);
return scr;
}
/*
* 銷毀一個 屏幕結構指針, 並為其置空
* struct screen** : 指向 屏幕結構指針的指針, 二級銷毀一級的
* */
void
screen_destory(struct screen** pscr)
{
if (NULL == pscr || NULL == *pscr)
return;
free(*pscr);
// 避免野指針
*pscr = NULL;
}
//構建開頭 的雪花,下面宏表示每 _INT_SHEAD 個步長,一個雪花,需要是2的冪
//static 可以理解為 private, 宏,位操作代碼多了確實難讀
#define _INT_SHEAD (1<<2)
static void __snow_head(char* snow, int len)
{
int r = 0;
//數據需要清空
memset(snow, 0, len);
for (;;) {
//取余一個技巧 2^3 - 1 = 7 => 111 , 並就是取余數
int t = rand() & (_INT_SHEAD - 1);
if (r + t >= len)
break;
snow[r + t] = 1;
r += _INT_SHEAD;
}
}
#undef _INT_SHEAD
//通過 上一個 scr->pix[scr->width*(idx-1)] => scr->pix[scr->width*idx]
//下面的宏 規定 雪花左右搖擺 0 向左一個像素, 1 表示 不變, 2表示向右一個像素
#define _INT_SWING (3)
static void __snow_next(struct screen* scr, int idx)
{
int width = scr->width;
char* psnow = scr->pix + width*(idx - 1);
char* snow = psnow + width;
int i, j, t; // i索引, j保存下一個瞬間雪花的位置,t 臨時補得,解決雪花重疊問題
//為當前行重置
memset(snow, 0, width);
//通過上一次雪花位置 計算下一次雪花位置
for (i = 0; i<width; ++i) {
for (t = psnow[i]; t>0; --t) { // 雪花可以重疊
// rand()%_INT_SWING - 1 表示 雪花 橫軸的偏移量,相對上一次位置
j = i + rand() % _INT_SWING - 1;
j = j<0 ? width - 1 : j >= width ? 0 : j; // j如果越界了,左邊越界讓它到右邊,右邊越界到左邊
++snow[j];
}
}
}
/**
* 屏幕繪制函數,主要生成一個雪花效果
*
* struct screen* : 屏幕數據
* return : 0表示可以繪制了,1表示圖案不變
*/
int
screen_draw_snow(struct screen* scr)
{
// 靜態變量,默認初始化為0,每次都共用
static int __speed = 0;
int idx;
if (++__speed != _INT_VSNOW)
return 1;
//下面 就是 到了雪花飄落的時刻了 既 __speed == _INT_VSNOW
__speed = 0;
//這裡重新構建雪花界面,先構建頭部,再從尾部開始構建
for (idx = scr->height - 1; idx > 0; --idx)
__snow_next(scr, idx);
//構建頭部
__snow_head(scr->pix, scr->width);
return 0;
}
//buf 保存scr 中pix 數據,構建後為 (width+1)*height, 後面宏是雪花圖案
#define _CHAR_SNOW '*'
static void __flash_snow_buffer(struct screen* scr, char* buf)
{
int i, j, rt;
int height = scr->height, width = scr->width;
int frate = scr->frate; //刷新的幀頻率
//每次都等一下
for (;;sleep_ms(frate)) {
//開始繪制屏幕
rt = screen_draw_snow(scr);
if (rt)
continue;
for (i = 0;i<height; ++i) {
char* snow = scr->pix + i*width;
for (j = 0; j<width; ++j)
buf[rt++] = snow[j] ? _CHAR_SNOW : ' ';
buf[rt++] = '\n';
}
buf[rt - 1] = '\0';
//正式繪制到屏幕上
puts(buf);
//清空老屏幕,屏幕光標回到最上面
__curup(height);
}
}
#undef _CHAR_SNOW
/**
* 屏幕繪制動畫效果, 繪制雪花動畫
*
* struct screen* : 屏幕結構指針
*/
void
screen_flash_snow(struct screen* scr)
{
char* buf = NULL;
// 初始化隨機數種子,改變雪花軌跡
srand((unsigned)time(NULL));
buf = malloc(sizeof(char)*(scr->width + 1)*scr->height);
if (NULL == buf) {
cerr("[FATAL]Out of memory!");
exit(EXIT_FAILURE);
}
__flash_snow_buffer(scr, buf);
//1.這裡理論上不會執行到這,沒加控制器. 2.對於buf=NULL,這種代碼 可以省掉,看編程習慣
free(buf);
buf = NULL;
}
後記
到這裡就結束了,這次分享的比較簡單,有興趣的同學可以 看看, 推薦寫一遍. 代碼看不懂的時候,多歇歇,看得懂的時候,多寫寫,
就有套路了. 歡迎吐槽. 錯誤是在所難免的.
這個冬天,雪花很美,(。⌒∇⌒)