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

StackOverflow https://stackoverflow.com/questions/949433

Вопрос

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

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

Итак, имея это в виду, почему возникают эти "проблемы"?Язык ясно говорит, что определенные вещи приводят к неопределенное поведение.Здесь нет никакой проблемы, здесь нет никакого "должен".Если неопределенное поведение изменяется при объявлении одной из задействованных переменных 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 выражений, §2

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

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

Поведение не может быть объяснено, потому что он вызывает оба неуточненное поведение и неопределенное поведение, поэтому мы не можем сделать никаких общих прогнозов об этом коде, хотя, если вы читаете Olve maudal's работа такая как Глубокий C. и Неопределенный и неопределенный

Так что движусь к неуточненное поведение, в Проект стандарта C99 раздел6.5 параграф 3 говорит (Упор мой):

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

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

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 в виде:

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

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

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

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

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

Следовательно, вышеуказанное выражение вызывает 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. Но есть несколько способов сделать это в 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 FAQ списка.

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.

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

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

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

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

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

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

В то время как синтаксис выражений, как a = a++ или a++ + a++ законно, поведение из этих конструкций неопределенный потому что а должен в С стандарте не подчиняются. :

  1. Между предыдущей и следующей точки последовательности. Объект должен иметь его сохраненное значение, модифицированное максимум один раз путем оценки выражения. [72] Furthermore, the prior value shall be read only to determine the value to be stored [73

С Сноска 73 дальнейшее уточнение этого

  1. Этот абзац оказывает неопределенные выражения выписки, такие как

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

    пока разрешаю

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

Различные точки последовательности перечислены в Приложении C C11.C99.):

  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); контролирующее выражение какое -то время или оператор DO (6.8.5); Каждое из (необязательных) выражений для утверждения (6.8.5.3); (необязательное) выражение в операторе возврата (6.8.6.4).
    • Непосредственно перед возвратом функции библиотеки (7.1.4).

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

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

Вы можете обнаружить такие ошибки в программе, например, с помощью последней версии GCC с -Wall и -Werror,

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

В 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);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

вопрос в том, прежде чем звонить printf, вычисляет ли компилятор значение x во-первых, или x++, или , может быть ++x?Но оказывается, что мы не знаем.В C нет правила, которое гласило бы, что аргументы функции вычисляются слева направо, или справа налево, или в каком-либо другом порядке.Таким образом, мы не можем сказать, будет ли компилятор выполнять x во-первых, тогда ++x, тогда x++, или x++ тогда ++x тогда x, или какой-то другой порядок.Но порядок явно имеет значение, потому что в зависимости от того, какой порядок использует компилятор, мы явно получим разные результаты, напечатанные printf.

А как насчет этого безумного выражения лица?

x = x++ + ++x;

Проблема с этим выражением заключается в том, что оно содержит три разные попытки изменить значение x:(1) тот самый x++ часть пытается добавить 1 к x, сохранить новое значение в x, и возвращает старое значение x;(2) тот самый ++x часть пытается добавить 1 к x, сохранить новое значение в x, и возвращает новое значение x;и (3) тот самый x = часть пытается присвоить сумму двух других значений обратно x .Какое из этих трех выполненных заданий "выиграет"?Какому из трех значений на самом деле будет присвоено x?Опять же, и это, возможно, удивительно, в C нет правила, которое указывало бы нам на это.

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


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

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

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

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

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

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

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

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

В качестве примера #1, в выражении

x = x++ + ++x;

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

В качестве примера #2, в выражении

y = x + x++;

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

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

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

Редактировать:

Другим хорошим источником для объяснения таких двусмысленностей являются записи из 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? Он оценивает Sub-выражения влево на правильный порядок для правой стороны (RHS), затем назначает значение левой стороны (LHS). Это именно то, как Java и C # ведут себя и определить их стандарты. (Да, эквивалентное программное обеспечение в Java и C # определили поведение). Он оценивает каждое подражание по одному в операторе RHS влево на правильный порядок; Для каждого выражения Sub: A ++ 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 Word, требуется другой подход, он оценивает все выражения предварительного приращения в первом проходе, затем использует значения переменных в операциях во втором проходе, присваивая от 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 
}

Как визуальные студии документации в Приоритет и порядок оценки:

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top