C 语言时代
在写程序的时候,我们经常希望能够获知某段程序运行的时间。一般来说,C 的时代最简单的做法是这样的:
time_t begin = time(NULL);
// do something
time_t end = time(NULL);
printf("%lf\n", difftime(endTime, beginTime));
众所周知,C 库函数 time_t time(time_t *seconds) 返回自 Unix 纪元 Epoch(1970-01-01 00:00:00 UTC)起经过的时间,以秒为单位。如果 seconds 不为空,则返回值也存储在变量 seconds 中。difftime() 返回两个时间相差的秒数,返回类型是 double。
这里提一句题外话:可能会有人疑惑,difftime 似乎只是在做直接作差然后转换成 double 类型,它和直接 end - begin 有什么区别?这里贴上其他论坛上见到的回答:

无论如何,我们使用 time() 函数只能得到精确到秒的时间,很多时候这还不够。
于是有了另一种计算时间的方式,可以得到毫秒级别的准确度:
clock_t startTime = clock();
// do something
clock_t endTime = clock();
printf("%lf\n", (double)(endTime - startTime) / CLOCKS_PER_SEC);
- sleep 函数的问题。如下:
// Windows系统下
#include <time.h>
#include <Windows.h>
clock_t startTime = clock();
Sleep(5000);
clock_t endTime = clock();
printf("%lf\n", (double)(endTime - startTime) / CLOCKS_PER_SEC);
// 运行结果:
// 5.001000
// Linux系统下
#include <time.h>
#include <unistd.h>
clock_t startTime = clock();
sleep(5);
clock_t endTime = clock();
printf("%lf\n", (double)(endTime - startTime) / CLOCKS_PER_SEC);
// 运行结果
// 0.000000
在 Windows 系统下,无论是 Windows.h 中的 Sleep() 函数(以毫秒数为参数)还是 unistd.h 中的 sleep() 函数(以秒为参数),都会记入时间,而 Linux 系统下,sleep() 函数的运行不计入时间。换句话说,在 Windows Sleep() 占用 processor time,Linux 下的 sleep() 不占用 processor time,这可能与底层的 sleep() 实现机制的不同所导致的。
2. clock() 函数得到的是【处理器时间】而不是真正的时间,有可能会产生误差。这取决于操作系统给予程序的执行资源。在单处理器的情况下,若 CPU 为其他进程所共享,clock 可能慢于挂钟,若当前进程为多线程,而有更多资源可用,clock 时间可能会快于挂钟。在多处理器情况下,若进程使用了多线程,那么 clock 时间可能要慢于挂钟。例如,当并行算法采用多核 cpu 时,某一进程或线程调用 clock,记录了当前核时钟。但在下次调用 clock 之前很可能发生 cpu 调度,进程或线程被调度到其他 cpu 上运行。这导致两次取得计时单元并不是同一个 cpu的,产生计时错误。
那么,有什么更好的方法吗?
Linux
在Linux 系统下,有些人会选择 gettimeofday() 函数,定义如下:
int gettimeofday (struct timeval *__restrict __tv,
void *__restrict __tz)
struct timeval
{
__time_t tv_sec; /* Seconds. */
__suseconds_t tv_usec; /* Microseconds. */
};
使用:
#include <sys/time.h>
double get_time()
{
struct timeval t;
gettimeofday(&t, NULL);
return t.tv_sec + t.tv_usec / 1000000.0;
}
// ...
double begin, end;
begin = get_time();
// do something
end = get_time();
printf("%lf\n", end - begin);
不过,C11 标准后的 clock_gettime() 函数是个更好的方法,而且,该方法甚至可以精确到纳秒,其定义如下:
/* Get current value of clock CLOCK_ID and store it in TP. */
extern int clock_gettime (clockid_t __clock_id, struct timespec *__tp) __THROW;
/* POSIX.1b structure for a time value. This is like a `struct timeval' but
has nanoseconds instead of microseconds. */
struct timespec
{
__kernel_time_t tv_sec;
long tv_nsec;
};
使用:
#include <time.h>
struct timespec begin, end;
clock_gettime(CLOCK_REALTIME, &begin);
// do something
clock_gettime(CLOCK_REALTIME, &end);
printf("%lf\n", (double)(end.tv_sec - begin.tv_sec) + (double)(end.tv_nsec - begin.tv_nsec) / 1000000000);
clock_gettime() 的第一个参数 clockid_t 类型常见的有四种:
- CLOCK_REALTIME:系统实时时间。
- CLOCK_MONOTONIC:从系统启动时开始计时,不受系统时间被用户改变的影响。
- CLOCK_PROCESS_CPUTIME_ID:本进程到当前代码系统CPU花费的时间,包含该进程下的所有线程。
- CLOCK_THREAD_CPUTIME_ID:本线程到当前代码系统CPU花费的时间。
与 clock_gettime() 类似的还有 timespec_get() 等。(之所以放在“非 Windows ”这章,纯粹是因为我本地的编译器还不支持 C11,只能开 WSL)
Windows
例如,Windows API 中的 GetLocalTime() 似乎是个不错的选择,我们直接用它写一个 gettimeofday():
#include <time.h>
#ifdef WIN32
#include <windows.h>
#else
#include <sys/time.h>
#endif
#ifdef WIN32
int gettimeofday(struct timeval *tp, void *tzp)
{
time_t clock;
struct tm tm;
SYSTEMTIME wtm;
GetLocalTime(&wtm);
tm.tm_year = wtm.wYear - 1900;
tm.tm_mon = wtm.wMonth - 1;
tm.tm_mday = wtm.wDay;
tm.tm_hour = wtm.wHour;
tm.tm_min = wtm.wMinute;
tm.tm_sec = wtm.wSecond;
tm.tm_isdst = -1;
clock = mktime(&tm);
tp->tv_sec = clock;
tp->tv_usec = wtm.wMilliseconds * 1000;
return (0);
}
#endif
double get_time()
{
struct timeval t;
gettimeofday(&t, NULL);
return t.tv_sec + t.tv_usec / 1000000.0;
}
同样可以实现毫秒级别的精度。这么一个计时的函数 get_time() 便可以为两个系统所用了。 除此之外,Windows API 中的 GetTickCount() 也可以实现毫秒级别的精度。GetTickCount() 返回从操作系统启动所经过的毫秒数,返回类型是 DWORD。
尤有甚者(褒义),QueryPerformanceCounter() 和 QueryPerformanceFrequency() 搭配使用,提供了windows环境下的高精度计时,前者获得的是 CPU 从开机以来执行的时钟周期数,后者用于获得机器一秒钟执行多少个时钟周期(类似于 clock() 函数和 CLOCK_PER_SEC,不过精度更高)。 该方法的精度误差一般不超过 1μs,可以认为是透微秒级的高精度计时方法。
#include <Windows.h>
// ...
LARGE_INTEGER t1, t2, tc;
QueryPerformanceFrequency(&tc);
QueryPerformanceCounter(&t1);
// do something
QueryPerformanceCounter(&t2);
printf("%lf\n", (double)(t2.QuadPart - t1.QuadPart) / (double)tc.QuadPart);
C++11的用法
// C++ 11
#include <chrono>
// ...
std::chrono::time_point<std::chrono::steady_clock> begin, end;
begin = std::chrono::steady_clock::now();
// do something
end = std::chrono::steady_clock::now();
std::chrono::duration<double> duration = end - begin;
std::cout << duration.count() << " sec\n";
(后面再讲)
基础常识
协调世界时(Coordinated Universial Time,简称UTC)是最主要的时间标准,其以原子时秒长为基础,在时刻上尽量接近于格林威治标准时间(GMT)。 协调世界时是世界上调节时钟和时间的主要时间标准,它与 0 度经线的平太阳时相差不超过 1 秒。因此 UTC 时间+8即可获得北京标准时间(UTC+8)。 本地时间与当地的时区相关,例如中国当地时间采用了北京标准时间(UTC+8)。 纪元时间(Epoch time)又被称为Unix时间(常用Linux的小伙伴可能会比较熟悉)。它表示 1970 年 1 月 1 日00:00UTC 以来所经历的秒数(不考虑闰秒)。 你应该很快就意识到这个大整数在储存上可能会产生很多问题,例如溢出。在一些历史机器上,使用了32位的有符号整数来储存这个时间戳,因此产生在结果就是:在 2038-01-19 03:14:07 这一刻,该值会溢出。 如果你对为什么选 1970-1-1日零点做位纪元时间起点,可以戳:
夏令时,(Daylight Saving Time:DST),也叫夏时制,又称“日光节约时制”和“夏令时间”,是一种为节约能源而人为规定地方时间的制度,在这一制度实行期间所采用的统一时间称为“夏令时间”。 (1916年德国实施的哪项政策使得西方世界陷入了黑暗抑郁?采用夏令时。) 系统时间(即time()返回的秒数)和真正的 UTC 时间有差距,这里涉及“闰秒”这个概念。闰秒由IERS(International Earth Rotation and Reference Systems Service)决定,具体不再赘述。
C语言日期时间库
C语言的日期时间库主要位于头文件中,下面给出头文件中包含的常用的类型和函数。
类型
| 类型名 | 说明 |
|---|---|
| clock_t | 进程运行时间(挂钟时间) |
| size_t | sizeof运算符返回的无符号整数类型 |
| time_t | 从纪元起的时间类型(秒数) |
| tm | 日历时间类型(是一个结构体) |
| timespec | 以秒和纳秒表示的时间(是一个结构体)(C11) |
函数
| 函数名 | 说明 |
|---|---|
| clock_t clock(void) | 返回程序执行起处理器时钟所使用的时间 |
| time_t time(time_t* timer) | 返回自纪元起计的系统当前时间 |
| double difftime(time_t time1, time_t time2) | 返回 time1 和 time2 之间相差的秒数 (time1-time2) |
| int timespec_get(struct timespec *ts, int base) | 返回基于给定时间基底的日历时间(C11,Linux下) |
| char ctime(const time_t timer) | 转换 time_t 对象为文本表示(返回一个表示当地时间的字符串,当地时间基于参数 timer) |
| char asctime(const struct tm timeptr) | 转换 tm 对象为文本表示(返回一个指向字符串的指针,代表了结构 timeptr 的日期和时间) |
| size_t strftime(char str, size_t maxsize, const char format, const struct tm *timeptr) | 转换 tm 对象到自定义的文本表示(根据 format 中定义的格式化规则,格式化结构 timeptr 表示的时间,并把它存储在 str 中) 类似的,宽字符传文本表示的函数为wcsftime() |
| struct tm gmtime(const time_t timer) | 将time_t转换成UTC表示的时间 |
| std::tm localtime(const std::time_t time) | 将time_t转换成本地时间 |
| std::time_t mktime(std::tm* time) | 将tm格式的时间转换成time_t表示的时间 |
使用 C++ 开发时,上述的 clock_t、time_t、tm 以及所有函数都已被划入 std 命名空间中,使用时要注意。 timespec 在 C11 引入,而 C++ 在 C++17 标准才正式引入 timespec 类型。 这里用一张图来理解:

