【C语言】localtime和localtime_r;strftime和strftime_l
文章目录
- `localtime` 和 `localtime_r`
- 一、函数原型与核心差异
- 二、关键区别详解
- 1. 存储方式:全局缓冲区 vs 用户缓冲区
- 2. 线程安全性:非线程安全 vs 线程安全
- 3. 可移植性
- 三、struct tm 结构体说明
- 四、使用场景与最佳实践
- 1. 何时用 `localtime`?
- 2. 何时用 `localtime_r`?
- 3. Windows 平台替代方案
- 五、常见错误提醒
- 总结
- `strftime` 和 `strftime_l`
- 一、函数原型与核心差异
- 关键参数说明:
- 二、核心区别详解
- 1. 区域(locale)依赖:全局 vs 自定义
- 2. 线程安全性:非线程安全 vs 线程安全
- 3. 可移植性
- 三、格式化字符串(format)常用说明符
- 四、返回值说明(两者完全一致)
- 五、使用场景与最佳实践
- 1. 何时用 `strftime`?
- 2. 何时用 `strftime_l`?
- 3. Windows 平台本地化替代方案
- 六、常见错误提醒
- 总结
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缓冲区,每次调用都会覆盖该缓冲区的数据,然后返回缓冲区的指针。
示例:#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的指针(方便链式调用)。
示例:#include <time.h> #include <stdio.h>int main() {time_t now = time(NULL);struct tm t1; // 用户分配缓冲区localtime_r(&now, &t1); // 结果写入 t1printf("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,不影响 t1printf("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 中,字段含义如下(注意月份和星期的起始值):
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 编译选项):
#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的格式化结果不可预期(非线程安全)。
示例:#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 格式化时间(线程安全)。
示例:#include <time.h> #include <stdio.h> #include <locale.h> // 需包含 locale.h 以使用 newlocale/duplocaleint 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 |
示例:格式化完整的本地化时间字符串
// 中文 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 结尾),因此失败后不应使用缓冲区内容。
示例:判断格式化是否成功
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 是线程局部的):
#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。
