Это неопределенное поведение C? - PullRequest
35 голосов
/ 10 августа 2010

Наш класс задал этот вопрос профессору C программирования:

Вам дан код:

int x=1;
printf("%d",++x,x+1);

Какую продукцию он будет производить всегда?

Большинство студентов сказали неопределенное поведение. Может кто-нибудь помочь мне понять, почему это так?

Спасибо за редактирование и ответы, но я все еще в замешательстве.

Ответы [ 8 ]

40 голосов
/ 10 августа 2010

Выходное значение , вероятно, равно 2 в каждом разумном случае. В действительности то, что у вас есть , это неопределенное поведение.

В частности, стандарт гласит:

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

Существует точка последовательности до оценки аргументов функции, и точка последовательности после все аргументы были оценены (но функция еще не вызвана). Между этими двумя (т. Е. Во время оценки аргументов) существует , а не точка последовательности (если аргумент не является выражением, включает внутреннюю точку, например, с использованием && || или , оператор).

Это означает, что вызов printf читает предыдущее значение и , чтобы определить сохраняемое значение (т. Е. ++x) и , чтобы определить значение Второй аргумент (т. е. x+1). Это явно нарушает указанное выше требование, что приводит к неопределенному поведению.

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

14 голосов
/ 10 августа 2010

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

Аргументы функции концептуально оцениваются параллельно (технический термин заключается в том, что между их оценкой нет последовательности).Это означает, что выражения ++x и x+1 могут оцениваться в этом порядке, в обратном порядке, или каким-либо чередующимся образом .Когда вы изменяете переменную и пытаетесь получить доступ к ее значению параллельно, поведение не определено.

Во многих реализациях аргументы оцениваются в последовательности (хотя не всегда слева направо).Таким образом, вы вряд ли увидите в реальном мире ничего, кроме 2.

Однако компилятор может сгенерировать такой код:

  1. Загрузить x в регистр r1.
  2. Вычислить x+1, добавив 1 к r1.
  3. Вычислить ++x, добавив 1 к r1.Это нормально, потому что x был загружен в r1.Учитывая то, как был разработан компилятор, шаг 2 не может изменить r1, потому что это может произойти, только если x прочитано так же, как записано между двумя точками последовательности.Что запрещено стандартом C.
  4. Сохранить r1 в x.

И на этом (гипотетическом, но правильном) компиляторе программа напечатает 3.

( РЕДАКТИРОВАТЬ: передача дополнительного аргумента в printf правильно (§7.19.6.1-2 в N1256 ; благодаря Prasoon Saurav ) для указания на это. Также: добавлен пример.)

11 голосов
/ 10 августа 2010

Правильный ответ: код вызывает неопределенное поведение.

Причина, по которой поведение не определено, состоит в том, что два выражения ++x и x + 1 модифицируют x и читают x по несвязанной (для модификации) причине, и эти два действия не разделены точкой последовательности , Это приводит к неопределенному поведению в C (и C ++). Требование приведено в 6.5 / 2 стандарта языка C.

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

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

int inc_x(int *x) { return ++*x; }
int x_plus_1(int x) { return x + 1; }

int x = 1;
printf("%d", inc_x(&x), x_plus_1(x));

Приведенный выше код «эквивалентен» исходному, за исключением того, что операции, в которых задействован наш x, заключены в функции. Что произойдет в этом последнем примере?

В этом коде нет неопределенного поведения. Но поскольку порядок вычисления printf аргументов равен неопределен , этот код создает неопределенное поведение , т. Е. Возможно, что printf будет называться printf("%d", 2, 2) или printf("%d", 2, 3). В обоих случаях результат действительно будет 2. Однако важным отличием этого варианта является то, что все обращения к x заключены в точки последовательности, присутствующие в начале и в конце каждой функции, поэтому этот вариант не вызывает неопределенного поведения.

Именно поэтому некоторые другие постеры пытаются навязать исходный пример. Но это не может быть сделано. Исходный пример показывает поведение undefined , которое является совершенно другим зверем. Они, очевидно, пытаются настаивать на том, что на практике неопределенное поведение всегда эквивалентно неопределенному поведению. Это абсолютно фиктивное утверждение, которое только указывает на отсутствие опыта у тех, кто его делает. Исходный код создает неопределенное поведение, точка.

Чтобы продолжить пример, давайте изменим предыдущий пример кода на

printf("%d %d", inc_x(&x), x_plus_1(x));

вывод кода станет в целом непредсказуемым. Может печатать 2 2 или печатать 2 3. Однако обратите внимание, что, хотя поведение непредсказуемо, оно по-прежнему не приводит к неопределенному поведению 1045 *. Поведение: не указано , бит не не определено . Неуказанное поведение ограничено двумя вариантами: 2 2 или 2 3. Неопределенное поведение не ограничено ничем. Он может отформатировать ваш жесткий диск вместо печати чего-либо. Почувствуй разницу.

2 голосов
/ 10 августа 2010

