Unicode存储在C字符中



我现在正在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语言中许多美丽的例外之一,但我真的很感激你的解释。这是操作系统、编译器、隐藏语言的额外超级功能的问题吗?

谢谢。

这里没有魔法-C语言可以访问存储在计算机内存中的原始字节。如果您的终端使用utf-8(这很可能),那么非ASCII字符占用内存中的多个字节。当您再次显示时,是我们的终端代码,它将这些序列转换为单个显示的字符。

只需更改代码以打印字符串的strlen,您就会明白我的意思。

为了正确处理C中的utf-8非ASCII字符,您必须使用一些库来处理它们,如glib、qt或许多其他库。

ASCII是一个7位字符集。在C中,通常用8位字符表示。如果设置了8位字节中的最高位,则不是ASCII字符。

还请注意,不能保证ASCII作为基础,因此许多情况会忽略其他情况。如果你想检查"基元"字节是否是阿尔法字符,换句话说,当注意所有系统时,你可以不检查,比如:

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("%dn", nine);
printf("%dn", eight);

但这并不能保证是"a":

alhpa_a = 0x61;

不基于ASCII的系统,即使用EBCDIC;C在这样的平台上仍然运行良好,但在这里它们(主要)使用8位而不是7,即A可以被编码为十进制193,而不是像在ASCII中那样的65


然而,对于ASCII;十进制128-255的字节(使用8位)是扩展的,而不是ASCII集的一部分。即ISO-8859使用这个范围。

经常做的事;也是将两个或多个字节组合为一个字符。因此,如果您在定义为utf8 0xc3 0x98==Å的两个字节之后打印,那么您将得到这个字符。

这同样取决于您所处的环境。在许多系统/环境中,打印ASCII值会在字符集、系统等之间产生相同的结果。但打印字节数>127或双字节字符会根据本地配置产生不同的结果。

即:

Mr。运行程序得到

Jasŋ€

而B先生得到

Jasπß

这可能与扩展字符的单字节表示的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,然后你有特殊的byes序列。
    • 每个序列以字节>127开始(这是最后一个ASCII字节)
    • 接着是全部以比特CCD_ 11开始的给定数量的字节
    • 换句话说,在多字节UTF-8表示中永远找不到ASCII字节

也就是说;UTF-8中的第一个字节,如果不是ASCII,则告诉该字符有多少字节。您也可以说ASCII字符表示后面不再有字节,因为最高的位是0。

I.e如果文件被解释为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表示。在xxd命令中添加-b,我们就得到了字节的二进制表示。我们对它们进行如下剖析:

 ___  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

这给了我们:001000011012=26910=0x10D=>未编码代码点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。通过这种方式,您可以将更多的字符打包到给定数量的字节中。Ie标准SMS 160字符变为1280位或160字节的ASCII和1120或140字节的SMS。

1并非无一例外,(更多的是故事)

例如,以SMS UDP格式保存为分隔符(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个字节"unpacked"变为10个字符。

ASCII是7位,而不是8位。char []包含字节,可以是任何编码——iso8859-1、utf-8,无论您想要什么。C不在乎。

这就是UTF-8的神奇之处,您甚至不必担心它是如何工作的。唯一的问题是,C数据类型被命名为char(用于字符),而它的实际含义是字节。字符和编码它们的字节之间没有1:1的对应关系。

代码中发生的情况是,从程序的角度来看,您输入一个字节的序列,它将字节存储在内存中,如果您打印文本,它将打印字节。这段代码并不关心这些字节是如何编码字符的,只是终端需要担心在输入时对它们进行编码,并在输出时正确解释它们。

当然有很多库可以完成这项工作,但要快速解码任何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]!=''; ) {
   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;
   }
}

非ASCII字符有一个数据类型wint_t#include <wchar.h>)。您可以使用方法getwchar()来读取它们。

最新更新