Каково обычное неопределенное/неопределенное поведение C, с которым вы сталкиваетесь?[закрыто]

StackOverflow https://stackoverflow.com/questions/98340

  •  01-07-2019
  •  | 
  •  

Вопрос

Примером неопределенного поведения в языке C является порядок вычисления аргументов функции.Это может быть слева направо или справа налево, вы просто не знаете.Это повлияет на то, как foo(c++, c) или foo(++c, c) получает оценку.

Какое еще неуказанное поведение может удивить неосведомленного программиста?

Это было полезно?

Решение

Вопрос языкового юриста.Хмкей.

Мой личный топ3:

  1. нарушение строгого правила псевдонимов
  2. нарушение строгого правила псевдонимов
  3. нарушение строгого правила псевдонимов

    :-)

Редактировать Вот небольшой пример, в котором это делается дважды неправильно:

(предположим, 32-битные целые числа с прямым порядком байтов)

float funky_float_abs (float a)
{
  unsigned int temp = *(unsigned int *)&a;
  temp &= 0x7fffffff;
  return *(float *)&temp;
}

Этот код пытается получить абсолютное значение числа с плавающей запятой путем изменения бита знака непосредственно в представлении числа с плавающей запятой.

Однако результат создания указателя на объект путем приведения из одного типа в другой недействителен C.Компилятор может предположить, что указатели на разные типы не указывают на один и тот же участок памяти.Это справедливо для всех типов указателей, кроме void* и char* (знак не имеет значения).

В приведенном выше случае я делаю это дважды.Один раз, чтобы получить int-алиас для числа с плавающей запятой a, и один раз, чтобы преобразовать значение обратно в число с плавающей запятой.

Есть три действенных способа сделать то же самое.

Во время приведения используйте указатель char или void.Они всегда ссылаются на что-либо, поэтому они безопасны.

float funky_float_abs (float a)
{
  float temp_float = a;
  // valid, because it's a char pointer. These are special.
  unsigned char * temp = (unsigned char *)&temp_float;
  temp[3] &= 0x7f;
  return temp_float;
}

Используйте мемкопию.Memcpy принимает недействительные указатели, поэтому он также принудительно применяет псевдонимы.

float funky_float_abs (float a)
{
  int i;
  float result;
  memcpy (&i, &a, sizeof (int));
  i &= 0x7fffffff;
  memcpy (&result, &i, sizeof (int));
  return result;
}

Третий действительный способ:используйте союзы.Это явно не неопределенно с C99:

float funky_float_abs (float a)
{
  union 
  {
     unsigned int i;
     float f;
  } cast_helper;

  cast_helper.f = a;
  cast_helper.i &= 0x7fffffff;
  return cast_helper.f;
}

Другие советы

Мое личное любимое неопределенное поведение заключается в том, что если непустой исходный файл не заканчивается новой строкой, поведение не определено.

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

Итак, что касается реальных проблем переносимости (которые в основном зависят от реализации, а не от неопределенных или неопределенных, но я думаю, что это соответствует духу вопроса):

  • char не обязательно (не)подписан.
  • int может быть любого размера от 16 бит.
  • числа с плавающей запятой не обязательно имеют формат или соответствие IEEE.
  • Целочисленные типы не обязательно являются дополнением до двух, а переполнение целочисленной арифметики приводит к неопределенному поведению (современное оборудование не дает сбоя, но некоторые оптимизации компилятора приведут к поведению, отличному от переноса, даже если это то, что делает оборудование.Например if (x+1 < x) может быть оптимизировано, как всегда ложно, когда x подписал тип:видеть -fstrict-overflow вариант в GCC).
  • "/", "." и «..» в #include не имеет определенного значения и может быть по -разному обращаться разными компиляторами (это действительно меняется, и если он пойдет не так, это разрушит ваш день).

