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

704
#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 %d\n", w++, ++w, w); // shouldn't this print 0 2 2

   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?
}
  • 42
    Домашнее задание? Не пытайтесь быть больно, но вы никогда не должны писать код с такими выражениями. Они обычно приводятся в качестве академических примеров, иногда показывая, что разные компиляторы дают разные результаты.
  • 11
    @Jarett, нет, просто нужно несколько указателей на «точки последовательности». Работая, я нашел фрагмент кода с i = i ++, но я подумал: «Это не изменяет значение i». Я проверял и задавался вопросом, почему. С тех пор я удалил этот статус и заменил его на i ++;
Показать ещё 18 комментариев
Теги:
increment
undefined-behavior
order-of-evaluation
sequence-points

15 ответов

533
Лучший ответ

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

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

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

Ваш наиболее интересный пример, один с

u = (u++);

- это пример текстовой книги поведения undefined (см. запись в Википедии точки последовательности).

  • 37
    Я знал, что это не определено, (идея использования этого кода в производстве пугает меня :)), но я пытался понять, в чем причина этих результатов. Особенно почему u = u ++ увеличил u. Например, в java: u = u ++ возвращает 0, как и ожидалось (мой мозг) :)
  • 2
    Очевидно, что из-за скобок вокруг u ++ компилятор решил усилить u, а затем вернуть его. Поскольку это неопределенное поведение в C, оно является легитимным. Другой компилятор или даже другой компьютер и тот же самый может дать другой ответ. Я не знаю Java, но, возможно, поведение четко определено.
Показать ещё 18 комментариев
73

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

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

$ 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.

(I... предположим, что команда 0x00000014 была какой-то оптимизацией компилятора?)

  • 0
    как я могу получить машинный код? Я использую Dev C ++, и я поэкспериментировал с опцией «Генерация кода» в настройках компилятора, но не выводил никаких дополнительных файлов или консольного вывода
  • 4
    @ronnieaka gcc evil.c -c -o evil.bin и gdb evil.bindisassemble evil , или каковы бы ни были их эквиваленты в Windows :)
Показать ещё 7 комментариев
58

Я думаю, что соответствующими частями стандарта C99 являются 6.5 Выражения, §2

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

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

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

  • 2
    Означает ли приведенное выше, что «i = i = 5;» будет неопределенным поведением?
  • 1
    @ Supercat, насколько я знаю, i=i=5 также неопределенное поведение
Показать ещё 7 комментариев
49

Большинство ответов здесь цитируется на стандарте C, подчеркивая, что поведение этих конструкций undefined. Чтобы понять , почему поведение этих конструкций undefined, давайте сначала понимать эти термины в свете стандарта 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 Выражения:

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

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

Давайте переименуем i слева от назначения be 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.

46

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

Итак, переходим к неуказанному поведению, в черновик c99 standard раздел 6.5 в пункте 3 говорится (внимание мое):

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

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

i = i++ + ++i;

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

Здесь также существует поведение undefined, так как программа несколько раз модифицирует переменные (i, u и т.д.) более чем один раз между точки последовательности. Из проекта стандартного раздела 6.5, параграф 2 (ударный удар):

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

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

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

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

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

i = (i++);
^    ^

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

u = (u++);
^    ^

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

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

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

и undefined поведение определено в разделе 3.4.3 следующим образом:

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

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

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

30

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

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

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

i = i + 1

C, конечно, имеет удобный ярлык:

i++

Это означает, что "добавьте 1 в я и присвойте результат обратно i". Поэтому, если мы построим мешанин из двух, написав

i = i++

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

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

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

  • 1
    Довольно неприятный глюк в отношении неопределенного поведения является то , что в то время как он используется , чтобы быть безопасными на 99,9% трансляторы использовать *p=(*q)++; означать, if (p!=q) *p=(*q)++; else *p= __ARBITRARY_VALUE; Это уже не так. Для гиперсовременного C потребовалось бы написать что-то похожее на последнюю формулировку (хотя нет стандартного способа указать, что коду не важно, что находится в *p ), чтобы достичь уровня эффективности, используемого компиляторами для первого (пункт else необходим в чтобы позволить компилятору оптимизировать, if это потребуется некоторым новым компиляторам).
  • 1
    Я видел по крайней мере 5 подобных вопросов об этих ++ и - безумии на прошлой неделе или около того. Кажется, это любимая тема некоторых профессоров, чтобы озадачивать своих студентов.
