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() — 设置或查询当前 locale
  • localeconv() — 获取当前 locale 的格式信息

16.2 setlocale — 设置程序的「国籍」

函数原型很简单:

#include <locale.h>
char *setlocale(int category, const char *locale);

参数说明:

  • category — 上面表格里的宏,比如 LC_ALL、LC_NUMERIC
  • locale — 字符串,比如 "C"、"en_US.UTF-8"、"zh_CN"、"de_DE"

返回值是设置后的 locale 名称字符串。如果失败,返回 NULL。

几个特殊值:

  • "C" — 默认的 POSIX locale,啥都不本地化
  • "" — 空字符串,表示使用系统环境变量指定的 locale
  • NULL — 只查询,不修改

我的习惯:程序启动时先调一次 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 的核心脉络:

locale.h 知识体系 locale.h setlocale() localeconv() LC_ALL LC_NUMERIC LC_MONETARY LC_TIME 其他... decimal_point thousands_sep currency_symbol frac_digits 货币格式化 日期时间显示 数字格式化

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