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

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

#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?
}
4b9b3361

Ответ 1

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

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

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

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

u = (u++);

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

Ответ 2

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

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

$ 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 была какой-то оптимизацией компилятора?)

Ответ 3

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

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

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

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

Ответ 4

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

Ответ 5

Большинство ответов здесь цитируется на стандарте 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.

Ответ 6

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

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

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

i = i + 1

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

i++

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

i = i++

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

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

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

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

Ответ 7

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

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.

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

Ответ 8

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

Ответ 9

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

Ответ 10

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

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

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

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

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

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

Ответ 11

В fooobar.com/questions/62/... кто-то спросил об утверждении вроде:

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

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

Ответ 12

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

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

Ответ 13

Хорошее объяснение того, что происходит в такого рода вычислениях, представлено в документе 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 (также опубликованные в виде книги), а именно здесь, здесь и здесь.

Ответ 14

Причина в том, что в программе работает поведение 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.

Ответ 15

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

 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