Why are these constructs using pre and post-increment undefined behavior?

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

15 ответов

Решение

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

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

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

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

u = (u++);

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

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

Важный момент относительно 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.

I think the relevant parts of the C99 standard are 6.5 Expressions, §2

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

and 6.5.16 Assignment operators, §4:

Порядок оценки операндов не указан. If an attempt is made to modify the result of an assignment operator or to access it after the next sequence point, the behavior is undefined.

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

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

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

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

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

Таким образом, переходя к неопределенному поведению, в проекте стандарта C99 6.5 пункт 3 говорит (выделение мое):

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

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

i = i++ + ++i;

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

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

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

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

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

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

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

i = (i++);
^    ^

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

u = (u++);
^    ^

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

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

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

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

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

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

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

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

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

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

i = i + 1

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

i++

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

i = i++

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

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

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

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

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.

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

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

Хотя синтаксис выражений типа 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); управляющее выражение оператора выбора (если или переключатель) (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 между предыдущей и следующей точкой последовательности.

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

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

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

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

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

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

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

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

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

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

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

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

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

вопрос перед звонком printfКомпилятор вычисляет значение 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 = part пытается присвоить сумму двух других обратно x. Какое из этих трех попыток будет "выиграно"? Какому из трех значений будет присвоено x? Опять же, и, возможно, что удивительно, в Си нет правил, чтобы говорить нам.

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


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

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

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

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

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

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

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

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

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

x = x++ + ++x;

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

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

y = x + x++;

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

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

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

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

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

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

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

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

В https://stackru.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++,

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

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

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

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

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

    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

документация Visual Studio гласит: " Приоритет и порядок оценки":

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

Ключом к пониманию этого является то, что значение выраженияi++есть, и его эффект заключается в добавлении 1 кi' (i.e. store the value я +1in the variablei`), но это не означает, что сохранение произойдет после определения значения.

В таком выражении значение левой части сложения равно, а правой части — «i+1».

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

Совершенно разумно (и, возможно, эффективно) заметить, что конечный результат состоит в увеличении на 2 и оценке (что составляетi+i+1и позже сохранитьi+2вi. Или нет.

Чего вам не следует делать, так это пытаться понять, что делает ваш компилятор, и играть с ним.

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

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

Напишите нужный вам код (например2*i+1; i+=2;) и осознавать, что все современные коммерческие компиляторы (при включенной оптимизации) преобразуют это в наиболее эффективный код для вашей платформы.

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

Другие вопросы по тегам