写控制台小游戏的时候,最烦人的问题是什么?刷屏闪烁。你高高兴兴写了个贪吃蛇,一运行屏幕闪得跟迪斯科舞厅似的,眼睛都快瞎了。这篇文章就把c/c++里各种清屏方法从头到尾捋一遍,从最基础的system("cls")到终极解决方案双缓冲,手把手教你告别闪烁。
一、为什么需要清屏?
控制台程序默认是逐行往下输出的,不会自动覆盖前面的内容。你要做贪吃蛇、俄罗斯方块、实时进度条这些东西,每一帧画面都不一样,不清掉旧内容的话,文字就会堆在一起,根本没法看。
所以清屏的本质就是:擦掉旧的,画上新的。但怎么擦、怎么画,差别可太大了。
二、方法一:system(“cls”) / system(“clear”) —— 最简单,但最闪
这是绝大多数新手入门时用的方法,简单到令人发指——一行代码搞定。
#include <stdlib.h> // c语言
// 或
#include <cstdlib> // c++
system("cls"); // windows
system("clear"); // linux / mac
system()函数是c标准库提供的“传令兵”,它的作用是把括号里的字符串命令交给操作系统去执行。"cls"是windows命令行里的清屏命令,"clear"是linux/mac里的清屏命令。
优点:
- 一行代码搞定,新手友好
- 不需要理解任何底层原理
缺点:
- 屏幕疯狂闪烁:system("cls")会瞬间清空整个屏幕→屏幕变黑→再重新渲染所有文字,这个“清空→渲染”的过程人眼能明显感知
- 跨平台差:windows用cls,linux用clear,代码不通用
- 效率极低:调用系统命令开销大,不适合高频刷新
- 无法精细控制:一清清全屏,不能只改局部
什么时候用: 只适合一次性清屏(比如程序启动时清一下),或者对性能完全没要求的场景。千万别在游戏主循环里用,否则你的玩家会感谢你治好了他多年的失眠。
三、方法二:\r回车覆盖 —— 单行刷新的救星
如果你的界面只需要刷新一行(比如进度条、计时器),根本不需要清屏。用\r就能搞定。
\r的作用是把光标移回到本行开头,不换行,然后新内容直接覆盖旧内容。
#include <iostream>
#include <windows.h> // sleep用,linux用<unistd.h>
int main() {
for (int i = 0; i <= 100; i++) {
std::cout << "\r下载进度:" << i << "%" << std::flush;
sleep(50);
}
std::cout << "\n下载完成!" << std::endl;
return 0;
}
关键点:
- 用\r回到行首,不是\n
- 用std::flush强制立刻输出,不要等缓冲区自动刷新
- 如果新内容比旧内容短,需要补空格把残留的旧字符盖掉
优点: 完全不闪烁,效率极高
缺点: 只能刷一行,多行界面搞不定
四、方法三:setconsolecursorposition —— 多行定点覆盖(windows)
对于多行界面,可以把光标精确移动到任意位置,然后覆盖写入新内容。
#include <windows.h>
#include <iostream>
void gotoxy(int x, int y) {
handle hconsole = getstdhandle(std_output_handle);
coord pos = { (short)x, (short)y };
setconsolecursorposition(hconsole, pos);
}
int main() {
int num = 0;
// 先打印框架
std::cout << "数字:" << std::endl;
std::cout << "----------------" << std::endl;
while (true) {
gotoxy(3, 0); // 定位到"数字:"后面
std::cout << num++ << " "; // 后面的空格覆盖旧数字
sleep(100);
}
return 0;
}
getstdhandle(std_output_handle)获取标准输出设备的句柄,setconsolecursorposition把光标移动到指定坐标。coord结构体的x是列、y是行,都是从0开始。
优点:
- 不清屏,不闪烁
- 可以精确控制刷新位置
- 效率高
缺点:
- windows专用(windows.h)
- 需要自己记录每一行该写什么
- 如果刷新内容太多,写起来麻烦
五、方法四:windows api暴力清屏 —— 不闪烁的“真清屏”
如果你真的需要“清空整个屏幕”的效果,但又不想用system("cls")那种闪烁的方式,可以用windows api直接操作控制台缓冲区。
#include <windows.h>
void clearscreen() {
handle hconsole = getstdhandle(std_output_handle);
coord coordscreen = { 0, 0 };
dword ccharswritten;
console_screen_buffer_info csbi;
dword dwconsize;
getconsolescreenbufferinfo(hconsole, &csbi);
dwconsize = csbi.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转义序列来控制终端。
#include <stdio.h>
void clearscreen() {
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函数来创建额外的屏幕缓冲区。
#include <windows.h>
#include <iostream>
#include <string>
class doublebuffer {
private:
handle houtput; // 当前显示的缓冲区句柄
handle hbuffer; // 后台缓冲区句柄
coord buffersize;
small_rect rect;
public:
doublebuffer(int width, int height) {
houtput = getstdhandle(std_output_handle);
// 获取当前缓冲区信息
console_screen_buffer_info csbi;
getconsolescreenbufferinfo(houtput, &csbi);
// 创建新的屏幕缓冲区
hbuffer = createconsolescreenbuffer(
generic_read | generic_write,
file_share_read | file_share_write,
null,
console_textmode_buffer,
null
);
// 设置缓冲区大小
buffersize.x = width;
buffersize.y = height;
setconsolescreenbuffersize(hbuffer, buffersize);
setconsolescreenbuffersize(houtput, buffersize);
// 设置窗口大小
rect.left = 0;
rect.top = 0;
rect.right = width - 1;
rect.bottom = height - 1;
setconsolewindowinfo(houtput, true, &rect);
setconsolewindowinfo(hbuffer, true, &rect);
// 隐藏两个缓冲区的光标
console_cursor_info cursorinfo;
getconsolecursorinfo(houtput, &cursorinfo);
cursorinfo.bvisible = false;
setconsolecursorinfo(houtput, &cursorinfo);
setconsolecursorinfo(hbuffer, &cursorinfo);
// 默认显示houtput
setconsoleactivescreenbuffer(houtput);
}
~doublebuffer() {
closehandle(hbuffer);
}
// 切换到后台缓冲区进行绘制
void begindraw() {
setconsoleactivescreenbuffer(hbuffer);
}
// 切换回前台缓冲区显示
void enddraw() {
setconsoleactivescreenbuffer(houtput);
// 交换句柄,让houtput始终是"当前显示"的那个
handle temp = houtput;
houtput = hbuffer;
hbuffer = temp;
}
// 清空后台缓冲区
void clear() {
console_screen_buffer_info csbi;
getconsolescreenbufferinfo(houtput, &csbi);
dword dwconsize = csbi.dwsize.x * csbi.dwsize.y;
dword written;
coord zero = { 0, 0 };
fillconsoleoutputcharacter(houtput, ' ', dwconsize, zero, &written);
setconsolecursorposition(houtput, zero);
}
// 在后台缓冲区定位光标
void gotoxy(int x, int y) {
coord pos = { (short)x, (short)y };
setconsolecursorposition(houtput, pos);
}
// 在后台缓冲区输出
void print(const std::string& str) {
dword written;
writeconsole(houtput, str.c_str(), str.length(), &written, null);
}
};
使用示例:
int main() {
doublebuffer db(80, 25);
int frame = 0;
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);
}
return 0;
}
核心操作:
- begindraw()切换到后台缓冲区
- 在后台缓冲区完成所有绘制
- enddraw()一次性切换到前台显示
优点:
- 彻底消除闪烁
- 适合高频刷新场景
- 画面过渡流畅
缺点:
- windows专用
- 代码量较大
- 需要理解控制台缓冲区的概念
7.3 linux下用ncurses实现双缓冲
linux下没有createconsolescreenbuffer,但可以用ncurses库。
#include <ncurses.h>
#include <unistd.h>
int main() {
initscr(); // 初始化ncurses
curs_set(0); // 隐藏光标
noecho(); // 不显示输入的字符
int frame = 0;
while (1) {
clear(); // 清屏(在ncurses内部缓冲区)
mvprintw(5, 10, "帧数:%d", frame++);
mvprintw(7, 10, "这是一个不会闪烁的控制台!");
refresh(); // 一次性刷新到屏幕
usleep(50000);
}
endwin(); // 结束ncurses
return 0;
}
编译时需要链接ncurses库:
gcc -o program program.c -lncurses
ncurses的原理和双缓冲类似:所有绘图操作先在内部缓冲区完成,调用refresh()时才一次性输出到屏幕。
八、方法对比
| 方法 | 平台 | 闪烁程度 | 代码复杂度 | 适用场景 |
|---|---|---|---|---|
| system("cls") | windows | ⭐⭐⭐⭐⭐ 严重闪烁 | 极低 | 一次性清屏 |
| system("clear") | linux/mac | ⭐⭐⭐⭐⭐ 严重闪烁 | 极低 | 一次性清屏 |
| \r回车覆盖 | 全平台 | ⭐ 几乎不闪 | 极低 | 单行进度条 |
| setconsolecursorposition | windows | ⭐ 几乎不闪 | 低 | 多行定点刷新 |
| windows api清屏 | windows | ⭐ 几乎不闪 | 中 | 需要真清屏的场景 |
| ansi转义序列 | 现代终端 | ⭐ 几乎不闪 | 低 | linux/现代windows |
| 双缓冲(windows api) | windows | 🌟 完全不闪 | 高 | 游戏、高频动画 |
| ncurses | linux/mac | 🌟 完全不闪 | 中 | linux终端应用 |
九、完整示例:一个不闪烁的移动小球
下面是一个完整的c++控制台程序,用双缓冲实现了一个在屏幕上弹跳的小球,完全不闪烁。
#include <windows.h>
#include <iostream>
#include <string>
// ============ 双缓冲类 ============
class doublebuffer {
private:
handle houtput;
handle hbuffer;
int width, height;
public:
doublebuffer(int w, int h) : width(w), height(h) {
houtput = getstdhandle(std_output_handle);
hbuffer = createconsolescreenbuffer(
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.bvisible = false;
setconsolecursorinfo(houtput, &cursorinfo);
setconsolecursorinfo(hbuffer, &cursorinfo);
setconsoleactivescreenbuffer(houtput);
}
~doublebuffer() {
closehandle(hbuffer);
}
void begindraw() {
setconsoleactivescreenbuffer(hbuffer);
}
void enddraw() {
setconsoleactivescreenbuffer(houtput);
handle temp = houtput;
houtput = hbuffer;
hbuffer = temp;
}
void clear() {
console_screen_buffer_info csbi;
getconsolescreenbufferinfo(houtput, &csbi);
dword size = csbi.dwsize.x * csbi.dwsize.y;
dword written;
coord zero = { 0, 0 };
fillconsoleoutputcharacter(houtput, ' ', size, zero, &written);
setconsolecursorposition(houtput, zero);
}
void gotoxy(int x, int y) {
coord pos = { (short)x, (short)y };
setconsolecursorposition(houtput, pos);
}
void print(const std::string& str) {
dword written;
writeconsole(houtput, str.c_str(), str.length(), &written, null);
}
};
// ============ 主程序 ============
int main() {
const int w = 40, h = 20;
doublebuffer db(w, h);
int ballx = 5, bally = 5;
int dx = 1, dy = 1;
int frame = 0;
while (true) {
// 更新小球位置
ballx += dx;
bally += dy;
if (ballx <= 0 || ballx >= w - 1) dx = -dx;
if (bally <= 0 || bally >= h - 1) dy = -dy;
// 绘制
db.begindraw();
db.clear();
// 画边框
for (int i = 0; i < w; i++) {
db.gotoxy(i, 0);
db.print("#");
db.gotoxy(i, h - 1);
db.print("#");
}
for (int i = 0; i < h; i++) {
db.gotoxy(0, i);
db.print("#");
db.gotoxy(w - 1, i);
db.print("#");
}
// 画小球
db.gotoxy(ballx, bally);
db.print("o");
// 显示帧数
db.gotoxy(2, h + 1);
db.print("帧数: " + std::to_string(frame++));
db.enddraw();
sleep(50);
}
return 0;
}
十、总结
做控制台小游戏,清屏是绕不开的问题。我的建议是:
- 如果只是写个demo练手,system("cls")凑合能用,别在主循环里用就行
- 如果是进度条、计时器这种单行刷新,用\r是最优雅的方案
- 如果是多行界面但内容固定(比如棋盘、菜单),用gotoxy定点覆盖
- 如果是正经做游戏,直接上双缓冲,一劳永逸
双缓冲虽然代码量大一些,但理解了原理之后其实就是个模板代码,复制过去改改就能用。为了玩家(和自己)的眼睛,这点功夫值得花。
到此这篇关于c/c++控制台清屏方法全攻略的文章就介绍到这了,更多相关c/c++控制台清屏内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论