27

我现在在 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 语言中许多美丽的例外之一,但我非常感谢您的解释。是操作系统、编译器、隐藏语言的附加超级功能的问题吗?

谢谢。

4

6 回答 6

31

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

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

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

于 2012-04-04T18:46:53.897 回答
24

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

但这不能保证是“a”:

alhpa_a = 0x61;

不基于 ASCII 的系统,即使用EBCDIC;C 在这样的平台上仍然可以正常运行,但在这里它们(大部分)使用 8 位而不是 7 位,即A可以编码为十进制193而不是65ASCII。


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

经常做什么;也就是将两个或多个字节组合成一个字符。因此,如果您连续打印两个字节,定义为utf8 0xc3 0x98 == Ø,那么您将得到这个字符。

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

IE:

A先生运行程序得到

贾斯€</p>

虽然 B 先生得到

亚斯派斯

这可能与 ISO-8859 系列和 Windows-1252 的扩展字符的单字节表示等特别相关。


  • UTF-8#Codepage_layout,在 UTF-8 中你有 ASCII,然后你有特殊的再见序列。
    • 每个序列都以 > 127 的字节开始(这是最后一个 ASCII 字节),
    • 后跟给定数量的字节,所有字节都以 bits 开头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 = 269 10 = 0x10D => Uncode 代码点 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 个字符。

于 2012-04-04T18:58:18.910 回答
6

ASCII 是 7 位,而不是 8 位。achar []保存字节,可以是任何编码 - iso8859-1、utf-8,无论你想要什么。C不在乎。

于 2012-04-04T18:45:55.987 回答
6

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

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

于 2012-05-02T07:56:16.893 回答
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;
   }
}
于 2016-02-11T06:12:02.027 回答
2

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

于 2012-04-04T18:48:53.593 回答