C/C控制台清屏方法全攻略从闪瞎眼到丝滑流畅写控制台小游戏的时候最烦人的问题是什么刷屏闪烁。你高高兴兴写了个贪吃蛇一运行屏幕闪得跟迪斯科舞厅似的眼睛都快瞎了。这篇文章就把C/C里各种清屏方法从头到尾捋一遍从最基础的system(cls)到终极解决方案双缓冲手把手教你告别闪烁。一、为什么需要清屏控制台程序默认是逐行往下输出的不会自动覆盖前面的内容。你要做贪吃蛇、俄罗斯方块、实时进度条这些东西每一帧画面都不一样不清掉旧内容的话文字就会堆在一起根本没法看。所以清屏的本质就是擦掉旧的画上新的。但怎么擦、怎么画差别可太大了。二、方法一system(“cls”) / system(“clear”) —— 最简单但最闪这是绝大多数新手入门时用的方法简单到令人发指——一行代码搞定。#includestdlib.h// C语言// 或#includecstdlib// Csystem(cls);// Windowssystem(clear);// Linux / Macsystem()函数是C标准库提供的“传令兵”它的作用是把括号里的字符串命令交给操作系统去执行。cls是Windows命令行里的清屏命令clear是Linux/Mac里的清屏命令。优点一行代码搞定新手友好不需要理解任何底层原理缺点屏幕疯狂闪烁system(cls)会瞬间清空整个屏幕→屏幕变黑→再重新渲染所有文字这个“清空→渲染”的过程人眼能明显感知跨平台差Windows用clsLinux用clear代码不通用效率极低调用系统命令开销大不适合高频刷新无法精细控制一清清全屏不能只改局部什么时候用只适合一次性清屏比如程序启动时清一下或者对性能完全没要求的场景。千万别在游戏主循环里用否则你的玩家会感谢你治好了他多年的失眠。三、方法二\r回车覆盖 —— 单行刷新的救星如果你的界面只需要刷新一行比如进度条、计时器根本不需要清屏。用\r就能搞定。\r的作用是把光标移回到本行开头不换行然后新内容直接覆盖旧内容。#includeiostream#includewindows.h// Sleep用Linux用unistd.hintmain(){for(inti0;i100;i){std::cout\r下载进度i%std::flush;Sleep(50);}std::cout\n下载完成std::endl;return0;}关键点用\r回到行首不是\n用std::flush强制立刻输出不要等缓冲区自动刷新如果新内容比旧内容短需要补空格把残留的旧字符盖掉优点完全不闪烁效率极高缺点只能刷一行多行界面搞不定四、方法三SetConsoleCursorPosition —— 多行定点覆盖Windows对于多行界面可以把光标精确移动到任意位置然后覆盖写入新内容。#includewindows.h#includeiostreamvoidgotoxy(intx,inty){HANDLE hConsoleGetStdHandle(STD_OUTPUT_HANDLE);COORD pos{(SHORT)x,(SHORT)y};SetConsoleCursorPosition(hConsole,pos);}intmain(){intnum0;// 先打印框架std::cout数字std::endl;std::cout----------------std::endl;while(true){gotoxy(3,0);// 定位到数字后面std::coutnum ;// 后面的空格覆盖旧数字Sleep(100);}return0;}GetStdHandle(STD_OUTPUT_HANDLE)获取标准输出设备的句柄SetConsoleCursorPosition把光标移动到指定坐标。COORD结构体的X是列、Y是行都是从0开始。优点不清屏不闪烁可以精确控制刷新位置效率高缺点Windows专用windows.h需要自己记录每一行该写什么如果刷新内容太多写起来麻烦五、方法四Windows API暴力清屏 —— 不闪烁的“真清屏”如果你真的需要“清空整个屏幕”的效果但又不想用system(cls)那种闪烁的方式可以用Windows API直接操作控制台缓冲区。#includewindows.hvoidClearScreen(){HANDLE hConsoleGetStdHandle(STD_OUTPUT_HANDLE);COORD coordScreen{0,0};DWORD cCharsWritten;CONSOLE_SCREEN_BUFFER_INFO csbi;DWORD dwConSize;GetConsoleScreenBufferInfo(hConsole,csbi);dwConSizecsbi.dwSize.X*csbi.dwSize.Y;// 用空格填满整个缓冲区FillConsoleOutputCharacter(hConsole, ,dwConSize,coordScreen,cCharsWritten);GetConsoleScreenBufferInfo(hConsole,csbi);FillConsoleOutputAttribute(hConsole,csbi.wAttributes,dwConSize,coordScreen,cCharsWritten);SetConsoleCursorPosition(hConsole,coordScreen);}这个函数做了什么获取控制台缓冲区的大小宽×高用空格字符填满整个缓冲区把光标移回左上角优点直接操作缓冲区比system(cls)快得多不闪烁缺点Windows专用代码比system复杂不少六、方法五ANSI转义序列 —— Linux / Mac的优雅方案在Linux和Mac下可以用ANSI转义序列来控制终端。#includestdio.hvoidClearScreen(){printf(\033[2J\033[H);fflush(stdout);}\033[2J清除整个屏幕\033[H把光标移动到(1,1)位置fflush(stdout)强制立即输出ANSI转义序列不是C标准而是终端控制的标准。好消息是现代Windows终端Windows 10以上也支持ANSI转义了需要启用虚拟终端处理。优点跨平台现代终端都支持不调用外部命令效率高缺点老版本Windows不支持需要额外配置七、方法六双缓冲 —— 终极解决方案前面几种方法各有局限。system(cls)闪\r只能刷一行gotoxy需要自己管理每个位置API清屏只是把cls用API重写了一遍——真正能完美解决控制台闪烁的是双缓冲。7.1 双缓冲的原理先搞清楚闪屏是怎么来的。控制台程序的显示结构是这样的程序→屏幕缓冲区→显示器。当你在高速循环里不断输出大量数据时数据到达屏幕缓冲区有先后顺序。显示器可能只拿到了部分数据就开始刷新了于是你看到的就是“一半新一半旧”的画面这就是闪烁的根本原因。双缓冲的思路很简单弄两个缓冲区一个给显示器看前台一个用来画后台。所有绘图操作都在后台缓冲区完成画好之后整个切换过来。显示器看到的一直是完整的画面没有中间状态闪烁自然消失。7.2 Windows控制台双缓冲实现Windows提供了CreateConsoleScreenBuffer函数来创建额外的屏幕缓冲区。#includewindows.h#includeiostream#includestringclassDoubleBuffer{private:HANDLE hOutput;// 当前显示的缓冲区句柄HANDLE hBuffer;// 后台缓冲区句柄COORD bufferSize;SMALL_RECT rect;public:DoubleBuffer(intwidth,intheight){hOutputGetStdHandle(STD_OUTPUT_HANDLE);// 获取当前缓冲区信息CONSOLE_SCREEN_BUFFER_INFO csbi;GetConsoleScreenBufferInfo(hOutput,csbi);// 创建新的屏幕缓冲区hBufferCreateConsoleScreenBuffer(GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ|FILE_SHARE_WRITE,NULL,CONSOLE_TEXTMODE_BUFFER,NULL);// 设置缓冲区大小bufferSize.Xwidth;bufferSize.Yheight;SetConsoleScreenBufferSize(hBuffer,bufferSize);SetConsoleScreenBufferSize(hOutput,bufferSize);// 设置窗口大小rect.Left0;rect.Top0;rect.Rightwidth-1;rect.Bottomheight-1;SetConsoleWindowInfo(hOutput,TRUE,rect);SetConsoleWindowInfo(hBuffer,TRUE,rect);// 隐藏两个缓冲区的光标CONSOLE_CURSOR_INFO cursorInfo;GetConsoleCursorInfo(hOutput,cursorInfo);cursorInfo.bVisibleFALSE;SetConsoleCursorInfo(hOutput,cursorInfo);SetConsoleCursorInfo(hBuffer,cursorInfo);// 默认显示hOutputSetConsoleActiveScreenBuffer(hOutput);}~DoubleBuffer(){CloseHandle(hBuffer);}// 切换到后台缓冲区进行绘制voidBeginDraw(){SetConsoleActiveScreenBuffer(hBuffer);}// 切换回前台缓冲区显示voidEndDraw(){SetConsoleActiveScreenBuffer(hOutput);// 交换句柄让hOutput始终是当前显示的那个HANDLE temphOutput;hOutputhBuffer;hBuffertemp;}// 清空后台缓冲区voidClear(){CONSOLE_SCREEN_BUFFER_INFO csbi;GetConsoleScreenBufferInfo(hOutput,csbi);DWORD dwConSizecsbi.dwSize.X*csbi.dwSize.Y;DWORD written;COORD zero{0,0};FillConsoleOutputCharacter(hOutput, ,dwConSize,zero,written);SetConsoleCursorPosition(hOutput,zero);}// 在后台缓冲区定位光标voidGotoxy(intx,inty){COORD pos{(SHORT)x,(SHORT)y};SetConsoleCursorPosition(hOutput,pos);}// 在后台缓冲区输出voidPrint(conststd::stringstr){DWORD written;WriteConsole(hOutput,str.c_str(),str.length(),written,NULL);}};使用示例intmain(){DoubleBufferdb(80,25);intframe0;while(true){db.BeginDraw();// 切换到后台缓冲区db.Clear();// 清空后台缓冲区// 在后台缓冲区绘制内容db.Gotoxy(10,5);db.Print(帧数std::to_string(frame));db.Gotoxy(10,7);db.Print(这是一个不会闪烁的控制台);db.EndDraw();// 切换到前台显示Sleep(50);}return0;}核心操作BeginDraw()切换到后台缓冲区在后台缓冲区完成所有绘制EndDraw()一次性切换到前台显示优点彻底消除闪烁适合高频刷新场景画面过渡流畅缺点Windows专用代码量较大需要理解控制台缓冲区的概念7.3 Linux下用ncurses实现双缓冲Linux下没有CreateConsoleScreenBuffer但可以用ncurses库。#includencurses.h#includeunistd.hintmain(){initscr();// 初始化ncursescurs_set(0);// 隐藏光标noecho();// 不显示输入的字符intframe0;while(1){clear();// 清屏在ncurses内部缓冲区mvprintw(5,10,帧数%d,frame);mvprintw(7,10,这是一个不会闪烁的控制台);refresh();// 一次性刷新到屏幕usleep(50000);}endwin();// 结束ncursesreturn0;}编译时需要链接ncurses库gcc-oprogram program.c-lncursesncurses的原理和双缓冲类似所有绘图操作先在内部缓冲区完成调用refresh()时才一次性输出到屏幕。八、方法对比方法平台闪烁程度代码复杂度适用场景system(cls)Windows⭐⭐⭐⭐⭐ 严重闪烁极低一次性清屏system(clear)Linux/Mac⭐⭐⭐⭐⭐ 严重闪烁极低一次性清屏\r回车覆盖全平台⭐ 几乎不闪极低单行进度条SetConsoleCursorPositionWindows⭐ 几乎不闪低多行定点刷新Windows API清屏Windows⭐ 几乎不闪中需要真清屏的场景ANSI转义序列现代终端⭐ 几乎不闪低Linux/现代Windows双缓冲(Windows API)Windows 完全不闪高游戏、高频动画ncursesLinux/Mac 完全不闪中Linux终端应用九、完整示例一个不闪烁的移动小球下面是一个完整的C控制台程序用双缓冲实现了一个在屏幕上弹跳的小球完全不闪烁。#includewindows.h#includeiostream#includestring// 双缓冲类 classDoubleBuffer{private:HANDLE hOutput;HANDLE hBuffer;intwidth,height;public:DoubleBuffer(intw,inth):width(w),height(h){hOutputGetStdHandle(STD_OUTPUT_HANDLE);hBufferCreateConsoleScreenBuffer(GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ|FILE_SHARE_WRITE,NULL,CONSOLE_TEXTMODE_BUFFER,NULL);COORD size{(SHORT)w,(SHORT)h};SetConsoleScreenBufferSize(hBuffer,size);SetConsoleScreenBufferSize(hOutput,size);SMALL_RECT rect{0,0,(SHORT)(w-1),(SHORT)(h-1)};SetConsoleWindowInfo(hOutput,TRUE,rect);SetConsoleWindowInfo(hBuffer,TRUE,rect);CONSOLE_CURSOR_INFO cursorInfo;GetConsoleCursorInfo(hOutput,cursorInfo);cursorInfo.bVisibleFALSE;SetConsoleCursorInfo(hOutput,cursorInfo);SetConsoleCursorInfo(hBuffer,cursorInfo);SetConsoleActiveScreenBuffer(hOutput);}~DoubleBuffer(){CloseHandle(hBuffer);}voidBeginDraw(){SetConsoleActiveScreenBuffer(hBuffer);}voidEndDraw(){SetConsoleActiveScreenBuffer(hOutput);HANDLE temphOutput;hOutputhBuffer;hBuffertemp;}voidClear(){CONSOLE_SCREEN_BUFFER_INFO csbi;GetConsoleScreenBufferInfo(hOutput,csbi);DWORD sizecsbi.dwSize.X*csbi.dwSize.Y;DWORD written;COORD zero{0,0};FillConsoleOutputCharacter(hOutput, ,size,zero,written);SetConsoleCursorPosition(hOutput,zero);}voidGotoxy(intx,inty){COORD pos{(SHORT)x,(SHORT)y};SetConsoleCursorPosition(hOutput,pos);}voidPrint(conststd::stringstr){DWORD written;WriteConsole(hOutput,str.c_str(),str.length(),written,NULL);}};// 主程序 intmain(){constintW40,H20;DoubleBufferdb(W,H);intballX5,ballY5;intdx1,dy1;intframe0;while(true){// 更新小球位置ballXdx;ballYdy;if(ballX0||ballXW-1)dx-dx;if(ballY0||ballYH-1)dy-dy;// 绘制db.BeginDraw();db.Clear();// 画边框for(inti0;iW;i){db.Gotoxy(i,0);db.Print(#);db.Gotoxy(i,H-1);db.Print(#);}for(inti0;iH;i){db.Gotoxy(0,i);db.Print(#);db.Gotoxy(W-1,i);db.Print(#);}// 画小球db.Gotoxy(ballX,ballY);db.Print(O);// 显示帧数db.Gotoxy(2,H1);db.Print(帧数: std::to_string(frame));db.EndDraw();Sleep(50);}return0;}十、总结做控制台小游戏清屏是绕不开的问题。我的建议是如果只是写个demo练手system(cls)凑合能用别在主循环里用就行如果是进度条、计时器这种单行刷新用\r是最优雅的方案如果是多行界面但内容固定比如棋盘、菜单用gotoxy定点覆盖如果是正经做游戏直接上双缓冲一劳永逸双缓冲虽然代码量大一些但理解了原理之后其实就是个模板代码复制过去改改就能用。为了玩家和自己的眼睛这点功夫值得花。