时间与日期(time.h):time函数、clock函数、difftime、mktime、localtime与gmtime、strftime格式化时间

时间处理,说实话是C语言里最容易被忽视的一块。我刚入行那会儿,总觉得时间嘛,不就是取个当前时间打印一下?直到有一次做嵌入式日志系统,发现时间戳全乱了,才老老实实把time.h翻了个底朝天。

今天咱们就把这块彻底讲透。你想想看,无论是做日志、性能分析、定时任务,还是做网络协议的时间戳,都离不开这几个函数。我保证,看完这一章,你写时间相关的代码会顺手很多。

1. time_t与time函数:获取当前时间

先看最基础的——time()函数。它的原型很简单:

#include <time.h>
time_t time(time_t *t);

time_t本质上是一个整数类型(通常是long),它存储的是从1970年1月1日0时0分0秒(UTC)到现在的秒数。这个值也叫「Unix时间戳」。

用法有两种:

// 方式一:通过返回值获取
time_t now = time(NULL);

// 方式二:通过参数获取
time_t now;
time(&now);

我个人习惯用第一种,简洁。但要注意,如果传入非空指针,函数会把结果也写入那个地址。两种方式效果一样。

小提示: time(NULL)返回的是秒级精度。如果你需要毫秒或微秒,那就得用clock_gettime()或者平台相关的API了。我在做网络延迟测试时就踩过这个坑——秒级精度根本不够用。

2. clock函数:测量程序运行时间

clock()time()不一样。它返回的是程序启动到当前时刻消耗的CPU时钟周期数,单位是CLOCKS_PER_SEC。说白了,它更适合用来做性能测量。

#include <time.h>
clock_t start, end;
double cpu_time_used;

start = clock();
// 这里放你要测量的代码
end = clock();

cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;
printf("耗时:%f 秒\n", cpu_time_used);

嗯,这里要注意:clock()测量的是CPU时间,不是墙上时间(wall clock time)。如果程序在sleep,CPU时间是不会增加的。我曾经用clock()去测量一个包含sleep(1)的循环,结果发现耗时几乎为0,当时还以为是代码优化得太好了……

警告: 在多线程环境下,clock()的行为是未定义的。不同平台实现不同,有的返回所有线程的总CPU时间,有的只返回当前线程的。如果你做多线程性能测试,建议用clock_gettime()

3. difftime:计算时间差

difftime()就是用来算两个time_t之间差了多少秒的。原型:

double difftime(time_t time1, time_t time0);

返回 time1 - time0 的秒数,类型是double。为什么不用直接相减?因为time_t不一定是整数类型(虽然绝大多数平台都是),用difftime更安全、更可移植。

time_t start, end;
double elapsed;

time(&start);
// 做点耗时的事情
time(&end);

elapsed = difftime(end, start);
printf("过去了 %.0f 秒\n", elapsed);

说实话,我平时直接相减的时候更多,因为方便。但如果你写跨平台代码,还是用difftime吧,省得哪天在某个奇怪的编译器上翻车。

4. mktime:把分解时间转成时间戳

mktime()的作用正好和localtime()相反——它把一个struct tm结构体转成time_t时间戳。原型:

time_t mktime(struct tm *timeptr);

这个函数有个很实用的特性:它会自动修正struct tm中的字段。比如你把tm_mon设为13,它会自动进位到下一年的一月。我当年做日历程序时就靠这个特性省了不少逻辑。

struct tm t = {0};
t.tm_year = 2024 - 1900;  // 年份从1900开始
t.tm_mon = 0;              // 0代表一月
t.tm_mday = 1;

time_t timestamp = mktime(&t);
printf("2024-01-01的时间戳:%ld\n", timestamp);
重点: struct tm的字段范围要注意:
  • tm_year:从1900开始的年数,比如2024年就是124
  • tm_mon:0-11,0代表一月
  • tm_mday:1-31
  • tm_hour:0-23
  • tm_wday:0-6,0代表周日(这个由mktime自动填充)

5. localtime与gmtime:时间戳转成可读结构

这两个函数都是把time_t转成struct tm,区别在于:

  • localtime():转成当地时区的时间
  • gmtime():转成UTC时间(格林威治时间)
time_t now = time(NULL);

struct tm *local = localtime(&now);
printf("本地时间:%d-%d-%d %d:%d:%d\n",
       local->tm_year + 1900,
       local->tm_mon + 1,
       local->tm_mday,
       local->tm_hour,
       local->tm_min,
       local->tm_sec);

