简介:C语言模拟时钟源码包,面向初学C语言且希望掌握日期时间处理技巧的读者。压缩包共包含1个C语言源文件,整体仅2KB,代码精简,便于直接阅读和编译。程序围绕时间处理展开:先获取当前时间戳,再转换为时间结构体并读取时分秒字段,最后按格式输出;主循环中设置延时实现每秒刷新,在控制台显示动态时钟。除基础逻辑外,还覆盖时区异常与函数调用失败等简单处理,有助于理解系统时间机制。通过这份代码可清晰看到时间戳到可读信息的转换过程,以及循环与延时刷新的配合方式,对巩固结构体、标准库用法很有帮助,也能为后续更复杂的项目打下基础。资源已有1090人学习,适合课程设计、自学练手或扩展为闹钟应用。
1. C语言模拟时钟:每个 C 程序员都绕不开的“会走”的课设
C 语言模拟时钟,几乎是每个学 C 的人都会撞上的第一道课设题。这题看起来只是把当前时间画到屏幕上,实际牵扯 time() 取时间、struct tm 字段换算、角度转坐标的数学、还有图形库或控制台光标的刷新机制,每一步都藏着坑。这篇内容把两条实现路线都拆开讲:一个字符版跑通逻辑,一个图形版画表盘秒针,再补上我踩过的环境问题和漂移问题。适合马上要交课设的学生,也适合想用源码复习时间与绘图接口的从业者。
2. 两条实现路线分清楚:图形表盘 vs 字符数字钟,先选对再动手
2.1 路线差异:不同效果对应不同的库和环境
模拟时钟不是只有一种写法。常见做法是先决定你要的是“像钟”还是“能跑”,这决定了后续所有代码结构。我见过不少同学上来就抄图形库代码,结果图形环境配了半天,机房电脑一编译就报错,最后只能临时改回字符版。
| 实现路线 | 关键头文件 | 本质 | 适合场景 | 工程成本 |
|---|---|---|---|---|
| 字符数字钟 | time.h + 控制台定位 | 每秒刷新一行字符串 | 机房演示、跨环境优先 | 低 |
| 图形表盘 | graphics.h + 图形库 | 画圆画线实时移动指针 | 课设答辩、视觉演示 | 中高 |
选型建议很直接:如果你要在自己不熟悉的电脑上演示,字符版几乎不会翻车;如果答辩现场需要视觉冲击力,图形表盘版更容易讲出东西。我一般会先写字符版验证“时间获取和刷新逻辑”,确认思路没问题后再套图形库画表盘,两边都保留一份。
2.2 时间从哪来:time_t、localtime 与 struct tm 字段偏移
时钟程序的数据源头只有一个:操作系统时间。C 标准库用 time_t 表示从某个基准时刻到现在的秒数,再用 localtime 把秒数拆成年月日时分秒,拆出来的结果放在 struct tm 里。
#include <stdio.h> #include <time.h> int main(void) { time_t raw; struct tm *now; time(&raw); now = localtime(&raw); printf("当前时间: %04d-%02d-%02d %02d:%02d:%02d\n", now->tm_year + 1900, now->tm_mon + 1, now->tm_mday, now->tm_hour, now->tm_min, now->tm_sec); return 0; }逻辑说明:time(&raw) 把当前的秒数写入 raw 这个变量;localtime(&raw) 返回一个 struct tm 指针,里面的 tm_hour、tm_min、tm_sec 就是我们要取的时分秒。打印时必须给 tm_year 加 1900、给 tm_mon 加 1,因为这两个字段的基准是“从 1900 年起的年数”和“从 0 起的月份”,这是 C 语言里一个典型的偏移陷阱。
参数说明:time(NULL) 也是常见写法,表示不关心返回值只获取当前时间戳。另外要注意,localtime 的返回值指向内部静态存储区,如果你连续调用两次并同时保存两个指针,第二个结果会覆盖第一个。多线程场景下建议用 localtime_s 或者先把结果 memcpy 到局部变量。
2.3 指针转动的坐标数学:为什么秒针每秒转 6 度
图形版的核心不只是画圆,而是要把“时间”换算成“角度”,再把“角度”换算成屏幕上的像素坐标。表盘一圈 360 度,分给 60 秒,所以秒针每秒转 6 度;分针每分钟转 6 度;时针每小时转 30 度。这个比例关系是整个模拟时钟的数学基础。
屏幕坐标系和数学课本坐标系有个关键差异:屏幕的 y 轴是向下增长的,如果直接用 sin/cos 计算,指针会倒着转。常见做法是以 12 点方向为 0 度,顺时针方向为正,此时 x 方向用 sin、y 方向用减号处理。
#include <math.h> void calc_clock_pos(int center_x, int center_y, int radius, double angle_deg, int *out_x, int *out_y) { double rad = angle_deg * 3.14159265 / 180.0; *out_x = (int)(center_x + radius * sin(rad)); *out_y = (int)(center_y - radius * cos(rad)); }逻辑说明:这个函数接收圆心坐标、指针长度和角度,输出指针末端点的坐标。rad 是把角度换算成弧度,sin 和 cos 完成投影,强制转 int 是告诉编译器我们只需要像素级精度。
参数说明:角度基准是 12 点方向为 0 度。比如秒针指向 3 点方向时角度是 90 度,x 坐标是 cx + r * sin(90°),y 坐标是 cy - r * cos(90°),算出来刚好落在圆心右侧,符合预期。指针长度 radius 的取值取决于画布大小,后面画表盘时再细调。
3. 先让数字钟跑起来:刷新循环、光标定位与时间校准
3.1 sleep(1000) 会越走越慢:三种刷新策略怎么选
很多初版代码长这样:while(1) { 取时间; 打印; Sleep(1000); }。跑五分钟没感觉,跑半小时就开始慢。原因是 Sleep(1000) 只保证“至少挂起 1000 毫秒”,而取时间、打印、循环开销都会让实际周期超过 1 秒,误差一步一步累积。
我踩过这个坑之后,改成“按绝对目标时间校准”的方案:记录下一次刷新的目标时刻,每次循环结束先算目标时刻和当前时刻的差值,只睡这个差值。这样即使某次循环慢了 10 毫秒,下一次也会自动补齐。
#include <stdio.h> #include <time.h> #include <windows.h> int main(void) { long next = GetTickCount(); int last_sec = -1; while (1) { time_t raw = time(NULL); struct tm *now = localtime(&raw); if (now->tm_sec != last_sec) { printf("\r%02d:%02d:%02d", now->tm_hour, now->tm_min, now->tm_sec); fflush(stdout); last_sec = now->tm_sec; } next += 1000; if (next < GetTickCount()) { /* 某次刷新超过1秒,直接追到当前时间,避免连跳 */ next = GetTickCount() + 1000; } Sleep(next - GetTickCount()); } return 0; }逻辑说明:GetTickCount() 返回开机以来的毫秒数,next 记录的是“下一次整秒”的目标时刻。每次循环结束时 next 加 1000,Sleep 只睡剩余时间,所以循环体本身的耗时会自动补偿。如果某次卡顿超过 1 秒,就重置为当前时刻加 1 秒,防止连续空转把时间追乱。
参数说明:Sleep 的单位是毫秒,最后一次 printf 加的 \r 是回车不换行,让时间原地更新而不是一行行往下刷,这是字符版不闪烁的关键之一。fflush(stdout) 是让输出立刻落屏,否则终端缓冲可能延迟显示。
3.2 用光标定位替代清屏:消灭闪烁最直接的一步
不少同学为了刷新干净,会用 system("cls") 清屏再重画。第一次跑没问题,跑起来会闪到怀疑人生,因为整个屏幕被反复擦掉重绘,人眼看到的是一明一暗的闪烁。正确做法是定位到固定位置,只覆盖那一段字符。
Windows 控制台里可以用 SetConsoleCursorPosition 把光标挪到指定坐标,Windows.h 里封装好了一套接口:
#include <windows.h> void gotoxy(int x, int y) { COORD pos; pos.X = (SHORT)x; pos.Y = (SHORT)y; SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos); }逻辑说明:COORD 是 Windows API 里的坐标结构体,x 和 y 是像素字符位置,SetConsoleCursorPosition 把光标移动到控制台的指定位置。时钟程序只往那一格输出,不再触碰屏幕其他区域,闪烁问题从根源上消失。
参数说明:x 是列号,y 是行号,都是从 0 开始。控制台窗口大小不同,可用的坐标范围也不同,写入超出窗口的位置会被忽略。如果想在 Linux 终端实现同样效果,可以用 ANSI 转义序列 printf("\033[H") 回到左上角,思路一致,接口不同。
3.3 组装一个最小可运行的数字钟:三段代码拼起来
有了取时间、定位光标、隐藏光标这三个能力,数字钟的主程序就非常短了。再补一个隐藏光标的小函数,因为光标停在时间数字后面一闪一闪很影响观感。
#include <stdio.h> #include <time.h> #include <windows.h> void gotoxy(int x, int y) { COORD pos = { (SHORT)x, (SHORT)y }; SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos); } void hide_cursor(void) { HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_CURSOR_INFO ci; GetConsoleCursorInfo(hOut, &ci); ci.bVisible = FALSE; SetConsoleCursorInfo(hOut, &ci); } int main(void) { hide_cursor(); while (1) { time_t raw = time(NULL); struct tm *now = localtime(&raw); gotoxy(0, 0); printf("%02d:%02d:%02d", now->tm_hour, now->tm_min, now->tm_sec); Sleep(200); } return 0; }逻辑说明:主循环里取一次时间、定位一次光标、打印一次。Sleep(200) 不是等 200 毫秒刷一次,这里的 200 毫秒只是轮询间隔,让程序不要太占 CPU;打印出来的内容只在秒变化时产生视觉差异,但每次 printf 都会覆盖旧内容,所以看起来是每秒跳一下。
参数说明:hide_cursor 的 bVisible 设为 FALSE 后,整个控制台光标会消失,退出程序前最好恢复可见,否则终端里光标一直不闪,用户会以为程序卡死。
4. 画出会转的表盘:图形初始化、刻度线与三针联动
4.1 图形库初始化:两种 initgraph 形态都要认识
图形表盘需要用图形库提供的画圆、画线函数。最经典的是早期 C 教材里带的 graphics.h,配合老式 16 位环境使用。这套环境的 initgraph 签名长这样:initgraph(&driver, &mode, "路径")。后来的一些兼容图形库改成了 initgraph(宽度, 高度),直接指定画布尺寸,不需要探测硬件。
#include <graphics.h> #include <conio.h> void init_clock_graph(void) { #ifdef OLD_GRAPHICS_LIB int driver = DETECT; int mode = 0; initgraph(&driver, &mode, ""); #else initgraph(640, 480); #endif }逻辑说明:宏开关用来区分两套图形的初始化方式。老式写法要先定义驱动类型 DETECT,让图形库自动检测硬件;新式写法直接给画布宽度和高度,640x480 是常见的 VGA 分辨率,大多数表盘都能放下。
参数说明:老式写法第三个参数是图形驱动所在路径,传空字符串表示使用当前目录。如果你从网上下载的源码编译报错,优先检查这段初始化代码和你安装的图形库版本匹不匹配。
4.2 刻度与数字:60 个刻度里隐藏的“整点加粗”
刻度是表盘的骨架。画刻度不需要一张图一张图去拼,一个 for 循环从 0 度到 354 度,每隔 6 度画一条短线段,就得到了 60 个刻度。整点刻度需要突出一点,判断条件是“当前刻度序号能否被 5 整除”。
void draw_ticks(int cx, int cy, int r) { int i; for (i = 0; i < 60; i++) { int len = (i % 5 == 0) ? 20 : 10; int x1, y1, x2, y2; calc_clock_pos(cx, cy, r - len, i * 6.0, &x1, &y1); calc_clock_pos(cx, cy, r, i * 6.0, &x2, &y2); line(x1, y1, x2, y2); } }逻辑说明:i 从 0 到 59 对应 60 个刻度位置,每个刻度的角度是 i * 6 度。刻度线是从半径 r - len 到半径 r 之间的一段短线,len 决定长短;整点刻度用 20 像素,普通刻度用 10 像素,视觉上一眼能分清。
参数说明:r 是整个表盘外圆半径,调用 calc_clock_pos 时注意半径参数一个是内端点一个外端点,方向都是从圆心往外。line 函数画的是像素线段,四个坐标就是它的两个端点。
4.3 三根指针的联动坐标:秒针和分针不是各转各的
指针转动的核心在一个角度计算函数:秒针每秒转 6 度,分针每分钟转 6 度,时针每小时转 30 度。但如果只按“当前分钟数”算分针角度,分针每秒都是静止的,只在分变化时跳一次。更接近真实时钟的做法是:分针角度 = 分钟 * 6 + 秒数 * 0.1,相当于秒针每走 60 秒,分针匀速走完 6 度。
void get_hand_angles(int hour, int min, int sec, double *a_s, double *a_m, double *a_h) { *a_s = sec * 6.0; *a_m = min * 6.0 + sec * 0.1; *a_h = (hour % 12) * 30.0 + min * 0.5; }逻辑说明:秒针角度直接用秒数乘 6;分针角度在分钟基础上补上 sec * 0.1 的插值;时针角度在小时基础上补上 min * 0.5,因为分针走 60 分钟,时针才走完 30 度,相当于每分钟 0.5 度。hour % 12 是为了把 12 小时制和 24 小时制统一,避免下午 3 点被算成 15 点。
参数说明:这个函数只负责输出角度,不负责画图。角度基准和 2.3 节的 calc_clock_pos 一致,12 点方向为 0 度,顺时针方向为正。
4.4 指针移动不拖尾:记录旧端点并用背景色补画
图形库画线默认是直接覆盖式绘制,新线画上之后,旧线不会自动消失。如果每帧只画新线,表盘很快就会变成一团乱线。解决思路是:每次绘制前,用背景色在旧位置重画一遍同一条线,把它“擦干净”,再画新线。
static int old_x[3], old_y[3]; void update_hands(int cx, int cy, int hour, int min, int sec) { double a_s, a_m, a_h; int lens[3] = { 70, 55, 40 }; int colors[3] = { RED, GREEN, WHITE }; int end_x[3], end_y[3]; int i; get_hand_angles(hour, min, sec, &a_s, &a_m, &a_h); calc_clock_pos(cx, cy, lens[0], a_s, &end_x[0], &end_y[0]); calc_clock_pos(cx, cy, lens[1], a_m, &end_x[1], &end_y[1]); calc_clock_pos(cx, cy, lens[2], a_h, &end_x[2], &end_y[2]); /* 先用背景色重画旧线,相当于擦除 */ setcolor(getbkcolor()); for (i = 0; i < 3; i++) { if (old_x[i] != 0 || old_y[i] != 0) { line(cx, cy, old_x[i], old_y[i]); } } /* 再画新位置的指针,并缓存端点 */ for (i = 0; i < 3; i++) { setcolor(colors[i]); line(cx, cy, end_x[i], end_y[i]); old_x[i] = end_x[i]; old_y[i] = end_y[i]; } }逻辑说明:三个指针的颜色、长度分别用数组管理。第一轮循环用 getbkcolor() 取出画布背景色,将旧指针重画一遍覆盖掉;第二轮循环画新位置。端点缓存在 static 数组里,保证第一次运行时不会拿未初始化的数据去画线。
参数说明:lens 数组的三针长度根据表盘半径调整,小时针最短、秒针最长才会有层次感。static 变量只初始化一次,主循环里反复调用 update_hands 也不会重新清零,这是它能记住旧位置的原因。
5. 模拟时钟五类翻车现场:环境、闪烁、残留、漂移与格式的排查
5.1 现象:graphics.h 头文件全家找不到
编译时直接报错 graphics.h: No such file or directory,或者链接时报错找不到图形库函数。最常见原因是当前开发环境根本没有装这套图形库,头文件和库文件都不在搜索路径里。
原因分析:新式开发环境默认不自带老式图形库,网上搜来的源码假设你已经配好了图形环境,实际上多数人第一步就没过。
解决方案:优先检查图形库是否安装,确认 include 路径是否指向图形库头文件目录;如果实在配不好,我建议直接退回 3.3 节的字符数字钟方案,同样能完成课程设计,只是视觉效果朴素一些。
5.2 现象:整个屏幕跟着时钟一起闪
用 system("cls") 每秒清一次屏,再重新打印时间,屏幕明显一明一暗。这是初学者最常见的翻车点,代码逻辑没有错,但渲染方式让人眼受不了。
原因分析:cls 会把整个屏幕缓冲区清空,操作系统再重新输出一行字符。全屏擦除和重绘的频率恰恰在人眼闪烁感知范围内,观感就是“闪”。
解决方案:弃用清屏,改用 3.2 节的光标定位或 3.1 节的 \r 回车符,只覆盖变化的那几个字节。printf("\r") 配合 fflush(stdout) 是成本最低的修复,我实际测试下来刷新和光标定位效果几乎一样。
5.3 现象:指针越走越多,表盘上全是残影
图形版跑几秒钟后,表盘上出现几十条长短不一的指针痕迹,像慢门摄影。这是“只画新线、不清旧线”导致的典型问题。
原因分析:图形库的 line 函数只负责往画布上叠加像素,不会去擦除之前画过的线。指针每变化一次就多一条线,旧线一直留在画布上。
解决方案:采用 4.4 节的“旧端点缓存 + 背景色重画”逻辑。每次更新前先把旧指针用背景色补画一遍,再画新线。更高阶的方案是 setwritemode(XOR_PUT) 异或模式,不过异或模式对颜色的叠加有额外要求,普通学生作业用背景色方案更稳妥。
5.4 现象:模拟时钟越走越慢,几分钟就慢十几秒
程序启动时时间是准的,跑十几分钟后对比系统时间,明显偏慢。这是循环里 sleep 策略的问题。
原因分析:Sleep(1000) 只保证休眠时间不少于 1000 毫秒,循环里的取时间、打印、坐标计算每次会额外消耗几毫秒,这个误差虽然在单次循环里可忽略,但累积到几百次循环后就变成肉眼可见的偏移。
解决方案:用 3.1 节的“目标时刻校准”方案,记录下一次整秒的目标值,只 Sleep 剩余时间,把误差在每圈循环内消化掉。如果还需要更高精度,可以直接用 GetTickCount() 取毫秒时间戳做绝对对齐。
5.5 现象:打印出来的年份是 1970,或月份对不上
时间显示成 1970-01-01,或者月份总是比真实月份小 1。问题出在对 struct tm 字段的理解上。
原因分析:tm_year 是从 1900 年起的年数,tm_mon 是从 0 起的月份。直接打印字段值就会看到“年份少 1900、月份少 1”的现象。另外 localtime 返回值是静态存储区的指针,连续调用会被覆盖,也会导致结果诡异。
解决方案:格式化输出时给 tm_year 加 1900,tm_mon 加 1。连续取时间时先复制结构体,别直接保存返回的指针。这两个习惯养成之后,在任何 C 时间处理代码里都不会再犯同样的错。
6. 再往前走一步:毫秒级平滑指针与容易被追问的答辩点
前面所有版本都是秒针每秒跳一格,这是大多数课设的最终形态。但真实时钟的秒针是连续摆动的,如果你想让指针平滑移动,只需把毫秒数也换算成角度。用 GetTickCount() % 1000 拿到当前毫秒,秒针角度变成 (秒 + 毫秒/1000) * 6,分针和时针也做同样的插值。
void smooth_hand_angles(int hour, int min, int sec, int ms, double *a_s, double *a_m, double *a_h) { *a_s = (sec + ms / 1000.0) * 6.0; *a_m = (min + sec / 60.0) * 6.0; *a_h = (hour % 12 + min / 60.0) * 30.0; }这个函数的精巧之处是把时间拆成“整数值 + 小数值”,小数值由毫秒或秒提供。把 smooth_hand_angles 替换掉原来的 get_hand_angles,整个图形逻辑不需要改其他部分。ms 参数用 GetTickCount() % 1000 获取,注意取模运算的位置。
答辩时老师大概率会问:“你的分针为什么是连续动的,不是每分钟跳一次?”答案就是这里的分针角度按 seconds 做了插值,秒针每走一步,分针就移动 0.1 度。这个细节比单纯画一个静态表盘加分得多。另外我还会做一个小实验:让程序跑 5 分钟,和系统时间对比,验证校准方案没有累积误差。
从那以后我每次写时钟类程序,都会先把“取时间、算角度、画指针”拆成三个独立函数,再决定要不要加平滑。这种拆分让程序好调试,也方便单独测试某个环节。希望这些经验能帮到你。
本文还有配套的精品资源,点击获取