Какое общее поведение undefined/неуказанное для C, с которым вы столкнулись?

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

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

Ответ 1

Вопрос о адвокатуре языка. Hmkay.

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

  • нарушение правила строгого псевдонира
  • нарушение правила строгого псевдонира
  • нарушающее правило строгого сглаживания

    : -)

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

(предположим, что 32-битные int и маленькие endian)

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

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

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

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

Существует три допустимых способа сделать то же самое.

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

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

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

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

Третий правильный способ: использовать союзы. Это явно не undefined, поскольку 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;
}

Ответ 2

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

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

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

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

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

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

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

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

  • НАРУШЕНИЕ ПРАВИЛА СТРОГО ИСКАЖЕНИЯ.

Ответ 3

Разделяя что-то указателем на что-то. Просто не будет компилироваться по какой-то причине...: -)

result = x/*y;

Ответ 4

Мое любимое:

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

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

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

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

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

Ответ 5

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

char - зло.

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

Ответ 6

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

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

Ответ 7

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

Ответ 8

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

Это:

"x"

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

Это:

'x'

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

Это:

'xy'

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

Ответ 9

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

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

Ответ 10

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

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

Ответ 11

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

Ответ 12

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

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

Ответ 13

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