Юникод, хранящийся в символе C

Я изучаю язык C на Linux сейчас, и я столкнулся с немного странной ситуацией.

Насколько мне известно, стандартный тип данных 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, но я бы очень благодарен за объяснение. Это вопрос ОС, компилятор, скрытый язык, дополнительная суперфункция?

Благодарю.

Ответ 1

Здесь нет никакой магии - язык C дает вам доступ к необработанным байтам, поскольку они хранятся в памяти comptuer. Если ваш терминал использует utf-8 (что вполне вероятно), символы не-ASCII берут более одного байта в памяти. Когда вы снова показываете, это наш код терминала, который преобразует эти последовательности в один отображаемый символ.

Просто измените свой код, чтобы напечатать strlen строк, и вы увидите, что я имею в виду.

Чтобы правильно обрабатывать символы не-ASCII utf-8 в C, вы должны использовать некоторую библиотеку для обработки их для вас, например glib, qt или многих других.

Ответ 2

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);

Но это не гарантируется как "а":

alhpa_a = 0x61;

Системы, не основанные на ASCII, т.е. с использованием EBCDIC; C на такой платформе все еще работает нормально, но здесь они (в основном) используют 8 бит вместо 7 и т.е. A можно закодировать как десятичную 193 а не 65 как в ASCII.


Однако для ASCII; байты, имеющие десятичное значение 128 - 255, (используется 8 бит), расширены, а не являются частью набора ASCII. Т.е. ISO-8859 использует этот диапазон.

Что часто делается; также должен объединять два или более байта с одним символом. Поэтому, если вы печатаете два байта после друг друга, которые определены как say, utf8 0xc3 0x98 == Ø, вы получите этот символ.

Это опять же зависит от того, в какой среде вы находитесь. Во многих системах/средах печать значений ASCII дает одинаковый результат для наборов символов, систем и т.д. Но печать байтов> 127 или двойных байтов приводит к другому результату в зависимости от локальной конфигурации.

То есть:

Г-н А работает программа получает

Jasŋ €

Пока мистер Б получает

Jasπß

Это, пожалуй, особенно актуально для серий ISO-8859 и Windows-1252 для однобайтового представления расширенных символов и т.д.


  • UTF-8 # Codepage_layout, в UTF-8 у вас есть ASCII, тогда у вас есть специальные последовательности байтов.
    • Каждая последовательность начинается с байта> 127 (который является последним байтом ASCII),
    • за которым следует заданное количество байтов, которое начинается с битов 10.
    • Другими словами, вы никогда не найдете байт ASCII в многобайтовом представлении UTF-8.

То есть; первый байт в 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 codepoint U + 010D == "č".

Этот номер также можно использовать в HTML как &#269; == č

Общим для этого и множеством других систем кода является то, что 8-битный байт является базой.


Часто это также вопрос о контексте. В качестве примера возьмем GSM SMS, с ETSI GSM 03.38/03.40 (3GPP TS 23.038, 3GPP 23038). Там мы также находим таблицу символов 7 бит, 7 -b, ее алфавит по умолчанию GSM, но вместо того, чтобы хранить их как 8 бит, они сохраняются как 7 бит 1. Таким образом, вы можете упаковать больше символов в заданное количество байтов. Т.е. стандартные SMS 160 символов становятся 1280 бит или 160 байтов в виде ASCII и 1120 или 140 байтов в виде SMS.

1 Не без исключения (это больше для истории).

Т.е. простой пример байтов, сохраненных как септеты (7 бит) C8329BFD06 в формате SMS UDP для 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 символами.

Ответ 3

ASCII - это 7 бит, а не 8 бит. char [] содержит байты, которые могут быть в любой кодировке - iso8859-1, utf-8, независимо от того, что вы хотите. C не заботится.

Ответ 4

Существует тип данных wint_t (#include <wchar.h>) для символов, отличных от ASCII. Вы можете использовать метод getwchar() для их чтения.

Ответ 5

Это волшебство UTF-8, что вам даже не нужно беспокоиться о том, как это работает. Единственная проблема заключается в том, что тип данных C имеет имя char (для символа), а то, что он на самом деле означает, это байт. между символами и байтами, которые их кодируют, нет соответствия 1:1.

Что происходит в вашем коде, так это то, что с точки зрения программы вы вводите последовательность байтов, она хранит байты в памяти, и если вы печатаете текст, он печатает байты. Этот код не волнует, как эти байты кодируют символы, это только терминал, который должен беспокоиться о кодировании их на входе и правильно интерпретировать их на выходе.

Ответ 6

Конечно, есть много библиотек, которые выполняют эту работу, но для быстрого декодирования любого юникода UTF8 эта небольшая функция удобна:

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;
}

Рассмотрение вашего кода; вы можете перебирать строку и читать значения юникода:

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\ %d bytes.\n",i,z,l);
      i += l;
   }
}