Поведение целочисленных типов

19/01/2010 - 07:33
   Несколько примеров возможного запутанного поведения описаны ниже и это далеко не то, что бы вы назвали закоулками языка Си, выражения подобные этим можно найти в реальном приложении. Но прежде чем погрузиться в поведение, которое может показаться запутанным, давайте сначала разберемся, что стандарт говорит относительно целочисленного расширения и целочисленных констант.

Целочисленное расширение

   Си стандарт 6.3.1.1: Если int может представить все значения оригинального типа, значение преобразуется к int; в противном случае, значение преобразуется к unsigned int. Это называется целочисленное расширение. Целочисленное расширение сохраняет значение переменной, включая ее знак.

   Согласно 6.3.1.1, bool, char, signed, unsigned char, short int и перечислимые типы данных конвертируются к int или unsigned int целочисленным расширением, когда они используются в выражениях. В большинстве случаев это дает результат, который вы бы получили из выражения математически. Например:

char c1,c2,c3;
c1 = 0xA
c2 = 0xB;
c3 = c1+c2;

Для компилятора с 16 разрядным типом int, это бы было выполнено как суммирование int с усечением результата к char
0x0A + 0x0B --> 0x000A+ 0x000B --> 0x0015 --> 0x15.

Заметьте, что для этого примера, результирующее значение вписывается в тип адресата. Когда этого не происходит, ситуация становится немного более сложной.

Константы целочисленного типа

   Согласно параграфу 6.4.4.1, тип целочисленной константы задается ее суффиксом (таким как – u или U – unsigned, l или L – long, ll или LL – long long и т.д.) и основанием системы исчисления (десятичной, восмеричной, шестнадцатиричной). Тип безсуффиксной шестнадцатиричной константы, в которой она может быть представлена – int, unsigned int, long int, unsigned long int, long long int, unsigned long long int. Интересно, что наименьший тип это int, а не char. Например:

char c1;
c1 = 0xA

   Для компилятора с 16 разрядным типом int, присваивание требует усечения. 0xA -> 0x000A и это число должно быть усечено, чтобы соответствовать char.

Возможное запутанное поведение

   Правила для целочисленных типов и их преобразования в некоторых ситуациях могут привести к запутывающему поведению. Например, присваивания и/или условные выражения, включающие типы с различным размером, и/или логические операции, например побитовая инверсия. Типы здесь также включают типы констант.

   В некоторых случаях при компиляции могут быть предупреждения (например, "constant conditional" – постоянное условное выражение, "pointless comparison" – бессмысленное сравнение), в других вы просто получите результат отличный от ожидаемого. При определенных обстоятельствах компилятор может выдать предупреждения только при высоком уровне оптимизации.

Пример 1
 
void f1(unsigned char c1)
{
    if (c1 == ~0x80)
    ;
}

Здесь тест всегда ложь!
И вот почему. С правой стороны: 0х80->0x0080 и ~0x0080 --> 0xFF7F. С левой стороны переменная с1 –  unsigned char. Она должна быть меньше чем 256 и положительной, таким образом, после целочисленного расширения ее значение никогда не будет иметь установленными больше 8 бит.  (Например, если с1 равна 0xFF, после расширения 0хFF -> 0x00FF)

Пример 2
 
void f2(void)
{
    char c1;
    c1 = ~0x80;
    if (c1 == ~0x80)
    ;
}

   Поразрядное отрицание выполняется на объекте типа int – (~0х80) --> (~0x0080) --> 0xFF7F.  Это значение затем приваивается char, то есть у с1 будет положительное значение 0х7F (127 в десятичной системе). В условии, оно расширяется до 0х007F, которое сравниваетвся с 0хFF7F и тест проваливается. Если char знаковый и константа имела старший бит очищенным (это любое значение в диапазоне 0x00-0x7F), поразрядно проинвертированное значение будет отрицательным и тест может быть успешным (смотрите пример ниже)

Пример 3
 
void f3(void)
{
    signed char c1;
    c1 = ~0x70;
    if (c1 == ~0x70)
    ;
}

   В присваивании, поразрядная инверсия выполняется над объектом типа int, (~0x70) --> (~0x0070) --> 0xFF8F. Это значение затем присваивается char, то есть char будет иметь значение 0x8F (-113 в десятичной системе). В сравнении значение расширяется до 0xFF8F (знаковый бит распространяется в старшие разряды) и тест работает.

Пример 4

void f4(void)
{
    signed char c1;
    signed int i1;
    i1 = 0xFF;
    c1 = 0xFF;
    if (c1 == i1)
    ;
}

   В первом присваивании i1 становится равным 255, тогда как во втором присваивании (с1) 255 не укладывается в тип  signed char (потому что диапазон его значений -127..127). В сравнении значение с1 преобразуется к 0xFFFF и тест проваливается.
   Заметьте, что эта проблема очевидна с явно объявленным signed char, гораздо сложнее  обнаружить ее, если используется char и по умолчанию он знаковый – signed char.
 
По материалам фирмы IAR Systems

P.S.

Интересная на мой взгляд статья. Перевел как мог. 

 

Comments   

# alexandershahbazov 2010-01-19 20:14
В С уже то хорошо что все переменные
объявляешь явно , а не по умолчанию и
потому уже за все сам отвечаешь .

Мне бы хотелось здесь спросить можно ли
в разделе "Как подключить" рассмотреть в
будущем подключение MMC/SD ?
# Pashgan 2010-01-20 06:36
Я эту тему сейчас как раз копаю.. правда не для статьи, а для себя. Вот не знаю, брать готовую библиотеку для MMC/SD или "изобретать велосипед" и писать свой исходник.
# Guest 2010-01-22 22:06
Большой респект автору за его труд, читаю все статьи программировани я на С, до этого программировал на асме...чувствую с таким проводником можно войти в мир высокоуровневых языков ;-)
# Pashgan 2010-02-01 18:58
спасибо
# lightweave 2010-11-05 09:29
Огромное спасибо автору, это именно то, что вводило меня в ступор в течении месяца программировани я в AVR Studio/GCC! Сравнение флага с константой в main всегда приводило к положительному результату, теперь мне все стало ясно благодаря этой статье. А ведь на что я только не грешил, и на неатомарный доступ и на неадекватный компилятор, что привело и к переходу на IAR и сюда в частности.

У вас недостаточно прав для комментирования.