16. 本地化(locale.h):setlocale、localeconv
说到本地化,我刚开始学C语言时觉得这玩意儿可有可无。直到有一次,我给一个外贸公司写订单系统,客户说金额显示不对——小数点位置错了,千位分隔符也没了。嗯,从那以后,我再也不敢忽略 locale.h 了。
说白了,本地化就是让程序「入乡随俗」。不同国家的人看数字、日期、货币的方式都不一样。美国人写 1,234.56,德国人写 1.234,56,日本人可能写 1,234。你的程序得能适应这些差异。
16.1 什么是 locale?
locale 是一组地域相关的设置。它告诉程序:
- 小数点用什么符号(. 还是 ,)
- 千位分隔符是什么
- 货币符号放前面还是后面
- 日期怎么写(月/日/年 还是 日/月/年)
- 字符编码怎么处理
C 标准库把 locale 分成几个类别,你可以单独设置:
| 类别宏 | 影响范围 |
|---|---|
| LC_ALL | 所有类别 |
| LC_COLLATE | 字符串排序规则 |
| LC_CTYPE | 字符分类与转换 |
| LC_MONETARY | 货币格式 |
| LC_NUMERIC | 数字格式(小数点、千位分隔) |
| LC_TIME | 日期时间格式 |
核心函数就两个:
setlocale()— 设置或查询当前 localelocaleconv()— 获取当前 locale 的格式信息
16.2 setlocale — 设置程序的「国籍」
函数原型很简单:
#include <locale.h>
char *setlocale(int category, const char *locale);
参数说明:
category— 上面表格里的宏,比如 LC_ALL、LC_NUMERIClocale— 字符串,比如 "C"、"en_US.UTF-8"、"zh_CN"、"de_DE"
返回值是设置后的 locale 名称字符串。如果失败,返回 NULL。
几个特殊值:
"C"— 默认的 POSIX locale,啥都不本地化""— 空字符串,表示使用系统环境变量指定的 localeNULL— 只查询,不修改
我的习惯:程序启动时先调一次 setlocale(LC_ALL, ""),让系统环境决定 locale。这样用户可以通过 LANG、LC_ALL 等环境变量控制程序行为,不用改代码。
看个例子:
#include <stdio.h>
#include <locale.h>
int main() {
// 先看看默认的 locale 是什么
printf("默认 locale: %s\n", setlocale(LC_ALL, NULL));
// 设置成系统环境指定的 locale
setlocale(LC_ALL, "");
printf("系统 locale: %s\n", setlocale(LC_ALL, NULL));
// 单独设置数字格式为德国风格
setlocale(LC_NUMERIC, "de_DE.UTF-8");
printf("数字 locale: %s\n", setlocale(LC_NUMERIC, NULL));
return 0;
}
输出可能像这样(取决于你的系统):
默认 locale: C
系统 locale: en_US.UTF-8
数字 locale: de_DE.UTF-8
注意:不是所有系统都支持所有 locale。Linux 上你得先安装对应的语言包。Windows 上 locale 名称可能不同,比如 "German_Germany.1252"。跨平台时,我建议用 "" 让系统自己选。
16.3 localeconv — 获取格式细节
这个函数返回一个 struct lconv 指针,里面全是当前 locale 的格式信息。结构体定义如下(我挑几个常用的说):
struct lconv {
char *decimal_point; // 小数点符号,比如 "." 或 ","
char *thousands_sep; // 千位分隔符,比如 "," 或 "."
char *grouping; // 数字分组规则(后面细说)
char *int_curr_symbol; // 国际货币符号,比如 "USD "
char *currency_symbol; // 本地货币符号,比如 "$"
char *mon_decimal_point; // 货币的小数点
char *mon_thousands_sep; // 货币的千位分隔符
char *mon_grouping; // 货币的分组规则
char *positive_sign; // 正数符号,比如 "+"
char *negative_sign; // 负数符号,比如 "-"
char frac_digits; // 货币小数位数
char p_cs_precedes; // 货币符号是否在正数前(1=前,0=后)
char n_cs_precedes; // 货币符号是否在负数前
char p_sep_by_space; // 货币符号与正数之间是否有空格
char n_sep_by_space; // 货币符号与负数之间是否有空格
// ... 还有不少字段,不一一列了
};
grouping 字段有点特殊。它是一个 char 数组,用数字表示分组规则。比如:
"\3"— 每3位一组(千、百万、十亿)"\3\2"— 先3位,再2位(印度风格)""或"\127"— 不分组的特殊标记
数组以 0 或 CHAR_MAX 结尾。
16.4 货币格式的本地化
我在项目中遇到过最头疼的事,就是给一个跨国电商平台做订单导出。不同国家的货币格式完全不一样,硬编码根本行不通。
看个完整的例子:
#include <stdio.h>
#include <locale.h>
#include <stdlib.h>
void print_currency_info() {
struct lconv *lc = localeconv();
printf("本地货币符号: %s\n", lc->currency_symbol);
printf("国际货币符号: %s\n", lc->int_curr_symbol);
printf("货币小数点: '%s'\n", lc->mon_decimal_point);
printf("货币千位分隔: '%s'\n", lc->mon_thousands_sep);
printf("小数位数: %d\n", lc->frac_digits);
printf("货币符号位置(正数): %s\n",
lc->p_cs_precedes ? "前面" : "后面");
printf("货币符号与数字间空格(正数): %s\n",
lc->p_sep_by_space ? "有" : "无");
}
int main() {
// 美国英语
setlocale(LC_ALL, "en_US.UTF-8");
printf("=== 美国 ===\n");
print_currency_info();
// 德国德语
setlocale(LC_ALL, "de_DE.UTF-8");
printf("\n=== 德国 ===\n");
print_currency_info();
// 中文(中国)
setlocale(LC_ALL, "zh_CN.UTF-8");
printf("\n=== 中国 ===\n");
print_currency_info();
return 0;
}
输出大概是这样:
=== 美国 ===
本地货币符号: $
国际货币符号: USD
货币小数点: '.'
货币千位分隔: ','
小数位数: 2
货币符号位置(正数): 前面
货币符号与数字间空格(正数): 无
=== 德国 ===
本地货币符号: €
国际货币符号: EUR
货币小数点: ','
货币千位分隔: '.'
小数位数: 2
货币符号位置(正数): 后面
货币符号与数字间空格(正数): 有
=== 中国 ===
本地货币符号: ¥
国际货币符号: CNY
货币小数点: '.'
货币千位分隔: ','
小数位数: 2
货币符号位置(正数): 前面
货币符号与数字间空格(正数): 无
看到了吧?德国人写 € 1.234,56,美国人写 $1,234.56。你想想看,如果硬编码一个格式,换到另一个国家就全乱了。
16.5 日期格式的本地化
日期格式的本地化主要靠 strftime() 函数。它配合 locale 设置,能输出不同语言的月份、星期名称。
#include <stdio.h>
#include <time.h>
#include <locale.h>
int main() {
time_t now = time(NULL);
struct tm *tm = localtime(&now);
char buf[128];
// 美国格式
setlocale(LC_TIME, "en_US.UTF-8");
strftime(buf, sizeof(buf), "%A, %B %d, %Y", tm);
printf("美国: %s\n", buf);
// 德国格式
setlocale(LC_TIME, "de_DE.UTF-8");
strftime(buf, sizeof(buf), "%A, %d. %B %Y", tm);
printf("德国: %s\n", buf);
// 中文格式
setlocale(LC_TIME, "zh_CN.UTF-8");
strftime(buf, sizeof(buf), "%Y年%m月%d日 %A", tm);
printf("中国: %s\n", buf);
return 0;
}
输出:
美国: Monday, March 17, 2025
德国: Montag, 17. März 2025
中国: 2025年03月17日 星期一
小技巧:strftime 的格式字符串里,%A 是完整星期名,%a 是缩写,%B 是完整月份名,%b 是缩写。配合不同的 locale,自动翻译成当地语言。
16.6 知识体系图
下面这张图帮你理清 locale.h 的核心脉络:
16.7 避坑指南
我曾经在一个项目里吃过亏,分享几个教训:
坑1:setlocale 是全局的
setlocale 修改的是整个进程的 locale。如果你在库函数里改了 locale,调用方可能莫名其妙。多线程环境下尤其要小心。我建议只在 main 函数开头设置一次,后面别乱动。
坑2:locale 名称不跨平台
Linux 上用 "en_US.UTF-8",Windows 上可能是 "English_United States.1252"。macOS 又不一样。跨平台代码最好用 "" 让系统环境决定,或者自己封装一层。
坑3:localeconv 返回的指针是静态的
localeconv 返回的 struct lconv 指针指向一个静态缓冲区。你调一次 setlocale,这个指针的内容就可能变了。别保存这个指针长期使用,每次用之前重新调一次 localeconv。
我的建议:如果你只是需要格式化数字或货币,别自己拼字符串。用 printf 的 %'d(GNU 扩展)或者 strfmon() 函数。后者是 POSIX 标准,专门格式化货币金额,省心很多。
strfmon 的用法:
#include <stdio.h>
#include <locale.h>
#include <monetary.h>
int main() {
setlocale(LC_ALL, "en_US.UTF-8");
char buf[64];
double price = 1234.56;
strfmon(buf, sizeof(buf), "%n", price);
printf("美国格式: %s\n", buf); // $1,234.56
setlocale(LC_ALL, "de_DE.UTF-8");
strfmon(buf, sizeof(buf), "%n", price);
printf("德国格式: %s\n", buf); // 1.234,56 €
return 0;
}
嗯,说到这,locale.h 的核心内容基本就这些了。记住一句话:别硬编码格式,让 locale 帮你做。你的代码会因此更干净,也更国际化。
公众号:蓝海资料掘金营,微信deep3321