Преобразувайки не-Ascii символи в int в C, допълнителните битове се допълват с 1, а не с 0

Когато кодирах в C, случайно открих, че що се отнася до не-Ascii символи, след като се преобразуват от char (1 байт) в int (4 байта), допълнителните битове (3 байта) се допълват с 1, а не с 0. ( Що се отнася до символите Ascii, допълнителните битове се допълват с 0.) Например:

char c[] = "ā";
int i = c[0];
printf("%x\n", i);

И резултатът е ffffffc4, а не самото c4. (Кодът UTF-8 за ā е \xc4\x81.)

Друг свързан проблем е, че при извършване на операции за десен преместване >> върху не-Ascii знак, допълнителните битове в левия край също се допълват с 1, а не с 0, въпреки че променливата char е изрично преобразувана в unsigned int (за както за signed int, допълнителните битове се допълват с 1 в моята ОС). Например:

char c[] = "ā";
unsigned int u_c;
int i = c[0];
unsigned int u_i = c[0];

c[0] = (unsigned int)c[0] >> 1; 
u_c = (unsigned int)c[0] >> 1;      
i = i >> 1;
u_i = u_i >> 1;
printf("c=%x\n", (unsigned int)c[0]); // result: ffffffe2. The same with the signed int i.
printf("u_c=%x\n", u_c); // result: 7fffffe2.
printf("i=%x\n", i); // result: ffffffe2.
printf("u_i=%x\n", u_i); // result: 7fffffe2. 

Сега съм объркан от тези резултати... Загрижени ли са за структурите от данни на char, int и unsigned int, или свързани с моята операционна система (ubuntu 14.04), или свързани с изискванията на ANSI C? Опитах се да компилирам тази програма както с gcc(4.8.4), така и с clang(3.4), но няма разлика.

Много благодаря!


person none    schedule 06.08.2017    source източник
comment
Не, това е просто разширение на знак и фактът, че във вашата архитектура char е тип със знак. Ако имате char c;, използвайте i = (unsigned char)c;, за да преобразувате c първо към тип unsigned char, така че кодът на знака винаги да е неотрицателен.   -  person Nominal Animal    schedule 06.08.2017
comment
можете да изберете, като използвате опциите на командния ред, например gcc -funsigned-char. Но ако някой трябва да има 8-битова подписана или неподписана стойност, по-добре е да използва типове с фиксиран размер вместо 'char': uint8_t или int8_t   -  person 0___________    schedule 06.08.2017
comment
А, да! След като използвам unsigned char и uint8_t, всичко се връща към нормалното. Благодаря! @NominalAnimal @PeterJ   -  person none    schedule 07.08.2017
comment
ако uint8_t съществува съвпада с unsigned char   -  person Antti Haapala    schedule 07.08.2017
comment
@AnttiHaapala Сега разбирам. Благодаря ви много за вашата помощ!   -  person none    schedule 07.08.2017


Отговори (1)


Определя се от внедряването дали char е подписано или неподписано. На x86 компютри char обикновено е целочислен тип със знак; и на ARM обикновено е целочислен тип без знак.

Цяло число със знак ще бъде разширено със знак, когато се преобразува в по-голям тип със знак;

цяло число със знак, преобразувано в цело число без знак, ще използва аритметиката по модул, за да обвие стойността със знак в знак в обхвата на типа без знак, сякаш чрез многократно добавяне или изваждане на максималната стойност на неподписан тип + 1.


Решението е да използвате/прехвърляте към unsigned char, ако искате стойността да бъде преносимо разширена до нула или за съхраняване на малки цели числа в диапазон 0..255.

По същия начин, ако искате да съхранявате цели числа със знак в диапазон -127..127/128, използвайте signed char.

Използвайте char, ако подписът няма значение - реализацията вероятно ще е избрала типа, който е най-ефективен за платформата.


По същия начин за заданието

unsigned int u_c; u_c = (uint8_t)c[0];,

Тъй като -0x3c или -60 не е в диапазона от uint16_t, тогава действителната стойност е стойността (mod UINT16_MAX + 1), която попада в диапазона от uint16_t; iow, ние добавяме или изваждаме UINT16_MAX + 1 (забележете, че целочислените промоции могат да измамят тук, така че може да имате нужда от преобразувания, ако сте в C код), докато стойността е в диапазона. UINT16_MAX естествено винаги е 0xFFFFF; добавете 1 към него, за да получите 0x10000. 0x10000 - 0x3C е 0xFFC4, което видяхте. И тогава стойността uint16_t се разширява с нула до стойността uint32_t.

Ако бяхте изпълнили това на платформа, където char е unsigned, резултатът щеше да бъде 0xC4!


BTW в i = i >> 1;, i е цяло число със знак с отрицателна стойност; C11 казва, че стойността е дефинирана от внедряването, така че действителното поведение може да се променя от компилатор на компилатор. В ръководствата на GCC се казва, че

Signed >> действа върху отрицателни числа чрез разширение за знак.

Една стриктно съобразена програма обаче не трябва да разчита на това.

person Antti Haapala    schedule 06.08.2017
comment
И, разбира се, въпросните 1-бита показват, че отрицателните числа, което не е изненадващо, са във формат на допълнение към две. - person Tom Blodget; 06.08.2017
comment
@TomBlodget GCC поддържа само допълване на 2, така че ако беше друго, наистина би било голяма изненада :D - person Antti Haapala; 06.08.2017
comment
@Antti Haapala виждал ли си компютърна система, разработена през последните 30-40 години, която използва 1complement? Единственото, за което съм чувал, е форматът на битовото поле на индекса в префикса на разширението Intel AVX - но не е много вероятно да го програмирам ръчно. - person 0___________; 06.08.2017
comment
@PeterJ Е, не съм виждал, не съм пипал нечие допълнение - никога лично. Изглежда, че нови системи UNIVAC са изградени дори през последните 20 години, за да подпомогнат миграцията към модерни системи... - person Antti Haapala; 06.08.2017
comment
@AnttiHaapala Благодаря ви много за отговора! Така че този проблем не е свързан с не-Ascii/Ascii, а е свързан с това дали променливата char има отрицателна или положителна стойност. Това, че изглежда се занимава с Ascii, е само защото кодовете на всички ascii знаци са в [0, 127] и следователно всички те имат положителна стойност в знак със знак, нали? - person none; 07.08.2017
comment
@AnttiHaapala Сега използвам unsigned char и той е с нулево разширение. :) Но имам още един въпрос. Ако използвам unsigned int u_c; u_c = (uint8_t)c[0];, той също е разширен с нула. Дали защото при преобразуването на signed char в uint8_t, тъй като и двете са с еднакъв размер (1 байт), няма разширен бит; и след това при преобразуване на uint8_t в unsigned int, тъй като и двете са цели числа без знак, допълнителните битове се допълват от 0? (И резултатът от unsigned int u_c; u_c = (uint16_t)c[0]; е ffc4. Това е така, защото в този процес само 1 байт се разширява от signed char до unsigned int, нали?) - person none; 07.08.2017
comment
@none Обърнах се и към това в отговора си. - person Antti Haapala; 07.08.2017