目录
[1. %-2d 格式化输出](#1. %-2d 格式化输出)
[2. \r 回车符](#2. \r 回车符)
[3. 为什么需要 %-2d?](#3. 为什么需要 %-2d?)
一、回车与换行概念
1、基本概念
-
回车(Carriage Return) : 将光标移动到当前行的行首,对应转义字符
\r
-
换行(Line Feed) : 将光标移动到下一行,对应转义字符
\n
2、历史背景
在老式打字机中:回车 :将打印头移回行首换行:将纸张向上移动一行
在老式键盘中的Enter键形象地表示了:
光标先换到下一行再回到行首位置,实际上就等价于\n+\r。

3、现代系统中的区别
-
Windows系统:使用
\r\n
表示换行 -
Unix/Linux系统:使用
\n
表示换行 -
Mac OS(早期):使用
\r
表示换行
二、行缓冲区实验引入
实验1:带换行符的输出
cpp
#include <stdio.h>
int main()
{
printf("hello bite!\n");
sleep(3);
return 0;
}
现象:立即输出"hello bite!",然后等待3秒后程序结束
实验2:不带换行符的输出
cpp
#include <stdio.h>
int main()
{
printf("hello bite!");
sleep(3);
return 0;
}
现象:先等待3秒,然后输出"hello bite!"并立即结束程序
这段代码仅删除了字符串末尾的'\n',但运行结果与之前有所不同:程序会先休眠3秒,然后才打印"hello world"并结束运行。这一现象证实了行缓冲区的存在。
显示器采用行刷新机制,只有当缓冲区遇到'\n'或被写满时才会输出内容。在修改后的代码中,由于缺少'\n',"hello world"首先被写入缓冲区。经过3秒休眠后,直到程序结束时才将该字符串输出到显示器上。
所以这里存在一个问题:我们如何在不使用'\n'的情况下将缓冲区中的数据输出?
三、标准I/O缓冲区机制
1、显示器刷新策略
-
行缓冲(Line Buffering):标准输出(stdout)默认使用行缓冲模式
-
遇到换行符
\n
时自动刷新 -
缓冲区(在内存中)满时自动刷新
-
程序正常结束时自动刷新
-
2、相关概念
-
标准流:
-
stdin
:标准输入(通常为键盘) -
stdout
:标准输出(通常为显示器) -
stderr
:标准错误(无缓冲,立即输出)
-
-
缓冲区类型:
-
全缓冲:文件I/O通常使用
-
行缓冲:终端I/O通常使用
-
无缓冲:如stderr
-
3、强制刷新缓冲区
fflush函数

功能:强制刷新指定流的输出缓冲区
然后回到上面的问题中,我们想到的解决方法是通过调用fflush函数强制刷新缓冲区,将数据立即显示在屏幕上:
实验3:手动刷新缓冲区
cpp
#include <stdio.h>
int main()
{
printf("hello bite!");
fflush(stdout); // 手动刷新输出缓冲区
sleep(3);
return 0;
}
现象:立即输出"hello bite!",然后等待3秒后程序结束
原理说明
-
标准输出通常是行缓冲的,遇到
\n
或缓冲区满时会自动刷新 -
使用
fflush(stdout)
可以强制刷新输出缓冲区
四、倒计时程序
cpp
#include <stdio.h>
#include <unistd.h>
int main()
{
int i = 10;
while(i >= 0)
{
printf("%-2d\r", i); // 使用\r回车不换行
fflush(stdout); // 刷新输出缓冲区
i--;
sleep(1); // 暂停1秒
}
printf("\n"); // 最后换行
return 0;
}
对此上面的结论,如上,我们可以根据其内容编写一个倒计时的程序。
功能:实现从10到0的倒计时显示,在输出下一个数之前都让光标先回到本行行首,数字在同一位置更新,就得到了倒计时的效果。

1. %-2d
格式化输出
-
%d
:以十进制形式输出整数i
。 -
%2d
:至少占用 2 字符宽度,如果数字不足 2 位,左侧补空格(右对齐)。-
例如:
-
10
→"10"
(刚好 2 位,不变) -
5
→" 5"
(左侧补 1 空格)
-
-
-
%-2d
:左对齐,不足 2 位时右侧补空格。-
例如:
-
10
→"10"
-
5
→"5 "
(右侧补 1 空格)
-
-
2. \r
回车符
-
\r
(Carriage Return):将光标移动到当前行的行首 ,但不换行。 -
效果:下一次输出会覆盖当前行的内容(实现倒计时的动态更新)。
3. 为什么需要 %-2d
?
-
如果直接用
%d\r
:- 当数字从
10
变为9
时,"10"
会被"9"
覆盖,但第二个字符0
可能残留,显示为"90"
(错误)。
- 当数字从
-
使用
%-2d
:-
强制每个数字占 2 位,右侧补空格,确保完全覆盖前一次输出。
-
例如:
-
10
→"10"
-
9
→"9 "
(覆盖"10"
,不会残留0
)
-
-
五、进度条实现
文件结构
process.h(头文件)
cpp
#pragma once
#include <stdio.h>
// 简单进度条版本
void process_v1();
// 可配置进度条版本
void FlushProcess(double total, double current);
process.c(进度条实现)
cpp
#include "process.h"
#include <string.h>
#include <unistd.h>
#define NUM 101 // 进度条长度+1
#define STYLE '=' // 进度条填充字符
// 版本1: 简单进度条
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(50000); // 50ms延迟
}
printf("\n");
}
// 版本2: 可配置进度条
void FlushProcess(double total, double current)
{
char buffer[NUM];
memset(buffer, 0, sizeof(buffer));
const char *lable = "|/-\\";
int len = strlen(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][%.1f%%][%c]\r", buffer, rate*100, lable[cnt]);
cnt++;
fflush(stdout);
}
当我们想make时,会发现下面的错误:

如果我们选择把定义放外面的话,for会不美观,但能解决错误:
cpp
int i = 0;
for(; i < num; i++)
{
buffer[i] = STYLE;
}
main.c(测试程序)
cpp
#include "process.h"
#include <stdio.h>
#include <unistd.h>
double total = 1024.0; // 模拟总下载量
double speed = 1.0; // 模拟下载速度
void DownLoad()
{
double current = 0;
while(current <= total)
{
FlushProcess(total, current);
// 模拟下载过程
usleep(3000); // 3ms延迟
current += speed; // 增加下载量
}
printf("\ndownload %.2lfMB Done\n", current);
}
int main()
{
// 模拟多次下载
for(int i = 0; i < 8; i++)
{
DownLoad();
}
return 0;
}
Makefile(构建文件)
bash
SRC=$(wildcard *.c)
OBJ=$(SRC:.c=.o)
BIN=processbar
$(BIN):$(OBJ)
gcc -o $@ $^
%.o:%.c
gcc -c $<
.PHONY:
clean:
rm -f $(OBJ) $(BIN)
结合上面的错误,我们还可以再Makefile中修改,直接按照报错要求来:
bash
SRC=$(wildcard *.c)
OBJ=$(SRC:.c=.o)
BIN=processbar
$(BIN): $(OBJ)
gcc -o $@ $^ -std=c99 # 修正:添加 -std=c99
%.o: %.c
gcc -c $< -std=c99 # 可选:如果其他 .c 文件也需要 C99
.PHONY: clean
clean:
rm -f $(OBJ) $(BIN)
但是我们按照要求来修改后,我们发现还是会出现有warning,但是能够成功构建可执行文件,我们不用管它:

-
在某些系统(如较新的 Linux)中,
usleep()
被标记为 废弃(obsolete),默认可能不暴露它的声明。 -
现代 Linux 推荐使用
nanosleep()
,usleep()
只是为了兼容性保留。 -
usleep()
确实被调用了,但编译器认为它的声明是"隐式"的(即没有找到正式的函数原型)。 -
程序能运行 ,是因为在链接阶段,
usleep()
的实现(在libc
或librt
中)被正确找到,但编译阶段仍然有警告。
功能说明
-
process_v1():简单的100步进度条,自带循环
-
FlushProcess():更灵活的进度条,根据传入的总量和当前值计算进度
-
DownLoad():模拟下载过程,展示进度条的实际应用
进度条包含以下元素:
-
进度条本身(用=填充)
-
完成百分比
-
旋转指示器(|/-\)表示程序正在运行