C++ STL 之 chrono 时间库详解
为什么需要 chrono
C 标准库的<ctime>提供time_t、clock_t、struct tm和clock()/time()等接口,但存在三个硬伤:
- 精度不可控:
time_t只能到秒,clock()虽返回 ticks 但单位由CLOCKS_PER_SEC定义,不同平台不一致 - 类型不安全:秒和毫秒都是算术类型,编译器不会阻止你把
time_t直接赋值给毫秒变量 - 单调性无保证:
time()返回墙上时间,系统时间被 NTP 同步或用户手动修改后可能倒退,测性能时会出现负数间隔
<chrono>(C++11 引入,C++20/23 大幅扩展)用强类型系统和编译期有理数运算解决了上述所有问题。
三种时钟
时钟是 chrono 的基石。C++ 标准定义了三种时钟,各有用途。
system_clock
表示系统范围的实时挂钟(wall clock),对应time_t。可调用to_time_t()转成传统time_t,也支持from_time_t()反向转换。
#include<chrono>#include<iostream>#include<iomanip>intmain(){autotp=std::chrono::system_clock::now();std::time_t t=std::chrono::system_clock::to_time_t(tp);std::cout<<"当前时间: "<<std::put_time(std::gmtime(&t),"%F %T")<<'\n';}steady_clock
单调时钟,保证了now()的返回值永远不会减小。适合测量代码段耗时:
#include<chrono>#include<iostream>#include<thread>intmain(){autostart=std::chrono::steady_clock::now();std::this_thread::sleep_for(std::chrono::milliseconds(100));autoend=std::chrono::steady_clock::now();autodiff=end-start;automs=std::chrono::duration_cast<std::chrono::milliseconds>(diff);std::cout<<"耗时: "<<ms.count()<<" ms\n";}high_resolution_clock
字面意思是"最高精度时钟"。在主流实现中(MSVC、libstdc++、libc++),它通常是steady_clock的类型别名,而非独立时钟。不要假定它与steady_clock不同。
duration:编译期单位转换
duration是 chrono 最精巧的设计:把数值和单位打包成一个类型。
模板签名:
template<classRep,classPeriod=ratio<1>>classduration;Rep:计数值类型(int64_t、double等)Period:编译期有理数,表示"每个 tick 对应多少秒"。ratio<1, 1000>表示毫秒,ratio<1, 1'000'000>表示微秒
标准库预定义了常用单位:
| 类型 | 定义 |
|---|---|
nanoseconds | duration<int64_t, nano> |
microseconds | duration<int64_t, micro> |
milliseconds | duration<int64_t, milli> |
seconds | duration<int64_t> |
minutes | duration<int64_t, ratio<60>> |
hours | duration<int64_t, ratio<3600>> |
单位转换链如下:
隐式转换 vs 显式转换
从低精度到高精度(无损)可以隐式转换:
automs=std::chrono::milliseconds(1500);std::chrono::microseconds us=ms;// OK,1500'000 us从高精度到低精度(可能截断)必须用duration_cast:
autous=std::chrono::microseconds(2500);automs=std::chrono::duration_cast<std::chrono::milliseconds>(us);// 2 ms(截断)C++17 新增floor、ceil、round:
usingnamespacestd::chrono;automs=duration_cast<milliseconds>(microseconds(2500));// 2automs2=floor<milliseconds>(microseconds(2500));// 2automs3=ceil<milliseconds>(microseconds(2500));// 3automs4=round<milliseconds>(microseconds(2499));// 2time_point:时间轴上的一个点
time_point绑定到特定时钟,表示从该时钟的 epoch 起经过的 duration。
template<classClock,classDuration=typenameClock::duration>classtime_point;关键成员:
time_since_epoch():返回 epoch 到该点之间的durationoperator +/operator -:与duration做算术
autotp=std::chrono::system_clock::now();autod=tp.time_since_epoch();automs=std::chrono::duration_cast<std::chrono::milliseconds>(d);std::cout<<"Unix 时间戳(毫秒): "<<ms.count()<<'\n';不同时钟的time_point不能混用。system_clock::time_point和steady_clock::time_point是不同的类型,编译器禁止直接运算。
C++20 日历扩展
C++20 为 chrono 增加了日历类型,彻底终结了tm+mktime的繁琐模式。
year_month_day
#include<chrono>intmain(){usingnamespacestd::chrono;autotoday=floor<days>(system_clock::now());year_month_day ymd{today};// 构造指定日期year_month_day ymd2{2026y,June,25d};autod=sys_days{ymd2};autotp=system_clock::time_point{d};}工作日计算
#include<chrono>#include<iostream>intmain(){usingnamespacestd::chrono;// 2026-06-25 是星期几?year_month_day ymd{2026y,June,25d};autowd=weekday{sys_days{ymd}};std::cout<<"weekday: "<<wd<<'\n';// 下一个周一autodays_until_mon=(Monday-wd).count();if(days_until_mon<=0)days_until_mon+=7;autonext_mon=sys_days{ymd}+days{days_until_mon};}日期加减
autod=sys_days{2026y,January,1d}+months{3}-days{5};// 约 2026-03-27注意事项
year_month_day存储在days精度,通过sys_days{}与system_clock::time_point互转- C++20 日期支持在 GCC 12+、Clang 16+、MSVC 2022 17.0+ 中可用
- 需要链接
-latomic(某些平台)
性能:high_resolution_clock::now() 开销
now()的调用成本因平台/时钟而异:
| 时钟 | 近似耗时 | 说明 |
|---|---|---|
system_clock::now() | ~25-50 ns | 通常调用GetSystemTimePreciseAsFileTime(Win)或clock_gettime(CLOCK_REALTIME)(Linux) |
steady_clock::now() | ~25-50 ns | 与 system_clock 接近,同样基于QueryPerformanceCounter或CLOCK_MONOTONIC |
high_resolution_clock::now() | ~同样 | 本质上是上述之一的别名 |
结论:单次now()开销约 50 ns,在宏观性能测量中可忽略;但在微基准测试中(测量耗时 < 1 us 的操作),需要重复执行并取平均值以稀释 overhead。
// 测量高精度小函数的正确姿势autoinvoke=[&](){/* 被测函数 */};autostart=std::chrono::steady_clock::now();for(inti=0;i<100'000;++i)invoke();autoend=std::chrono::steady_clock::now();autoavg=(end-start)/100'000;std::cout<<"平均耗时: "<<std::chrono::duration_cast<std::chrono::nanoseconds>(avg).count()<<" ns\n";面试题
1.steady_clock和system_clock的根本区别是什么?分别用于什么场景?
steady_clock保证单调递增,不受系统时间调整影响,用于性能测量。system_clock表示墙上时间,可转time_t,用于时间戳和日志记录。
2. 为什么不能用steady_clock打印"当前时间"?
steady_clock的 epoch 通常是系统启动时间(而非 Unix 纪元),无法映射到人类可读的日历时间。要打印当前时间必须用system_clock。
3.duration_cast和floor/ceil/round有什么区别?
duration_cast向零截断(truncate toward zero),floor向下取整、ceil向上取整、round四舍五入到最近的整数。对正数duration_cast和floor结果相同,但对负数不同。
4. 为什么high_resolution_clock::now()不能保证高精度?
它是实现定义的别名,在主流实现中就是steady_clock或system_clock之一,并未提供额外精度。steady_clock::now()实际精度受操作系统定时器分辨率限制(Windows 默认 ~15.6 ms,除非使用多媒体定时器)。
5. 如何将system_clock::time_point转为 C++20 的year_month_day?
autotp=std::chrono::system_clock::now();autodays=std::chrono::floor<std::chrono::days>(tp);std::chrono::year_month_day ymd{days};6.duration的Period参数是运行时指定的还是编译期指定的?它如何影响代码生成?
编译期指定。Period是std::ratio的特化,所有单位转换通过有理数算术在编译期完成。编译器能在常数时间内完成乘除优化,运行时零开销。
7. C++20 之前,如何计算两个日期之间的天数差?C++20 有什么改进?
C++20 之前需要用mktime转time_t再相除86400,但受时区影响可能有偏差。C++20 的sys_days将日期映射到 days 精度的时间点,直接相减就是精确天数差,且不涉及时区。
autod1=sys_days{2026y,January,1d};autod2=sys_days{2026y,December,31d};autodiff=(d2-d1).count();// 3648. 什么是 epoch?system_clock和steady_clock的 epoch 分别是什么?
epoch 是时间轴的零点。system_clock的 epoch 是 1970-01-01 00:00:00 UTC(Unix 纪元)。steady_clock的 epoch 是平台相关的(通常是系统启动时间),且不保证不同进程间一致。