Почему эти конструкции используют неопределенное поведение до и после приращения?

815
#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?
}
PiX
источник
12
@Jarett, нет, просто нужны были указатели на «точки последовательности». Работая, я нашел фрагмент кода с i = i ++, но я подумал: «Это не изменяет значение i». Я проверял и задавался вопросом, почему. С тех пор я удалил этот статус и заменил его на i ++;
PiX
198
Я думаю, что интересно, что все ВСЕГДА предполагают, что такие вопросы задаются, потому что спрашивающий хочет ИСПОЛЬЗОВАТЬ рассматриваемую конструкцию. Мое первое предположение состояло в том, что PiX знает, что это плохо, но любопытно, почему они ведут себя так, как на том компиляторе, который он / она использовал ... И да, что сказал unwind ... он не определен, он может делать все что угодно. .. в том числе JCF (Прыгать и
загораться
32
Мне любопытно: почему компиляторы не предупреждают о таких конструкциях, как "u = u ++ + ++ u;" если результат не определен?
Изучите OpenGL ES
5
(i++)по-прежнему оценивается в 1, независимо от скобок
Дрю Макгоуэн
2
Что бы ни i = (i++);было предназначено, безусловно, есть более ясный способ написать это. Это было бы правдой, даже если бы оно было четко определено. Даже в Java, которая определяет поведение i = (i++);, это все еще плохой код. Просто напишитеi++;
Кит Томпсон

Ответы:

566

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

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

Итак, с учетом этого, почему эти "проблемы"? Язык ясно говорит, что определенные вещи приводят к неопределенному поведению . Нет проблем, нет «надо» участвовать. Если неопределенное поведение изменяется при объявлении одной из задействованных переменных volatile, это ничего не доказывает и не меняет. Это не определено ; Вы не можете рассуждать о поведении.

Ваш самый интересный пример, с

u = (u++);

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

размотать
источник
8
@PiX: вещи не определены по ряду возможных причин. К ним относятся: не существует четкого «правильного результата», разные архитектуры машин будут сильно благоприятствовать различным результатам, существующая практика не соответствует или выходит за рамки стандарта (например, какие имена файлов допустимы).
Ричард
Просто чтобы запутать всех, некоторые такие примеры теперь четко определены в C11, например i = ++i + 1;.
ММ
2
Читая стандарт и опубликованное обоснование, становится понятно, почему существует концепция UB. Стандарт никогда не предназначался для полного описания всего, что должна делать реализация C, чтобы она подходила для какой-либо конкретной цели (см. Обсуждение правила «Одна программа»), но вместо этого полагается на мнение разработчиков и желание производить полезные реализации качества. Качественная реализация, подходящая для низкоуровневого системного программирования, должна определять поведение действий, которые не потребуются в высокопроизводительных числах crunching.applications. Вместо того, чтобы пытаться усложнить Стандарт ...
суперкат
3
... вдаваясь в подробности о том, какие угловые случаи определены или не определены, авторы Стандарта признали, что разработчикам следует лучше разбираться в том, какие типы поведения потребуются для тех программ, которые они должны поддерживать , Гипермодернистские компиляторы делают вид, что выполнение определенных действий UB подразумевало, что ни одна качественная программа не нуждается в них, но Стандарт и обоснование несовместимы с таким предполагаемым намерением.
суперкат
1
@jrh: я написал этот ответ, прежде чем понял, как вышла из-под контроля гипер-модернистская философия. Что меня раздражает, так это переход от «Нам не нужно официально распознавать это поведение, потому что платформы, где это необходимо, может поддерживать его в любом случае», - «Мы можем удалить это поведение, не предоставляя замену, пригодную для использования, потому что оно никогда не распознавалось и, следовательно, любой код нуждающийся в этом был сломан ". Многие виды поведения давно должны были устареть в пользу замен, которые были во всех отношениях лучше , но для этого требовалось бы признать их законность.
Суперкат
78

Просто скомпилируйте и разберите вашу строку кода, если вы так хотите узнать, как именно вы получаете то, что получаете.

Вот что я получаю на своей машине вместе с тем, что я думаю:

$ cat evil.c
void evil(){
  int i = 0;
  i+= i++ + ++i;
}
$ gcc evil.c -c -o evil.bin
$ gdb evil.bin
(gdb) disassemble evil
Dump of assembler code for function evil:
   0x00000000 <+0>:   push   %ebp
   0x00000001 <+1>:   mov    %esp,%ebp
   0x00000003 <+3>:   sub    $0x10,%esp
   0x00000006 <+6>:   movl   $0x0,-0x4(%ebp)  // i = 0   i = 0
   0x0000000d <+13>:  addl   $0x1,-0x4(%ebp)  // i++     i = 1
   0x00000011 <+17>:  mov    -0x4(%ebp),%eax  // j = i   i = 1  j = 1
   0x00000014 <+20>:  add    %eax,%eax        // j += j  i = 1  j = 2
   0x00000016 <+22>:  add    %eax,-0x4(%ebp)  // i += j  i = 3
   0x00000019 <+25>:  addl   $0x1,-0x4(%ebp)  // i++     i = 4
   0x0000001d <+29>:  leave  
   0x0000001e <+30>:  ret
End of assembler dump.

(Я ... полагаю, что инструкция 0x00000014 была своего рода оптимизацией компилятора?)

badp
источник
как я могу получить машинный код? Я использую Dev C ++, и я
поэкспериментировал с
5
@ronnieaka gcc evil.c -c -o evil.binи gdb evil.bindisassemble evil, или каковы бы ни были их эквиваленты в Windows :)
badp
21
Этот ответ на самом деле не относится к вопросу о Why are these constructs undefined behavior?.
Шафик Ягмур
9
Кроме того, будет проще скомпилировать в сборку (с gcc -S evil.c), что все, что здесь необходимо. Сборка и разборка это всего лишь окольный путь.
Кат
50
Для справки: если по какой-то причине вы задаетесь вопросом, что делает данная конструкция - и особенно если есть подозрение, что это может быть неопределенное поведение - извечный совет «просто попробуйте с вашим компилятором и посмотрите»: потенциально довольно опасно. В лучшем случае вы узнаете, что он делает в этой версии вашего компилятора, в этих условиях сегодня . Вы мало что узнаете о том, что он гарантированно сделает. В общем, «просто попробуйте с вашим компилятором» приводит к непортативным программам, которые работают только с вашим компилятором.
Стив Саммит
64

