Почему эти конструкции используют неопределенное поведение до и после приращения? - PullRequest
760 голосов
/ 04 июня 2009
#include <stdio.h>

int main(void)
{
   int i = 0;
   i = i++ + ++i;
   printf("%d\n", i); // 3

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

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

   u = 1;
   u = (u++);
   printf("%d\n", u); // 2 Should also be one, no ?

   register int v = 0;
   v = v++ + ++v;
   printf("%d\n", v); // 3 (Should be the same as u ?)

   int w = 0;
   printf("%d %d %d\n", w++, ++w, w); // shouldn't this print 0 2 2

   int x[2] = { 5, 8 }, y = 0;
   x[y] = y ++;
   printf("%d %d\n", x[0], x[1]); // shouldn't this print 0 8? or 5 0?
}

Ответы [ 14 ]

9 голосов
/ 08 апреля 2015

В https://stackoverflow.com/questions/29505280/incrementing-array-index-in-c кто-то спросил о таком утверждении, как:

int k[] = {0,1,2,3,4,5,6,7,8,9,10};
int i = 0;
int num;
num = k[++i+k[++i]] + k[++i];
printf("%d", num);

, который печатает 7 ... ОП ожидал, что он напечатает 6.

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

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

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

5 голосов
/ 16 августа 2018

Ваш вопрос, вероятно, не был: «Почему эти конструкции неопределенного поведения в 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);

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

Как вы уже слышали, все эти выражения undefined , что означает, что язык 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. В частности, если в одном выражении есть несколько В разных местах, где мы пытаемся присвоить новое значение чему-то вроде 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 и модифицируем его.

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

4 голосов
/ 13 октября 2017

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

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

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

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

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

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

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

i=i++

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

tmp = i
i=i+1
i = tmp

или как

tmp = i
i = tmp
i=i+1

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

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

EDIT:

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

3 голосов
/ 11 июня 2017

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...