ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C语言时间函数详解:time、ctime、difftime、clock的底层逻辑与避坑指南

C语言时间函数详解:time、ctime、difftime、clock的底层逻辑与避坑指南 1. 时间处理在C语言里到底难在哪很多人学C语言的时候指针、链表、文件IO都啃下来了偏偏在时间处理上反复翻车。原因不复杂时间这个东西本身就有多重语义。你嘴里的现在几点可能是墙上挂钟的本地时间可能是服务器记录的UTC时间也可能是程序从启动到现在消耗的CPU时钟周期数。C标准库把这几类需求分别交给了不同的函数用错了函数结果就是差8小时、差时区、甚至算出负数。time.h这个头文件里最常打交道的四个函数——time、ctime、difftime、clock——看起来简单但每一个都有它的适用边界。time拿的是日历时间clock拿的是处理器时间这两个东西完全不是一回事。ctime负责把时间戳转成人类可读的字符串difftime负责算两个时间点之间的秒数差。这篇文章就把这四个函数的底层逻辑、UTC与本地时间的转换关系、以及实际项目中容易踩的坑一次性讲透。不管你是刚学完printf和scanf的初学者还是已经在做嵌入式或者后台开发的老手只要你的代码里出现过时间戳、日志时间、超时计算这篇内容都值得过一遍。我会尽量用生活化的类比把概念讲清楚同时给出可以直接复制运行的代码让你看完就能上手。2. time函数拿到那个从某个起点开始数的秒数2.1 time返回的到底是什么time函数的原型是time_t time(time_t *timer)。它返回的是从Unix纪元1970年1月1日00:00:00 UTC到当前时刻所经过的秒数。这个秒数通常被称为Unix时间戳或者日历时间。你可以把它想象成一个巨大的秒表这个秒表在1970年那个时刻被按下开始键之后一直在走。不管你在哪个国家、哪个时区这个秒表的读数都是一样的。这就是为什么它叫UTC时间——它不偏向任何一个地区。#include stdio.h #include time.h int main(void) { time_t now time(NULL); if (now (time_t)-1) { perror(time); return 1; } printf(当前Unix时间戳: %ld\n, (long)now); return 0; }传入NULL表示你不需要把结果同时存到指针指向的变量里直接用返回值就行。如果你传了一个time_t变量的地址函数会把结果同时写进去返回值也是同一个值。两种写法等价实际项目里传NULL更常见。2.2 为什么time_t不一定是长整型很多教程直接告诉你time_t就是long这在32位系统上曾经成立但现在不准确了。C标准只规定time_t是一种算术类型具体是int、long还是long long由实现决定。在64位Linux上time_t通常是8字节的long在一些嵌入式平台或者旧编译器上它可能是4字节的int。这个差异带来的直接后果就是2038年问题。如果time_t是32位有符号整数它能表示的最大秒数在2038年1月19日就会溢出时间会跳回1901年。所以你在写代码时打印time_t不要想当然用%d正确做法是强制转换成long或者long long再用对应的格式符。提示在嵌入式项目里如果工具链的time_t是32位而你的设备需要长期运行务必提前评估2038年问题必要时改用64位时间类型或者自定义时间结构。2.3 time的精度与单调性time的精度是秒级。如果你需要毫秒甚至微秒级的时间time不够用得用gettimeofdayPOSIX或者clock_gettimePOSIX支持CLOCK_REALTIME和CLOCK_MONOTONIC。但这两个不属于C标准库跨平台性不如time。还有一个容易被忽略的点time返回的是墙上时钟它可能因为系统时间被手动调整、NTP同步而回退或跳跃。如果你用time来测量一段代码的运行耗时一旦期间系统时间被校准算出来的差值可能是负数或者明显偏大。测量耗时应该用clock或者clock_gettime(CLOCK_MONOTONIC)。3. ctime函数把时间戳翻译成人能看懂的样子3.1 ctime的输出格式与隐含换行ctime的原型是char *ctime(const time_t *timer)。它接收一个time_t指针返回一个静态分配的字符串格式固定为Wed Jun 30 21:49:08 1993\n注意末尾那个\n是函数自己加的你不需要再手动加换行。这个格式是C标准规定的固定25个字符加一个结束符总共26字节。#include stdio.h #include time.h int main(void) { time_t now time(NULL); char *s ctime(now); if (s NULL) { return 1; } printf(本地时间: %s, s); return 0; }3.2 ctime返回的是本地时间不是UTC这是初学者最容易搞混的地方。ctime内部实际上等价于asctime(localtime(timer))。也就是说它先把时间戳转成本地时间结构再格式化成字符串。所以你在东八区运行输出的就是东八区的墙上时间。如果你需要UTC时间的字符串不能用ctime得用gmtime配合asctimetime_t now time(NULL); struct tm *utc gmtime(now); printf(UTC时间: %s, asctime(utc));gmtime和localtime的区别就是前者不做时区偏移后者会根据系统时区设置做偏移。这两个函数返回的都是指向静态内部结构的指针下一次调用会覆盖上一次的结果。3.3 静态缓冲区带来的线程安全问题ctime、asctime、gmtime、localtime这四个函数都使用静态分配的共享缓冲区。这意味着在多线程环境里两个线程同时调用ctime结果会互相覆盖拿到的字符串可能是错的。POSIX提供了可重入版本ctime_r、asctime_r、gmtime_r、localtime_r需要你自己传入缓冲区。在写多线程程序时优先用带_r后缀的版本。time_t now time(NULL); char buf[26]; if (ctime_r(now, buf) ! NULL) { printf(线程安全输出: %s, buf); }注意ctime_r的缓冲区至少需要26字节少了会缓冲区溢出。这个数字来自标准规定的固定格式长度。3.4 一个实际场景日志时间戳写日志的时候通常需要年-月-日 时:分:秒这种格式ctime的默认格式不太符合习惯。这时候更常用的是strftimetime_t now time(NULL); struct tm *local localtime(now); char buf[64]; strftime(buf, sizeof(buf), %Y-%m-%d %H:%M:%S, local); printf([%s] 程序启动\n, buf);strftime的格式化能力远超ctime支持几十种转换说明符是做日志和时间展示的首选。ctime更适合快速调试打印正式项目里用得反而少。4. difftime函数算两个时间点之间差了多少秒4.1 为什么不能直接相减理论上两个time_t相减就能得到秒数差但C标准没有规定time_t的具体类型和单位直接相减在不同平台上可能出问题。difftime的作用就是提供一个可移植的、返回double类型秒数差的接口。原型是double difftime(time_t time1, time_t time0)返回的是time1 - time0的秒数结果是double。#include stdio.h #include time.h int main(void) { time_t start time(NULL); // 模拟一段耗时操作 volatile long sum 0; for (long i 0; i 100000000L; i) { sum i; } time_t end time(NULL); double elapsed difftime(end, start); printf(耗时: %.0f 秒\n, elapsed); return 0; }4.2 参数顺序决定正负号difftime(time1, time0)算的是time1 - time0。如果你把顺序写反了结果就是负数。这个细节在计算超时、判断先后顺序时特别重要。比如判断一个缓存是否过期time_t now time(NULL); double age difftime(now, cache_time); if (age 3600.0) { printf(缓存已过期\n); }这里now是较晚的时间cache_time是较早的时间结果为正。如果写反了age永远是负数判断逻辑就失效了。4.3 精度限制与替代方案difftime的精度受限于time_t的精度也就是秒级。如果你要测量毫秒级的耗时difftime无能为力。这时候应该用clock下一节讲或者平台相关的clock_gettime。另外difftime返回double在比较时要注意浮点误差。虽然秒级差值用double表示整数是精确的但如果你的逻辑里涉及小数秒就要小心了。4.4 实际案例计算程序运行天数假设你在做一个打卡系统需要算用户距离上次打卡过了多少天time_t last_checkin get_last_checkin_time(); time_t now time(NULL); double seconds difftime(now, last_checkin); int days (int)(seconds / 86400.0); printf(距离上次打卡已过 %d 天\n, days);这里86400是一天的秒数。注意用整数除法会截断如果你需要四舍五入得自己处理。还有一个坑如果用户修改了系统时间difftime的结果可能不符合预期所以关键业务逻辑不能完全依赖本地时间。5. clock函数测量CPU时间的正确姿势5.1 clock返回的不是秒数clock的原型是clock_t clock(void)。它返回的是程序从启动到当前时刻消耗的处理器时间单位是CLOCKS_PER_SEC分之一秒。注意它返回的不是秒数而是时钟滴答数。要换算成秒必须除以CLOCKS_PER_SEC#include stdio.h #include time.h int main(void) { clock_t start clock(); volatile long sum 0; for (long i 0; i 100000000L; i) { sum i; } clock_t end clock(); double cpu_seconds (double)(end - start) / CLOCKS_PER_SEC; printf(CPU耗时: %.6f 秒\n, cpu_seconds); return 0; }CLOCKS_PER_SEC在大多数系统上是1000000也就是微秒级精度。但C标准只保证它是个常量具体值由实现决定。5.2 clock和time的本质区别这两个函数测的是完全不同的东西对比项timeclock测量对象墙上时钟日历时间处理器时间起点1970-01-01 UTC程序启动时刻单位秒时钟滴答数受系统时间调整影响是否受多任务调度影响否是只算本进程占用CPU的时间适用场景记录时间戳、日志测量代码执行耗时举个例子你的程序睡眠了10秒期间没有占用CPU。用time测出来是10秒用clock测出来接近0秒。因为clock只统计CPU实际执行的时间睡眠期间进程被挂起不计入。5.3 clock在测量耗时时的陷阱用clock测耗时有一个常见误区如果程序是多线程的clock统计的是所有线程消耗的CPU时间总和可能远大于墙上时间。比如4个线程并行跑了1秒clock可能返回4秒左右的值。另外clock的起点是程序启动不是main函数开始。如果你在main之前有全局对象的构造函数C场景那部分时间也会被算进去。提示测量单线程代码块的执行时间clock是合适的选择。测量多线程或者需要墙上时间的场景用clock_gettime(CLOCK_MONOTONIC)更准确。5.4 一个综合示例对比两种计时方式#include stdio.h #include time.h #include unistd.h int main(void) { time_t t_start time(NULL); clock_t c_start clock(); sleep(2); // 睡眠2秒不消耗CPU time_t t_end time(NULL); clock_t c_end clock(); printf(墙上时间差: %.0f 秒\n, difftime(t_end, t_start)); printf(CPU时间差: %.6f 秒\n, (double)(c_end - c_start) / CLOCKS_PER_SEC); return 0; }运行结果会显示墙上时间差约2秒CPU时间差接近0。这个对比能帮你直观理解两者的差异。6. UTC与本地时间转换的完整链路6.1 时间转换涉及的三层结构C语言的时间处理实际上涉及三种表示形式time_t时间戳一个整数表示从纪元开始的秒数struct tm分解时间包含年、月、日、时、分、秒等字段字符串人类可读的格式化输出转换关系是这样的time_t→struct tm用gmtimeUTC或localtime本地struct tm→time_t用mktime按本地时区解释struct tm→ 字符串用asctime或strftimetime_t→ 字符串用ctime本地时间6.2 struct tm的字段陷阱struct tm里最容易搞错的是tm_mon和tm_yeartm_mon的范围是0到110代表一月tm_year是从1900年起的年数比如2024年存的是124struct tm t {0}; t.tm_year 2024 - 1900; t.tm_mon 6 - 1; // 6月 t.tm_mday 15; t.tm_hour 10; t.tm_min 30; t.tm_sec 0; t.tm_isdst -1; // 让系统自动判断夏令时 time_t ts mktime(t); printf(时间戳: %ld\n, (long)ts);tm_isdst设为-1表示让系统自己判断是否夏令时设为0表示不是设为正数表示是。中国目前不实行夏令时但代码要考虑到跨时区运行的场景。6.3 本地时间转UTC的实操假设你有一个本地时间的struct tm想转成UTC时间戳struct tm local_tm {0}; local_tm.tm_year 2024 - 1900; local_tm.tm_mon 0; local_tm.tm_mday 1; local_tm.tm_hour 8; local_tm.tm_min 0; local_tm.tm_sec 0; local_tm.tm_isdst -1; // mktime把本地时间转成时间戳 time_t ts mktime(local_tm); // 再用gmtime转成UTC的struct tm struct tm *utc_tm gmtime(ts); printf(UTC: %04d-%02d-%02d %02d:%02d:%02d\n, utc_tm-tm_year 1900, utc_tm-tm_mon 1, utc_tm-tm_mday, utc_tm-tm_hour, utc_tm-tm_min, utc_tm-tm_sec);东八区的2024年1月1日8点对应UTC的2024年1月1日0点。这个转换链路在跨时区系统里非常常用。6.4 时区设置对localtime的影响localtime的行为依赖系统的时区设置。在Linux上时区由/etc/localtime或者TZ环境变量决定。你可以在程序里临时设置setenv(TZ, UTC, 1); tzset(); // 此后localtime会按UTC解释这个技巧在测试时很有用可以模拟不同时区的行为。但要注意setenv和tzset不是C标准库函数属于POSIX。在Windows上要用_putenv和_tzset。注意修改时区是全局操作会影响同进程内所有线程。多线程程序里不要随意改时区最好在程序启动时一次性设定好。7. 那些年我踩过的时间函数坑7.1 用time测耗时导致负数早期写代码时我用time测一段循环的耗时结果偶尔出现负数。排查后发现是系统在后台做了时间同步把时钟往回拨了一点。difftime(end, start)算出来就是负的。正确做法测量代码耗时用clock或者clock_gettime(CLOCK_MONOTONIC)这两个不受系统时间调整影响。7.2 ctime返回值被覆盖有一次写了个函数先调用ctime拿到开始时间再调用ctime拿结束时间然后想把两个字符串一起打印。结果发现两个字符串一模一样。原因是ctime返回的是静态缓冲区指针第二次调用把第一次的内容覆盖了。正确做法拿到ctime的返回值后立即拷贝到自己的缓冲区或者用ctime_r。char start_buf[26], end_buf[26]; time_t t1 time(NULL); ctime_r(t1, start_buf); // ... 一些操作 ... time_t t2 time(NULL); ctime_r(t2, end_buf); printf(开始: %s结束: %s, start_buf, end_buf);7.3 mktime的tm_isdst没设导致时间偏移有次构造struct tm时忘了设tm_isdst在某些系统上mktime会按夏令时处理导致时间戳差了一小时。虽然中国不用夏令时但如果代码部署到使用夏令时的地区就会出问题。正确做法始终显式设置tm_isdst -1让系统自动判断。7.4 clock_t溢出在32位系统上clock_t可能是32位有符号整数。如果CLOCKS_PER_SEC是1000000那么大约2147秒约35分钟后就会溢出。长时间运行的程序用clock测耗时结果会变成负数。正确做法长时间计时用time或者clock_gettimeclock只适合短时间、单线程的耗时测量。7.5 跨平台时间格式化差异strftime的某些格式符在不同平台上行为不一致。比如%e日期补空格在Windows上可能不支持%z时区偏移的格式也可能不同。正确做法用最通用的格式符比如%Y-%m-%d %H:%M:%S这些在所有平台上都支持。需要特殊格式时先查目标平台的文档。8. 把四个函数用对地方选型建议与代码模板8.1 按场景选函数场景推荐函数理由记录日志时间戳timelocaltimestrftime可读性好格式灵活生成UTC时间字符串timegmtimestrftime避免时区干扰计算两个日期差difftime可移植返回double测量代码执行耗时单线程clock不受系统时间调整影响测量代码执行耗时多线程clock_gettime(CLOCK_MONOTONIC)墙上时间单调递增快速调试打印时间ctime一行搞定但注意线程安全8.2 一个可直接复用的时间工具模块#include stdio.h #include time.h #include string.h // 获取当前本地时间字符串格式2024-01-01 08:00:00 void get_local_time_str(char *buf, size_t len) { time_t now time(NULL); struct tm local_tm; localtime_r(now, local_tm); strftime(buf, len, %Y-%m-%d %H:%M:%S, local_tm); } // 获取当前UTC时间字符串 void get_utc_time_str(char *buf, size_t len) { time_t now time(NULL); struct tm utc_tm; gmtime_r(now, utc_tm); strftime(buf, len, %Y-%m-%d %H:%M:%S, utc_tm); } // 计算两个时间戳之间的天数差 int days_between(time_t t1, time_t t0) { double seconds difftime(t1, t0); return (int)(seconds / 86400.0); } // 测量函数执行耗时CPU时间秒 double measure_cpu_time(void (*func)(void)) { clock_t start clock(); func(); clock_t end clock(); return (double)(end - start) / CLOCKS_PER_SEC; }这个模块把常用的时间操作封装好了直接拿去用就行。注意localtime_r和gmtime_r是POSIX函数Windows上要用localtime_s和gmtime_s参数顺序也不一样。8.3 嵌入式环境的特殊考虑在单片机上标准库的时间函数支持情况差异很大。有些工具链根本不提供time和clock或者需要你自己实现底层的时间获取。这时候通常的做法是用硬件定时器提供毫秒级滴答自己维护一个从某个起点开始计数的秒数需要日历时间时用RTC实时时钟芯片读取如果工具链提供了time.h但time返回的是固定值因为没有RTC那所有依赖time的函数都会失效。这种情况下clock反而更可靠因为它只依赖CPU时钟。8.4 关于2038问题的实际应对如果你的项目生命周期可能延续到2038年之后现在就要考虑这个问题。应对方案有几个确保time_t是64位现代64位系统默认就是如果平台只支持32位time_t考虑用自定义的64位时间类型在存储时间戳时用64位整数而不是time_t定期检查工具链和运行环境的time_t宽度#include limits.h #if defined(__STDC_VERSION__) __STDC_VERSION__ 199901L #include stdint.h typedef int64_t safe_time_t; #else typedef long long safe_time_t; #endif这个做法牺牲了和标准库函数的直接兼容性但能保证时间计算不会溢出。具体怎么取舍取决于项目的时间跨度和精度要求。时间处理这件事说难不难说简单也不简单。核心就是搞清楚每个函数测的是什么时间、返回的是什么类型、有没有线程安全问题。把这三点想明白大部分坑都能提前避开。我在实际项目里最深的体会是不要假设时间会按你想象的方式流动系统时间可能被调整时区可能被改变time_t的宽度可能和你想的不一样。代码里多做一层校验比事后排查省事得多。
返回列表