22

Хотя маловероятно, что какие-либо компиляторы и процессоры действительно это сделают, было бы законно в соответствии со стандартом 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.

20

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

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

или

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

или аналогичных вариантов.

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

   x = i++ + i++;

В следующем утверждении:

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

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

Приложение J, неуказанное поведение

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

3.4.4, неуказанное поведение

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

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

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

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

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

Что делает следующий оператор

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

как поведение undefined заключается в том, что эти два выражения изменяют один и тот же объект 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++);

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


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

Это сообщение: Undefined, неуказанное и определенное по реализации поведение также имеет значение.

  • 0
    Эта последовательность int a = 10, b = 20, c = 30; printf("a=%db=%dc=%d\n", (a = a + b + c), (b = b + b), (c = c + c)); похоже, дает стабильное поведение (оценка аргумента справа налево в gcc v7.3.0; результат "a = 110 b = 40 c = 60"). Это потому, что назначения рассматриваются как «полные операторы» и, таким образом, вводят точку последовательности? Разве это не должно приводить к оценке аргумента / утверждения слева направо? Или это просто проявление неопределенного поведения?
  • 0
    @kavadias Этот оператор printf использует неопределенное поведение по той же причине, что и описанная выше. Вы пишете b и c в 3-м и 4-м аргументах соответственно и читаете во 2-м аргументе. Но между этими выражениями нет последовательности (2-й, 3-й и 4-й аргументы). У gcc / clang есть опция -Wsequence-point которая также может помочь найти их.
13

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

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

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

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

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

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

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

Хотя синтаксис выражений, как 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).
    • Между оценками первого и второго операндов следующих операторов: логическое AND && (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); (необязательное) выражение в операторе return (6.8.6.4).
    • Непосредственно перед возвратом функции библиотеки (7.1.4).
    • После действий, связанных с каждым форматированным спецификатором преобразования функции ввода/вывода (7.21.6, 7.29.2).
    • Непосредственно перед и сразу после каждого вызова функции сравнения, а также между любым вызовом функции сравнения и любым перемещением объектов, переданных в качестве аргументов для этого вызова (7.22.5).

Формулировка того же абзаца на С11:

  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, между предыдущим и следующим точка последовательности.

9

В https://stackoverflow.com/questions/949433/why-are-these-constructs-using-pre-and-post-increment-undefined-behavior кто-то спросил об утверждении вроде:

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... OP ожидал, что он напечатает 6.

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

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

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

5

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

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

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

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

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

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

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

i=i++

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

tmp = i
i=i+1
i = tmp

или как

tmp = i
i = tmp
i=i+1

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

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

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

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

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

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

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

Я предполагаю, что вы уже слышали базовое определение операторов C ++ и -- и как префиксная форма ++x отличается от 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 компилятор вычисляет значение x сначала, или 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 нет правила говорить нам.

Вы можете себе представить, что приоритет или ассоциативность или оценка слева направо говорит вам, в каком порядке происходят события, но они этого не делают. Вы можете мне не верить, но, пожалуйста, запомните мое слово, и я скажу еще раз: приоритет и ассоциативность не определяют каждый аспект порядка оценки выражения в 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

Причина в том, что в программе работает поведение undefined. Проблема заключается в порядке оценки, поскольку в соответствии со стандартом С++ 98 нет никаких точек последовательности (никакие операции не секвенируются до или после другого в соответствии с терминологией С++ 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 и С# и определяют их стандарты. (Да, эквивалентное программное обеспечение на Java и С# определило поведение). Он оценивает каждое вспомогательное выражение один за другим в Заявлении RHS в порядке слева направо; для каждого вспомогательного выражения: сначала выполняется оценка ++ c (pre-increment), затем значение c используется для операции, а затем приращение post С++).

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

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

эквивалентный код в определенном поведении С++, как понимает 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 
}

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

Итак, эквивалент в определенном поведении С++, как понимает Visual С++:

#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.

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

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

 i=10;
 i == i++;

вышеприведенное выражение вернет false, потому что

 here while evaluating 
   10 is given to the right hand side and then incremented 
   but the incremented 11 will be given to the left hand side
   proof : 10 == i++  will return true whereas,
           11 == i++ will return false

Ещё вопросы

Сообщество Overcoder
Наверх
Меню