存储在 C char 中的 Unicode
Posted
技术标签:
【中文标题】存储在 C char 中的 Unicode【英文标题】:Unicode stored in C char 【发布时间】:2012-04-18 12:39:41 【问题描述】:我现在在 Linux 上学习 C 语言,遇到了一些奇怪的情况。
据我所知,标准 C 的 char
数据类型是 ASCII,1 字节(8 位)。这应该意味着,它只能保存 ASCII 字符。
在我的程序中,我使用char input[]
,它由getchar
函数填充,如下伪代码:
char input[20];
int z, i;
for(i = 0; i < 20; i++)
z = getchar();
input[i] = z;
奇怪的是,它不仅适用于 ASCII 字符,而且适用于我想象的任何字符,例如输入中的@&@čřžŧ¶'`[łĐŧđж←^€~[←^ø&čž
。
我的问题是 - 这怎么可能?它似乎是 C 语言中许多美丽的例外之一,但我非常感谢您的解释。是操作系统、编译器、隐藏语言的附加超级功能的问题吗?
谢谢。
【问题讨论】:
这不是真正的字符,而是通过getchar()
获得的字节。每个字符都被编码为一个字节序列。
这些都是比较正常的字符。试着扩大你的想象力,包括一些中文或日文字母。或者尝试西里尔字母进行更改 :) 这是俄语中的“Hello”:“Привет”。
@DanielFischer 我了解,getchar()
将其解码为字节。但我已经不明白,这些字节如何保存在 char
数据类型中,应该是 one 字节。
不,getchar()
不会将其解码为字节。 getchar()
从中读取的输入缓冲区已经包含可能构成您键入的字符的几个字节。每个getchar()
为您获取一个字节,因此对于UTF-8 编码输入,一个字符最多可以占用四个getchar()
。当您打印出来时,字节序列会被发送到终端并将其转换为字形。
太好了,谢谢,我现在完全明白了!
【参考方案1】:
这里没有魔法 - C 语言让您可以访问原始字节,因为它们存储在计算机内存中。 如果您的终端使用 utf-8(很可能),非 ASCII 字符在内存中占用的字节数超过一个字节。当您再次显示时,我们的终端代码会将这些序列转换为单个显示的字符。
只需更改您的代码以打印字符串的strlen
,您就会明白我的意思。
要在 C 中正确处理 utf-8 非 ASCII 字符,您必须使用一些库来为您处理它们,例如 glib、qt 或许多其他库。
【讨论】:
或尝试仅打印 input[ 0 ] 以查看它不会打印第一个字符,而仅打印最有可能是不可打印字符的第一个字节,然后尝试打印 input[ 0 ] 和 input[ 1 ] 一起查看多字节字符。 好的,我刚刚尝试了一些代码修改,它的工作方式与描述的完全一样。谢谢你。仅关于宽字符的注释 -<wchar.h>
不足以正确处理宽字符?【参考方案2】:
ASCII 是一个 7 位字符集。在 C 中通常由 8 位字符表示。如果设置了 8 位字节中的最高位,则它不是 ASCII 字符。
还请注意,您不保证 ASCII 作为基础,许多人忽略了其他情况。如果你想检查一个 "primitive" 字节是否是一个字母字符,你可以换句话说不是,当注意所有系统时,说:
is_alpha = (c > 0x40 && c < 0x5b) || (c > 0x60 && c < 0x7b);
相反,您必须使用 ctype.h
并说:
isalpha(c);
AFAIK 唯一的例外是数字,至少在大多数表格上,它们具有连续的值。
这样就可以了;
char ninec = '9';
char eightc = '8';
int nine = ninec - '0';
int eight = eightc - '0';
printf("%d\n", nine);
printf("%d\n", eight);
但这不保证是'a':
alhpa_a = 0x61;
不基于 ASCII 的系统,即使用EBCDIC; C 在这样的平台上仍然可以正常运行,但在这里它们(大部分)使用 8 位而不是 7,即 A
可以编码为十进制 193
而不是65
,因为它是 ASCII 格式。
但是对于 ASCII;具有十进制 128 - 255 的字节(使用 8 位)是扩展的,而不是 ASCII 集的一部分。 IE。 ISO-8859 使用这个范围。
经常做的事;也就是将两个或多个字节组合成一个字符。因此,如果您将定义为 utf8 0xc3 0x98
== Ø 的两个字节一个接一个地打印出来,那么您将得到这个字符。
这又取决于您所处的环境。在许多系统/环境中,打印 ASCII 值在字符集、系统等之间给出相同的结果。但打印字节 > 127 或双字节字符会根据本地配置给出不同的结果。
即:
先生。一个正在运行的程序得到
贾斯欧元
当 B 先生得到时
贾斯派斯
这可能与 ISO-8859 系列和 Windows-1252 的扩展字符的单字节表示等特别相关。
ASCII_printable_characters ,注意它们是 7 位而不是 8 位。 ISO_8859-1 和 ISO_8859-15,广泛使用的集合,以 ASCII 为核心。 Windows-1252,Windows 的遗留系统。UTF-8#Codepage_layout,在 UTF-8 中你有 ASCII,然后你有特殊的再见序列。 每个序列都以 > 127 的字节开始(这是最后一个 ASCII 字节), 后跟给定数量的字节,这些字节都以
10
位开头。
换句话说,您永远不会在多字节 UTF-8 表示中找到 ASCII 字节。
也就是说; UTF-8 中的第一个字节,如果不是 ASCII,则表示该字符有多少字节。你也可以说 ASCII 字符表示后面没有更多字节 - 因为最高位是 0。
即如果文件被解释为 UTF-8:
fgetc(c);
if c < 128, 0x80, then ASCII
if c == 194, 0xC2, then one more byte follow, interpret to symbol
if c == 226, 0xE2, then two more byte follows, interpret to symbol
...
举个例子。如果我们看一下您提到的角色之一。如果在 UTF-8 终端中:
$ echo -n "č" | xxd
应该让步:
0000000: c48d ..
换句话说,“č”由两个字节0xc4和0x8d表示。将 -b 添加到 xxd 命令,我们得到字节的二进制表示。我们将它们分解如下:
___ byte 1 ___ ___ byte 2 ___
| | | |
0xc4 : 1100 0100 0x8d : 1000 1101
| |
| +-- all "follow" bytes starts with 10, rest: 00 1101
|
+ 11 -> 2 bits set = two byte symbol, the "bits set" sequence
end with 0. (here 3 bits are used 110) : rest 0 0100
Rest bits combined: xxx0 0100 xx00 1101 => 00100001101
\____/ \_____/
| |
| +--- From last byte
+------------ From first byte
这给了我们:00100001101 2 = 26910 = 0x10D => Uncode codepoint U+010D == "č"。
这个数字也可以在 html 中用作&#269;
== č
这个和许多其他代码系统的共同点是 8 位字节是基础。
通常这也是一个关于上下文的问题。以 GSM SMS 为例,ETSI GSM 03.38/03.40 (3GPP TS 23.038, 3GPP 23038)。在那里我们还找到了一个 7 位字符表,7 位 GSM 默认字母表,但不是将它们存储为 8 位,而是将它们存储为 7 位1。通过这种方式,您可以将更多字符打包到给定数量的字节中。即标准 SMS 160 字符变成 1280 位或 160 字节作为 ASCII 和 1120 或 140 字节作为 SMS。
1 并非没有例外,(故事更重要)。
即以 SMS UDP 格式保存为 septets (7bit) C8329BFD06 到 ASCII 的字节的简单示例:
_________
7 bit UDP represented | +--- Alphas has same bits as ASCII
as 8 bit hex '0.......'
C8329BFDBEBEE56C32 1100100 d * Prev last 6 bits + pp 1
| | | | | | | | +- 00 110010 -> 1101100 l * Prev last 7 bits
| | | | | | | +--- 0 1101100 -> 1110010 r * Prev 7 + 0 bits
| | | | | | +----- 1110010 1 -> 1101111 o * Last 1 + prev 6
| | | | | +------- 101111 10 -> 1010111 W * Last 2 + prev 5
| | | | +--------- 10111 110 -> 1101111 o * Last 3 + prev 4
| | | +----------- 1111 1101 -> 1101100 l * Last 4 + prev 3
| | +------------- 100 11011 -> 1101100 l * Last 5 + prev 2
| +--------------- 00 110010 -> 1100101 e * Last 6 + prev 1
+----------------- 1 1001000 -> 1001000 H * Last 7 bits
'------'
|
+----- GSM Table as binary
而9个字节“解压”变成了10个字符。
【讨论】:
这篇文章简直太棒了!感谢您的总结和概述。 @Mimars;变得有点长,但是,:)。这是一个有趣的话题,看看事情是如何解决的很有趣。还认为它具有教育意义,因为人们可以在编码时使用类似的逻辑 - 也可以使用完全不同的东西。 ASCII 还有很多美女以及所有内容的排列和排序方式 - 即:此处为 pp3 faculty.kfupm.edu.sa/ics/said/ics232Lectures/…。 - 查看 /usr/include/ctype.h 等也很有教育意义。【参考方案3】:ASCII 是 7 位,而不是 8 位。 char []
保存字节,可以是任何编码 - iso8859-1、utf-8,无论你想要什么。 C 不在乎。
【讨论】:
【参考方案4】:这就是UTF-8 的魔力,您甚至不必担心它是如何工作的。唯一的问题是 C 数据类型被命名为 char
(代表 character),而它的实际含义是 byte。字符和编码它们的字节之间没有 1:1 的对应关系。
在您的代码中发生的情况是,从程序的角度来看,您输入了一个 bytes 序列,它将字节存储在内存中,如果您打印文本,它会打印字节。这段代码不关心这些字节如何编码字符,只有终端需要担心在输入时对它们进行编码并在输出时正确解释它们。
【讨论】:
这是最好的答案。它真的就这么简单,您绝对应该 (!) 使用char[]
来存储 UTF-8 字符串和 (!) 字符。【参考方案5】:
当然有很多库可以完成这项工作,但是要快速解码任何 UTF8 unicode,这个小函数很方便:
typedef unsigned char utf8_t;
#define isunicode(c) (((c)&0xc0)==0xc0)
int utf8_decode(const char *str,int *i)
const utf8_t *s = (const utf8_t *)str; // Use unsigned chars
int u = *s,l = 1;
if(isunicode(u))
int a = (u&0x20)? ((u&0x10)? ((u&0x08)? ((u&0x04)? 6 : 5) : 4) : 3) : 2;
if(a<6 || !(u&0x02))
int b,p = 0;
u = ((u<<(a+1))&0xff)>>(a+1);
for(b=1; b<a; ++b)
u = (u<<6)|(s[l++]&0x3f);
if(i) *i += l;
return u;
考虑您的代码;您可以迭代字符串并读取 unicode 值:
int l;
for(i=0; i<20 && input[i]!='\0'; )
if(!isunicode(input[i])) i++;
else
l = 0;
z = utf8_decode(&input[i],&l);
printf("Unicode value at %d is U+%04X and it\'s %d bytes.\n",i,z,l);
i += l;
【讨论】:
看起来那将是完全可移植的..不需要库? 嗯,不完全是,在 16 位系统上 int 将是 16 位,而函数要求 int 至少为 32 位。您可以使用 long 或包含 stdint 并使用例如 uint32_t,但在大多数情况下 int 可以,并且示例函数是展示如何解码 UTF8,因此还有改进的余地。【参考方案6】:有一个数据类型 wint_t
(#include <wchar.h>
) 用于非 ASCII 字符。您可以使用getwchar()
的方法来阅读它们。
【讨论】:
以上是关于存储在 C char 中的 Unicode的主要内容,如果未能解决你的问题,请参考以下文章