struct tm *utc = gmtime(&now);
printf("UTC时间:%d-%d-%d %d:%d:%d\n",
       utc->tm_year + 1900,
       utc->tm_mon + 1,
       utc->tm_mday,
       utc->tm_hour,
       utc->tm_min,
       utc->tm_sec);
注意: localtime()gmtime()返回的指针指向一个静态内部变量,不是线程安全的。多线程环境下请用localtime_r()gmtime_r()(POSIX标准)。我曾经在服务器程序里因为这个吃了大亏——两个线程同时调用localtime(),结果互相覆盖了数据。

6. strftime:格式化时间字符串

strftime()是时间格式化的终极武器。它类似于printf(),但专门用来格式化时间。原型:

size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);

常用的格式化占位符:

占位符 含义 示例
%Y 四位年份 2024
%m 两位月份(01-12) 03
%d 两位日期(01-31) 15
%H 24小时制小时(00-23) 14
%M 分钟(00-59) 30
%S 秒(00-59) 45
%A 星期几的全名 Monday
%B 月份的全名 March
%c 日期和时间的标准表示 Mon Mar 15 14:30:45 2024
time_t now = time(NULL);
struct tm *local = localtime(&now);

char buf[64];
strftime(buf, sizeof(buf), "现在是 %Y年%m月%d日 %H:%M:%S (%A)", local);
printf("%s\n", buf);
// 输出:现在是 2024年03月15日 14:30:45 (Friday)

我个人特别喜欢用strftime来做日志文件名。比如:

char filename[128];
strftime(filename, sizeof(filename), "log_%Y%m%d_%H%M%S.txt", local);
// 生成:log_20240315_143045.txt

这样每天、每小时甚至每分钟的日志都不会重名,而且按文件名排序就是按时间排序,非常方便。

知识体系总览

下面这张图把今天讲的内容串起来了。你一看就明白这些函数之间的关系:

time.h 时间函数核心关系图 time_t(时间戳) time() difftime() clock() (CPU时间,非墙上时间) struct tm(分解时间) localtime() (本地时区) gmtime() (UTC时区) mktime() (struct tm → time_t) strftime() (格式化输出字符串) 箭头方向表示数据流向:time_t ↔ struct tm ↔ 字符串

从这张图可以看得很清楚:time()clock()是获取原始时间数据的入口,difftime()用来做差值计算,localtime()/gmtime()把时间戳转成可读结构,mktime()反向转换,最后strftime()把结构体格式化成字符串。整个流程非常清晰。

实战小例子:完整的日志时间戳

最后,我写一个完整的例子,把今天讲的函数串起来用:

#include <stdio.h>
#include <time.h>

int main() {
    // 1. 获取当前时间戳
    time_t now = time(NULL);

    // 2. 转成本地时间
    struct tm *local = localtime(&now);

    // 3. 格式化输出
    char time_str[64];
    strftime(time_str, sizeof(time_str),
             "[%Y-%m-%d %H:%M:%S]", local);
    printf("%s 程序启动\n", time_str);

    // 4. 模拟一些工作,测量耗时
    clock_t start = clock();
    for (volatile int i = 0; i < 100000000; i++);
    clock_t end = clock();

    double cpu_time = (double)(end - start) / CLOCKS_PER_SEC;
    printf("%s 工作完成,CPU耗时:%.3f秒\n", time_str, cpu_time);

    // 5. 计算墙上时间差
    time_t end_time = time(NULL);
    double wall_time = difftime(end_time, now);
    printf("%s 墙上时间:%.0f秒\n", time_str, wall_time);

    return 0;
}

输出效果:

[2024-03-15 14:30:45] 程序启动
[2024-03-15 14:30:45] 工作完成,CPU耗时:0.245秒
[2024-03-15 14:30:45] 墙上时间:0秒

嗯,这里有个有意思的现象:CPU耗时0.245秒,但墙上时间显示0秒。为什么?因为difftime是秒级精度,0.245秒被截断了。这也印证了我之前说的——做精细测量时,time()的精度不够用。

好了,时间处理这块的核心函数就这些。你只要记住:time()拿时间戳,localtime()/gmtime()拆成结构体,strftime()格式化输出,mktime()反向组装,difftime()算差值,clock()测性能。这套组合拳打下来,大部分时间相关的需求都能搞定。


专注资料整理