Почему эти конструкции используют неопределенное поведение до и после инкремента?
#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?
}
Ответы
C имеет концепцию неопределенного поведения, т.е. некоторые языковые конструкции являются синтаксически допустимыми, но вы не можете предсказать поведение при запуске кода.
Насколько мне известно, в стандарте прямо не говорится, почему существует концепция неопределенного поведения. На мой взгляд, это просто потому, что разработчики языка хотели иметь некоторую свободу действий в семантике, вместо того, чтобы требовать, чтобы все реализации обрабатывали целочисленное переполнение точно таким же образом, что, скорее всего, привело бы к серьезным потерям производительности, они просто оставили поведение undefined, так что если вы напишете код, вызывающий целочисленное переполнение, все может случиться.
Итак, имея это в виду, почему эти «проблемы»? Язык четко говорит, что определенные вещи приводят к неопределенному поведению . Нет никаких проблем, нет никаких «должен». Если неопределенное поведение изменяется при объявлении одной из задействованных переменных volatile
, это ничего не доказывает и не меняет. Это не определено ; вы не можете рассуждать о поведении.
Ваш самый интересный пример, с
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 $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 была своего рода оптимизацией компилятора?)
Я думаю, что соответствующие части стандарта C99 - это 6.5 Expressions, §2
Между предыдущей и следующей точкой последовательности объект должен иметь свое сохраненное значение, измененное не более одного раза при оценке выражения. Кроме того, предыдущее значение должно считываться только для определения значения, которое будет сохранено.
и 6.5.16 Операторы присваивания, §4:
Порядок оценки операндов не указан. Если предпринята попытка изменить результат оператора присваивания или получить к нему доступ после следующей точки последовательности, поведение не определено.
Большинство ответов здесь цитируются из стандарта C, в котором подчеркивается, что поведение этих конструкций не определено. Чтобы понять, почему поведение этих конструкций не определено , давайте сначала разберемся с этими терминами в свете стандарта C11:
Последовательно: (5.1.2.3)
Для любых двух оценок
A
иB
, если ониA
выполняются последовательноB
, выполнениеA
должно предшествовать выполнениюB
.
Без последовательности:
Если
A
последовательность не установлена до или послеB
, тоA
и не имеют последовательностиB
.
Оценка может быть одной из двух:
- вычисления значений , которые определяют результат выражения; и
- побочные эффекты , представляющие собой модификации объектов.
Точка последовательности:
Наличие точки последовательности между вычислением выражений
A
иB
подразумевает, что каждое вычисление значения и связанный с ним побочный эффектA
упорядочивается перед каждым вычислением значения и связанным с ним побочным эффектомB
.
Теперь перейдем к вопросу о таких выражениях, как
int i = 1;
i = i++;
стандарт говорит, что:
6.5 Выражения:
Если побочный эффект от скалярного объекта unsequenced по отношению к любой другой побочный эффект на тот же объект скалярного или вычисления значения с использованием значения одного и того же скалярного объекта, его поведение не определено . [...]
Следовательно, приведенное выше выражение вызывает UB, потому что два побочных эффекта для одного и того же объекта i
не упорядочены относительно друг друга. Это означает, что не определяется, будет ли побочный эффект по назначению i
выполняться до или после побочного эффекта по ++
.
В зависимости от того, происходит ли присвоение до или после приращения, будут получены разные результаты, и это один из случаев неопределенного поведения .
Давайте переименуем i
слева от присваивания быть il
и справа от присваивания (в выражении i++
) быть ir
, тогда выражение будет как
il = ir++ // Note that suffix l and r are used for the sake of clarity.
// Both il and ir represents the same object.
Важным моментом относительно ++
оператора Postfix является то, что:
просто потому, что
++
стоит после переменной, это не означает, что приращение происходит поздно . Приращение может происходить так рано, как пожелает компилятор, если компилятор гарантирует, что используется исходное значение .
Это означает, что выражение il = ir++
может быть оценено как
temp = ir; // i = 1
ir = ir + 1; // i = 2 side effect by ++ before assignment
il = temp; // i = 1 result is 1
или же
temp = ir; // i = 1
il = temp; // i = 1 side effect by assignment before ++
ir = ir + 1; // i = 2 result is 2
приводит к двум разным результатам 1
и 2
зависит от последовательности побочных эффектов при назначении ++
и, следовательно, вызывает UB.
Поведение не может быть действительно объясняется тем , что он вызывает как неопределенное поведение и неопределенное поведение , поэтому мы не можем делать какие - либо общие прогнозы относительно этого кода, хотя , если вы читаете Olve Maudal в работе , такие как Deep C и неконкретный и Undefined иногда вы можете сделать хорошее догадки в очень конкретных случаях с конкретным компилятором и средой, но, пожалуйста, не делайте этого где-либо рядом с производством.
Итак, переходя к неопределенному поведению , в пункте 3 стандартного раздела проекта c99 говорится ( выделено мной ):6.5
Группировка операторов и операндов указывается в синтаксисе.74) За исключением случаев, указанных ниже (для операторов вызова функции (), &&, ||,?: И запятой), порядок оценки подвыражений и порядок в какие побочные эффекты имеют место, не уточняются.
Итак, когда у нас есть такая строка:
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
У C, конечно же, есть удобный ярлык:
i++
Это означает «прибавить 1 к i, а результат вернуть i». Итак, если мы построим солянку из двух, написав
i = i++
на самом деле мы говорим: «прибавьте 1 к i, присвойте результат обратно i и присвойте результат обратно i». Мы запутались, поэтому меня не слишком беспокоит, если компилятор тоже запутается.
На самом деле, эти сумасшедшие выражения пишутся только тогда, когда люди используют их в качестве искусственных примеров того, как должен работать ++. И, конечно же, важно понимать, как работает ++. Но есть одно практическое правило использования ++: «Если не ясно, что означает выражение, использующее ++, не пишите его».
Раньше мы проводили на comp.lang.c бесчисленные часы, обсуждая подобные выражения и почему они не определены. Два моих более длинных ответа, которые пытаются объяснить почему, заархивированы в Интернете:
- Почему Стандарт не определяет, что они делают?
- Разве приоритет оператора не определяет порядок оценки?
Смотрите также вопрос 3.8 и остальные вопросы в разделе 3 из списка 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++
является законным, то поведение этих конструкций является неопределенным , так как должен в стандарте C не выполняется. C99 6.5p2 :
- Между предыдущей и следующей точкой последовательности объект должен иметь свое сохраненное значение, измененное не более одного раза при оценке выражения. [72] Кроме того, предыдущее значение должно читаться только для определения значения, которое будет сохранено [73]
В сноске 73 дополнительно уточняется, что
В этом абзаце представлены неопределенные выражения операторов, такие как
i = ++i + 1; a[i++] = i;
позволяя
i = i + 1; a[i] = i;
Различные точки последовательности перечислены в Приложении C к C11 (и C99 ):
Ниже приведены точки последовательности, описанные в 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 :
- Если побочный эффект для скалярного объекта не упорядочен относительно другого побочного эффекта для того же скалярного объекта или вычисления значения с использованием значения того же скалярного объекта, поведение не определено. Если существует несколько допустимых порядков подвыражений выражения, поведение не определено, если такой неупорядоченный побочный эффект возникает в любом из порядков.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 говорит, что переменная должна быть назначена не более одного раза между двумя точками последовательности. Например, точка с запятой - это точка последовательности.
Итак, каждое утверждение в форме:
i = i++;
i = i++ + ++i;
и так далее нарушают это правило. В стандарте также говорится, что поведение не определено и не определено. Некоторые компиляторы обнаруживают их и выдают определенный результат, но это не соответствует стандарту.
Однако две разные переменные могут увеличиваться между двумя точками последовательности.
while(*src++ = *dst++);
Вышесказанное является обычной практикой кодирования при копировании / анализе строк.
В 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 ... OP ожидал, что он напечатает 6.
Эти ++i
приращения не гарантируется для всех завершена до остальных расчетов. Фактически, разные компиляторы здесь получат разные результаты. В примере , который вы указали, первый 2 ++i
выполняется, то значения k[]
были прочитаны, то последний ++i
тогда k[]
.
num = k[i+1]+k[i+2] + k[i+3];
i += 3
Современные компиляторы это очень хорошо оптимизируют. Фактически, возможно, лучше, чем код, который вы изначально написали (при условии, что он сработал так, как вы надеялись).
Ваш вопрос, вероятно, был не «Почему эти конструкции ведут себя неопределенно в 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
first или 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 в выражении
x = x++ + ++x;
есть три попытки изменить `x.
В качестве примера №2 в выражении
y = x + x++;
мы оба используем значение x
и изменяем его.
Вот и ответ: убедитесь, что в любом написанном вами выражении каждая переменная изменяется не более одного раза, и если переменная изменяется, вы также не пытаетесь использовать значение этой переменной где-то еще.
Хорошее объяснение того, что происходит в этом виде вычислений, представлено в документе n1188 с сайта ISO W14 .
Объясняю идеи.
Основное правило стандарта ISO 9899, которое применяется в этой ситуации, - 6.5p2.
Между предыдущей и следующей точкой последовательности объект должен иметь свое сохраненное значение, измененное не более одного раза при оценке выражения. Кроме того, предыдущее значение должно считываться только для определения значения, которое будет сохранено.
Точки последовательности в выражении like 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 ++ 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.