Какой выход он будет производить всегда?

Это даст 2 во всех средах, которые я могу придумать. Строгая интерпретация стандарта C99, однако, делает поведение неопределенным, поскольку доступ к x не соответствует требованиям, существующим между точками последовательности.

Большинство студентов сказали неопределенное поведение. Может кто-нибудь помочь мне понять, почему это это так?

Теперь я отвечу на второй вопрос, который я понимаю как «Почему большинство учеников моего класса говорят, что показанный код представляет собой неопределенное поведение?» и я думаю, что ни один другой постер не ответил до сих пор. Одна часть студентов будет помнить примеры неопределенных значений выражений, таких как

f(++i,i)

Код, который вы даете, соответствует этому шаблону, но студенты ошибочно думают, что поведение определено так или иначе, потому что printf игнорирует последний параметр. Этот нюанс смущает многих учеников. Другая часть ученика будет так же хорошо знакома со стандартом, как и Дэвид Торнли, и скажет «неопределенное поведение» по правильным причинам, объясненным выше.

2 голосов
/ 10 августа 2010

Большинство студентов сказали неопределенное поведение.Может кто-нибудь помочь мне понять, почему это так?

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

1 голос
/ 10 августа 2010

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

0 голосов
/ 11 августа 2010

Повторяющий кодаддикт ответ 2.

printf будет вызываться с аргументом 2 и печатать его.

Если этот код помещен в такой контекст, как:

void do_something()
{
    int x=1;
    printf("%d",++x,x+1);
}

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

0 голосов
/ 10 августа 2010

Выход будет всегда (для 99,98% наиболее важных совместимых со стандартами компиляторов и систем) 2.

В соответствии со стандартом это выглядит так: по определению , "неопределенное поведение ", самооправдание определения / ответа, которое ничего не говорит о том, что на самом деле может произойти, и особенно почему .

утилита шина (котораяне является инструментом проверки соответствия стандарту std), и поэтому программисты splint считают это «неопределенным поведением».В основном это означает, что оценка (x+1) может дать 1 + 1 или 2 + 1, в зависимости от того, когда обновление x действительно выполнено.Поскольку, однако, выражение отбрасывается (формат printf читает 1 аргумент), результат не изменяется, и мы все еще можем сказать, что он равен 2.

undefined.c: 7: 20: Аргумент 2 изменяет x,используется аргументом 3 (порядок вычисления фактических параметров не определен): printf ("% d \ n", ++ x, x + 1) Код имеет неопределенное поведение.Порядок вычисления параметров функции или подвыражений не определен, поэтому, если значение используется и изменяется в разных местах, не разделенных порядком оценки, ограничивающим точку последовательности, то результат выражения не определен.

Как уже говорилось, неуказанное поведение влияет только на оценку (x+1), а не на весь оператор или другие его выражения.Таким образом, в случае «неопределенного поведения» мы можем сказать, что вывод равен 2, и никто не может возражать.

Но это не неопределенное поведение, это, кажется, «неопределенное поведение».И «неопределенное поведение», похоже, должно быть чем-то, что влияет на весь оператор, а не на одно выражение.Это связано с загадкой, вокруг которой фактически происходит «неопределенное поведение» (то есть, что именно влияет).

Если будут мотивации для присоединения «неопределенного поведения» только к (x+1) выражение, как в случае «неопределенного поведения», тогда мы все еще можем сказать, что вывод всегда (100%) 2. Присоединение «неопределенного поведения» только к (x+1) означает, что мы не можем сказать, если оно1 + 1 или 2 + 1;это просто " что-нибудь ".Но опять же, это «что-нибудь» отбрасывается из-за printf, и это означает, что ответ будет «всегда (100%) 2».

Вместо этого из-за таинственной асимметрии «неопределенное поведение» может присоединено только к x+1, но на самом деле должно повлиять как минимум на ++x (что, кстати, является причиной неопределенного поведения), если нетвсе утверждение.Если он заражает только выражение ++x, выводом является «неопределенное значение», т. Е. Любое целое число, например -5847834 или 9032. Если оно заражает весь оператор, то вы можете увидеть gargabe в выводе консоли, вероятно, вы могли бы иметьчтобы остановить программу с помощью ctrl-c, возможно, до того, как она начнет душить ваш процессор.

Согласно городской легенде, «неопределенное поведение» поражает не только всю программу, но также ваш компьютер и законыфизику, чтобы таинственные существа могли быть созданы вашей программой и улетели или съели вас.

Нет ответов, что-то компетентно объясняет по этой теме.Они просто «о, видите, стандарт говорит об этом» (и это просто интерпретация, как обычно!).Итак, по крайней мере, вы узнали, что «существуют стандарты», и они ставят и образовательные вопросы (поскольку, конечно, не забывайте, что ваш код неправильный , независимо от неопределенного / неуказанного поведения и других стандартных фактов)Бесполезные логические аргументы и бесцельные глубокие исследования и понимание.

...