Я думаю, что соответствующие части стандарта C99 - 6,5 выражений, §2

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

и 6.5.16 Операторы присваивания, §4:

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

Christoph
источник
2
Означает ли вышесказанное, что «i = i = 5;» будет неопределенным поведением?
суперкат
1
@ Насколько мне известно i=i=5, поведение
@supercat
2
@Zaibis: логическое обоснование, которое я люблю использовать для большинства мест, заключается в том, что теоретически многопроцессорная платформа может реализовывать что-то вроде 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;, однако, ...
суперкат
1
... и компилятор не заметил, что обе записи были в одном и том же месте (если одно или оба значения содержат указатели, что может быть трудно определить), сгенерированный код может зайти в тупик. Я не думаю, что какие-либо реальные реализации реализуют такую ​​блокировку как часть их нормального поведения, но это было бы допустимо в соответствии со стандартом, и если бы аппаратное обеспечение могло реализовывать такое поведение дешево, это могло бы быть полезно. На современном оборудовании такое поведение было бы слишком дорого для реализации по умолчанию, но это не значит, что так будет всегда.
суперкат
1
@supercat, но не будет ли достаточно правила доступа к точке последовательности c99, чтобы объявить его неопределенным поведением? Так что не имеет значения, что технически может реализовать аппаратное обеспечение?
Дэйн
55

Большинство ответов здесь процитированы из стандарта C, подчеркивая, что поведение этих конструкций не определено. Чтобы понять, почему поведение этих конструкций не определено , давайте сначала разберемся в этих терминах в свете стандарта C11:

Последовательность: (5.1.2.3)

С учетом любых двух оценок Aи B, если они Aупорядочены ранее B, выполнение Aдолжно предшествовать выполнению B.

Unsequenced:

Если Aне упорядочено до или после B, то Aи Bне упорядочено.

Оценки могут быть одной из двух вещей:

  • вычисления значений , которые вырабатывают результат выражения; а также
  • побочные эффекты , которые представляют собой модификации объектов.

Точка последовательности:

Наличие точки последовательности между оценкой выражений Aи Bподразумевает, что каждое вычисление значения и связанный с ним побочный эффектA секвенируется перед каждым вычислением значения и побочным эффектом, связанным с B.

Теперь перейдем к вопросу, для выражений, как

int i = 1;
i = i++;

Стандарт говорит, что:

6.5 Выражения:

Если побочный эффект от скалярного объекта unsequenced по отношению к любой другой побочный эффект на тот же объект скалярного или вычисления значения с использованием значения одного и того же скалярного объекта, его поведение не определено . [...]