前面我们已经简单讲述了如何获取时间、计算时间差。这里再简单举几个函数的例子。 tm 结构体的定义如下(在前面也已经见过了):
struct tm {
int tm_sec; /* 秒,范围从 0 到 59 */
int tm_min; /* 分,范围从 0 到 59 */
int tm_hour; /* 小时,范围从 0 到 23 */
int tm_mday; /* 一月中的第几天,范围从 1 到 31 */
int tm_mon; /* 月,范围从 0 到 11 */
int tm_year; /* 自 1900 年起的年数 */
int tm_wday; /* 一周中的第几天,范围从 0 到 6 */
int tm_yday; /* 一年中的第几天,范围从 0 到 365 */
int tm_isdst; /* 夏令时 */
};
有两点我们需要注意:
- tm_mon 表示的范围为 [0, 11]。转换成日常使用的月份表示需要+1。
- tm_year 表示的是自 1900 年之后所过的年份数。转换成日常使用的年份表示需要+1900。
(而 Windows.h 中的 SYSTEMTIME 结构体不一样,其中的月份和年份就是日常使用的)
time_t now = time(NULL);
struct tm* gm_time = gmtime(&now);
printf("gmtime: %s", asctime(gm_time));
struct tm* local_time = localtime(&now);
printf("localtime: %s", asctime(local_time));
// 运行结果:
// gmtime: Mon Aug 15 07:08:26 2022
// localtime: Mon Aug 15 15:08:26 2022
需要注意的是,gmtime 和 localtime 的返回值指向 statically-allocated 结构,该结构可能会被对任何日期和时间函数的后续调用覆盖。如果你先调用 gmtime,然后调用 localtime,你会得到两个指向同一个结构的指针,这个结构只包含第二个结果,再进行 printf 输出的话只会得到两个相同的结果。要么在调用后立即打印每个结果,要么使用 localtime_r 和 gmtime_r 指向自己分配的 struct tm 的指针。

