文章目录
- [`localtime` 和 `localtime_r`](#
localtime和localtime_r) -
-
- 一、函数原型与核心差异
- 二、关键区别详解
-
- [1. 存储方式:全局缓冲区 vs 用户缓冲区](#1. 存储方式:全局缓冲区 vs 用户缓冲区)
- [2. 线程安全性:非线程安全 vs 线程安全](#2. 线程安全性:非线程安全 vs 线程安全)
- [3. 可移植性](#3. 可移植性)
- [三、struct tm 结构体说明](#三、struct tm 结构体说明)
- 四、使用场景与最佳实践
-
- [1. 何时用 `localtime`?](#1. 何时用
localtime?) - [2. 何时用 `localtime_r`?](#2. 何时用
localtime_r?) - [3. Windows 平台替代方案](#3. Windows 平台替代方案)
- [1. 何时用 `localtime`?](#1. 何时用
- 五、常见错误提醒
- 总结
-
- [`strftime` 和 `strftime_l`](#
strftime和strftime_l) -
-
- 一、函数原型与核心差异
- 二、核心区别详解
-
- [1. 区域(locale)依赖:全局 vs 自定义](#1. 区域(locale)依赖:全局 vs 自定义)
- [2. 线程安全性:非线程安全 vs 线程安全](#2. 线程安全性:非线程安全 vs 线程安全)
- [3. 可移植性](#3. 可移植性)
- 三、格式化字符串(format)常用说明符
- 四、返回值说明(两者完全一致)
- 五、使用场景与最佳实践
-
- [1. 何时用 `strftime`?](#1. 何时用
strftime?) - [2. 何时用 `strftime_l`?](#2. 何时用
strftime_l?) - [3. Windows 平台本地化替代方案](#3. Windows 平台本地化替代方案)
- [1. 何时用 `strftime`?](#1. 何时用
- 六、常见错误提醒
- 总结
-
localtime 和 localtime_r
localtime 和 localtime_r 都是 C 标准库中用于将时间戳(time_t 类型,从 1970-01-01 00:00:00 UTC 起的秒数) 转换为本地时间(struct tm 结构体) 的函数,但核心区别在于 线程安全性 和 返回值存储方式,这也是实际开发中选择的关键依据。
一、函数原型与核心差异
先明确两个函数的标准原型(头文件:<time.h>):
| 函数 | 原型 | 核心特点 |
|---|---|---|
localtime |
struct tm *localtime(const time_t *timer); |
非线程安全,返回全局静态缓冲区的指针 |
localtime_r |
struct tm *localtime_r(const time_t *timer, struct tm *result); |
线程安全(_r 代表 reentrant 可重入),结果存储在用户提供的缓冲区中 |
二、关键区别详解
1. 存储方式:全局缓冲区 vs 用户缓冲区
-
localtime:函数内部维护一个 全局静态的
struct tm缓冲区 ,每次调用都会覆盖该缓冲区的数据,然后返回缓冲区的指针。示例:
c#include <time.h> #include <stdio.h> int main() { time_t now = time(NULL); // 第一次调用:返回全局缓冲区指针,存储当前时间 struct tm *t1 = localtime(&now); printf("t1: %02d:%02d\n", t1->tm_hour, t1->tm_min); // 模拟延迟(或另一个线程调用) sleep(2); time_t later = time(NULL); // 第二次调用:覆盖全局缓冲区,t1 指向的数据也会被修改! struct tm *t2 = localtime(&later); printf("t2: %02d:%02d\n", t2->tm_hour, t2->tm_min); printf("t1(被覆盖后): %02d:%02d\n", t1->tm_hour, t1->tm_min); // 与 t2 相同 return 0; }输出会发现
t1的值被第二次调用覆盖------因为t1和t2指向同一个全局缓冲区。 -
localtime_r:不使用全局缓冲区,而是要求用户提前分配
struct tm变量(作为第二个参数result),函数将转换结果写入该变量,并返回result的指针(方便链式调用)。示例:
c#include <time.h> #include <stdio.h> int main() { time_t now = time(NULL); struct tm t1; // 用户分配缓冲区 localtime_r(&now, &t1); // 结果写入 t1 printf("t1: %02d:%02d\n", t1.tm_hour, t1.tm_min); sleep(2); time_t later = time(NULL); struct tm t2; // 独立缓冲区 localtime_r(&later, &t2); // 结果写入 t2,不影响 t1 printf("t2: %02d:%02d\n", t2.tm_hour, t2.tm_min); printf("t1(未被覆盖): %02d:%02d\n", t1.tm_hour, t1.tm_min); // 保持原值 return 0; }输出中
t1和t2各自保持独立------因为使用了不同的用户缓冲区。
2. 线程安全性:非线程安全 vs 线程安全
-
localtime非线程安全 :全局缓冲区是所有线程共享的。如果多个线程同时调用
localtime,会导致缓冲区数据被交叉覆盖,最终返回错误的时间(竞态条件)。 -
localtime_r线程安全 :每个线程可以传入自己的
struct tm变量作为缓冲区,线程间的缓冲区相互独立,不会出现数据竞争,因此适合多线程环境(如服务器、多线程程序)。
3. 可移植性
localtime:是 C89 标准函数,所有符合 C 标准的编译器都支持(跨平台兼容性强)。localtime_r:是 POSIX 标准(如 Linux、Unix、macOS)定义的可重入函数,Windows 平台不支持 (Windows 对应的线程安全函数是localtime_s,属于 C11 标准的边界检查函数,原型不同:errno_t localtime_s(struct tm *result, const time_t *timer);)。
三、struct tm 结构体说明
两个函数的转换结果都存储在 struct tm 中,字段含义如下(注意月份和星期的起始值):
c
struct tm {
int tm_sec; // 秒(0-60,允许闰秒)
int tm_min; // 分(0-59)
int tm_hour; // 时(0-23)
int tm_mday; // 日(1-31)
int tm_mon; // 月(0-11,0=1月,11=12月)
int tm_year; // 年(从 1900 开始,如 2024 对应 tm_year=124)
int tm_wday; // 星期(0-6,0=周日,6=周六)
int tm_yday; // 年内天数(0-365)
int tm_isdst; // 夏令时标记(正数=启用,0=禁用,负数=未确定)
};
四、使用场景与最佳实践
1. 何时用 localtime?
- 单线程程序(无线程安全问题);
- 对可移植性要求极高(需兼容非 POSIX 平台,且无需线程安全);
- 简单工具类程序(无需长期保存
struct tm数据,调用后立即使用)。
2. 何时用 localtime_r?
- 多线程程序(如 Linux 服务器、多线程服务);
- 需要保存多次转换的结果(避免数据被覆盖);
- POSIX 平台(Linux、Unix、macOS)开发。
3. Windows 平台替代方案
Windows 不支持 localtime_r,需使用 localtime_s(C11 标准,需开启 /std:c11 编译选项):
c
#include <time.h>
#include <stdio.h>
int main() {
time_t now = time(NULL);
struct tm t;
// localtime_s 返回错误码(0=成功),而非指针
if (localtime_s(&t, &now) == 0) {
printf("当前时间:%04d-%02d-%02d %02d:%02d:%02d\n",
t.tm_year + 1900, t.tm_mon + 1, t.tm_mday,
t.tm_hour, t.tm_min, t.tm_sec);
}
return 0;
}
五、常见错误提醒
-
忘记
tm_mon和tm_year的偏移:月份需
+1(0=1月),年份需+1900(tm_year=124 对应 2024),否则会输出错误日期(如 2024 显示为 124 年)。 -
复用
localtime返回的指针:不要长期保存
localtime的返回值,后续调用(或其他线程调用)会覆盖该指针指向的全局缓冲区。 -
多线程中误用
localtime:多线程环境下必须使用线程安全版本(
localtime_r/localtime_s),否则会出现随机的时间错误。
总结
| 特性 | localtime |
localtime_r |
|---|---|---|
| 线程安全 | ❌ 非线程安全(全局缓冲区) | ✅ 线程安全(用户缓冲区) |
| 存储方式 | 全局静态缓冲区 | 用户提供的缓冲区 |
| 可移植性 | 跨平台(C89 标准) | 仅 POSIX 平台(Linux/Unix/macOS) |
| 适用场景 | 单线程、简单程序 | 多线程、POSIX 平台、需保存多份结果 |
最佳实践 :在 Linux/Unix 多线程程序中优先使用 localtime_r;单线程或需兼容非 POSIX 平台时用 localtime;Windows 平台用 localtime_s。
strftime 和 strftime_l
strftime 和 strftime_l 都是 C 标准库中用于将 struct tm 类型的时间数据 格式化为 字符串 的函数,核心区别在于 时区/区域(locale)的指定方式 和 线程安全性,适用于不同的本地化与多线程场景。
一、函数原型与核心差异
先明确两个函数的标准原型(头文件:<time.h>),以及关键参数的含义:
| 函数 | 原型 | 核心特点 |
|---|---|---|
strftime |
size_t strftime(char *restrict s, size_t maxsize, const char *restrict format, const struct tm *restrict tm); |
使用 当前进程的全局区域(locale),非线程安全(全局 locale 可能被其他线程修改) |
strftime_l |
size_t strftime_l(char *restrict s, size_t maxsize, const char *restrict format, const struct tm *restrict tm, locale_t loc); |
显式传入 自定义区域(locale_t 类型),线程安全(不依赖全局 locale) |
关键参数说明:
s:存储格式化结果的字符串缓冲区(用户需提前分配内存);maxsize:缓冲区的最大大小(包括字符串结束符\0);format:格式化字符串(与printf类似,支持时间相关的转换说明符,如%Y表示4位年、%m表示2位月);tm:待格式化的struct tm时间数据(可由localtime_r/localtime等函数生成);loc(仅strftime_l):显式指定的区域对象(locale_t类型),用于控制日期、时间的本地化显示(如星期名称、月份名称的语言)。
二、核心区别详解
1. 区域(locale)依赖:全局 vs 自定义
区域(locale)决定了时间的本地化表现,例如:
-
英文 locale(如
en_US.UTF-8):月份显示为Jan/Feb,星期显示为Mon/Tue; -
中文 locale(如
zh_CN.UTF-8):月份显示为一月/二月,星期显示为星期一/星期二。 -
strftime:依赖进程的 全局 locale (通过
setlocale(LC_TIME, "...")设置)。如果多个线程修改全局 locale,会导致strftime的格式化结果不可预期(非线程安全)。示例:
c#include <time.h> #include <stdio.h> int main() { // 设置全局 locale 为中文(Linux/macOS 示例,Windows 需用 "Chinese_China.936") setlocale(LC_TIME, "zh_CN.UTF-8"); time_t now = time(NULL); struct tm t; localtime_r(&now, &t); char buf[64]; // 使用全局 locale 格式化:结果为中文(如 "2024年10月01日 星期二") strftime(buf, sizeof(buf), "%Y年%m月%d日 %A", &t); printf("中文格式:%s\n", buf); // 切换全局 locale 为英文 setlocale(LC_TIME, "en_US.UTF-8"); // 再次格式化:结果为英文(如 "2024-10-01 Tuesday") strftime(buf, sizeof(buf), "%Y-%m-%d %A", &t); printf("英文格式:%s\n", buf); return 0; } -
strftime_l:不依赖全局 locale,而是通过第5个参数
loc显式指定区域。即使其他线程修改全局 locale,也不会影响当前调用的结果,因此支持 多线程同时使用不同 locale 格式化时间 (线程安全)。示例:
c#include <time.h> #include <stdio.h> #include <locale.h> // 需包含 locale.h 以使用 newlocale/duplocale int main() { time_t now = time(NULL); struct tm t; localtime_r(&now, &t); char buf[64]; // 1. 创建中文 locale 对象(POSIX 平台写法) locale_t zh_loc = newlocale(LC_TIME_MASK, "zh_CN.UTF-8", (locale_t)0); // 2. 用中文 locale 格式化 strftime_l(buf, sizeof(buf), "%Y年%m月%d日 %A", &t, zh_loc); printf("中文格式:%s\n", buf); freelocale(zh_loc); // 释放 locale 对象(避免内存泄漏) // 3. 创建英文 locale 对象 locale_t en_loc = newlocale(LC_TIME_MASK, "en_US.UTF-8", (locale_t)0); // 4. 用英文 locale 格式化 strftime_l(buf, sizeof(buf), "%Y-%m-%d %A", &t, en_loc); printf("英文格式:%s\n", buf); freelocale(en_loc); // 释放 locale 对象 return 0; }
2. 线程安全性:非线程安全 vs 线程安全
-
strftime非线程安全 :全局 locale 是进程级共享资源。如果多个线程同时调用
strftime,且其中一个线程通过setlocale修改了全局 locale,会导致其他线程的格式化结果错乱(竞态条件)。 -
strftime_l线程安全 :每个调用独立传入
locale_t对象,线程间的 locale 互不干扰。只要每个线程使用自己的locale_t(或确保locale_t不被并发修改),即可安全并发调用。
3. 可移植性
strftime:是 C90 标准函数,所有符合 C 标准的编译器都支持(跨平台兼容性强,Windows、Linux、macOS 均支持)。strftime_l:是 POSIX 标准 (如 Linux、Unix、macOS)定义的函数,Windows 平台不支持 (Windows 无locale_t类型,需通过其他方式实现本地化,如SetThreadLocale结合strftime)。
三、格式化字符串(format)常用说明符
strftime 和 strftime_l 的 format 参数格式完全一致,支持以下常用转换说明符(完整列表见 C 标准或 POSIX 标准):
| 说明符 | 含义 | 示例(中文 locale) | 示例(英文 locale) |
|---|---|---|---|
%Y |
4位年份(完整年份) | 2024 | 2024 |
%y |
2位年份(00-99) | 24 | 24 |
%m |
2位月份(01-12) | 10 | 10 |
%b |
缩写月份名称(本地化) | 十月 | Oct |
%B |
完整月份名称(本地化) | 十月 | October |
%d |
2位日期(01-31) | 01 | 01 |
%A |
完整星期名称(本地化) | 星期二 | Tuesday |
%a |
缩写星期名称(本地化) | 周二 | Tue |
%H |
24小时制(00-23) | 14 | 14 |
%I |
12小时制(01-12) | 02 | 02 |
%M |
2位分钟(00-59) | 30 | 30 |
%S |
2位秒(00-60,支持闰秒) | 45 | 45 |
%p |
上/下午标记(本地化) | 下午 | PM |
%c |
完整日期时间(本地化默认格式) | 2024年10月01日 14:30:45 | Tue Oct 1 14:30:45 2024 |
示例:格式化完整的本地化时间字符串
c
// 中文 locale 下输出:2024年10月01日 星期二 14:30:45
strftime(buf, sizeof(buf), "%Y年%m月%d日 %A %H:%M:%S", &t);
// 英文 locale 下输出:Tuesday, October 01, 2024 14:30:45
strftime(buf, sizeof(buf), "%A, %B %d, %Y %H:%M:%S", &t);
四、返回值说明(两者完全一致)
两个函数的返回值规则相同,核心用于判断格式化是否成功:
- 成功 :返回写入缓冲区的字符数(不包括结束符
\0); - 失败 :返回
0(常见原因:缓冲区大小maxsize不足,或format包含非法说明符)。
注意 :返回 0 时,缓冲区可能会被写入部分数据,但未包含完整的格式化结果(且不一定以 \0 结尾),因此失败后不应使用缓冲区内容。
示例:判断格式化是否成功
c
char buf[16]; // 假设缓冲区过小
size_t ret = strftime(buf, sizeof(buf), "%Y年%m月%d日 %H:%M:%S", &t);
if (ret == 0) {
printf("格式化失败:缓冲区不足!\n");
} else {
printf("格式化结果:%s(长度:%zu)\n", buf, ret);
}
五、使用场景与最佳实践
1. 何时用 strftime?
- 单线程程序(无线程安全问题);
- 不需要多语言/多区域切换(全局 locale 固定);
- 跨平台兼容性要求高(需支持 Windows);
- 简单场景(无需复杂本地化配置)。
2. 何时用 strftime_l?
- 多线程程序(需并发使用不同 locale 格式化时间);
- 需动态切换区域(如同一进程同时输出中文、英文时间);
- POSIX 平台(Linux、Unix、macOS)开发;
- 对线程安全和本地化灵活性有要求(如多语言服务器)。
3. Windows 平台本地化替代方案
Windows 不支持 strftime_l,需通过 SetThreadLocale 设置线程级 locale,再调用 strftime(线程安全,因为 SetThreadLocale 是线程局部的):
c
#include <time.h>
#include <stdio.h>
#include <windows.h> // 需包含 Windows 头文件
int main() {
time_t now = time(NULL);
struct tm t;
localtime_s(&t, &now); // Windows 线程安全的 localtime 替代
char buf[64];
// 1. 设置线程 locale 为中文(Windows locale ID:0x0804 对应 zh-CN)
SetThreadLocale(MAKELCID(MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED), SORT_DEFAULT));
strftime(buf, sizeof(buf), "%Y年%m月%d日 %A", &t);
printf("中文格式:%s\n", buf);
// 2. 设置线程 locale 为英文(locale ID:0x0409 对应 en-US)
SetThreadLocale(MAKELCID(MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), SORT_DEFAULT));
strftime(buf, sizeof(buf), "%Y-%m-%d %A", &t);
printf("英文格式:%s\n", buf);
return 0;
}
六、常见错误提醒
-
缓冲区大小不足:
格式化后的字符串长度(包括
\0)不能超过maxsize,否则返回0。建议根据格式化字符串预估长度(如包含完整日期时间需至少 32 字节)。 -
忽略 locale 初始化:
使用
strftime_l时,需通过newlocale创建locale_t对象(不能直接传NULL),且使用后需调用freelocale释放(避免内存泄漏)。 -
混淆
%M和%m:
%M是分钟(Minute),%m是月份(Month),误用会导致日期时间错误(如%m写成%M会显示分钟作为月份)。 -
多线程中误用
strftime:多线程环境下,若使用
strftime且修改全局 locale,会导致结果错乱,需改用strftime_l(POSIX)或SetThreadLocale + strftime(Windows)。
总结
| 特性 | strftime |
strftime_l |
|---|---|---|
| 区域依赖 | 全局 locale | 显式传入的 locale_t 对象 |
| 线程安全 | ❌ 非线程安全(全局 locale 共享) | ✅ 线程安全(独立 locale 对象) |
| 可移植性 | 跨平台(C90 标准) | 仅 POSIX 平台(Linux/Unix/macOS) |
| 本地化灵活性 | 低(仅支持全局 locale) | 高(支持动态切换多 locale) |
| 返回值规则 | 成功返回字符数,失败返回 0 | 与 strftime 完全一致 |
最佳实践:
- 单线程、跨平台场景用
strftime; - POSIX 多线程、多语言场景用
strftime_l(注意locale_t的创建与释放); - Windows 多语言场景用
SetThreadLocale + strftime。