Следовательно, вышеприведенное выражение вызывает UB, потому что два побочных эффекта для одного и того же объекта iне секвенированы относительно друг друга. Это означает, что не определено, будет ли побочный эффект путем присвоения iвыполняться до или после побочного эффекта ++.
В зависимости от того, происходит ли присвоение до или после приращения, будут получены разные результаты, и это один из случаев неопределенного поведения .

Позволяет переименовать iслева от назначения быть ilи справа от назначения (в выражении i++) быть ir, тогда выражение будет как

il = ir++     // Note that suffix l and r are used for the sake of clarity.
              // Both il and ir represents the same object.  

Важным моментом в отношении ++оператора Postfix является то, что:

просто потому, что ++после переменной идет, не означает, что приращение происходит поздно . Приращение может произойти, как только компилятор захочет, если компилятор гарантирует, что используется исходное значение .

Это означает, что выражение il = ir++может быть оценено как

temp = ir;      // i = 1
ir = ir + 1;    // i = 2   side effect by ++ before assignment
il = temp;      // i = 1   result is 1  

или

temp = ir;      // i = 1
il = temp;      // i = 1   side effect by assignment before ++
ir = ir + 1;    // i = 2   result is 2  

что приводит к двум различным результатам 1и 2зависит от последовательности побочных эффектов при назначении ++и, следовательно, вызывает UB.

haccks
источник
52

Поведение не может быть действительно объясняется тем , что он вызывает как неопределенное поведение и неопределенное поведение , поэтому мы не можем делать какие - либо общие прогнозы относительно этого кода, хотя , если вы читаете Olve Maudal в работе , таких как Deep C и неконкретные и Undefined иногда вы можете сделать хорошее угадывает в очень специфических случаях с определенным компилятором и средой, но, пожалуйста, не делайте этого где-нибудь рядом с производством.

Таким образом, переходя к неопределенному поведению , в черновике стандартного раздела c996.5 параграф 3 ( выделено мной ):

Группировка операторов и операндов указывается синтаксисом.74) За исключением случаев, указанных далее (для операторов function-call (), &&, ||,?: И запятых), порядок вычисления подвыражений и порядок в какие побочные эффекты имеют место, оба не определены.

Итак, когда у нас есть такая строка:

i = i++ + ++i;

мы не знаем, будет ли i++или ++iбудет оцениваться в первую очередь. Это главным образом, чтобы дать компилятору лучшие варианты для оптимизации .

У нас также есть неопределенное поведение здесь, а поскольку программа изменения переменных ( i, uи т.д ..) больше , чем один раз между точками последовательности . Из проекта стандартного раздела 6.5параграфа 2 ( выделено мое ):

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

он цитирует следующие примеры кода как неопределенные:

i = ++i + 1;
a[i++] = i; 

Во всех этих примерах код пытается изменить объект более одного раза в одной и той же точке последовательности, которая заканчивается ;в каждом из следующих случаев:

i = i++ + ++i;
^   ^       ^

i = (i++);
^    ^

u = u++ + ++u;
^   ^       ^

u = (u++);
^    ^

v = v++ + ++v;
^   ^       ^

Неопределенное поведение определяется в проекте стандарта c99 в разделе 3.4.4как:

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

и неопределенное поведение определяется в разделе 3.4.3как:

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

и отмечает, что:

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

Шафик Ягмур
источник
33

Другой способ ответить на это, вместо того, чтобы увязнуть в тайных деталях точек последовательности и неопределенного поведения, - просто спросить, что они должны означать? Что пытался сделать программист?

Первый фрагмент, о котором спрашивают, i = i++ + ++iдовольно явно безумен в моей книге. Никто никогда не напишет ее в реальной программе, не очевидно, что она делает, нет мыслимого алгоритма, который кто-то мог бы пытаться кодировать, который привел бы к этой конкретной надуманной последовательности операций. И поскольку для нас и для нас не очевидно, что именно он должен делать, в моей книге хорошо, если компилятор не может понять, что он должен делать.

Второй фрагмент i = i++немного проще для понимания. Кто-то явно пытается увеличить i и присвоить результат обратно i. Но есть несколько способов сделать это в C. Самый простой способ добавить 1 к i и присвоить результат обратно i, одинаков почти для любого языка программирования:

i = i + 1

С, конечно же, есть удобный ярлык:

i++

Это означает «добавить 1 к i и присвоить результат обратно i». Так что, если мы создадим мешанину из двух, написав

i = i++

что мы на самом деле говорим: «добавьте 1 к i, присвойте результат обратно i и присвойте результат обратно i». Мы в замешательстве, поэтому меня не слишком беспокоит, если и компилятор тоже запутается.

