如何在 C 代码中使用 UTF-8?
Posted
技术标签:
【中文标题】如何在 C 代码中使用 UTF-8?【英文标题】:How to use UTF-8 in C code? 【发布时间】:2015-08-03 23:14:01 【问题描述】:我的设置:gcc-4.9.2,UTF-8 环境。
以下 C 程序在 ASCII 中工作,但在 UTF-8 中不工作。
创建输入文件:
echo -n 'привет мир' > /tmp/вход
这是 test.c:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SIZE 10
int main(void)
char buf[SIZE+1];
char *pat = "привет мир";
char str[SIZE+2];
FILE *f1;
FILE *f2;
f1 = fopen("/tmp/вход","r");
f2 = fopen("/tmp/выход","w");
if (fread(buf, 1, SIZE, f1) > 0)
buf[SIZE] = 0;
if (strncmp(buf, pat, SIZE) == 0)
sprintf(str, "% 11s\n", buf);
fwrite(str, 1, SIZE+2, f2);
fclose(f1);
fclose(f2);
exit(0);
检查结果:
./test; grep -q ' привет мир' /tmp/выход && echo OK
应该做些什么来使 UTF-8 代码像 ASCII 代码一样工作 - 不要打扰符号占用多少字节等。换句话说:在示例中更改什么以处理任何 UTF-8 符号作为一个单元(包括 argv、STDIN、STDOUT、STDERR、文件输入、输出和程序代码)?
【问题讨论】:
您可以查看链接nubaria.com/en/blog/?p=289 你的grep
模式有一个前导空格。
另外,不要将你的程序命名为test
,因为这是一个内置的shell。 (./test
当然可以;但这是你不想保持的习惯。)
@tripleee grep
中的空格是故意的(请参阅sprintf
)。关于测试的好点。
"привет мир" 在 UTF-8 中是 D0 BF D1 80 D0 B8 D0 B2 D0 B5 D1 82 20 D0 BC D0 B8 D1 80
,远远超过 10 个字符
【参考方案1】:
#define SIZE 10
缓冲区大小 10 不足以存储 UTF-8 字符串 привет мир
。尝试将其更改为更大的值。在我的系统(Ubuntu 12.04,gcc 4.8.1)上,将其更改为 20,运行良好。
UTF-8 是一种多字节编码,每个字符使用 1 到 4 个字节。因此,使用 40 作为上面的缓冲区大小更安全。 How many bytes does one Unicode character take? 有一个大讨论,可能很有趣。
【讨论】:
将 SIZE 更改为 20 不起作用 - 不打印 OK(请参阅echo OK
检查我的问题)。
您需要做的不仅仅是将 SIZE 更改为 20,但这是过程中的关键步骤。【参考方案2】:
Siddhartha Ghosh 的answer 为您提供基本问题。不过,修复代码需要更多的工作。
我使用了以下脚本 (chk-utf8-test.sh
):
echo -n 'привет мир' > вход
make utf8-test
./utf8-test
grep -q 'привет мир' выход && echo OK
我调用了你的程序 utf8-test.c
并像这样修改了源代码,删除了对 /tmp
的引用,并且更加注意长度:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SIZE 40
int main(void)
char buf[SIZE + 1];
char *pat = "привет мир";
char str[SIZE + 2];
FILE *f1 = fopen("вход", "r");
FILE *f2 = fopen("выход", "w");
if (f1 == 0 || f2 == 0)
fprintf(stderr, "Failed to open one or both files\n");
return(1);
size_t nbytes;
if ((nbytes = fread(buf, 1, SIZE, f1)) > 0)
buf[nbytes] = 0;
if (strncmp(buf, pat, nbytes) == 0)
sprintf(str, "%.*s\n", (int)nbytes, buf);
fwrite(str, 1, nbytes, f2);
fclose(f1);
fclose(f2);
return(0);
当我运行脚本时,我得到了:
$ bash -x chk-utf8-test.sh
+ '[' -f /etc/bashrc ']'
+ . /etc/bashrc
++ '[' -z '' ']'
++ return
+ alias 'r=fc -e -'
+ echo -n 'привет мир'
+ make utf8-test
gcc -O3 -g -std=c11 -Wall -Wextra -Werror utf8-test.c -o utf8-test
+ ./utf8-test
+ grep -q 'привет мир' $'в?\213?\205од'
+ echo OK
OK
$
作为记录,我在 Mac OS X 10.10.3 上使用 GCC 5.1.0。
【讨论】:
您忘记了 sprintf 中的% 11s
和 grep 中的前导空格。不过,OK
没有打印出来。
哦,我忘了说我的编译器对象是空间。 (它对您有什么作用——gnu_printf
提到的消息?空格标志与数字转换相关,但与字符串转换无关)。如果我想在开头有一个空格,它会在%
之前。我没有忘记 11;我将11
更改为.*
,并将正确的字节数作为int
参数传递给printf()
。您没有使用宽字符;您正在使用字节字符串,并且 UTF-8 字符的宽度是可变的,尽管除了空格之外,您在 UTF-8 中的长度都是 2 个字节。您必须使用字节。
如果它工作得很好,你为什么要问这个问题?它没有完美地工作,是吗?除非您确定您正在使用的编译器选项,否则没有错误的编译是没有意义的。 GCC 将(按设计)默认接受最骇人听闻的代码而不会抱怨。
我一直在研究这两个命令之间的区别。这是可怕的微妙。两者看起来像 A 实际上是两个不同的字符:第一个是普通的 Unicode U+0041 LATIN CAPITAL LETTER A,第二个是 U+0410 CYRILLIC CAPITAL LETTER A。当我运行脚本时,第一个命令打印空格 A,但第二个命令只打印 А。除了默认情况下 Perl 不理解 UTF-8 之外,我不确定这显示了什么。请注意,Perl Unicode 文档讨论了“字节和字符语义”。
我保留判断您的代码显示的内容。 %11s
的使用完全使工作中的扳手,AFAICS。我当然完全不确定它应该展示什么。考虑到工作等其他承诺,我需要时间——可能需要几天的时间——来找出发生了什么以及如何解决它。可以说printf()
使用单字节代码集就足够了,并且容忍 UTF-8,但不知道它的含义并且仍然计算字节,而不是字符。处理角色需要更多的工作。我尝试了一些宽字符代码,但失败了。 …时间…【参考方案3】:
这更像是其他答案的必然结果,但我会尝试从稍微不同的角度来解释这一点。
这是 Jonathan Leffler 的代码版本,有三处细微的变化:(1) 我明确表示了 UTF-8 字符串中的实际单个字节; (2) 我修改了sprintf
格式化字符串宽度说明符,希望能做你真正想做的事情。另外,(3) 我使用了perror
,以便在出现故障时获得更有用的错误消息。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SIZE 40
int main(void)
char buf[SIZE + 1];
char *pat = "\320\277\321\200\320\270\320\262\320\265\321\202"
" \320\274\320\270\321\200"; /* "привет мир" */
char str[SIZE + 2];
FILE *f1 = fopen("\320\262\321\205\320\276\320\264", "r"); /* "вход" */
FILE *f2 = fopen("\320\262\321\213\321\205\320\276\320\264", "w"); /* "выход" */
if (f1 == 0 || f2 == 0)
perror("Failed to open one or both files"); /* use perror() */
return(1);
size_t nbytes;
if ((nbytes = fread(buf, 1, SIZE, f1)) > 0)
buf[nbytes] = 0;
if (strncmp(buf, pat, nbytes) == 0)
sprintf(str, "%*s\n", 1+(int)nbytes, buf); /* nbytes+1 length specifier */
fwrite(str, 1, 1+nbytes, f2); /* +1 here too */
fclose(f1);
fclose(f2);
return(0);
sprintf
带有正数宽度说明符的行为是从左侧用空格填充,因此您尝试使用的空格是多余的。但是您必须确保目标字段比您要打印的字符串宽,以便实际发生任何填充。
为了使这个答案自成一体,我将重复其他人已经说过的话。传统的char
总是正好是一个字节,但 UTF-8 中的一个字符通常不正好是一个字节,除非你的所有字符实际上都是 ASCII。 UTF-8 的吸引力之一是遗留的 C 代码不需要了解任何关于 UTF-8 的信息就可以继续工作,但是当然,一个字符是一个字形的假设是不成立的。 (例如,如您所见,“привет мир”中的字形 п 映射到两个字节——因此,两个 char
s——"\320\277"
。)
这显然不太理想,但表明如果您的代码不特别关心字形语义,您可以将 UTF-8 视为“仅字节”。如果你这样做,你最好切换到wchar_t
,如概述的那样。这里:http://www.gnu.org/software/libc/manual/html_node/Extended-Char-Intro.html
但是,当标准期望是 UTF-8 时,标准 wchar_t
不太理想。参见例如GNU libunistring documentation 是一种侵入性较小的替代方案,以及一些背景知识。有了这个,您应该能够用uint8_t
替换char
和用u8_str*
替换的各种str*
函数并完成。一个字形等于一个字节的假设仍然需要解决,但这在您的示例程序中成为一个次要的技术问题。 http://ideone.com/p0VfXq 上提供了改编版本(但遗憾的是,http://ideone.com/ 上没有该库,因此无法在那里演示)。
【讨论】:
实际上,我是在问如何在我的程序中使用普通的 UTF-8,即如何在C
中完成相当于 perl -CSDA -Mutf8
您的示例并没有解决我的问题,尽管链接你提供的绝对是主题。
添加了另一个关于wchar_t
替代方案的简短段落。【参考方案4】:
您的test.c
文件可能不是以 UTF-8 格式存储的,因此“привет мир”字符串是 ASCII - 比较失败。更改源文件的文本编码,然后重试。
【讨论】:
【参考方案5】:以下代码按要求工作:
#include <stdio.h>
#include <locale.h>
#include <stdlib.h>
#include <wchar.h>
#define SIZE 10
int main(void)
setlocale(LC_ALL, "");
wchar_t buf[SIZE+1];
wchar_t *pat = L"привет мир";
wchar_t str[SIZE+2];
FILE *f1;
FILE *f2;
f1 = fopen("/tmp/вход","r");
f2 = fopen("/tmp/выход","w");
fgetws(buf, SIZE+1, f1);
if (wcsncmp(buf, pat, SIZE) == 0)
swprintf(str, SIZE+2, L"% 11ls", buf);
fputws(str, f2);
fclose(f1);
fclose(f2);
exit(0);
【讨论】:
以上是关于如何在 C 代码中使用 UTF-8?的主要内容,如果未能解决你的问题,请参考以下文章
如何在 C++ 中使用 UTF-8 和 Unicode? C++20 char8_t 有多大?
如何从 UTF-8 字符串的每个字符中获取 UNICODE 代码?
如何在 C# 中使用 UTF-8 以外的代码页写出文本文件?