#include <stdio.h>
int main(void)
{
int i = 0;
i = i++ + ++i;
printf("%d\n", i); // 3
i = 1;
i = (i++);
printf("%d\n", i); // 2 Should be 1, no ?
volatile int u = 0;
u = u++ + ++u;
printf("%d\n", u); // 1
u = 1;
u = (u++);
printf("%d\n", u); // 2 Should also be one, no ?
register int v = 0;
v = v++ + ++v;
printf("%d\n", v); // 3 (Should be the same as u ?)
int w = 0;
printf("%d %d\n", ++w, w); // shouldn't this print 1 1
int x[2] = { 5, 8 }, y = 0;
x[y] = y ++;
printf("%d %d\n", x[0], x[1]); // shouldn't this print 0 8? or 5 0?
}
815
(i++)
по-прежнему оценивается в 1, независимо от скобокi = (i++);
было предназначено, безусловно, есть более ясный способ написать это. Это было бы правдой, даже если бы оно было четко определено. Даже в Java, которая определяет поведениеi = (i++);
, это все еще плохой код. Просто напишитеi++;
Ответы:
C имеет концепцию неопределенного поведения, то есть некоторые языковые конструкции синтаксически допустимы, но вы не можете предсказать поведение при запуске кода.
Насколько я знаю, в стандарте прямо не говорится, почему существует понятие неопределенного поведения. На мой взгляд, это просто потому, что разработчики языка хотели, чтобы в семантике была некоторая свобода, вместо того, чтобы требовать, чтобы все реализации обрабатывали целочисленное переполнение точно таким же образом, что, скорее всего, повлекло бы за собой серьезные потери производительности, они просто оставили поведение не определено, так что если вы напишите код, который вызывает целочисленное переполнение, все может произойти.
Итак, с учетом этого, почему эти "проблемы"? Язык ясно говорит, что определенные вещи приводят к неопределенному поведению . Нет проблем, нет «надо» участвовать. Если неопределенное поведение изменяется при объявлении одной из задействованных переменных
volatile
, это ничего не доказывает и не меняет. Это не определено ; Вы не можете рассуждать о поведении.Ваш самый интересный пример, с
пример учебника из неопределенного поведения (см. статью Википедии о точках последовательности ).
источник
i = ++i + 1;
.Просто скомпилируйте и разберите вашу строку кода, если вы так хотите узнать, как именно вы получаете то, что получаете.
Вот что я получаю на своей машине вместе с тем, что я думаю:
(Я ... полагаю, что инструкция 0x00000014 была своего рода оптимизацией компилятора?)
источник
gcc evil.c -c -o evil.bin
иgdb evil.bin
→disassemble evil
, или каковы бы ни были их эквиваленты в Windows :)Why are these constructs undefined behavior?
.gcc -S evil.c
), что все, что здесь необходимо. Сборка и разборка это всего лишь окольный путь.Я думаю, что соответствующие части стандарта C99 - 6,5 выражений, §2
и 6.5.16 Операторы присваивания, §4:
источник
i=i=5
, поведениеA=B=5;
: «запись-блокировка A; запись-блокировка B; сохранение 5 в A; сохранение 5 в B; разблокировка B» ; Разблокировать A; "и оператор, такойC=A+B;
как" Блокировка чтения A; Блокировка чтения B; Вычислить A + B; Разблокировать A и B; Блокировка записи C; Сохранить результат; Разблокировать C; ". Это гарантировало бы, что если один поток сделал, вA=B=5;
то время как другой сделал,C=A+B;
последний поток либо видел бы обе записи как выполненные, либо ни один из них. Потенциально полезная гарантия. Если бы одна нить сделалаI=I=5;
, однако, ...Большинство ответов здесь процитированы из стандарта C, подчеркивая, что поведение этих конструкций не определено. Чтобы понять, почему поведение этих конструкций не определено , давайте сначала разберемся в этих терминах в свете стандарта C11:
Последовательность: (5.1.2.3)
Unsequenced:
Оценки могут быть одной из двух вещей:
Точка последовательности:
Теперь перейдем к вопросу, для выражений, как
Стандарт говорит, что:
6.5 Выражения:
Следовательно, вышеприведенное выражение вызывает UB, потому что два побочных эффекта для одного и того же объекта
i
не секвенированы относительно друг друга. Это означает, что не определено, будет ли побочный эффект путем присвоенияi
выполняться до или после побочного эффекта++
.В зависимости от того, происходит ли присвоение до или после приращения, будут получены разные результаты, и это один из случаев неопределенного поведения .
Позволяет переименовать
i
слева от назначения бытьil
и справа от назначения (в выраженииi++
) бытьir
, тогда выражение будет какВажным моментом в отношении
++
оператора Postfix является то, что:Это означает, что выражение
il = ir++
может быть оценено какили
что приводит к двум различным результатам
1
и2
зависит от последовательности побочных эффектов при назначении++
и, следовательно, вызывает UB.источник
Поведение не может быть действительно объясняется тем , что он вызывает как неопределенное поведение и неопределенное поведение , поэтому мы не можем делать какие - либо общие прогнозы относительно этого кода, хотя , если вы читаете Olve Maudal в работе , таких как Deep C и неконкретные и Undefined иногда вы можете сделать хорошее угадывает в очень специфических случаях с определенным компилятором и средой, но, пожалуйста, не делайте этого где-нибудь рядом с производством.
Таким образом, переходя к неопределенному поведению , в черновике стандартного раздела c99
6.5
параграф 3 ( выделено мной ):Итак, когда у нас есть такая строка:
мы не знаем, будет ли
i++
или++i
будет оцениваться в первую очередь. Это главным образом, чтобы дать компилятору лучшие варианты для оптимизации .У нас также есть неопределенное поведение здесь, а поскольку программа изменения переменных (
i
,u
и т.д ..) больше , чем один раз между точками последовательности . Из проекта стандартного раздела6.5
параграфа 2 ( выделено мое ):он цитирует следующие примеры кода как неопределенные:
Во всех этих примерах код пытается изменить объект более одного раза в одной и той же точке последовательности, которая заканчивается
;
в каждом из следующих случаев:Неопределенное поведение определяется в проекте стандарта c99 в разделе
3.4.4
как:и неопределенное поведение определяется в разделе
3.4.3
как:и отмечает, что:
источник
Другой способ ответить на это, вместо того, чтобы увязнуть в тайных деталях точек последовательности и неопределенного поведения, - просто спросить, что они должны означать? Что пытался сделать программист?
Первый фрагмент, о котором спрашивают,
i = i++ + ++i
довольно явно безумен в моей книге. Никто никогда не напишет ее в реальной программе, не очевидно, что она делает, нет мыслимого алгоритма, который кто-то мог бы пытаться кодировать, который привел бы к этой конкретной надуманной последовательности операций. И поскольку для нас и для нас не очевидно, что именно он должен делать, в моей книге хорошо, если компилятор не может понять, что он должен делать.Второй фрагмент
i = i++
немного проще для понимания. Кто-то явно пытается увеличить i и присвоить результат обратно i. Но есть несколько способов сделать это в C. Самый простой способ добавить 1 к i и присвоить результат обратно i, одинаков почти для любого языка программирования:С, конечно же, есть удобный ярлык:
Это означает «добавить 1 к i и присвоить результат обратно i». Так что, если мы создадим мешанину из двух, написав
что мы на самом деле говорим: «добавьте 1 к i, присвойте результат обратно i и присвойте результат обратно i». Мы в замешательстве, поэтому меня не слишком беспокоит, если и компилятор тоже запутается.
На самом деле, эти сумасшедшие выражения пишутся только тогда, когда люди используют их как искусственные примеры того, как ++ должен работать. И, конечно, важно понимать, как работает ++. Но одно практическое правило использования ++ таково: «Если неясно, что означает выражение с использованием ++, не пишите его».
Мы привыкли тратить бесчисленные часы на comp.lang.c, обсуждая подобные выражения и почему они не определены. Два моих более длинных ответа, которые пытаются действительно объяснить, почему, заархивированы в Интернете:
Смотрите также вопрос 3.8 и остальные вопросы в разделе 3 из списка C Справка .
источник
*p=(*q)++;
не означает ,if (p!=q) *p=(*q)++; else *p= __ARBITRARY_VALUE;
что это уже не так. Hyper-modern C потребует написания чего-то вроде последней формулировки (хотя нет стандартного способа указать, что коду не важно, что находится внутри*p
), чтобы достичь уровня эффективности, используемого компиляторами для первого (else
пункт необходим для того, чтобы компилятор оптимизирует тот,if
который потребуется некоторым новым компиляторам).assert
операторы, чтобы программист мог предшествовать рассматриваемой строке простымassert(p != q)
. (Конечно, для прохождения этого курса также потребуется переписывание,<assert.h>
чтобы не удалять утверждения напрямую в не отладочных версиях, а вместо этого превращать их в нечто вроде__builtin_assert_disabled()
того, что собственно компилятор может видеть, а затем не генерировать код для него.)Часто этот вопрос связан как дубликат вопросов, связанных с кодом, как
или
или похожие варианты.
Хотя это также неопределенное поведение, как уже говорилось, существуют небольшие различия в
printf()
сравнении с такими утверждениями, как:В следующем заявлении:
порядок оценки аргументов в
printf()
это не определено . Это означает, что выраженияi++
и++i
могут быть оценены в любом порядке. Стандарт C11 имеет некоторые соответствующие описания по этому вопросу:Приложение J, неопределенное поведение
3.4.4, неопределенное поведение
Неопределенные поведение само по себе не является проблемой. Рассмотрим этот пример:
Это также имеет неопределенное поведение, потому что порядок оценки
++x
иy++
не определен. Но это совершенно законное и обоснованное утверждение. Там нет неопределенного поведения в этом утверждении. Потому что изменения (++x
иy++
) сделаны для различных объектов.Что делает следующее утверждение
в качестве неопределенного поведения является тот факт , что эти два выражения изменения же объект
i
без промежуточной точки последовательности .Другая деталь заключается в том, что запятая, используемая в вызове printf (), является разделителем , а не оператором запятой .
Это важное различие, потому что оператор запятой вводит точку последовательности между оценкой их операндов, что делает следующее законным:
Оператор запятой оценивает свои операнды слева направо и выдает только значение последнего операнда. Так
j = (++i, i++);
, с++i
шагомi
до6
иi++
дает старое значениеi
(6
) , который присваиваетсяj
. Тогдаi
становится7
за счет постинкремент.Так что, если запятая в вызове функции должна быть оператором запятой, то
не будет проблемой. Но это вызывает неопределенное поведение, потому что запятая здесь является разделителем .
Для тех, кто плохо знаком с неопределенным поведением, было бы полезно прочитать, что каждый программист C должен знать о неопределенном поведении, чтобы понять концепцию и многие другие варианты неопределенного поведения в C.
Этот пост: Неопределенное, неопределенное и определяемое реализацией поведение также имеет отношение к делу.
источник
int a = 10, b = 20, c = 30; printf("a=%d b=%d c=%d\n", (a = a + b + c), (b = b + b), (c = c + c));
видимому, обеспечивает стабильное поведение (оценка аргумента справа налево в gcc v7.3.0; результат "a = 110 b = 40 c = 60"). Это потому, что назначения рассматриваются как «полные операторы» и, таким образом, вводят точку последовательности? Разве это не должно приводить к оценке аргумента / утверждения слева направо? Или это просто проявление неопределенного поведения?b
иc
в 3-м и 4-м аргументах соответственно и читаете во 2-м аргументе. Но между этими выражениями нет последовательности (2-й, 3-й и 4-й аргументы). У gcc / clang есть опция,-Wsequence-point
которая также может помочь найти их.Хотя маловероятно, что какие-либо компиляторы и процессоры действительно будут это делать, в соответствии со стандартом C было бы законно, чтобы компилятор реализовал «i ++» с последовательностью:
Хотя я не думаю, что какие-либо процессоры поддерживают аппаратное обеспечение, позволяющее эффективно выполнять такие действия, можно легко представить себе ситуации, в которых такое поведение могло бы упростить многопоточный код (например, это гарантировало бы, что если два потока попытаются выполнить описанное выше) Одновременно последовательность
i
будет увеличена на два), и не исключено, что какой-нибудь будущий процессор может обеспечить такую функцию.Если компилятор должен был писать,
i++
как указано выше (законно в соответствии со стандартом), и должен был перемежать вышеприведенные инструкции во время оценки общего выражения (также законно), и если не случилось, заметил, что произошла одна из других инструкций чтобы получить доступi
, компилятор мог бы (и законно) сгенерировать последовательность инструкций, которые бы зашли в тупик. Безусловно, компилятор почти наверняка обнаружит проблему в случаеi
, когда одна и та же переменная используется в обоих местах, но если подпрограмма принимает ссылки на два указателяp
иq
, и использует(*p)
и(*q)
в вышеприведенном выражении (вместо того, чтобы использоватьi
дважды) компилятору не потребуется распознавать или избегать тупиковой ситуации, которая возникнет, если один и тот же адрес объекта будет передан для обоихp
иq
.источник
Хотя синтаксис выражений нравится
a = a++
илиa++ + a++
является законным, то поведение этих конструкций является неопределенным , так как должен в стандарте C не выполняется. C99 6.5p2 :В сноске 73 уточняется, что
Различные точки последовательности перечислены в Приложении C к C11 (и C99 ):
Формулировка того же параграфа в C11 :
Вы можете обнаружить такие ошибки в программе, например, используя последнюю версию GCC с
-Wall
и-Werror
, и тогда GCC полностью откажется компилировать вашу программу. Ниже приведен вывод gcc (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005:Важная часть состоит в том, чтобы знать, что такое точка последовательности - и что такое точка последовательности, а что нет . Например, оператор запятой является точкой последовательности, поэтому
четко определен и будет увеличиваться
i
на единицу, давая старое значение, отбрасывать это значение; затем в оператор запятой, урегулировать побочные эффекты; а затем увеличиваетсяi
на единицу, и результирующее значение становится значением выражения - то есть это просто надуманный способ записи,j = (i += 2)
который снова является «умным» способом записиОднако
,
списки аргументов в функции не являются запятыми, и между оценками различных аргументов нет точки последовательности; вместо этого их оценки не последовательны по отношению друг к другу; поэтому вызов функцииимеет неопределенное поведение, потому что между оценками
i++
и++i
в аргументах функции отсутствует точка последовательности , иi
поэтому значение изменяется дважды, какi++
и++i
между предыдущей, так и следующей точкой последовательности.источник
Стандарт C говорит, что переменная должна назначаться не более одного раза между двумя точками последовательности. Точка с запятой, например, является точкой последовательности.
Итак, каждое утверждение формы:
и так далее нарушать это правило. Стандарт также говорит, что поведение не определено и не определено. Некоторые компиляторы обнаруживают их и дают некоторый результат, но это не соответствует стандартам.
Тем не менее, две разные переменные могут быть увеличены между двумя точками последовательности.
Вышесказанное является обычной практикой кодирования при копировании / анализе строк.
источник
В /programming/29505280/incrementing-array-index-in-c кто-то спросил о таком утверждении, как:
который печатает 7 ... ОП ожидал, что он напечатает 6.
Эти
++i
приращения не гарантируется для всех завершена до остальных расчетов. На самом деле, разные компиляторы будут получать разные результаты здесь. В примере , который вы указали, первый 2++i
выполняется, то значенияk[]
были прочитаны, то последний++i
тогдаk[]
.Современные компиляторы оптимизируют это очень хорошо. На самом деле, возможно, лучше, чем код, который вы изначально написали (при условии, что он работал так, как вы надеялись).
источник
Хорошее объяснение того, что происходит в таких вычислениях, приведено в документе n1188 с сайта ISO W14 .
Я объясняю идеи.
Основное правило из стандарта ISO 9899, которое применяется в этой ситуации, это 6.5p2.
Точки последовательности в выражении, как
i=i++
доi=
и послеi++
.В статье, которую я цитировал выше, объясняется, что вы можете понять, что программа состоит из маленьких прямоугольников, каждый из которых содержит инструкции между двумя последовательными точками последовательности. Точки последовательности определены в приложении C к стандарту, в случае если
i=i++
есть две точки последовательности, которые ограничивают полное выражение. Такое выражение синтаксически эквивалентно записиexpression-statement
в форме грамматики Бэкуса-Наура (грамматика приведена в приложении А к Стандарту).Таким образом, порядок инструкций внутри коробки не имеет четкого порядка.
можно интерпретировать как
или как
поскольку обе эти формы для интерпретации кода
i=i++
являются действительными и поскольку обе выдают разные ответы, поведение не определено.Таким образом, точка последовательности может быть видна в начале и конце каждого блока, составляющего программу [блоки представляют собой атомные единицы в C], а внутри блока порядок инструкций определяется не во всех случаях. Изменяя этот порядок, можно иногда изменить результат.
РЕДАКТИРОВАТЬ:
Другой источник хорошо для объяснения таких неоднозначностей являются записями из с-чаво сайта (также опубликованным в книге ), а именно здесь и здесь и здесь .
источник
i=i++
очень похожи на этот ответ .Ваш вопрос, вероятно, не был: «Почему эти конструкции неопределенного поведения в C?». Ваш вопрос был, вероятно, «Почему этот код (использование
++
) не дал мне ожидаемого значения?», И кто-то отметил ваш вопрос как дубликат и отправил вас сюда.Этот ответ пытается ответить на этот вопрос: почему ваш код не дал ожидаемого вами ответа, и как вы можете научиться распознавать (и избегать) выражения, которые не будут работать должным образом.
Я предполагаю, что вы уже слышали базовое определение C
++
и--
операторов, и чем форма префикса++x
отличается от формы постфиксаx++
. Но об этих операторах трудно думать, поэтому, чтобы убедиться, что вы поняли, возможно, вы написали крошечную тестовую программу, включающую что-то вродеНо, к вашему удивлению, эта программа не помогла вам понять - она напечатала какой-то странный, неожиданный, необъяснимый вывод, предполагая, что, возможно,
++
делает что-то совершенно другое, совсем не то, что вы думали.Или, возможно, вы смотрите на трудное для понимания выражение, как
Возможно, кто-то дал вам этот код в виде головоломки. Этот код также не имеет смысла, особенно если вы запускаете его - и если вы скомпилируете и запустите его под двумя разными компиляторами, вы, вероятно, получите два разных ответа! Что с этим? Какой ответ правильный? (И ответ таков: оба они или нет)
Как вы уже слышали, все эти выражения не определены , что означает, что язык C не дает никаких гарантий относительно того, что они будут делать. Это странный и удивительный результат, потому что вы, вероятно, думали, что любая программа, которую вы могли бы написать, пока она компилируется и запускается, будет генерировать уникальный, четко определенный вывод. Но в случае неопределенного поведения это не так.
Что делает выражение неопределенным? Являются ли выражения вовлеченными
++
и--
всегда неопределенными? Конечно, нет: это полезные операторы, и если вы используете их правильно, они совершенно четко определены.Для выражений, о которых мы говорим, то, что делает их неопределенными, - это когда слишком много происходит одновременно, когда мы не уверены, в каком порядке произойдут события, но когда порядок имеет значение для результата, который мы получаем.
Давайте вернемся к двум примерам, которые я использовал в этом ответе. Когда я написал
вопрос перед вызовом
printf
, компилятор вычисляет значениеx
first, илиx++
, или может быть++x
? Но оказывается, что мы не знаем . В C нет правила, согласно которому аргументы функции оцениваются слева направо, справа налево или в каком-либо другом порядке. Поэтому мы не можем сказать , будет ли компилятор сделатьx
первый, затем++x
, затемx++
, илиx++
потом++x
потомx
, или какой -либо другой порядок. Но порядок явно имеет значение, потому что в зависимости от того, какой порядок использует компилятор, мы получим разные результатыprintf
.Как насчет этого сумасшедшего выражения?
Проблема с этим выражением состоит в том, что оно содержит три различные попытки изменить значение x: (1)
x++
деталь пытается добавить 1 к x, сохранить новое значениеx
и вернуть старое значениеx
; (2)++x
деталь пытается добавить 1 к x, сохранить новое значениеx
и вернуть новое значениеx
; и (3)x =
часть пытается присвоить сумму двух других обратно x. Какое из этих трех попыток будет «выиграно»? Какое из трех значений будет назначеноx
? Опять же, и, возможно, что удивительно, в Си нет правил, чтобы говорить нам.Вы можете себе представить, что приоритет или ассоциативность или оценка слева направо говорит вам, в каком порядке происходят вещи, но это не так. Вы можете мне не верить, но, пожалуйста, поверьте мне на слово, и я скажу это снова: приоритет и ассоциативность не определяют каждый аспект порядка вычисления выражения в C. В частности, если в одном выражении есть несколько различные места, где мы пытаемся присвоить новое значение чему-то вроде
x
приоритета и ассоциативности, не говорят нам, какая из этих попыток происходит первой, последней или какой-либо другой.Итак, со всем этим фоном и введением, если вы хотите убедиться, что все ваши программы четко определены, какие выражения вы можете написать, а какие вы не можете написать?
Эти выражения все в порядке:
Все эти выражения не определены:
И последний вопрос: как определить, какие выражения определены, а какие нет?
Как я сказал ранее, неопределенные выражения - это те, в которых слишком много всего происходит одновременно, где вы не можете быть уверены, в каком порядке происходят вещи и где порядок имеет значение:
Как пример № 1, в выражении
Есть три попытки изменить `x.
Как пример № 2, в выражении
мы оба используем значение
x
и изменяем его.Вот и ответ: убедитесь, что в любом написанном вами выражении каждая переменная изменяется не более одного раза, и если переменная изменяется, вы также не пытаетесь использовать значение этой переменной где-либо еще.
источник
Причина в том, что программа работает с неопределенным поведением. Проблема заключается в порядке оценки, поскольку в соответствии со стандартом C ++ 98 нет точек последовательности, требуемых (никакие операции не выполняются до или после другой в соответствии с терминологией C ++ 11).
Однако, если вы будете придерживаться одного компилятора, вы обнаружите, что поведение будет постоянным, если вы не добавляете вызовы функций или указатели, что сделало бы поведение более беспорядочным.
Итак, сначала GCC: Используя Nuwen MinGW 15 GCC 7.1, вы получите:
}
Как работает GCC? он оценивает подвыражения в порядке слева направо для правой стороны (RHS), затем присваивает значение левой стороне (LHS). Именно так ведут себя Java и C # и определяют свои стандарты. (Да, эквивалентное программное обеспечение на Java и C # имеет определенное поведение). Он оценивает каждое подвыражение одно за другим в выражении RHS в порядке слева направо; для каждого подвыражения: сначала вычисляется ++ c (преинкремент), затем для операции используется значение c, затем постинкремент c ++).
согласно GCC C ++: операторы
эквивалентный код в определенном поведении C ++, как понимает GCC:
Затем мы идем в Visual Studio . Visual Studio 2015, вы получаете:
Как работает Visual Studio, он использует другой подход, он оценивает все выражения перед приращениями на первом проходе, затем использует значения переменных в операциях на втором проходе, присваивает их от RHS к LHS на третьем проходе, затем на последнем проходе он оценивает все выражения после приращения за один проход.
Таким образом, эквивалент в определенном поведении C ++, как понимает Visual C ++:
как указано в документации Visual Studio в порядке приоритета и порядке оценки :
источник