На самом деле, эти сумасшедшие выражения пишутся только тогда, когда люди используют их как искусственные примеры того, как ++ должен работать. И, конечно, важно понимать, как работает ++. Но одно практическое правило использования ++ таково: «Если неясно, что означает выражение с использованием ++, не пишите его».

Мы привыкли тратить бесчисленные часы на comp.lang.c, обсуждая подобные выражения и почему они не определены. Два моих более длинных ответа, которые пытаются действительно объяснить, почему, заархивированы в Интернете:

Смотрите также вопрос 3.8 и остальные вопросы в разделе 3 из списка C Справка .

Стив Саммит
источник
1
Довольно неприятный глюк в отношении неопределенного поведения является то , что в то время как он используется , чтобы быть безопасными на 99,9% компилятор использовать *p=(*q)++;не означает , if (p!=q) *p=(*q)++; else *p= __ARBITRARY_VALUE;что это уже не так. Hyper-modern C потребует написания чего-то вроде последней формулировки (хотя нет стандартного способа указать, что коду не важно, что находится внутри *p), чтобы достичь уровня эффективности, используемого компиляторами для первого ( elseпункт необходим для того, чтобы компилятор оптимизирует тот, ifкоторый потребуется некоторым новым компиляторам).
суперкат
@supercat Теперь я считаю, что любой «достаточно умный» компилятор для выполнения такого рода оптимизации также должен быть достаточно умен, чтобы взглянуть на assertоператоры, чтобы программист мог предшествовать рассматриваемой строке простым assert(p != q). (Конечно, для прохождения этого курса также потребуется переписывание, <assert.h>чтобы не удалять утверждения напрямую в не отладочных версиях, а вместо этого превращать их в нечто вроде __builtin_assert_disabled()того, что собственно компилятор может видеть, а затем не генерировать код для него.)
Стив Саммит,
25

Часто этот вопрос связан как дубликат вопросов, связанных с кодом, как

printf("%d %d\n", i, i++);

или

printf("%d %d\n", ++i, i++);

или похожие варианты.

Хотя это также неопределенное поведение, как уже говорилось, существуют небольшие различия в printf()сравнении с такими утверждениями, как:

x = i++ + i++;

В следующем заявлении:

printf("%d %d\n", ++i, i++);

порядок оценки аргументов в printf()это не определено . Это означает, что выражения i++и ++iмогут быть оценены в любом порядке. Стандарт C11 имеет некоторые соответствующие описания по этому вопросу:

Приложение J, неопределенное поведение

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

3.4.4, неопределенное поведение

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

ПРИМЕР. Примером неуказанного поведения является порядок, в котором оцениваются аргументы функции.

Неопределенные поведение само по себе не является проблемой. Рассмотрим этот пример:

printf("%d %d\n", ++x, y++);

Это также имеет неопределенное поведение, потому что порядок оценки ++xи y++не определен. Но это совершенно законное и обоснованное утверждение. Там нет неопределенного поведения в этом утверждении. Потому что изменения ( ++xи y++) сделаны для различных объектов.

Что делает следующее утверждение

printf("%d %d\n", ++i, i++);

в качестве неопределенного поведения является тот факт , что эти два выражения изменения же объект iбез промежуточной точки последовательности .


Другая деталь заключается в том, что запятая, используемая в вызове printf (), является разделителем , а не оператором запятой .

Это важное различие, потому что оператор запятой вводит точку последовательности между оценкой их операндов, что делает следующее законным:

int i = 5;
int j;

j = (++i, i++);  // No undefined behaviour here because the comma operator 
                 // introduces a sequence point between '++i' and 'i++'

printf("i=%d j=%d\n",i, j); // prints: i=7 j=6

Оператор запятой оценивает свои операнды слева направо и выдает только значение последнего операнда. Так j = (++i, i++);, с ++iшагом iдо 6и i++дает старое значение i( 6) , который присваивается j. Тогда iстановится7 за счет постинкремент.

Так что, если запятая в вызове функции должна быть оператором запятой, то

printf("%d %d\n", ++i, i++);

не будет проблемой. Но это вызывает неопределенное поведение, потому что запятая здесь является разделителем .


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

Этот пост: Неопределенное, неопределенное и определяемое реализацией поведение также имеет отношение к делу.

