在C++开发时,经常会用到与时间相关的函数或者库,例如C风格的时间函数、std::chrono库、Boost.Date_Time库。
他们都包含哪些时间函数或者类?它们的设计思想是什么?什么情况选择什么样的时间函数?怎么使用呢?大家可能都会有这些方面的疑问,于是我决定就对这些库总结梳理一下。
由于文章太长,分成四部分:C风格的时间函数、std::chrono库、Boost.Date_Time库和总结。
C风格的时间类型和函数,有C和C++两个版本,如果使用C版本需要包含#include <time.h>,如果使用C++版本需要包含#include <ctime>,并且使用命名空间std。与平台相关的类型和函数会特别指出。
数据类型
time_t
日历时间(calendar time)。在C/C++编程中经常会遇到它,它定义为从协调时间时(UTC)1970年01月01日00时00分00秒起至现在的总秒数,被用于表示Unix时间(戳)。
一般编译器使用32位整数表示time_t,因此这个时间最多使用到2038年1月18日19时14分07秒。一些编译器使用64位整数来表示time_t,可以避免这个问题。
struct tm
分解时间(broken-down time)。是一个存储了日期和时间的结构体,定义如下:1
2
3
4
5
6
7
8
9
10
11
12struct tm
{
    int tm_sec;  /*秒,正常范围0-59, 但允许至61, C99(C++11)改为60*/
    int tm_min;  /*分钟,0-59*/
    int tm_hour; /*小时, 0-23*/
    int tm_mday; /*日,即一个月中的第几天,1-31*/
    int tm_mon;  /*月, 从一月算起,0-11,1+tm_mon*/
    int tm_year;  /*年, 从1900至今已经多少年, 1900+tm_year*/  
    int tm_wday; /*星期,一周中的第几天, 从星期日算起,0-6*/
    int tm_yday; /*从今年1月1日到目前的天数,范围0-365,其中0代表1月1日,1代表1月2日,以此类推*/
    int tm_isdst; /* 夏令时标识符,tm_isdst>0,实行夏令时的时候;tm_isdst=0, 不实行夏令时的进候;tm_isdst<0, 表示未知。*/
};
需要特别注意的是,年份是从1900年起至今多少年,而不是直接存储如2011年,月份从0开始的,0表示一月,星期也是从0开始的,0表示星期日,1表示星期一。
clock_t
时钟滴答数(clock tick)。记录从进程开始的时间,是一个相对时间,与CLOCKS_PER_SEC(每秒内时钟滴答数,定义在time.h中,一般为1000)相关。一般用32位整数表示clock_t。
struct timeval
它表示一个时间间隔,这个类型不是标准的C++类型,而是定义在Berkeley Software Distribution (BSD)的Time.h中的一个类型,定义如下:1
2
3
4struct timeval {
  long tv_sec;   //秒
  long tv_usec;  //微秒
} timeval;
这个类型具有更高的精度,但不具有跨平台性,主要在Unix或类Unix系统中使用,需要包含头文件<sys/time.h>,与gettimeofday函数配合使用。
在Windows下也定义了此结构,主要被Windows Sockets函数select使用,定义在头文件Winsock2.h中。
为了跨平台,尽量不要使用此类型。
获取时间函数
time
1  | time_t time (time_t* timer);  | 
返回自从00:00 hours, Jan 1, 1970 UTC (i.e., the current unix timestamp)到现在的秒数。1
2time_t timer;
time(&timer);  /* get current time; same as: timer = time(NULL)  */
clock
1  | clock_t clock(void);  | 
得到从进程启动到此次函数调用的累计的时钟滴答数。可以用于算法计时。1
2
3
4
5
6clock_t start, finish;
start = clock();
...
finish = clock();
duration = (double)(finish - start) / CLOCKS_PER_SEC;
printf( "%2.1f seconds\n", duration );
GetLocalTime
1  | void WINAPI GetLocalTime(  | 
Windows函数,能获取当前日期和时间(UTC),能精确到毫秒,不具有跨平台性。
如果想获取更高精度的时间(<1us),可以用QueryPerformanceCounter和QueryPerformanceFrequency。
时间日期数据类型的转换函数
gmtime (time_t -> tm)
1  | struct tm * gmtime (const time_t * timer);  | 
转换time_t成UTC时间的tm结构体。1
2
3
4time_t rawtime;
struct tm * ptm;
time ( &rawtime );
ptm = gmtime ( &rawtime );
localtime (time_t -> tm)
1  | struct tm * localtime (const time_t * timer);  | 
转换time_t成本地时区的tm结构体。
多个线程同时调用localtime会引起数据竞争,下面的线程安全部分会讲到它,也可以参看localtime的安全性总结。1
2
3
4
5
6time_t rawtime;
struct tm * timeinfo;
time (&rawtime);
timeinfo = localtime (&rawtime);
printf ("Current local time and date: %s", asctime(timeinfo));
mktime (tm -> time_t)
1  | time_t mktime (struct tm * timeptr);  | 
转换本地时间的tm结构体成time_t。
这个函数是localtime的逆操作。1
2
3
4
5
6
7
8
9
10
11
12
13#include <iostream>
#include <iomanip>
#include <ctime>
int main()
{
    std::time_t t = std::time(NULL);
    std::tm tm = *std::localtime(&t);
    std::cout << "Today is           " << std::put_time(&tm, "%c %Z") <<'\n';
    tm.tm_mon -= 100;  // tm_mon is now outside its normal range
    std::mktime(&tm);
    std::cout << "100 months ago was " << std::put_time(&tm, "%c %Z") << '\n';
}
结果1
2Today is           Wed Dec 28 09:56:10 2011 EST
100 months ago was Thu Aug 28 10:56:10 2003 EDT
获取时间函数与时区关系

关于时区的总结,请看时区和Unix时间戳的整理
时间日期数据的格式化函数
ctime (time_t -> char*)
1  | char* ctime (const time_t * timer);  | 
转换time_t成人类可读的字符串,是本地时间。
这个函数等价:1
asctime(localtime(timer))
格式如下:1
Www Mmm dd hh:mm:ss yyyy
Www是周几,Mmm是月份(字母表示),dd是当月的一天,hh::mm::ss是时间,yyyy是年份1
2
3
4
5
6
7
8
9
10
11
12#include <stdio.h>      /* printf */
#include <time.h>       /* time_t, time, ctime */
int main ()
{
  time_t rawtime;
  time (&rawtime);
  printf ("The current local time is: %s", ctime (&rawtime));
  return 0;
}
asctime (tm -> char*)
1  | char* asctime (const struct tm * timeptr);  | 
转换tm结构体成字符串。
这个函数等价:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18char* asctime(const struct tm *timeptr)
{
  static const char wday_name[][4] = {
    "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
  };
  static const char mon_name[][4] = {
    "Jan", "Feb", "Mar", "Apr", "May", "Jun",
    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
  };
  static char result[26];
  sprintf(result, "%.3s %.3s%3d %.2d:%.2d:%.2d %d\n",
    wday_name[timeptr->tm_wday],
    mon_name[timeptr->tm_mon],
    timeptr->tm_mday, timeptr->tm_hour,
    timeptr->tm_min, timeptr->tm_sec,
    1900 + timeptr->tm_year);
  return result;
}
字符串格式同ctime。1
2
3
4
5
6
7
8
9
10
11
12
13
14#include <stdio.h>      /* printf */
#include <time.h>       /* time_t, struct tm, time, localtime, asctime */
int main ()
{
  time_t rawtime;
  struct tm * timeinfo;
  time ( &rawtime );
  timeinfo = localtime ( &rawtime );
  printf ( "The current date/time is: %s", asctime (timeinfo) );
  return 0;
}
strftime (tm -> char*)
1  | size_t strftime (char* ptr, size_t maxsize, const char* format,  | 
转换结构体tm为自定义格式的字符串,类似于sprintf。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17#include <stdio.h>      /* puts */
#include <time.h>       /* time_t, struct tm, time, localtime, strftime */
int main ()
{
  time_t rawtime;
  struct tm * timeinfo;
  char buffer [80];
  time (&rawtime);
  timeinfo = localtime (&rawtime);
  strftime (buffer,80,"Now it's %I:%M%p.",timeinfo);
  puts (buffer);
  return 0;
}
strptime (char* -> tm)
1  | char * strptime(const char* buf, const char* format, struct tm* tptr)  | 
strftime逆操作,把字符串按照自定义的格式转换为tm。
std::put_time
1  | template< class CharT >  | 
转换结构体tm为自定义格式的字符串,类似于strftime,使用上比strftime方便,可以直接输出到stream中1
cout << std::put_time(ptm,"%c") << endl;
对时间数据的操作
difftime
1  | double difftime (time_t end, time_t beginning);  | 
计算两个时间的差值,单位为秒。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22#include <stdio.h>      /* printf */
#include <time.h>       /* time_t, struct tm, difftime, time, mktime */
int main ()
{
  time_t now;
  struct tm newyear;
  double seconds;
  time(&now);  /* get current time; same as: now = time(NULL)  */
  newyear = *localtime(&now);
  newyear.tm_hour = 0; newyear.tm_min = 0; newyear.tm_sec = 0;
  newyear.tm_mon = 0;  newyear.tm_mday = 1;
  seconds = difftime(now,mktime(&newyear));
  printf ("%.f seconds since new year in the current timezone.\n", seconds);
  return 0;
}
线程安全
ctime和asctime
ctime和asctime不是线程安全的,原因是返回的字符串指针指向了一个静态分配的字符串,会被后续函数调用复写掉。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/** asctime returns a static allocated string shared with other calls
* to asctime or ctime. Every time these functions are called the content
* of the static string is overwritten*/
int main()
{
  std::time_t t1 = std::time(nullptr);
  std::tm* t1_tm = std::localtime(&t1);
  const char* t1_as_text = std::asctime(t1_tm); // save as dangerous string
  std::cout << "t1 (original)   : " << t1_as_text << "\n";
  std::this_thread::sleep_for(std::chrono::seconds(5));
  std::time_t t2 = std::time(nullptr);
  std::tm* t2_tm = std::localtime(&t2);
  const char* t2_as_text = std::asctime(t2_tm);  // overwrite t1_as_text
  std::cout << "t1 (overwritten): " << t1_as_text;
  std::cout << "t2              : " << t2_as_text << std::endl;
  return 0;
}
输出:1
2
3
4t1 (original) : Mon Jan 21 13:56:30 2013
t1 (overwrite): Mon Jan 21 13:56:35 2013
t2            : Mon Jan 21 13:56:35 2013
在Linux系统下,可以使用ctime_r和asctime_r这两个线程安全的函数,它们线程安全的原因是使用了用户自己分配的buffer,这个buffer至少有26字节的空间。
在Windows系统下,可以使用ctime_s和asctime_s这两个函数。1
2
3
4
5
6
7
8
9
10
11
12
13char *ctime_r(const time_t *timep, char *buf);
char *asctime_r(const struct tm *tm, char *buf);
errno_t ctime_s(
   char* buffer,
   size_t numberOfElements,
   const time_t *time
);
errno_t asctime_s(
   char* buffer,
   size_t numberOfElements,
   const struct tm *_tm
);
localtime和gmtime
localtime和gmtime也不是线程安全的。原因是,返回的std::tm指针在内部指向了静态的std::tm结构。
在Linux系统下,可以使用localtime_r和gmtime_r这两个线程安全的函数。
在Windows系统下,可以使用localtime_s和gmtime_s这两个函数。1
2
3
4
5
6
7
8
9
10
11struct tm *localtime_r(const time_t *timep, struct tm *result);
struct tm *gmtime_r(const time_t *timep, struct tm *result);
errno_t localtime_s(
   struct tm* _tm,
   const time_t *time
);
errno_t gmtime_s(
   struct tm* _tm,
   const __time_t* time
);
为了跨平台并且多线程使用这些功能,下面定义安全的版本1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47namespace g2{
typedef std::chrono::time_point<std::chrono::system_clock>  system_time_point;
tm localtime(const std::time_t& time)
{
  std::tm tm_snapshot;
#if (defined(WIN32) || defined(_WIN32) || defined(__WIN32__))
  localtime_s(&tm_snapshot, &time);
#else
  localtime_r(&time, &tm_snapshot); // POSIX
#endif
  return tm_snapshot;
}
// To simplify things the return value is just a string. I.e. by design!
std::string put_time(const std::tm* date_time, const char* c_time_format)
{
#if (defined(WIN32) || defined(_WIN32) || defined(__WIN32__))
  std::ostringstream oss;
  // BOGUS hack done for VS2012: C++11 non-conformant since it SHOULD take a "const struct tm*  "
  // ref. C++11 standard: ISO/IEC 14882:2011, § 27.7.1,
  oss << std::put_time(const_cast<std::tm*>(date_time), c_time_format);
  return oss.str();
#else    // LINUX
  const size_t size = 1024;
  char buffer[size];
  auto success = std::strftime(buffer, size, c_time_format, date_time);
  if (0 == success)
    return c_time_format;
  return buffer;
#endif
}
// extracting std::time_t from std:chrono for "now"
std::time_t systemtime_now()
{
  system_time_point system_now = std::chrono::system_clock::now();
  return std::chrono::system_clock::to_time_t(system_now);
}
} // g2-namespace
参考
- 维基百科time.h
 - cppreference.com
 - cplusplus
 - c++ 时间类型详解 time_t
 - Exploring C++11, part 2 (localtime and time again)
 - Unix, C, and C++Function Reference Time
 
版权声明