Действительно серьезные, которые могут удивить даже на той платформе, на которой вы разрабатывали, потому что поведение неопределенно/не указано лишь частично:

  • Потоки POSIX и модель памяти ANSI.Параллельный доступ к памяти не так хорошо определен, как думают новички.летучий не делает то, что думают новички.Порядок доступа к памяти не так четко определен, как думают новички.Доступы может перемещаться через барьеры памяти в определенных направлениях.Согласованность кэша памяти не требуется.

  • Профилирование кода не так просто, как вы думаете.Если ваш тестовый цикл не оказывает никакого эффекта, компилятор может удалить его часть или весь.inline не имеет определенного эффекта.

И, как мне кажется, Нильс мимоходом упомянул:

  • НАРУШЕНИЕ СТРОГО ПРАВИЛА Псевдонимов.

Деление чего-либо указателем на что-либо.Просто почему-то не компилируется...:-)

result = x/*y;

Мой любимый это:

// what does this do?
x = x++;

Отвечая на некоторые комментарии, согласно стандарту это неопределенное поведение.Увидев это, компилятору разрешено делать что угодно, вплоть до форматирования вашего жесткого диска.См. например этот комментарий здесь.Дело не в том, что вы можете увидеть возможное разумное ожидание определенного поведения.Из-за стандарта C++ и способа определения точек последовательности эта строка кода фактически имеет неопределенное поведение.

Например, если бы у нас был x = 1 перед строкой выше, каким будет действительный результат после этого?Кто-то заметил, что так и должно быть

x увеличивается на 1

поэтому после этого мы должны увидеть x == 2.Однако на самом деле это не так: впоследствии вы встретите некоторые компиляторы, у которых x == 1 или, может быть, даже x == 3.Вам придется внимательно посмотреть на сгенерированную сборку, чтобы понять, почему это может быть, но различия связаны с основной проблемой.По сути, я думаю, это связано с тем, что компилятору разрешено оценивать два оператора присваивания в любом порядке, который ему нравится, поэтому он может выполнить x++ сначала, или x = первый.

Еще одна проблема, с которой я столкнулся (определенная, но определенно неожиданная).

Чар это зло.

  • со знаком или без знака в зависимости от того, что чувствует компилятор
  • нет обязательно как 8 бит

Я не могу сосчитать, сколько раз я исправлял спецификаторы формата printf, чтобы они соответствовали их аргументам. Любое несоответствие является неопределенным поведением..

  • Нет, вы не должны проходить int (или long) к %x - unsigned int требуется
  • Нет, вы не должны проходить unsigned int к %d - int требуется
  • Нет, вы не должны проходить size_t к %u или %d - использовать %zu
  • Нет, вы не должны печатать указатель с помощью %d или %x - использовать %p и отбросить к void *

Компилятору не обязательно сообщать вам, что вы вызываете функцию с неправильным количеством параметров/неправильными типами параметров, если прототип функции недоступен.

Я видел много относительно неопытных программистов, которых укусили многосимвольные константы.

Этот:

"x"

является строковым литералом (типа char[2] и распадается на char* в большинстве контекстов).

Этот:

'x'

— обычная символьная константа (которая по историческим причинам имеет тип int).

Этот:

'xy'

также является совершенно допустимой символьной константой, но ее значение (которое по-прежнему имеет тип int) определяется реализацией.Это почти бесполезная языковая функция, которая в основном вызывает путаницу.

Разработчики clang опубликовали некоторые отличные примеры некоторое время назад в посте, который должен прочитать каждый программист на языке C.Некоторые интересные, не упомянутые ранее:

  • Переполнение целого числа со знаком - нет, нельзя переносить знаковую переменную за пределы максимального значения.
  • Разыменование NULL-указателя — да, это не определено и может быть проигнорировано, см. часть 2 ссылки.

EE только что обнаружил, что a>>-2 немного чревато.

Я кивнул и сказал им, что это неестественно.

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

Использование макроверсий функций, таких как «max» или «isupper».Макросы оценивают свои аргументы дважды, поэтому вы получите неожиданные побочные эффекты при вызове max(++i, j) или isupper(*p++)

Вышеупомянутое относится к стандарту C.В C++ эти проблемы практически исчезли.Функция max теперь является шаблонной функцией.

забыл добавить static float foo(); в заголовочном файле только для того, чтобы вызвать исключения с плавающей запятой, когда он вернет 0.0f;

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top