C语言与中文的一些测试 (Win, UTF8源码)

C语言与中文的一些测试 (Win, UTF8源码)

C语言如何输出中文?

测试前提:1. 源代码为UTF8编码的文件。2. 中文Windows系统。

测试环境:Windows 10,MinGW-w64 8.1。

如果用Windows SDK,效果可能和我的测试有极大不同,可能可以用UTF8,具体见评论区。

你好的编码信息

  • GBK: C4E3 BAC3
  • UTF8: E4BDA0 E5A5BD
  • UTF16 LE:4F60 597D

char硬编码字符串

#include <stdio.h>
#include <stdlib.h>

void ShowBytes(char *str)
{
    if (*str == '\0')
        printf("EMPTY");
    while (*str != '\0')
        printf("%hhX ", (unsigned char)*str++);
    // 中文的char的最高位为1,如果直接赋值给int或unsigned,会先进行位扩展,再改变类型,结果就是负的
}

int main()
{
    system("chcp 936");
    // system("chcp 65001");

    char str[] = "你好";
    puts(str);
    ShowBytes(str);
}

char硬编码结果

chcp 936:

浣犲ソ
E4 BD A0 E5 A5 BD

chcp 65001:

你好
E4 BD A0 E5 A5 BD

char硬编码结论

  1. 硬编码的char字符串与文件编码一致,此处是UTF8。
  2. 输出字符串时用的编码与chcp一致。

wchar_t硬编码字符串

#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <wchar.h>

void ShowBytesWide(wchar_t *str)
{
    if (*str == '\0')
        printf("EMPTY");
    while (*str != '\0')
        wprintf(L"%hX ", (wchar_t)*str++);
}

int main()
{
    system("chcp 936");
    // system("chcp 65001");
    setlocale(LC_ALL, "chs");

    wchar_t str[] = L"你好";
    wprintf(L"%ls\n", str);
    // fputws(str, stdout);
    ShowBytesWide(str);
}

wchar_t硬编码结果

chcp 936与chcp 65001:

你好
4F60 597D

如果注释掉setlocale,函数不会输出。

如果注释掉wprintf而改用fputws,函数不会输出,原因不明。

wchar_t硬编码结论

  1. 硬编码的wcahr_t字符串储存的是UTF16的编码。
  2. 必须使用setlocale,否则无法输出。
  3. wprintf输出时,会把wchar_t*转换成当前locale的编码,然后输出。

char手动输入输出

int main()
{
    // system("chcp 936");
    system("chcp 65001");

    char str[10];
    gets(str);
    // scanf("%s", str);
    puts(str);
    ShowBytes(str);
}

char手动结果

chcp 936:

你好
你好
C4 E3 BA C3

chcp 65001:

你好

EMPTY

char手动结论

  1. chcp 936时能成功读入,且编码为GBK。
  2. chcp 65001时gets和scanf无法成功读入!

wchar_t手动输入输出

int main()
{
    system("chcp 936");
    // system("chcp 65001");
    setlocale(LC_ALL, "chs");

    wchar_t str[10];
    wscanf(L"%ls", str);
    wprintf(L"%ls\n", str);
    ShowBytesWide(str);
}

wchar_t手动结果

chcp 936:

你好
你好
4F60 597D

chcp 65001:

你好

EMPTY

wchar_t手动结论

  1. chcp 936时能成功读入,且编码为UTF16。
  2. chcp 65001时wscanf无法成功读入!

最终结论

  1. 说好的UTF8呢?怎么全都是GBK和UTF16?
    理论上char+chcp 65001就是UTF8,然而读取失败。没有办法手动输入UTF8编码的字符串,只有编译时已经存在的能用。
  2. 应该如何处理中文?
    chcp 936 + wchar_t。因为这是唯一成功的方案。

附加测试:wprintf的%ls

根据cppreference,%s对应的是char*,%ls对应的才是wchar_t*

#include <locale.h>
#include <stdio.h>
#include <wchar.h>

int main()
{
#ifdef linux
    setlocale(LC_ALL, "zh_CN.UTF-8");
#else
    setlocale(LC_ALL, "chs");
#endif

    char *strc = "c你好";
    wchar_t *strw = L"w你好";
    wprintf(L"%s\n", strc);
    wprintf(L"%s\n", strw);
    wprintf(L"%ls\n", strc);
    wprintf(L"%ls\n", strw);
}

Windows下的测试结果:

w你好
w你好
w你好
w你好

Debian下的测试结果:

c你好
w
?�����w你好
w你好

结论:应该用printf+%s输出char*,用wprintf+%ls输出wchar_t*,混用无法正常输出。

其它说明

  • 在复制chcp 65001的文本时,换行会消失。原因不明
  • setlocale的第一个参数自行百度。第二个参数"chs"即对应chcp 936,为""则与当前chcp一致,为NULL则仅返回当前locale;默认为"C"
  • setlocale(LC_ALL, "zh-CN");无效,原因不明。按理说微软的文档中写的是可以用的,反而文档中没有提到chs是什么意思
  • C语言存在一些”空终止多字节字符串“函数,但是对本文的测试没有帮助,除非你真的打算宽字符读入后再调用它们转换成UTF8来解决输入的问题
  • Linux下直接用char就是UTF8,wchar_t对应的是UTF32
  • WSL中的Debian无法输出宽字符,我猜其它的也不可以
  • 曾经chcp 65001时会出现中文只占一个英文字符宽度的问题,但是现在1903好像没有这个问题了
  • C#乱改Console.OutputEncoding和chcp有时也能正常输出中文(比如设为UTF7),这可能是因为它调用的是WriteConsoleW这个API。StackOverflow上有相关讨论
  • 微软还提供了TCHAR类型:如果定义了_UNICODE宏,就为WCHAR(typedef wchar_t得到),否则为char。但这些不属于C标准库,如果要用,就要再去学一下与TCHAR相关的一堆非标准库函数,我就没有研究;而且我写的代码太少,也看不出为什么要这样做
编辑于 2019-07-08