在 C++11 之前,C++ 編程只能使用 C-style 日期時間庫,其精度只有秒級別,這對於有高精度要求的程式來說,是不夠的。但這個問題在C++11 中得到瞭解決,C++11 中不僅擴展了對於精度的要求,也為不同系統的時間要求提供了支持。另一方面,對於只能使用 C-style 日期時間庫的程式來... ...
C++11
的日期和時間編程內容在 C++ Primer(第五版)這本書並沒有介紹,目前網上的文章又大多質量堪憂或者不成系統,故寫下這篇文章用作自己的技術沉澱和技術分享,大部分內容來自網上資料,文末也給出了參考鏈接。
日期和時間庫是每個編程語言都會提供的內部庫,其可以用列印模塊耗時,從而方便做性能分析,也可以用作列印運行時間點。本文的內容著重於 C++11-C++17的內容,C++20的日期和時鐘庫雖然使用更方便也更強大,但是考慮到版本相容和程式移植問題,故不做深入探討。
一,概述
C++ 中可以使用的日期時間 API 分為兩類:
C-style
日期時間庫,位於頭文件中。這是原先 <time.h> 頭文件的 C++ 版本。 chrono
庫:C++ 11 中新增API,增加了時間點,時長和時鐘等相關介面(使用較為複雜)。
在 C++11 之前,C++ 編程只能使用 C-style 日期時間庫,其精度只有秒級別,這對於有高精度要求的程式來說,是不夠的。但這個問題在C++11 中得到瞭解決,C++11 中不僅擴展了對於精度的要求,也為不同系統的時間要求提供了支持。另一方面,對於只能使用 C-style 日期時間庫的程式來說,C++17 中也增加了 timespec 將精度提升到了納秒級別。
二,C-style 日期和時間庫
#include <ctime>
該頭文件包含了獲取和操作日期和時間的函數和相關數據類型定義。
2.1,數據類型
名稱 | 說明 |
---|---|
time_t |
能夠表示時間的基本算術類型的別名,能夠表示函數 time 返回的時間,單位為秒級別。 |
clock_t |
能夠表示時鐘滴答計數的基本算術類型的別名(可用作進程運行時間) |
size_t |
sizeof 運算符返回的無符號整數類型。 |
struct tm |
包含日曆日期和時間的結構體類型 |
timespec* | 以秒和納秒錶示的時間 |
2.2,函數
C-style
日期時間庫中包含的時間操作函數如下:
函數 | 說明 |
---|---|
std::clock_t clock() |
返回自程式啟動時起的處理器時鐘時間 |
double difftime(std::time_t time_end, std::time_t time_beg) |
計算開始和結束之間的秒數差 |
std::time_t time (time_t* timer) |
返回自紀元起計的系統當前時間, 函數可以為空指針 |
std::time_t mktime (struct tm * timeptr) |
將 tm 格式的時間轉換成 time_t 表示的時間 |
時間轉換函數如下:
函數 | 說明 |
---|---|
char* asctime(const struct tm* timeptr) |
將 tm 結構體對象轉換為字元串的文本 |
char* ctime(const time_t* timer) |
將 time_t 對象轉換為 C 字元串,用於表示日曆時間 |
struct tm* gmtime(const time_t* time) |
將 time_t 轉換成 UTC 表示的時間 |
struct tm* localtime(const time_t* timer) |
將 time_t 轉換成本地時間 |
localtime
函數使用參數timer
指向的值來填充tm
結構體,其中的值表示對應的時間,以本地時區表示。
strftime
和 wcsftime
函數一般不常用,故不做介紹。tm
結構體的一般定義如下:
/* Used by other time functions. */
struct tm
{
int tm_sec; /* Seconds. [0-60] (1 leap second) */
int tm_min; /* Minutes. [0-59] */
int tm_hour; /* Hours. [0-23] */
int tm_mday; /* Day. [1-31] */
int tm_mon; /* Month. [0-11] */
int tm_year; /* Year - 1900. */
int tm_wday; /* Day of week. [0-6] */
int tm_yday; /* Days in year.[0-365] */
int tm_isdst; /* DST. [-1/0/1]*/
};
2.3,數據類型與函數關係梳理
時間和日期相關的函數及數據類型比較多,單純看表格和代碼不是很好記憶,第一個參考鏈接的作者給出瞭如下所示的思維導圖,方便記憶與理解上面所有函數及數據類型之間各自的聯繫。
在這幅圖中,以數據類型為中心,帶方向的實線箭頭表示該函數能返回相應類型的結果。
clock
函數是相對獨立的一個函數,它返回進程運行的時間,具體描述見下文。time_t
描述了紀元時間,通過time
函數可以獲得它,但它只能精確到秒級別。timespec
類型在time_t
的基礎上,增加了納秒的精度,通過timespec_get
獲取。這是C++17
上新增的特性。tm
是日曆類型,因為它其中包含了年月日等信息。通過 gmtime,localtime 和 mktime 函數可以將 time_t 和 tm 類型互相轉換。- 考慮到時區的差異,因此存在 gmtime 和 localtime 兩個函數。
- 無論是
time_t
還是tm
結構,都可以將其以字元串格式輸出。ctime 和 asctime 輸出的格式是固定的。如果需要自定義格式,需要使用 strftime 或者 wcsftime 函數。
2.4,時間類型
2.4.1,UTC 時間
協調世界時(Coordinated Universial Time,簡稱 UTC)是最主要的時間標準,其以原子時秒長為基礎,在時刻上儘量接近於格林威治標準時間。
協調世界時是世界上調節時鐘和時間的主要時間標準,它與0度經線的平太陽時相差不超過 1 秒。因此UTC時間+8即可獲得北京標準時間(UTC+8)。
2.4.2,本地時間
本地時間與當地的時區相關,例如中國當地時間採用了北京標準時間(UTC+8
)。
2.4.3,紀元時間
紀元時間(Epoch time)又叫做 Unix 時間或者 POSIX 時間。它表示自1970 年 1 月 1 日 00:00 UTC 以來所經過的秒數(不考慮閏秒)。它在操作系統和文件格式中被廣泛使用。
紀元時間這個想法很簡單:以一個時間為起點加上一個偏移量便可以表達任何一個其他的時間。
為什麼選這個時間作為起點,可以點擊這裡:Why is 1/1/1970 the “epoch time”?。
通過 time
函數獲取當前時刻的紀元時間示例代碼如下:
time_t epoch_time = time(nullptr);
cout << "Epoch time: " << epoch_time << endl;
// Epoch time: 1660039180 (日曆時間: Tue Aug 9 17:59:40 2022)
time
函數接受一個指針,指向要存儲時間的對象,通常可以傳遞一個空指針,然後通過返回值來接受結果。雖然標準中沒有給出定義,但time_t
通常使用整形值來實現。
2.5,輸出時間和日期
使用 ctime
函數,可以將時間以固定格式的字元串的形式列印出來,格式為:Www Mmm dd hh:mm:ss yyyy\n。代碼示例如下:
// 以字元串形式輸出當前時間和日期
time_t now = time(nullptr);
cout << "Now is: " << ctime(&now);
// Now is: Tue Aug 9 18:06:38 2022
2.6,綜合示例代碼
asctime()
和 difftime()
函數等sample
代碼如下(複製可直接運行):
/* asctime example */
#include <stdio.h> /* printf */
#include <time.h> /* time_t, struct tm, time, localtime, asctime */
#include <vector>
#include <iostream>
using namespace std;
// 冒泡排序: 將數據從小到大排序
void bubbleSort(vector<int> &arr){
size_t number = arr.size();
if (number <= 1) return;
int temp;
for(int i = 0; i < number; i++){
for(int j = 0; j < number-i; j++){
if (temp > arr[j+1]){
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
// difftime() 函數: 計算時間差,單位為 s
void difftime_test()
{
vector<int> input_array;
for (int i = 90000; i > 0; i--) {
input_array.emplace_back(i);
}
time_t time1 = time(nullptr);
bubbleSort(input_array);
time_t time2 = time(nullptr);
double time_diff = difftime(time2, time1);
cout << "input array size is " << input_array.size() << " after bubbleSort time_diff: " << time_diff << "s" << endl;
}
// astime() 函數: 將本地時間 tm 結構體對象轉換為字元串文本
void astime_test()
{
time_t raw_time = time(nullptr); // 獲取當前時刻日曆時間
struct tm* local_timeinfo = localtime(&raw_time);
printf ( "The current date/time is: %s", asctime (local_timeinfo) );
}
int main()
{
difftime_test();
astime_test();
// 3, 輸出當前紀元時間
time_t epoch_time = time(nullptr);
cout << "Epoch time: " << epoch_time << endl;
// 4,以字元串形式輸出當前時間和日期
time_t now = time(nullptr);
cout << "Now is: " << ctime(&now);
}
g++ time_demo.cpp -std=c++11
編譯後,運行程式 ./a.out
後,輸出結果:
三,chrono 庫
“chrono” 是英文 chronology 的縮寫,其含義是“年表;年代學”。
chrono
既是頭文件名字也是子命名空間的名字,chrono
頭文件下的所有 elements
都是在 std::chrono
命名空間下定義的。
std::chrono
是 C++11 引入的日期時間處理庫,chrono
庫里包括三種主要類型:Clocks
,Time points
和 Durations
。
3.1,時鐘
C++11
chrono
庫中包含了三種的時鐘類:
名稱 | 說明 |
---|---|
chrono::system_clock |
系統時鐘(可以調整) |
chrono::steady_clock |
單調遞增時鐘(不能調整) |
chrono::high_resolution_clock |
擁有可用的最短嘀嗒周期的時鐘 |
system_clock
是當前所在系統的時鐘。因為系統時鐘隨時都可能被調整,所以如果想要計算兩個時間點的時間差,是不推薦使用系統時鐘的。
steady_clock
會保證時間的單調遞增性,只會向前移動不會減少,所以最適合用來度量時間間隔。
high_resolution_clock
表示實現提供的擁有最小計次周期的時鐘。它可以是 system_clock 或 steady_clock 的別名,也可能是第三個獨立時鐘。在不同的標準庫中,high_resolution_clock 的實現不一致,所以官方不建議使用這個時鐘。
這三個時鐘類有一些共同的成員函數和數據類型,如下所示:
名稱 | 說明 |
---|---|
now() |
靜態成員函數,返回當前時間,類型為 clock::time_point |
time_point |
成員類型,當前時鐘的時間點類型,用於表示一個具體時間,詳情見下文“時間點” |
duration |
成員類型,時鐘的時長類型,用於表示時間間隔(一段時間),詳情見下文“時長” |
rep |
成員類型,時鐘的 tick 類型,等同於 clock::duration::rep |
period |
成員類型,時鐘的單位,等同於 clock::duration::period |
is_steady |
靜態成員類型:是否是穩定時鐘,對於 steady_clock 來說該值一定是 true |
每一個時鐘類都有一個 now()
靜態函數來獲取當前時間,返回的類型由 time_*point 描述。std::chrono::time_point 是模板類,模版類實例如:std::chrono::time_pointstd::chrono::steady\_*clock,這樣寫比較長,慶幸的是在 C++11 中可以通過 auto
關鍵字來自動推導變數類型。
std::chrono::time_point<std::chrono::steady_clock> now1 = std::chrono::steady_clock::now();
auto now2 = std::chrono::steady_clock::now();
3.2,與C-style轉換
system_clock 與另外兩個 clock 不一樣的地方在於,它還提供了兩個靜態函數用來將 time_point 與 std::time_t 來迴轉換。
名稱 | 說明 |
---|---|
to_time_t | 將系統時鐘時間點轉換為 time_t |
from_time_t | 將 time_t 轉換到系統時鐘時間點 |
第一篇參考鏈接的文章給出了下麵這幅圖來描述 c 風格和 c++11 的幾種時間類型的轉換:
3.3,時長 ratio
為了支持更高精度的系統時鐘,C++11
新增了一個新的頭文件 <ratio>
和類型,用於自定義時間單位。std::ratio
是一個模板類,提供了編譯期的比例計算功能,為 std::chrono::duration 提供基礎服務。其聲明如下:
template<
std::intmax_t Num,
std::intmax_t Denom = 1
> class ratio;
第一個模板參數 Num
(numerator) 表示分子,第二個參數 Denom
(denominator) 表示分母。typedef ratio<1, 1000> milli;
表示一千分之一,因為約定了基本計算單位是秒,所以 milli
表示一千分之一秒。所以通過 ratio
可以表示毫秒、微秒、納秒等。
typedef ratio<1,1000000000> nano; // 納秒單位
typedef ratio<1,1000000> micro; // 微秒單位
typedef ratio<1,1000> milli; // 毫秒單位
typedef ratio<1,1> s // 秒單位
ratio 能表達的數值不僅僅是以 10 為基底的,同時也可以表達任意的分數秒,例如:5/7秒,89/23409 秒等等對於一個具體的 ratio 來說,可以通過 den 獲取分母的值,num 獲取分子的值。不僅僅如此,ratio_add,ratio_subtract,ratio_multiply,ratio_divide
來完成分數的加減乘除四則運算。例如,想要計算 5/7+59/1023,可以用以下代碼表示:
ratio_add<ratio<5, 7>, ratio<59, 1023>> result;
double value = ((double) result.num) / result.den;
cout << result.num << "/" << result.den << " = " << value << endl;
// 代碼輸出結果是 5528/7161 = 0.771959
在C++中,如果分子和分母都是整形,則整形除法結果依然是整形,即小數點右邊部分會被拋棄,因此想要獲取
double
類型的結果,需要先將其轉換成double
。
3.3.1,時長運算
時長對象之間可以進行相加或相減運算。chrono
提供了以下幾個常用時長運算的函數:
函數 | 說明 |
---|---|
duration_cast |
進行時長的轉換 |
floor(C++17) |
以向下取整的方式,將一個時長轉換為另一個時長 |
ceil(C++17) |
以向上取整的方式,將一個時長轉換為另一個時長 |
round(C++17) |
轉換時長到另一個時長,就近取整,偶數優先 |
abs(C++17) |
獲取時長的絕對值 |
3.4,時間間隔 duration
類模板 std::chrono::duration 表示時間間隔,其聲明如下:
template<
class Rep,
class Period = std::ratio<1>
> class duration;
類成員類型描述:
member type | definition | notes |
---|---|---|
rep | The first template parameter (Rep ) |
Representation type used as the type for the internal count object. |
period | The second template parameter (Period ) |
The ratio type that represents a period in seconds. |
duration
由Rep
類型的計次數和Period
類型的計次周期組成,其中計次周期是一個編譯期有理數常量,表示從一個計次到下一個的秒數。存儲於 duration 的數據僅有 Rep 類型的計次數。若 Rep 是浮點數,則 duration 能表示小數的計次數。 Period 被包含為時長類型的一部分,且只在不同時長間轉換時使用。
Rep
表示一種數值類型,用來表示 Period 的數量,比如 int float double (count of ticks)。Period
是 std::ratio 類型,用來表示【用秒錶示的時間單位】比如 second milisecond (a tick period)。- 成員函數
count()
返回Rep
類型的Period
數量。
常用的 duration<Rep, Period>
已經定義好了,在 std::chrono
頭文件中,常用時長單位的代碼如下:
/// nanoseconds
typedef duration<int64_t, nano> nanoseconds;
/// microseconds
typedef duration<int64_t, micro> microseconds;
/// milliseconds
typedef duration<int64_t, milli> milliseconds;
/// seconds
typedef duration<int64_t> seconds;
/// minutes
typedef duration<int, ratio< 60>> minutes;
/// hours
typedef duration<int, ratio<3600>> hours;
類型 | 定義 |
---|---|
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» |
duration
類的 count()
成員函數返回時間間隔的具體數值。
3.4.1,時間間隔轉換函數 duration_cast
因為有各種 duration
表示不同的時長單位,所以 chrono 庫提供了 duration_cast
函數來換 duration
類型,其聲明如下:
template <class ToDuration, class Rep, class Period>
constexpr ToDuration duration_cast(const duration<Rep,Period>& d);
其定義比較複雜,但是我們日常使用可以直接使用 auto
推導函數返回對象類型,示例代碼如下:
#include <iostream>
#include <chrono>
#include <ratio>
#include <thread>
void f()
{
std::this_thread::sleep_for(std::chrono::seconds(1));
}
int main()
{
auto t1 = std::chrono::high_resolution_clock::now();
f();
auto t2 = std::chrono::high_resolution_clock::now();
// 整數時長:要求 duration_cast
auto int_ms = std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1);
// 小數時長:不要求 duration_cast
std::chrono::duration<double, std::milli> fp_ms = t2 - t1;
std::cout << "f() took " << fp_ms.count() << " ms, "
<< "or " << int_ms.count() << " whole milliseconds\n";
// 程式輸出結果: f() took 1000.23 ms, or 1000 whole milliseconds
}
3.5,時間點 time_point
std::chrono::time_point
表示時間中的一個點(一個具體時間),如上個世紀80年代、你的生日、今天下午、火車出發時間等,只要它能用電腦時鐘錶示。其包含了時鐘和時長兩個信息。它被實現成如同存儲一個 Duration
類型的自 Clock
的紀元起始開始的時間間隔的值。其聲明如下:
template<
class Clock,
class Duration = typename Clock::duration
> class time_point;
時鐘的 now()
函數返回的值就是一個時間點。time_point 中的 time_since_epoch() 返回從其時鐘起點開始的時長。可以通過兩個時間點相減計算一個時間間隔,下麵是代碼示例:
#include <stdio.h> /* printf */
#include <iostream>
#include <chrono>
#include <math.h>
using namespace std;
void time_point_test()
{
auto start = chrono::steady_clock::now();
double sum = 0;
for(int i = 0; i < 100000000; i++) {
sum += sqrt(i);
}
auto end = chrono::steady_clock::now();
// 通過兩個時間點相減計算一個時間間隔
auto time_diff = end - start;
// 將時間間隔單位轉化為毫秒
auto duration = chrono::duration_cast<chrono::milliseconds>(time_diff);
cout << "Sqrt Operation cost : " << duration.count() << "ms" << endl;
}
int main()
{
time_point_test();
// 程式輸出結果: Sqrt Operation cost : 838ms
}
3.5.1,時間點運算
時間點有加法和減法操作,計算結果和常識一致:時間點 + 時長 = 時間點;時間點 - 時間點 = 時長。