1. 项目概述与核心价值
最近在社区里看到不少朋友在讨论C/C++的学习路径,尤其是如何从枯燥的语法学习过渡到有实际手感的小项目。很多人卡在“看得懂代码,但自己写不出来”的阶段。今天,我想分享一个我当年用来“破冰”的经典练手项目:用C/C++在控制台模拟倒计时与下载进度条。别看它名字简单,麻雀虽小,五脏俱全。它几乎涵盖了初学者从“会写”到“会用”的所有关键节点:控制台光标操作、时间处理、循环控制、格式化输出,以及最重要的——如何将抽象的逻辑转化为直观的、动态的视觉反馈。
这个项目能做什么?简单说,就是让你写的程序不再是冷冰冰地输出一行行静态文字。你可以实现一个从10秒开始的爆炸倒计时,数字在屏幕上动态跳动;或者模拟一个文件下载过程,进度条从0%逐渐填充到100%,旁边还显示着实时的下载速度。它解决的问题,正是初学者对“程序交互性”和“实时性”的初步感知缺失。无论是刚学完C语言基础语法,想找点有成就感的小玩意练手的新人,还是学了C++想巩固一下标准库(如<chrono>,<thread>)用法的朋友,这个项目都非常合适。通过它,你能真切地感受到,代码是如何“活”起来,并与用户(哪怕只是通过控制台)进行动态交流的。
2. 项目整体设计与思路拆解
2.1 为什么选择控制台模拟?
很多初学者一听到图形界面就头大,Qt、MFC门槛不低,容易让人在环境配置和复杂API中迷失,忘了编程的乐趣。而控制台(Console)是每个C/C++学习者最早接触的“画布”。在这个黑白(或彩色)的字符矩阵里实现动态效果,其核心挑战在于如何在不借助图形库的情况下,制造“动画”错觉。这迫使我们去理解更底层的原理:屏幕上的内容是如何被刷新的?答案是:通过不断输出字符并控制光标的位置,覆盖之前的内容。
我们的核心思路就是**“擦除-重绘”**。想象一下画黑板报,要更新一个数字,我们不是重新画整块黑板,而是用板擦擦掉旧数字,在同一个位置写上新的。在控制台里,“板擦”就是输出空格或退格符,“重绘”就是输出新的内容。这个项目的高明之处在于,它用两个看似简单的需求(倒计时、进度条),引出了控制台编程的几个关键技术点,让我们在解决具体问题的过程中,自然而然地掌握它们。
2.2 倒计时 vs. 进度条:两种不同的动态模型
虽然都是动态显示,但倒计时和进度条在实现逻辑上有显著区别,理解这点对设计程序结构至关重要。
倒计时模型(离散、状态驱动): 倒计时是一个离散的、跳跃的过程。它的状态(剩余秒数)每隔一个固定的时间单位(比如1秒)才改变一次。在两次状态改变之间,屏幕上的内容是完全静止的。因此,它的核心逻辑是一个带延迟的循环:显示当前秒数 -> 睡眠1秒 -> 清除当前显示 -> 秒数减1 -> 重复。它的动态感来源于状态切换的间隔和切换瞬间的视觉更新。
进度条模型(连续、过程驱动): 进度条模拟的是一个连续的过程(如下载)。它的状态(完成百分比)理论上可以非常频繁地更新(比如每100毫秒)。更重要的是,它的视觉表现(条的长度)是随着百分比连续增长的。即使后台的完成度是离散更新的(如下载了若干个数据块),前端的显示也需要营造出一种平滑、连续的错觉。因此,它的核心逻辑是在循环中频繁计算和更新一个基于比例的视觉长度。
这两种模型几乎涵盖了所有简单动态效果的设计思路。通过这个项目,你不仅能学会实现它们,更能理解其背后的状态机思想和时间-视觉映射关系。
2.3 关键技术点预剖析
在动手编码前,我们先梳理一下需要攻克的技术山头:
- 光标控制:这是实现“定点擦除-重绘”的基础。我们需要把光标移动到控制台特定位置,而不是永远在下一行输出。在Windows和Linux/macOS下,方法完全不同,这是我们遇到的第一个平台差异点。
- 时间控制:倒计时需要“等一秒”,进度条需要“定期更新”。如何让程序精确地暂停一段时间?这里会涉及到
sleep函数,但直接使用可能会带来一些问题(比如阻塞整个线程),我们会探讨更优的做法。 - 进度计算与可视化:给定一个总大小和当前已完成的量,如何计算百分比?如何将这个百分比转换成屏幕上一定长度的“条”?这里会有整数和浮点数运算的考量,以及如何用简单的字符(如
=,#,-)拼出进度条。 - 控制台刷新与性能:频繁地清屏和重绘整个控制台 (
system(“cls”/“clear”)) 会导致屏幕闪烁,体验很差。如何局部更新,只重绘需要变化的部分?这关系到程序的流畅度和观感。
理清了这些思路,我们就可以开始搭建开发环境,并一步步实现它了。
3. 核心细节解析与实操要点
3.1 跨平台的光标控制:第一个拦路虎
控制台光标操作没有C/C++标准库的支持,必须使用平台相关的API或转义序列。这是本项目第一个需要处理兼容性的地方。
Windows平台:Windows提供了windows.h头文件中的控制台API。核心函数是SetConsoleCursorPosition。
#include <windows.h> void gotoxy(int x, int y) { COORD coord; coord.X = x; coord.Y = y; SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord); }这个gotoxy函数将光标移动到控制台窗口的指定列(x)和行(y)。注意,坐标系的原点(0, 0)通常是屏幕左上角。
Linux/macOS (Unix-like) 平台:这类系统通常使用VT100终端转义序列。通过向标准输出打印特定的字符串来控制光标。
#include <stdio.h> void gotoxy(int x, int y) { printf(“\033[%d;%dH”, y+1, x+1); // 转义序列的行列通常从1开始计数 }这里的\033是ESC键的ASCII码(八进制33,十六进制1B),[是控制序列引导码,%d;%dH表示将光标移动到第y行第x列。
实操心得:封装与条件编译我强烈建议你将
gotoxy这样的平台相关函数封装起来,并使用预处理器进行条件编译。这样主逻辑代码可以保持干净。// console_utils.h #ifdef _WIN32 #include <windows.h> #else #include <stdio.h> #endif void gotoxy(int x, int y); // console_utils.c void gotoxy(int x, int y) { #ifdef _WIN32 COORD coord = { (SHORT)x, (SHORT)y }; SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), coord); #else printf(“\033[%d;%dH”, y + 1, x + 1); fflush(stdout); // Linux/macOS下确保立即输出 #endif }这样,在你的主程序中,只需
#include “console_utils.h”并调用gotoxy(),编译器会自动选择正确的实现。
3.2 时间控制:sleep的陷阱与更佳选择
让程序暂停,第一个想到的可能是sleep()或_sleep()。在Windows下是Sleep(ms)(注意大写S,单位毫秒),在Linux下是sleep(s)(单位秒)或usleep(微秒)。
不推荐直接使用sleep的原因:在倒计时中,如果你写Sleep(1000),意味着程序会阻塞整整1秒。在这1秒内,你的程序无法响应任何其他事件(虽然我们这个简单项目可能没有其他事件)。更关键的是,sleep的精度并不高,它通常保证至少睡眠指定的时间,但可能会更长,尤其是系统负载高的时候。
一个更好的模式:基于时间点的循环对于倒计时,我们可以换一种思路:不是“执行一次,睡一秒”,而是“在正确的时刻执行”。我们记录一个目标时间点,然后循环检查当前时间是否到达了那个点。
// C++11 示例,使用 <chrono> #include <chrono> #include <thread> auto start = std::chrono::steady_clock::now(); int countdown = 10; while (countdown > 0) { // 计算下一次应该更新的时间点 auto next_time = start + std::chrono::seconds(10 - countdown + 1); // 忙等待或睡眠一小段时间来接近目标时间点(为了降低CPU占用) std::this_thread::sleep_until(next_time); // 更新显示 std::cout << “\rTime left: “ << countdown << “ “ << std::flush; // ‘\r’回到行首 --countdown; }这种方法更精确,并且将“时间管理”的逻辑和“状态更新”的逻辑分离开,程序结构更清晰。对于进度条,我们则是在循环中计算从开始到现在经过的时间占总时间的比例。
注意事项:关于
std::flush和‘\r’‘\r’(回车符)可以将光标移回当前行的行首,但不换行。这是实现单行动态更新的关键(比如倒计时数字原地变化)。std::flush用于立即刷新输出缓冲区,确保内容立刻显示在屏幕上。在动态更新时,这两个操作经常一起使用。
3.3 进度条的可视化:从百分比到图形
进度条的本质是将一个0%到100%的数值,映射成一段固定宽度的视觉表示。假设我们决定进度条总长度为50个字符。
步骤1:计算填充长度
int total_width = 50; int filled_length = (int)(percentage / 100.0 * total_width);这里percentage是当前进度百分比。注意类型转换,用浮点数计算保证精度,再转为整数决定实际填充多少个字符。
步骤2:构造进度条字符串我们可以用两种字符表示:已填充部分和未填充部分。
char bar[total_width + 1]; // +1 给字符串结尾的 ‘\0’ for (int i = 0; i < total_width; ++i) { if (i < filled_length) { bar[i] = ‘#’;// 或用 ‘=’ } else { bar[i] = ‘-’; } } bar[total_width] = ‘\0’; // 字符串终止符现在,bar就是一个像“###############—————————”的字符串。
步骤3:添加装饰和文本通常我们会在进度条两边加上括号,并显示百分比数字。
printf(“[%s] %.1f%%“, bar, percentage);为了在同一行更新,记得使用‘\r’。
进阶美化:
- 动态头部:可以让进度条头部用一个特殊字符,如
‘>’,随着进度移动。 - 颜色:Windows下可用
SetConsoleTextAttribute,Linux下可用类似“\033[32m”(绿色)的转义序列来给进度条上色。 - 速度显示:在进度条旁边,可以计算并显示平均下载速度或剩余时间。
3.4 避免闪烁:局部更新与双缓冲思想
如果你在每次更新时都使用system(“cls”)清屏,然后重绘所有内容,屏幕会明显闪烁,因为清屏到重绘之间有一个短暂的空白帧。
局部更新是王道: 对于倒计时,如果数字位置固定,就用gotoxy定位到那个位置,只重写那个数字。 对于进度条,如果它始终在同一行,就用‘\r’回到行首,重写整行。
双缓冲的启发: 在复杂图形界面中,双缓冲是解决闪烁的经典技术:先在内存里画好一整帧图像,然后一次性快速切换到屏幕上。在控制台里,我们虽然不能真正实现双缓冲,但可以借鉴其思想:尽量减少不必要的输出,只更新发生变化的部分。在每次循环绘制前,先想清楚哪些字符变了,只输出那些变化的字符,而不是整行或整屏。
4. 实操过程与核心环节实现
下面,我将分别用C语言和C++的风格来实现倒计时和进度条,并融合上面提到的技巧。
4.1 基础倒计时实现(C语言风格)
我们先实现一个最简单的、单行更新的倒计时。
#include <stdio.h> #include <stdlib.h> #ifdef _WIN32 #include <windows.h> #define SLEEP_MS(ms) Sleep(ms) #else #include <unistd.h> #define SLEEP_MS(ms) usleep((ms) * 1000) #endif int main() { int countdown = 10; // 从10秒开始 printf(“倒计时开始:\n”); while (countdown >= 0) { // 使用 \r 回到行首,覆盖上一次的输出 // 用空格覆盖旧的数字,防止残留字符(例如从10到9,’0‘可能残留) printf(“\r倒计时: %2d 秒 “, countdown); fflush(stdout); // 立即输出 if (countdown == 0) { printf(“\n时间到!\n”); break; } SLEEP_MS(1000); // 睡眠1秒 countdown--; } return 0; }这个版本非常简单,但有几个问题:1) 使用SLEEP_MS(1000)会阻塞;2) 如果倒计时位置不在行首,或者想在其他位置显示,‘\r’就不好用了。
4.2 增强版倒计时(支持任意位置,C++风格)
我们引入之前封装的gotoxy,并尝试使用更精确的时间控制。
// console_utils.h 和 .cpp 如上文所述,此处省略 #include “console_utils.h” #include <iostream> #include <chrono> #include <thread> int main() { const int start_x = 10; const int start_y = 5; int countdown = 10; std::cout << “倒计时将在指定位置显示...\n”; // 给一点时间让用户看到提示 std::this_thread::sleep_for(std::chrono::seconds(1)); auto start_time = std::chrono::steady_clock::now(); for (int i = countdown; i >= 0; --i) { // 计算本次应该显示的时间点 auto display_time = start_time + std::chrono::seconds(countdown - i); std::this_thread::sleep_until(display_time); gotoxy(start_x, start_y); std::cout << “[“; if (i > 3) { std::cout << “ “; // 可以在这里根据时间改变颜色或样式 } else { std::cout << “!”; // 最后3秒用感叹号警示 } std::cout << “] 剩余时间: “ << i << “ 秒 “; std::cout.flush(); // C++中等效于fflush(stdout) } gotoxy(start_x, start_y + 1); std::cout << “*** 倒计时结束! ***” << std::endl; return 0; }这个版本将倒计时固定在了屏幕的(10, 5)位置,并且最后3秒改变了提示符,体验更好。
4.3 基础下载进度条模拟(C语言风格)
我们来模拟一个总大小为 1024*1024 字节(1MB)的文件下载。
#include <stdio.h> #include <stdlib.h> #include <time.h> #ifdef _WIN32 #include <windows.h> #define SLEEP_MS(ms) Sleep(ms) #else #include <unistd.h> #define SLEEP_MS(ms) usleep((ms) * 1000) #endif int main() { const long long total_size = 1024 * 1024; // 1 MB long long downloaded = 0; const int bar_width = 50; clock_t start_time = clock(); srand((unsigned int)time(NULL)); // 用随机数模拟不稳定的下载速度 printf(“开始下载模拟...\n”); while (downloaded < total_size) { // 模拟每次下载一个数据块,大小随机 int chunk_size = rand() % 4096 + 1024; // 1KB ~ 5KB if (downloaded + chunk_size > total_size) { chunk_size = (int)(total_size - downloaded); } downloaded += chunk_size; SLEEP_MS(50 + rand() % 100); // 模拟网络延迟 // 计算进度 double percentage = (double)downloaded / total_size * 100.0; int filled_len = (int)(percentage / 100.0 * bar_width); // 计算速度 clock_t current_time = clock(); double elapsed_sec = (double)(current_time - start_time) / CLOCKS_PER_SEC; double speed_kbps = (elapsed_sec > 0) ? (downloaded / 1024.0 / elapsed_sec) : 0; // 绘制进度条 printf(“\r[“); for (int i = 0; i < bar_width; ++i) { if (i < filled_len) putchar(‘#’); else if (i == filled_len) putchar(‘>’); // 动态头部 else putchar(‘-’); } printf(“] %6.2f%% | Speed: %7.2f KB/s”, percentage, speed_kbps); fflush(stdout); } printf(“\n下载完成!\n”); return 0; }这个程序模拟了真实的下载过程:不固定的数据块、随机的网络延迟、实时计算下载速度。进度条有动态头部‘>’,视觉上更生动。
4.4 面向对象的进度条设计(C++风格)
用C++的类来封装进度条逻辑,可以使代码更模块化、可复用。
// progress_bar.h #ifndef PROGRESS_BAR_H #define PROGRESS_BAR_H #include <string> #include <chrono> class ProgressBar { public: ProgressBar(long long total, int width = 50); void update(long long current); void display() const; bool is_complete() const; private: long long m_total; long long m_current; int m_width; std::chrono::steady_clock::time_point m_start_time; mutable std::string m_bar_string; // 缓存进度条字符串,避免每次重建 void update_bar_string(); }; #endif // PROGRESS_BAR_H// progress_bar.cpp #include “progress_bar.h” #include <iostream> #include <iomanip> ProgressBar::ProgressBar(long long total, int width) : m_total(total), m_current(0), m_width(width), m_start_time(std::chrono::steady_clock::now()) { m_bar_string.reserve(width + 10); // 预分配空间 } void ProgressBar::update(long long current) { m_current = (current > m_total) ? m_total : current; update_bar_string(); } void ProgressBar::update_bar_string() { double percentage = (m_total > 0) ? (static_cast<double>(m_current) / m_total * 100.0) : 100.0; int filled_len = static_cast<int>(percentage / 100.0 * m_width); m_bar_string.clear(); m_bar_string.push_back(‘[’); for (int i = 0; i < m_width; ++i) { if (i < filled_len) m_bar_string.push_back(‘=’); else if (i == filled_len) m_bar_string.push_back(‘>’); else m_bar_string.push_back(‘ ‘); } m_bar_string.push_back(‘]’); // 计算速度和剩余时间 auto now = std::chrono::steady_clock::now(); auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_start_time); double elapsed_sec = elapsed.count() / 1000.0; double speed = (elapsed_sec > 0) ? (m_current / 1024.0 / 1024.0 / elapsed_sec) : 0; // MB/s char buffer[256]; if (speed > 0 && percentage < 100) { double remaining_sec = (m_total - m_current) / (speed * 1024 * 1024); snprintf(buffer, sizeof(buffer), “ %6.2f%% | %.2f MB/s | ETA: %.0fs “, percentage, speed, remaining_sec); } else { snprintf(buffer, sizeof(buffer), “ %6.2f%% “, percentage); } m_bar_string += buffer; } void ProgressBar::display() const { std::cout << “\r” << m_bar_string << std::flush; } bool ProgressBar::is_complete() const { return m_current >= m_total; } // main.cpp 中使用 #include “progress_bar.h” #include <thread> #include <random> int main() { const long long total_size = 100 * 1024 * 1024LL; // 100MB ProgressBar bar(total_size, 60); std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<> dis(50, 200); // 模拟50-200ms的延迟 std::uniform_int_distribution<> chunk_dis(512, 8192); // 模拟0.5KB-8KB的数据块 long long downloaded = 0; while (!bar.is_complete()) { int chunk = chunk_dis(gen); if (downloaded + chunk > total_size) chunk = total_size - downloaded; downloaded += chunk; bar.update(downloaded); bar.display(); std::this_thread::sleep_for(std::chrono::milliseconds(dis(gen))); } std::cout << “\n下载模拟完成!” << std::endl; return 0; }这个C++版本将进度条的状态和行为封装在一个类里。主程序逻辑非常清晰:创建进度条,在循环中更新进度并显示。ProgressBar类内部负责计算百分比、生成可视化字符串、估算下载速度和剩余时间(ETA)。这种设计使得进度条成为一个独立的、可重用的组件。
5. 常见问题与排查技巧实录
在实际编写和运行这类控制台动态程序时,你可能会遇到下面这些问题。这里是我踩过坑后总结的排查思路。
5.1 问题一:输出乱码或光标位置不对
现象:进度条或倒计时没有在预期位置显示,或者出现了奇怪的字符(如[2J,[H)。原因:这几乎肯定是平台相关代码(尤其是光标移动的转义序列)在错误的平台上运行了。排查:
- 检查你的条件编译宏是否正确。确保
#ifdef _WIN32和#else分支覆盖了你的目标平台。 - 在Linux/macOS下,确保你的终端支持VT100或ANSI转义序列。绝大多数现代终端(如gnome-terminal, iterm2, xterm)都支持。如果你在极简环境(如某些嵌入式串口终端)下运行,可能需要额外配置。
- 在Windows CMD中测试转义序列:旧版Windows CMD默认不启用VT转义序列支持。但从Windows 10周年更新(1607)开始,可以通过API启用。一个更简单的方法是使用
system(“”)或直接调用SetConsoleMode来启用虚拟终端处理。不过,为了最大兼容性,在Windows下坚持使用windows.h的API是最稳妥的。
5.2 问题二:屏幕闪烁严重
现象:更新进度条或倒计时时,整个屏幕或区域有明显的闪烁感。原因:你很可能在每次更新时都清除了整个屏幕(如调用system(“cls”)),或者输出了大量不必要的换行符,导致控制台频繁滚动。解决:
- 绝对避免在循环内使用
system(“cls/clear”)。这是导致闪烁的元凶。 - 坚持局部更新原则:
- 对于单行进度条,只用
‘\r’。 - 对于固定位置的倒计时,用
gotoxy回到那个坐标再输出。 - 如果需要更新屏幕多个不同位置的内容,分别对每个位置使用
gotoxy,而不是清屏重绘。
- 对于单行进度条,只用
- 减少输出频率:如果更新太快(比如每毫秒更新一次),人眼来不及看,也会造成视觉疲劳和闪烁感。可以考虑在循环内增加一个小的延迟(如
Sleep(10)或std::this_thread::sleep_for(10ms)),或者根据实际进度变化来决定是否更新显示(比如百分比整数部分变化了再更新)。
5.3 问题三:进度条跑到下一行或覆盖了其他输出
现象:进度条显示一次后,下一次更新跑到了新的一行,或者把之前的提示文字给覆盖了。原因:你没有处理好行尾。printf或cout在输出字符串后,如果字符串末尾没有换行符‘\n’,光标会停在字符串的后面。当你下次使用‘\r’时,光标会回到当前行的行首。但是,如果你的进度条字符串长度变化了(比如从“[###—] 10%”变成“[####—] 20%”),更短的新字符串无法完全覆盖旧字符串,旧字符串的残留部分(如那个“0%”的0)就会显示出来。解决:
- 固定输出宽度:确保每次输出的字符串长度一致。可以在格式字符串中指定宽度,比如用空格填充。
// 假设进度条固定部分长50,我们固定总输出长度为70 printf(“\r[%-50s] %6.2f%%“, bar, percentage); // %-50s 表示左对齐且宽度为50的字符串,不足补空格 - 手动添加尾随空格:在输出字符串的末尾,添加足够的空格,以确保能覆盖上一次可能更长的输出。
printf(“\r[%s] %.1f%% “, bar, percentage); // 末尾多打几个空格 - 清空到行尾:一些终端支持转义序列
\033[K来清除从光标位置到行尾的内容。在输出前先清除,再绘制。
这种方法更彻底,但同样是平台相关的(在Windows CMD下可能不生效)。printf(“\r\033[K[%s] %.1f%%“, bar, percentage); // ‘\033[K’ 清除行
5.4 问题四:程序占用CPU过高(忙等待)
现象:运行倒计时或进度条模拟时,电脑风扇狂转,任务管理器显示CPU使用率很高。原因:你很可能使用了一个“忙等待”循环。例如:
while (!is_time_to_update()) { // 空循环,疯狂检查条件 }这个循环会不停地检查条件,占满一个CPU核心。解决:
- 使用睡眠函数:在循环体内,如果不需要实时响应到微秒级别,一定要加入睡眠。
while (!is_time_to_update()) { SLEEP_MS(10); // 睡眠10毫秒,大大降低CPU占用 } - 使用基于时间的等待:如前文所述,使用
sleep_until或计算需要睡眠的时间,然后调用sleep_for。这是最优雅和高效的方式,它让出CPU时间片,直到需要工作时才被唤醒。
5.5 问题五:下载速度或ETA计算不准
现象:进度条旁边显示的下载速度跳动非常剧烈,或者剩余时间(ETA)估计得离谱。原因:你是用“瞬时速度”来计算的。瞬时速度 = 最近一个数据块大小 / 下载该数据块的时间。由于网络波动,数据块大小和下载时间都是随机的,导致瞬时速度波动极大。优化:
- 使用移动平均:维护一个最近N次下载速度的队列,计算其平均值。这能平滑掉短期波动,显示的速度更稳定。
- 使用总平均速度:
总平均速度 = 总下载量 / 总耗时。这个值比较稳定,但无法反映当前网络的最新状态。 - ETA的计算:
剩余时间 = 剩余数据量 / 当前速度。这里的“当前速度”如果用瞬时速度,ETA会跳来跳去。通常采用平滑后的速度或总平均速度来计算ETA会更友好。一个常见的技巧是,在刚开始下载时(比如前3秒),由于数据量小,速度计算可能不准,可以先不显示ETA,或者显示“计算中…”。
// 一个简单的移动平均示例 #include <deque> class SpeedCalculator { std::deque<double> recent_speeds; const size_t window_size = 10; // 计算最近10次速度的平均 public: void add_sample(double speed) { recent_speeds.push_back(speed); if (recent_speeds.size() > window_size) { recent_speeds.pop_front(); } } double get_average_speed() const { if (recent_speeds.empty()) return 0.0; double sum = 0.0; for (double s : recent_speeds) sum += s; return sum / recent_speeds.size(); } };在进度条更新时,调用add_sample记录当前瞬时速度,然后用get_average_speed来显示和计算ETA,效果会好很多。
这个项目虽然小,但就像一颗棱镜,能折射出C/C++编程中许多基础而重要的概念。从平台差异处理、时间管理、循环控制,到字符串格式化、算法思维(如移动平均),再到代码封装和模块化设计,每一步都值得细细琢磨。我建议你不要止步于复制代码,而是尝试去修改它:改变进度条的样式、增加暂停/继续功能、用多线程模拟同时下载多个文件、甚至尝试将其封装成一个真正的库。这些探索,才是从小项目中学到大本领的关键。