tm 储存的日期时间结构,我们使用的是 asctime 函数将其转换为字符串格式。如果是希望直接打印表示时间的字符串的话,可以使用可以使用 ctime 函数。
ctime 函数打印的格式是固定的,和 asctime 相同:
time_t now = time(NULL);
printf("%s", ctime(&now));
// ctime()打印格式:
// Www Mmm dd hh:mm:ss yyyy\n
// 运行结果
// Mon Aug 15 15:12:31 2022
在有格式要求的情况下,我们通常有两种做法:
- 拆分 tm 结构体的字段;
- 使用 strftime 或者 wcsftime 函数来指定格式输出。
#include <stdio.h>
#include <time.h>
int main ()
{
time_t rawtime;
struct tm *info;
char buffer[80];
time( &rawtime );
info = localtime( &rawtime );
strftime(buffer, 80, "%Y-%m-%d %H:%M:%S", info);
printf("|%s|\n", buffer );
return 0;
}
// 运行结果
// |2022-08-15 15:22:22|
可以参考:
C 库函数 - strftime()www.runoob.com/cprogramming/c-function-strftime.html
std::strftime - cppreference.comzh.cppreference.com/w/cpp/chrono/c/strftime
我们在前面使用 clock_gettime() 的时候已经了解了,timespec 类型提供了纳秒级别的精度。
#include <stdio.h>
#include <time.h>
int main ()
{
struct timespec ts;
timespec_get(&ts, TIME_UTC);
char buff[100];
strftime(buff, sizeof(buff), "%D %T", gmtime(&ts.tv_sec));
printf("Current time: %s.%09ld UTC\n", buff, ts.tv_nsec);
return 0;
}
// 运行结果
// Current time: 08/15/22 07:51:55.493055600 UTC
C++ chrono库
你的托福老师可能会和你讲过:英语中 chrono-词根就是表示“时间”,例如 chronic、chronicle、chronology、synchronous 等等都与“时间”有关。这个词起源于古希腊的时间之神柯罗诺斯(Chronos / Khronos)。 C++ 的 chrono 库是以各种精度跟踪时间的类型的灵活汇集。chrono 库定义三种主要的时钟以及工具函数和常用的类型:
- 时钟
- 时长
- 时间点
时钟
C++11 的 chrono 库主要包含了三种类型的时钟:
| 名称 | 说明 |
|---|---|
| system_clock | 来自系统范畴实时时钟的挂钟时间 |
| steady_clock | 决不会调整的单调时钟 |
| high_resolution_clock | 可用的最短周期的时钟 |
- system_clock 来源是系统时钟。
- steady_clock 是一个单调时钟。此时钟的时间点无法减少,按照物理时间向前移动。steady_clock 的单调递增在应用中有个很大的优势,就是不受系统时间修改影响。因而 steady_clock 是度量间隔的最适宜的选择。
- high_resolution_clock 表示实现提供的拥有最小计次周期的时钟。它可以是 system_clock 或 steady_clock 的别名,或者第三个独立时钟。(在不同标准库实现之间实现并不一致,尽量不要使用。)
std::cout << "system clock : "
<< std::chrono::system_clock::period::num << "/" << std::chrono::system_clock::period::den << "s" << '\n'
<< "steady clock : "
<< std::chrono::steady_clock::period::num << "/" << std::chrono::steady_clock::period::den << "s" << '\n'
<< "high resolution clock : "
<< std::chrono::high_resolution_clock::period::num << "/" << std::chrono::high_resolution_clock::period::den << "s" << '\n';
// 本地运行结果:
// system clock : 1/1000000000s
// steady clock : 1/1000000000s
// high resolution clock : 1/1000000000s
可见最高可以精确到纳秒。 (C++20 还有其它的很多 clock,不管了QWQ) 对于这三个时钟类,有着以下共同的成员:
| 名称 | 说明 |
|---|---|
| now() | 静态成员函数,返回当前时间,类型为clock::time_point |
| time_point | 成员类型,当前时钟的时间点类型。 |
| duration | 成员类型,时钟的时长类型。 |
| rep | 成员类型,时钟的tick类型,等同于clock::duration::rep |
| period | 成员类型,时钟的单位,等同于clock::duration::period |
| is_steady | 静态成员类型:是否是稳定时钟,对于steady_clock来说该值一定是true |
每个时钟类都有着一个静态成员函数 new() 来获取当前时间。该函数的返回类型则是由该时钟类的 time_point 描述,例如 std::chrono::time_point<:chrono::system_clock> 或者std::chrono::time_point<:chrono::steady_clock>。我们可以使用 auto 关键字来简写(auto 是个好文明)。 阅读文档,我们不难发现system_clock有着与另外两个 clock 所不具有的特性:它是唯一有能力映射其时间点到 C-Style 时间的 C++ 时钟。system_clock 提供了两个静态成员函数来与 std::time_t 进行互相转换:
| 名称 | 说明 |
|---|---|
| to_time_t | 转换系统时钟时间点为 std::time_t |
| from_time_t | 转换 std::time_t 到系统时钟时间点 |