PP
источник
Эта последовательность, по- 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"). Это потому, что назначения рассматриваются как «полные операторы» и, таким образом, вводят точку последовательности? Разве это не должно приводить к оценке аргумента / утверждения слева направо? Или это просто проявление неопределенного поведения?
kavadias
@kavadias Этот оператор printf использует неопределенное поведение по той же причине, что и описанная выше. Вы пишете bи cв 3-м и 4-м аргументах соответственно и читаете во 2-м аргументе. Но между этими выражениями нет последовательности (2-й, 3-й и 4-й аргументы). У gcc / clang есть опция, -Wsequence-pointкоторая также может помочь найти их.
PP
23

Хотя маловероятно, что какие-либо компиляторы и процессоры действительно будут это делать, в соответствии со стандартом C было бы законно, чтобы компилятор реализовал «i ++» с последовательностью:

In a single operation, read `i` and lock it to prevent access until further notice
Compute (1+read_value)
In a single operation, unlock `i` and store the computed value

Хотя я не думаю, что какие-либо процессоры поддерживают аппаратное обеспечение, позволяющее эффективно выполнять такие действия, можно легко представить себе ситуации, в которых такое поведение могло бы упростить многопоточный код (например, это гарантировало бы, что если два потока попытаются выполнить описанное выше) Одновременно последовательность iбудет увеличена на два), и не исключено, что какой-нибудь будущий процессор может обеспечить такую ​​функцию.

Если компилятор должен был писать, i++как указано выше (законно в соответствии со стандартом), и должен был перемежать вышеприведенные инструкции во время оценки общего выражения (также законно), и если не случилось, заметил, что произошла одна из других инструкций чтобы получить доступ i, компилятор мог бы (и законно) сгенерировать последовательность инструкций, которые бы зашли в тупик. Безусловно, компилятор почти наверняка обнаружит проблему в случае i, когда одна и та же переменная используется в обоих местах, но если подпрограмма принимает ссылки на два указателя pи q, и использует (*p)и(*q) в вышеприведенном выражении (вместо того, чтобы использоватьiдважды) компилятору не потребуется распознавать или избегать тупиковой ситуации, которая возникнет, если один и тот же адрес объекта будет передан для обоих pи q.

Supercat
источник
16

Хотя синтаксис выражений нравится a = a++или a++ + a++является законным, то поведение этих конструкций является неопределенным , так как должен в стандарте C не выполняется. C99 6.5p2 :

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

В сноске 73 уточняется, что

  1. Этот абзац отображает неопределенные выражения оператора, такие как

    i = ++i + 1;
    a[i++] = i;

    позволяя

    i = i + 1;
    a[i] = i;

Различные точки последовательности перечислены в Приложении C к C11C99 ):

  1. Ниже приведены точки последовательности, описанные в 5.1.2.3:

    • Между оценками обозначения функции и фактическими аргументами в вызове функции и фактическим вызовом. (6.5.2.2).
    • Между вычислениями первого и второго операндов используются следующие операторы: логическое И & & (6.5.13); логическое ИЛИ || (6.5.14); запятая, (6.5.17).
    • Между оценками первого операнда условного? : оператор и любой второй и третий операнды оцениваются (6.5.15).
    • Конец полного объявления: заявители (6.7.6);
    • Между оценкой полного выражения и следующим полным выражением, которое будет оценено. Ниже приведены полные выражения: инициализатор, который не является частью составного литерала (6.7.9); выражение в выражении выражения (6.8.3); управляющее выражение оператора выбора (if или switch) (6.8.4); управляющее выражение оператора while или do (6.8.5); каждое из (необязательных) выражений оператора for (6.8.5.3); (необязательное) выражение в операторе возврата (6.8.6.4).
    • Непосредственно перед возвратом библиотечной функции (7.1.4).
    • После действий, связанных с каждым отформатированным спецификатором преобразования функций ввода / вывода (7.21.6, 7.29.2).
    • Непосредственно перед и сразу после каждого вызова функции сравнения, а также между любым вызовом функции сравнения и любым движением объектов, переданных в качестве аргументов для этого вызова (7.22.5).

Формулировка того же параграфа в C11 :

  1. Если побочный эффект на скалярный объект не секвенирован относительно другого побочного эффекта на тот же скалярный объект или вычисления значения с использованием значения того же скалярного объекта, поведение не определено. Если существует несколько допустимых упорядочений подвыражений выражения, поведение не определено, если такой непоследовательный побочный эффект возникает в любом из упорядочений.84)

