@@ -1541,11 +1541,18 @@ void u8print(std::string msg) {
1541
1541
1542
1542
> {{ icon.detail }} 此处 ` static int dummy_init = ` 是一种静态初始化钩子的小技巧,之后设计模式课程的单例模式中会详细讲解。
1543
1543
1544
- > {{ icon.detail }} 更多细节用法见官方文档:https://www.boost.org/doc/libs/1_81_0/libs/locale/doc/html/group__codepage.html
1545
-
1546
1544
#### 更多功能?!
1547
1545
1548
- 编码转换只是 ` boost::locale::conv ` 这个子模块下的一个小功能而已!` boost::locale ` 还提供了更多功能,如按照地域语言规范格式化数字、货币、日期、时间等,下一小节中我们继续介绍。完全是 ` std::locale ` 的上位替代。
1546
+ | 函数| 从| 到|
1547
+ | ----| --| --|
1548
+ | utf_to_utf| UTF 系列| UTF 系列|
1549
+ | from_utf| UTF 系列| 杂牌字符编码|
1550
+ | to_utf| 杂牌字符编码| UTF 系列|
1551
+ | between| 杂牌字符编码| 杂牌字符编码|
1552
+
1553
+ 更多细节用法见官方文档:https://www.boost.org/doc/libs/1_81_0/libs/locale/doc/html/group__codepage.html
1554
+
1555
+ 不可思议的是:编码转换只是 ` boost::locale::conv ` 这个子模块下的一个小功能而已!` boost::locale ` 还提供了更多功能,如按照地域语言规范格式化数字、货币、日期、时间等,下一小节中我们继续介绍。完全是 ` std::locale ` 的上位替代。
1549
1556
1550
1557
> {{ icon.fun }} Boost 哪里都好,你想要的功能应有尽有。而且不需要 C++20,很低版本的 C++ 也能用。唯一缺点可能就是太肥了,编译慢。
1551
1558
@@ -1852,6 +1859,8 @@ zh_CN.UTF-8: 1
1852
1859
1853
1860
要注意的是,用户必须已经安装过该区域设置,程序才能使用 setlocale 设置,否则会出现找不到 locale 的错误。
1854
1861
1862
+ > {{ icon.detail }} 这几乎导致你没法用除默认外的任何 locale,比如 `"zh_CN.UTF-8"`,因为你不能确定用户有没有安装他。但你可以用 `boost::locale::generator` 凭空生成一个系统里没有安装过的 locale,绕开标准库的限制,稍后介绍。
1863
+
1855
1864
Linux 用户可以通过 修改 `/etc/locale.gen` 取消注释要启用的语言和编码格式,保存后,运行 `locale-gen` 即可安装所有没注释的语言。
1856
1865
1857
1866
```bash
@@ -2058,7 +2067,7 @@ int main() {
2058
2067
2024年 07月 19日 星期五 16时 01分
2059
2068
```
2060
2069
2061
- #### ` std::locale ` 对象
2070
+ ### ` std::locale ` 对象
2062
2071
2063
2072
C 语言的 ` setlocale ` 设置的是全局 locale,全局 locale 只有一个,一设就影响所有线程,非常沙雕。因此提倡“不要状态机要对象”的 C++,封装了 ` std::locale ` 对象。
2064
2073
@@ -2123,6 +2132,15 @@ Fri 19 Jul 2024 04:33:39 PM CST
2123
2132
2124
2133
> {{ icon.tip }} 关于 ` "%c" ` 、` "%Y" ` 这些格式化字符串的更多详细用法,参见 [ ` man strftime ` ] ( http://man7.org/linux/man-pages/man3/strftime.3.html ) 。我们作为字符编码的课程不再赘述,之后的时间与日期专题课也会稍微讲一下。
2125
2134
2135
+ #### ` boost::locale::generator ` 凭空创建一个用户没安装过的 locale
2136
+
2137
+ ``` cpp
2138
+ boost::locale::generator gen;
2139
+ auto loc = gen(" zh_CN.UTF-8" );
2140
+ boost::locale::date_time dt = boost::locale::date_time::now(loc);
2141
+ std::cout << boost::locale::as::date(dt) << ' \n ' ;
2142
+ ```
2143
+
2126
2144
## 宽字符流
2127
2145
2128
2146
之所以把宽字符流放到最后,是因为,首先 ` iostream ` 本来就是一个失败的设计。
@@ -2172,10 +2190,6 @@ std::string to_os_string(std::string const &u8s) {
2172
2190
2173
2191
这就是为什么宽字符流很糟糕,说是跨平台,跨了个寂寞。
2174
2192
2175
- ### `wchar_t` 系列函数
2176
-
2177
- TODO
2178
-
2179
2193
### `std::wcout` 的使用坑点科普
2180
2194
2181
2195
#### `std::wcout` 必须设了 locale 才能用
@@ -2318,22 +2332,132 @@ C++ 真正的文本流实际上是宽字符流 `std::wifstream`,而指定编
2318
2332
2319
2333
> {{ icon.fun }} 理论上所有的程序都应该像这样,只不过是因为劳保教材从来不提,一口一个 ` char [] ` 就是字符串,搞得 ` wchar_t ` 在除了 GNU 这种“体制内”环境之外,根本没人用了。现在为了处理中文字符,才闹出了 ` char ` 当 UTF-8 使这种招数,令人唏嘘。
2320
2334
2335
+ 总之,` .imbue(std::locale("zh_CN.GBK")) ` 可以把 ` GBK ` 设为当前文本文件的编码格式,宽文件流将会按照这个编码和解码所有的字符串。
2336
+
2337
+ ` std::locale ` 的字符串构造函数,他的参数必须是用户系统里已经安装过的 locale(通过修改 ` /etc/locale.gen ` 和 ` locale-gen ` 命令安装)。但是,你无法确保用户的系统安装了 ` "GBK" ` locale。` std::locale("zh_CN.GBK") ` 在没有安装 GBK 的用户电脑上运行就会抛出错误表示找不到该 locale。因此,如果要指定按 GBK 读取文件,不建议依赖系统中自带的 ` std::locale("zh_CN.GBK")) ` ,而是调用 ` boost::locale::generator ` 就地生成一个 locale,这样程序无论系统有没有安装都能运行了:
2338
+
2339
+ ``` cpp
2340
+ #include < boost/locale.hpp>
2341
+ #include < fstream>
2342
+
2343
+ int main () {
2344
+ std::wofstream fout;
2345
+ boost::locale::generator gen;
2346
+ std::locale loc = gen("zh_CN.GBK");
2347
+ fout.imbue(loc);
2348
+ fout << L"你好,世界\n"; // 以 GBK 编码写出文本文件
2349
+ }
2350
+ ```
2351
+
2352
+ ```
2353
+ $ cat build/你好.txt
2354
+ ����
2355
+ $ cat build/你好.txt | iconv -f GBK -t UTF-8
2356
+ 你好,世界
2357
+ $
2358
+ ```
2359
+
2360
+ > {{ icon.detail }} 这是因为 ` boost_locale ` 链接了 ` icu ` ,其内部包含了所有编码格式的字符映射表。` boost::locale::generator ` 首先创建了一个 ` std::locale ` ,然后通过虚函数重载的方式把 ` std::locale ` 对象中的 ` std::codecvt ` 替换成 ` icu ` 的映射表。从而让 ` std::wofstream ` 调用这个 ` icu ` 的映射函数,实现了 UTF-32 到 GBK 的转换。
2361
+
2362
+ 此外,你还可以选择覆盖 locale 的部分方面 (facet),比如在文件编码时,我们只需要用 ` "zh_CN.GBK" ` 的 ` LC_CTYPE ` 方面就可以了,其他的例如时间格式、语言信息等,我们还是想保留默认的。为此,我们可以利用 locale 的“杂交”拷贝构造函数,保留老 locale 的绝大部分方面,只替换一个方面为新 locale 的:
2363
+
2364
+ ``` cpp
2365
+ std::locale old_loc = std::locale(" " ); // 环境 locale
2366
+ boost::locale::generator gen;
2367
+ std::locale new_loc = gen(" zh_CN.GBK" ); // 全 GBK locale
2368
+ std::locale loc = std::locale(old_loc, new_loc, std::locale::ctype); // 杂交:继承 old_loc 的其余全部,只替换掉 LC_CTYPE 部分为 new_loc 的
2369
+ fout.imbue(loc);
2370
+ ```
2371
+
2321
2372
### locale 用于字符编码转换
2322
2373
2323
- #### C 语言标准库的字符编码转换
2374
+ ``` cpp
2375
+ // 以 loc 规定的编码,把内码编码成外码
2376
+ std::string narrow (std::locale const &loc, std::wstring const &wstr) {
2377
+ // use_facet 函数获得 locale 在字符转换 方面的 facet
2378
+ auto const &cvt = std::use_facet<Codecvt >(loc);
2379
+ std::string str(wstr.size() * 4, '\0'); // 预留 4 倍空间
2380
+ wchar_t const * from_next;
2381
+ char * to_next;
2382
+ std::mbstate_t state{};
2383
+ auto res = cvt.in(state, wstr.data(), wstr.data() + wstr.size(), from_next, str.data(), str.data() + str.size(), to_next);
2384
+ if (res == Codecvt::ok) {
2385
+ // 转换成功
2386
+ str.resize(to_next - str.data());
2387
+ return str;
2388
+ } else if (res == Codecvt::partial) {
2389
+ // 转换部分成功
2390
+ str.resize(to_next - str.data());
2391
+ return str;
2392
+ } else {
2393
+ // 转换失败
2394
+ return "";
2395
+ }
2396
+ }
2324
2397
2325
- TODO
2398
+ // 以 loc 规定的编码,把外码解码成内码
2399
+ std::wstring widen(std::locale const &loc, std::string const &str) {
2400
+ // use_facet 函数获得 locale 在字符转换 方面的 facet
2401
+ auto const &cvt = std::use_facet<Codecvt >(loc);
2402
+ std::wstring wstr(str.size(), L'\0'); // 预留空间
2403
+ char const * from_next;
2404
+ wchar_t * to_next;
2405
+ std::mbstate_t state{};
2406
+ auto res = cvt.out(state, str.data(), str.data() + str.size(), from_next, wstr.data(), wstr.data() + wstr.size(), to_next);
2407
+ if (res == Codecvt::ok) {
2408
+ // 转换成功
2409
+ wstr.resize(to_next - wstr.data());
2410
+ return wstr;
2411
+ } else if (res == Codecvt::partial) {
2412
+ // 转换部分成功
2413
+ wstr.resize(to_next - wstr.data());
2414
+ return wstr;
2415
+ } else {
2416
+ // 转换失败
2417
+ return L"";
2418
+ }
2419
+ }
2420
+ ```
2326
2421
2327
- #### C++ 标准库的字符编码转换
2422
+ ```cpp
2423
+ std::wstring wstr = L"你好";
2424
+ std::cout << narrow(std::locale("zh_CN.GBK"), wstr);
2425
+ ```
2328
2426
2329
- TODO
2427
+ 不过,我们都有更方便的 ` boost::locale::conv ` 了,还何必还用这么繁琐的 ` std::locale ` 呢?所以我是不推荐再用这破玩意,无论是易用性还是扩展性都是 Boost 完胜。
2428
+
2429
+ ### C 语言中的 ` wchar_t ` 系列函数
2430
+
2431
+ 对于所有的 ` strcpy ` 、` strcmp ` 、` strlen ` 这类 ` str*** ` 系函数,都有一个相应的 ` wcs*** ` 函数。
2330
2432
2331
- > ` wchar_t ` 、` char16_t ` 、` char32_t ` 之间的转换,可以用 ` std::mbrtoc16 ` 、 ` std::mbrtoc32 ` 、 ` std::c16rtomb ` 、 ` std::c32rtomb ` 函数 。
2433
+ 例如 ` wcscpy ` 、` wcscmp ` 、` wcslen ` 。
2332
2434
2333
- ### C++ 字符串编码转换 ` <codecvt> `
2435
+ 它们的原型如下:
2436
+
2437
+ ``` c
2438
+ wchar_t *wcscpy (wchar_t * dest, const wchar_t * src);
2439
+ int wcscmp(const wchar_t * s1, const wchar_t * s2);
2440
+ size_t wcslen(const wchar_t * s);
2441
+ ```
2442
+
2443
+ 它们的作用和 `str***` 系函数一样,但是它们操作的是 `wchar_t` 字符串。
2444
+
2445
+ 对于所有的 `fputc`、`printf`,`fprintf`,`fgets` 这类操作文件的函数,也都有一个配套的 `fw***` 函数。
2446
+
2447
+ 第一次使用过这些函数后,`FILE *` 将会被“宽化”(`fwiden`)。宽化的文件流今后将只能输入宽字符串。
2448
+
2449
+ > {{ icon.tip }} 但是,既然 C++ 已经有 `std::wstring`,就不建议再学 C 语言 `L'\0'` 结尾字符串了。
2450
+
2451
+ #### C 语言标准库的字符编码转换
2334
2452
2335
2453
TODO
2336
2454
2455
+ #### C++ 标准库的字符编码转换 `<codecvt>`
2456
+
2457
+ `wchar_t`、`char16_t`、`char32_t` 与 `char` 之间的转换,可以用 `std::mbrtoc16`、`std::mbrtoc32`、`std::c16rtomb`、`std::c32rtomb` 函数。
2458
+
2459
+ 然而,又臭又长,用封装好的 `boost::locale::utf_to_utf/from_utf/to_utf/between` 不香吗?
2460
+
2337
2461
<!--
2338
2462
//=== 跨平台软件何去何从?
2339
2463
//
@@ -2935,6 +3059,8 @@ COW 字符串的缺点是:当你写多线程并发时,本来多线程只读
2935
3059
2936
3060
### 字符的显示宽度计算
2937
3061
3062
+ TODO
3063
+
2938
3064
### Grapheme
2939
3065
2940
3066
TODO
0 commit comments