时长
类模板 std::chrono::duration 表示时间间隔。
template <class Rep, class Period = ratio<1> >
class duration;
模板类如上,通过数值(Rep)+单位(Period,即小时,分,秒)来进行表示时间范围(1小时),单位通过ratio 来进行表示;ratio 是个 C++11 提供的十分简单的编译时有理数算数支持类。
template <intmax_t N, intmax_t D = 1>
class ratio;
N 为分子,D 为分母,duration 中以 N/D 秒表示单位,如 std::ratio<60,1> 或 std::ratio<60> 为分钟,std::ratio<1:1000>为毫秒。为了方便使用,ratio 头文件中定义了常用比率的别名。
typedef ratio<1, 1000000000000000000> atto;
typedef ratio<1, 1000000000000000> femto;
typedef ratio<1, 1000000000000> pico;
typedef ratio<1, 1000000000> nano;
typedef ratio<1, 1000000> micro;
typedef ratio<1, 1000> milli;
typedef ratio<1, 100> centi;
typedef ratio<1, 10> deci;
typedef ratio< 10, 1> deca;
typedef ratio< 100, 1> hecto;
typedef ratio< 1000, 1> kilo;
typedef ratio< 1000000, 1> mega;
typedef ratio< 1000000000, 1> giga;
typedef ratio< 1000000000000, 1> tera;
typedef ratio< 1000000000000000, 1> peta;
typedef ratio< 1000000000000000000, 1> exa;
下面是chrono库中提供的很常用的几个时长单位:
| 类型 | 定义 |
|---|---|
| std::chrono::nanoseconds | duration<至少 64 位的有符号整数类型, std::nano> |
| std::chrono::microseconds | duration<至少 55 位的有符号整数类型, std::micro> |
| std::chrono::milliseconds | duration<至少 45 位的有符号整数类型, std::milli> |
| std::chrono::seconds | duration<至少 35 位的有符号整数类型> |
| std::chrono::minutes | duration<至少 29 位的有符号整数类型, std::ratio<60>> |
| std::chrono::hours | duration<至少 23 位的有符号整数类型, std::ratio<3600>> |
(C++20还提供了 year、month、week 和 day。) 我们当然也可以自己定义时长单位:
using one_quarter_of_an_hour = std::chrono::duration<long long, std::ratio<60 * 15>>;
auto t = std::chrono::duration_cast<one_quarter_of_an_hour>(std::chrono::hours(1)).count(); // t == 4;
我们可以调用 duration 类的 count() 成员函数来获取具体数值。 时长运算可以直接使用“+”或“-”相加相减。chrono 库也提供了几个常用的函数:
| 函数 | 说明 |
|---|---|
| duration_cast | 进行时长的转换 |
| floor (C++17) | 以向下取整的方式,将一个时长转换为另一个时长 |
| ceil (C++17) | 以向上取整的方式,将一个时长转换为另一个时长 |
| round (C++17) | 转换时长到另一个时长,就近取整,偶数优先 |
| abs (C++17) | 获取时长的绝对值 |
例如:想要知道 2 个小时零 5 分钟一共是多少秒,可以这样写:
auto duration = std::chrono::hours(2) + std::chrono::minutes(5);
auto seconds = std::chrono::duration_cast<std::chrono::seconds>(duration);
std::cout << "02:05 is " << seconds.count() << " seconds" << std::endl;
// 运行结果
// 02:05 is 7500 seconds
从 C++14 开始,你甚至可以用字面值来描述常见的时长。这些字面值在 std::literals::chrono_literals 命名空间下(可以顺便了解一下C++11中的 inline 命名空间)。包括:
- h 表示小时
- min 表示分钟
- s 表示秒
- ms 表示毫秒
- us 表示微妙
- ns 表示纳秒
// before:
auto duration = std::chrono::hours(2) + std::chrono::minutes(5);
// after:
using namespace std::chrono_literals;
auto duration = 2h + 5min;
时间点
时钟的 now 函数返回的值就是一个时间点,时间点包含了时钟和时长两个信息。 类模板 std::chrono::time_point 表示时间中的一个点,定义如下:
template<
class Clock,
class Duration = typename Clock::duration
> class time_point;
与我们的常识一致,时间点具有加法和减法操作。
- 时间点 + 时长 = 时间点
- 时间点 - 时间点 = 时长
两个时间点也存在着比较操作,用于判断一个时间点在另外一个时间点之前还是之后,std::chrono::time_point 重载了==,!=,<,<=,>,>=操作符来实现比较操作。 这也就是我们前文已经提到过的,使用 chrono 库计算时间差的姿势。例如,你希望计算一段程序运行了多长时间,单位是毫秒:
auto start = std::chrono::steady_clock::now();
// do something
auto end = std::chrono::steady_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << duration.count() << "ms" << std::endl;
事实上,time point 内部维护了一个 duration 私有成员,通过制定的时钟,来确定距离 epoch 时间点的间隔。利用 time_since_epoch 成员函数,可以返回从纪元到当前的时间间隔,返回类型是一个 duration。
再向下一步
现在我们将目光朝向 system_clock 的具体实现。system_clock 恰如其名,这个时间是通过系统调用从 OS 获取的。具体涉及的系统调用如下表:
| 平台 | 系统调用 |
|---|---|
| Windows | GetSystemTimePreciseAsFileTime(&ft) GetSystemTimeAsFileTime(&ft) |
| 其他 | clock_gettime(CLOCK_MONOTONIC, &tp) gettimeofday(&tv, 0) |
再次声明,系统时间可能是会发生调整的,例如NTP时间同步,或者用户手动设置(就像用变速齿轮改游戏速度……)。因此要注意,这个时间是有可能发生时间回退等情况的。可以查看 std::chrono::system_clock::is_steady 这个值,一般是false(0)。 相比于 system_clock,steady_clock 最为重要的特性就是它一定是单调增加的,而且通常来说,它能够提供系统所支持的最高精度时间。另一个要注意的点是它的起始时间是不一定的,例如,它的起点可能是系统启动的时间,因此,它非常适合于测试时间区间,典型的应用就是测试程序运行时长。从代码中可以看出,steady_clock 同样是由操作系统的系统调用完成,关键的系统调用如下表:
| 平台 | 系统调用 |
|---|---|
| z/OS | gettimeofdayMonotonic(&ts) |
| Windows | __QueryPerformanceFrequency() QueryPerformanceCounter(&counter) |
| Linux | clock_gettime(CLOCK_MONOTONIC, &tp) |
| macOS | mach_timebase_info(&MachInfo); |
high_resolution_clock 嘛,就像之前说过的,它可能是个别名,而且标准不同,不建议使用。 更深层次的、关于计时功能在 OS、硬件方面的实现,可以参考大佬的文章:
966

被折叠的 条评论
为什么被折叠?



