Каково общее неопределенное / неопределенное поведение для C, с которым вы сталкиваетесь? [закрытый]
примером неопределенного поведения в языке C является порядок вычисления аргументов функции. Это может быть слева направо или справа налево, вы просто не знаете. Это повлияет на то, как foo(c++, c)
или foo(++c, c)
получает оценку.
какое еще неопределенное поведение может удивить неосведомленного программиста?
13 ответов:
вопрос юристу языка. Хмкей.
мой личный top3:
- нарушение строгого правила сглаживания
- нарушение строгого правила сглаживания
нарушение строгого правила сглаживания
: -)
Edit вот небольшой пример, который делает это неправильно дважды:
(предположим, 32-битные ints и little endian)
float funky_float_abs (float a) { unsigned int temp = *(unsigned int *)&a; temp &= 0x7fffffff; return *(float *)&temp; }
этот код пытается получить абсолютное значение поплавка путем битового скручивания со знаковым битом непосредственно в представлении поплавка.
однако результат создания указателя на объект путем приведения от одного типа к другому недопустим C. компилятор может предположить, что указатели на разные типы не указывают на один и тот же фрагмент памяти. Это верно для всех видов указателей, кроме void* и char* (знак не имеет значения).
в случае выше, я делаю это дважды. Один раз, чтобы получить int-псевдоним поплавок, и один раз, чтобы преобразовать значение с плавающей точкой.
существует три способа сделать то же самое.
используйте указатель 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; }
использовать 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; }
третий способ: использовать союзы. Это явно не определено с 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 или соответствуют ему.
- целочисленные типы не обязательно дополняют два, а переполнение целочисленной арифметики вызывает неопределенное поведение (современное оборудование не будет сбой, но некоторые оптимизации компилятора приведут к поведению, отличному от wraparound, даже если это то, что делает аппаратное обеспечение. Например
if (x+1 < x)
может быть оптимизирован как всегда false, когда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 =
первый.
еще одна проблема, с которой я столкнулся (которая определена, но определенно неожиданная).
char является злом.
- подписано или без знака в зависимости от того, что чувствует компилятор
- не мандат как 8 бит
компилятор не должен сообщать вам, что вы вызываете функцию с неправильным количеством параметров/неправильными типами параметров, если прототип функции недоступен.
я не могу подсчитать, сколько раз я исправил спецификаторы формата printf, чтобы соответствовать их аргументу. любое несоответствие является неопределенным поведением.
- нет, вы не должны пройти
int
(илиlong
) в%x
- anunsigned int
требуется- нет, вы не должны пройти
unsigned int
до%d
- anint
требуется- нет, вы не должны пройти
size_t
до%u
или%d
- используйте%zu
- нет, вы не должны печать указателя с помощью
%d
или%x
- используйте%p
и вvoid *
Я видел много относительно неопытных программистов, укушенных многосимвольными константами.
это:
"x"
- строковый литерал (который имеет тип
char[2]
и распадается наchar*
в большинстве случаев).это:
'x'
- это обычная символьная константа (которая по историческим причинам относится к типу
int
).это:
'xy'
также является совершенно правовой константой характера, но ее значение (которое есть еще типа
int
) определяется реализацией. Это почти бесполезная языковая функция, которая служит в основном для того, чтобы вызвать путаницу.
разработчики clang опубликовали некоторые великие примеры некоторое время назад, в посте каждый программист C должен читать. Некоторые интересные из них не упоминались ранее:
- переполнение целого числа со знаком-нет, это не нормально, чтобы обернуть знаковую переменную мимо ее макс.
- разыменование нулевого указателя-да, это не определено и может быть проигнорировано, см. Часть 2 ссылки.
обязательно инициализируйте переменные перед их использованием! Когда я только начал с C, это вызвало у меня ряд головных болей.
использование макрос версий функций, таких как" max "или"isupper". Макросы оценивают свои аргументы дважды, поэтому вы получите неожиданные побочные эффекты при вызове max (++i, j) или isupper (*p++)
выше для стандарта C. В C++ эти проблемы в значительной степени исчезли. Функция max теперь является шаблонной функцией.