Вы можете обнаружить такие ошибки в программе, например, используя последнюю версию GCC с -Wallи -Werror, и тогда GCC полностью откажется компилировать вашу программу. Ниже приведен вывод gcc (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005:

% gcc plusplus.c -Wall -Werror -pedantic
plusplus.c: In function main’:
plusplus.c:6:6: error: operation on i may be undefined [-Werror=sequence-point]
    i = i++ + ++i;
    ~~^~~~~~~~~~~
plusplus.c:6:6: error: operation on i may be undefined [-Werror=sequence-point]
plusplus.c:10:6: error: operation on i may be undefined [-Werror=sequence-point]
    i = (i++);
    ~~^~~~~~~
plusplus.c:14:6: error: operation on u may be undefined [-Werror=sequence-point]
    u = u++ + ++u;
    ~~^~~~~~~~~~~
plusplus.c:14:6: error: operation on u may be undefined [-Werror=sequence-point]
plusplus.c:18:6: error: operation on u may be undefined [-Werror=sequence-point]
    u = (u++);
    ~~^~~~~~~
plusplus.c:22:6: error: operation on v may be undefined [-Werror=sequence-point]
    v = v++ + ++v;
    ~~^~~~~~~~~~~
plusplus.c:22:6: error: operation on v may be undefined [-Werror=sequence-point]
cc1: all warnings being treated as errors

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

j = (i ++, ++ i);

четко определен и будет увеличиваться iна единицу, давая старое значение, отбрасывать это значение; затем в оператор запятой, урегулировать побочные эффекты; а затем увеличивается iна единицу, и результирующее значение становится значением выражения - то есть это просто надуманный способ записи, j = (i += 2)который снова является «умным» способом записи

i += 2;
j = i;

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

int i = 0;
printf("%d %d\n", i++, ++i, i);

имеет неопределенное поведение, потому что между оценками i++и ++iв аргументах функции отсутствует точка последовательности , и iпоэтому значение изменяется дважды, как i++и ++iмежду предыдущей, так и следующей точкой последовательности.

Антти Хаапала
источник
14

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

i = i++;
i = i++ + ++i;

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

Тем не менее, две разные переменные могут быть увеличены между двумя точками последовательности.

while(*src++ = *dst++);

Вышесказанное является обычной практикой кодирования при копировании / анализе строк.

Нихил Видхани
источник
Конечно, это не относится к разным переменным в одном выражении. Это было бы полным провалом дизайна, если бы это произошло! Все, что вам нужно во 2-м примере, - это чтобы оба увеличивались между окончанием оператора и следующим началом, и это гарантировано именно благодаря концепции точек последовательности в центре всего этого.
underscore_d
11

В /programming/29505280/incrementing-array-index-in-c кто-то спросил о таком утверждении, как:

int k[] = {0,1,2,3,4,5,6,7,8,9,10};
int i = 0;
int num;
num = k[++i+k[++i]] + k[++i];
printf("%d", num);

который печатает 7 ... ОП ожидал, что он напечатает 6.

Эти ++iприращения не гарантируется для всех завершена до остальных расчетов. На самом деле, разные компиляторы будут получать разные результаты здесь. В примере , который вы указали, первый 2 ++iвыполняется, то значения k[]были прочитаны, то последний ++iтогда k[].

num = k[i+1]+k[i+2] + k[i+3];
i += 3

Современные компиляторы оптимизируют это очень хорошо. На самом деле, возможно, лучше, чем код, который вы изначально написали (при условии, что он работал так, как вы надеялись).

TomOnTime
источник
5

Хорошее объяснение того, что происходит в таких вычислениях, приведено в документе n1188 с сайта ISO W14 .

Я объясняю идеи.

Основное правило из стандарта ISO 9899, ​​которое применяется в этой ситуации, это 6.5p2.

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

Точки последовательности в выражении, как i=i++до i=и после i++.

В статье, которую я цитировал выше, объясняется, что вы можете понять, что программа состоит из маленьких прямоугольников, каждый из которых содержит инструкции между двумя последовательными точками последовательности. Точки последовательности определены в приложении C к стандарту, в случае если i=i++есть две точки последовательности, которые ограничивают полное выражение. Такое выражение синтаксически эквивалентно записи expression-statementв форме грамматики Бэкуса-Наура (грамматика приведена в приложении А к Стандарту).

Таким образом, порядок инструкций внутри коробки не имеет четкого порядка.

i=i++

можно интерпретировать как

tmp = i
i=i+1
i = tmp

или как

tmp = i
i = tmp
i=i+1

поскольку обе эти формы для интерпретации кода i=i++являются действительными и поскольку обе выдают разные ответы, поведение не определено.

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

РЕДАКТИРОВАТЬ:

Другой источник хорошо для объяснения таких неоднозначностей являются записями из с-чаво сайта (также опубликованным в книге ), а именно здесь и здесь и здесь .

alinsoar
источник
Как этот ответ добавил новое к существующим ответам? Также объяснения i=i++очень похожи на этот ответ .
Хак
@ haccks Я не читал другие ответы. Я хотел объяснить на своем родном языке то, что я узнал из упомянутого документа с официального сайта ISO 9899 open-std.org/jtc1/sc22/wg14/www/docs/n1188.pdf
alinsoar
5

Ваш вопрос, вероятно, не был: «Почему эти конструкции неопределенного поведения в C?». Ваш вопрос был, вероятно, «Почему этот код (использование ++) не дал мне ожидаемого значения?», И кто-то отметил ваш вопрос как дубликат и отправил вас сюда.

Этот ответ пытается ответить на этот вопрос: почему ваш код не дал ожидаемого вами ответа, и как вы можете научиться распознавать (и избегать) выражения, которые не будут работать должным образом.

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

int x = 5;
printf("%d %d %d\n", x, ++x, x++);

Но, к вашему удивлению, эта программа не помогла вам понять - она ​​напечатала какой-то странный, неожиданный, необъяснимый вывод, предполагая, что, возможно, ++делает что-то совершенно другое, совсем не то, что вы думали.

Или, возможно, вы смотрите на трудное для понимания выражение, как

int x = 5;
x = x++ + ++x;
printf("%d\n", x);

Возможно, кто-то дал вам этот код в виде головоломки. Этот код также не имеет смысла, особенно если вы запускаете его - и если вы скомпилируете и запустите его под двумя разными компиляторами, вы, вероятно, получите два разных ответа! Что с этим? Какой ответ правильный? (И ответ таков: оба они или нет)

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

Что делает выражение неопределенным? Являются ли выражения вовлеченными ++и --всегда неопределенными? Конечно, нет: это полезные операторы, и если вы используете их правильно, они совершенно четко определены.

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

Давайте вернемся к двум примерам, которые я использовал в этом ответе. Когда я написал

printf("%d %d %d\n", x, ++x, x++);

вопрос перед вызовом printf, компилятор вычисляет значение xfirst, или x++, или может быть ++x? Но оказывается, что мы не знаем . В C нет правила, согласно которому аргументы функции оцениваются слева направо, справа налево или в каком-либо другом порядке. Поэтому мы не можем сказать , будет ли компилятор сделать xпервый, затем ++x, затем x++, или x++потом ++xпотом x, или какой -либо другой порядок. Но порядок явно имеет значение, потому что в зависимости от того, какой порядок использует компилятор, мы получим разные результаты printf.

Как насчет этого сумасшедшего выражения?

x = x++ + ++x;

Проблема с этим выражением состоит в том, что оно содержит три различные попытки изменить значение x: (1) x++деталь пытается добавить 1 к x, сохранить новое значение xи вернуть старое значение x; (2) ++xдеталь пытается добавить 1 к x, сохранить новое значение xи вернуть новое значение x; и (3) x =часть пытается присвоить сумму двух других обратно x. Какое из этих трех попыток будет «выиграно»? Какое из трех значений будет назначено x? Опять же, и, возможно, что удивительно, в Си нет правил, чтобы говорить нам.

Вы можете себе представить, что приоритет или ассоциативность или оценка слева направо говорит вам, в каком порядке происходят вещи, но это не так. Вы можете мне не верить, но, пожалуйста, поверьте мне на слово, и я скажу это снова: приоритет и ассоциативность не определяют каждый аспект порядка вычисления выражения в C. В частности, если в одном выражении есть несколько различные места, где мы пытаемся присвоить новое значение чему-то вроде xприоритета и ассоциативности, не говорят нам, какая из этих попыток происходит первой, последней или какой-либо другой.


Итак, со всем этим фоном и введением, если вы хотите убедиться, что все ваши программы четко определены, какие выражения вы можете написать, а какие вы не можете написать?

Эти выражения все в порядке:

y = x++;
z = x++ + y++;
x = x + 1;
x = a[i++];
x = a[i++] + b[j++];
x[i++] = a[j++] + b[k++];
x = *p++;
x = *p++ + *q++;

Все эти выражения не определены:

x = x++;
x = x++ + ++x;
y = x + x++;
a[i] = i++;
a[i++] = i;
printf("%d %d %d\n", x, ++x, x++);

И последний вопрос: как определить, какие выражения определены, а какие нет?

Как я сказал ранее, неопределенные выражения - это те, в которых слишком много всего происходит одновременно, где вы не можете быть уверены, в каком порядке происходят вещи и где порядок имеет значение:

  1. Если есть одна переменная, которая модифицируется (присваивается) в двух или более разных местах, как узнать, какая модификация произойдет первой?
  2. Если есть переменная, которая модифицируется в одном месте, а ее значение используется в другом месте, как вы узнаете, использует ли оно старое или новое значение?

Как пример № 1, в выражении

x = x++ + ++x;

Есть три попытки изменить `x.

Как пример № 2, в выражении

y = x + x++;

мы оба используем значение xи изменяем его.

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

Стив Саммит
источник
3

Причина в том, что программа работает с неопределенным поведением. Проблема заключается в порядке оценки, поскольку в соответствии со стандартом C ++ 98 нет точек последовательности, требуемых (никакие операции не выполняются до или после другой в соответствии с терминологией C ++ 11).

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

  • Итак, сначала GCC: Используя Nuwen MinGW 15 GCC 7.1, вы получите:

    #include<stdio.h>
    int main(int argc, char ** argv)
    {
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 2
    
    i = 1;
    i = (i++);
    printf("%d\n", i); //1
    
    volatile int u = 0;
    u = u++ + ++u;
    printf("%d\n", u); // 2
    
    u = 1;
    u = (u++);
    printf("%d\n", u); //1
    
    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); //2

    }

Как работает GCC? он оценивает подвыражения в порядке слева направо для правой стороны (RHS), затем присваивает значение левой стороне (LHS). Именно так ведут себя Java и C # и определяют свои стандарты. (Да, эквивалентное программное обеспечение на Java и C # имеет определенное поведение). Он оценивает каждое подвыражение одно за другим в выражении RHS в порядке слева направо; для каждого подвыражения: сначала вычисляется ++ c (преинкремент), затем для операции используется значение c, затем постинкремент c ++).

согласно GCC C ++: операторы

В GCC C ++ приоритет операторов управляет порядком, в котором оцениваются отдельные операторы

эквивалентный код в определенном поведении C ++, как понимает GCC:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    //i = i++ + ++i;
    int r;
    r=i;
    i++;
    ++i;
    r+=i;
    i=r;
    printf("%d\n", i); // 2

    i = 1;
    //i = (i++);
    r=i;
    i++;
    i=r;
    printf("%d\n", i); // 1

    volatile int u = 0;
    //u = u++ + ++u;
    r=u;
    u++;
    ++u;
    r+=u;
    u=r;
    printf("%d\n", u); // 2

    u = 1;
    //u = (u++);
    r=u;
    u++;
    u=r;
    printf("%d\n", u); // 1

    register int v = 0;
    //v = v++ + ++v;
    r=v;
    v++;
    ++v;
    r+=v;
    v=r;
    printf("%d\n", v); //2
}

Затем мы идем в Visual Studio . Visual Studio 2015, вы получаете:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 3

    i = 1;
    i = (i++);
    printf("%d\n", i); // 2 

    volatile int u = 0;
    u = u++ + ++u;
    printf("%d\n", u); // 3

    u = 1;
    u = (u++);
    printf("%d\n", u); // 2 

    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); // 3 
}

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

Таким образом, эквивалент в определенном поведении C ++, как понимает Visual C ++:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int r;
    int i = 0;
    //i = i++ + ++i;
    ++i;
    r = i + i;
    i = r;
    i++;
    printf("%d\n", i); // 3

    i = 1;
    //i = (i++);
    r = i;
    i = r;
    i++;
    printf("%d\n", i); // 2 

    volatile int u = 0;
    //u = u++ + ++u;
    ++u;
    r = u + u;
    u = r;
    u++;
    printf("%d\n", u); // 3

    u = 1;
    //u = (u++);
    r = u;
    u = r;
    u++;
    printf("%d\n", u); // 2 

    register int v = 0;
    //v = v++ + ++v;
    ++v;
    r = v + v;
    v = r;
    v++;
    printf("%d\n", v); // 3 
}

как указано в документации Visual Studio в порядке приоритета и порядке оценки :

Если несколько операторов появляются вместе, они имеют одинаковый приоритет и оцениваются в соответствии с их ассоциативностью. Операторы в таблице описаны в разделах, начинающихся с Postfix Operators.

Мохамед Эль-Накиб
источник
1
Я отредактировал вопрос, чтобы добавить UB при оценке аргументов функции, так как этот вопрос часто используется как дубликат для этого. (Последний пример)
Антти Хаапала
1
Также вопрос касается c сейчас, а не C ++
Антти Хаапала