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

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

14 ответов


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

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

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

ваш самый интересный пример, тот, который с

u = (u++);

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


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

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

$ 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    x10,%esp
   0x00000006 <+6>:   movl   x0,-0x4(%ebp)  // i = 0   i = 0
   0x0000000d <+13>:  addl   x1,-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   x1,-0x4(%ebp)  // i++     i = 4
   0x0000001d <+29>:  leave  
   0x0000001e <+30>:  ret
End of assembler dump.

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


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

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

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

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


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

Итак, переходим к неуказанному поведению, в проект стандарта c99 раздел6.5 абзац 3 говорит(выделено мной):

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

поэтому, когда у нас есть такая строка:

i = i++ + ++i;

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

у нас тоже есть неопределенное поведение здесь, так как программа изменяет переменные(i, u, etc..) более одного раза между точки последовательности. От проект стандартного раздела 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 as:

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

и неопределенное поведение определен в разделе 3.4.3 as:

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

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

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


большинство ответов здесь цитата из стандарта 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 выражения:

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

поэтому приведенное выше выражение вызывает 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.  

важное замечание в отношении постфикс ++ оператор это:

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

это означает выражение 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.


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

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

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

i = i + 1

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

i++

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

i = i++

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

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

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


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


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

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++) для distinct объекты.

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

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

as неопределено поведение заключается в том, что эти два выражения изменяют то же самое объект 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 to 6 и i++ дает старое значение i (6), который назначается j. Тогда i будет 7 из-за пост-инкремент.

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

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

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


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

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


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

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

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

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

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

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


С синтаксис таких выражений, как 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); (необязательное) выражение в операторе return (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 по одному, давая старое значение, отбросьте это значение; затем в операторе запятой, установите побочные эффекты; а затем increment i один, и полученное значение становится значением выражения - т. е. это просто надуманный способ писать j = (i += 2) что еще раз является "умным" способом писать

i += 2;
j = i;
на , в списках аргументов функции не оператор запятой, и нет точки последовательности между оценками различных аргументов; вместо этого их оценки не согласованы друг с другом; поэтому функция вызывает
int i = 0;
printf("%d %d\n", i++, ++i, i);

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


в https://stackoverflow.com/questions/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

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


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

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

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

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

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

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

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

i=i++

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

tmp = i
i=i+1
i = tmp

или

tmp = i
i = tmp
i=i+1

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

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

EDIT:

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


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

таким образом, эквивалент в определенном поведение 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 операторов.


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

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

я полагаю, вы слышали основное определение C's ++ и -- операторы сейчас, и как префикс form ++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, приоритет и ассоциативность do не скажите нам, какая из этих попыток происходит первой, или последний, или ничего.


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

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

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, и изменить его.

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