Вопрос

Возможный дубликат:
Поведение оператора инкремента до и после в C, C++, Java и C#

Вот тестовый пример:


void foo(int i, int j)
{
   printf("%d %d", i, j);
}
...
test = 0;
foo(test++, test);

Я ожидаю получить выход «0 1», но я получаю «0 0», что дает ??

Это было полезно?

Решение

Это пример неопределенного поведения.Стандарт делает нет сказать, в каком порядке следует оценивать аргументы.Это решение реализации компилятора.Компилятор может оценивать аргументы функции в любом порядке.

В этом случае похоже, что на самом деле аргументы обрабатываются справа налево вместо ожидаемого слева направо.

В общем, использование побочных эффектов в аргументах — плохая практика программирования.

Вместо Фу (тест++, тест); тебе следует написать Фу(тест, тест+1);тест++;

Это было бы семантически эквивалентно тому, чего вы пытаетесь достичь.

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

Другие советы

Это не просто неопределенные поведение, это на самом деле неопределенное поведение .

Да, порядок оценки аргументов неопределенные, но это неопределенный читать и изменять одну переменную без промежуточной точки последовательности, если только чтение не предназначено исключительно для вычисления нового значения.Между оценками аргументов функции нет точки последовательности, поэтому f(test,test++) является неопределенное поведение: test читается для одного аргумента и изменяется для другого.Если вы переместите модификацию в функцию, все будет в порядке:

int preincrement(int* p)
{
    return ++(*p);
}

int test;
printf("%d %d\n",preincrement(&test),test);

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

Обратите также внимание на то, что запятая оператор обеспечивает точку последовательности, поэтому

int dummy;
dummy=test++,test;

все в порядке --- приращение происходит до чтения, поэтому dummy устанавливается новое значение.

Все, что я сказал изначально, НЕПРАВИЛЬНО!Момент времени, в который рассчитывается побочный эффект является неопределенные.Visual C++ выполнит приращение после вызова foo(), если test является локальной переменной, но если test объявлен как статический или глобальный, он будет увеличен перед вызовом foo() и даст разные результаты, хотя окончательное значение тест будет правильным.

Увеличение действительно должно быть выполнено в отдельном операторе после вызова foo().Даже если бы такое поведение было указано в стандарте C/C++, оно могло бы сбить с толку.Можно подумать, что компиляторы C++ пометят это как потенциальную ошибку.

Здесь является хорошим описанием точек последовательности и неопределенного поведения.

<----НАЧАЛО НЕПРАВИЛЬНО НЕПРАВИЛЬНО---->

Бит «++» команды «test++» выполняется после вызова foo.Итак, вы передаете (0,0) в foo, а не (1,0)

Вот результат ассемблера Visual Studio 2002:

mov ecx, DWORD PTR _i$[ebp]
push    ecx
mov edx, DWORD PTR tv66[ebp]
push    edx
call    _foo
add esp, 8
mov eax, DWORD PTR _i$[ebp]
add eax, 1
mov DWORD PTR _i$[ebp], eax

Увеличение выполняется ПОСЛЕ вызова foo().Хотя такое поведение является задуманным, оно, безусловно, сбивает с толку обычного читателя, и его, вероятно, следует избегать.Увеличение действительно должно выполняться в отдельном операторе после вызова foo().

<----КОНЕЦ НЕПРАВИЛЬНО НЕПРАВИЛЬНО ---->

Это «неопределенное поведение», но на практике, учитывая способ указания стека вызовов C, он почти всегда гарантирует, что вы увидите его как 0, 0 и никогда 1, 0.

Как кто-то заметил, вывод ассемблера VC сначала помещает самый правый параметр в стек.Вот как вызовы функций C реализуются на ассемблере.Это сделано для того, чтобы обеспечить возможность C «бесконечного списка параметров».Если помещать параметры в порядке справа налево, первый параметр гарантированно будет верхним элементом в стеке.

Возьмите подпись printf:

int printf(const char *format, ...);

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

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

Однако даже это не спасет вас, поскольку большинство компиляторов C имеют возможность анализировать функции в стиле Паскаля.И все это означает, что параметры функции помещаются в стек слева направо.Если, например, printf был скомпилирован с параметром Pascal, то вывод, скорее всего, будет 1, 0 (однако, поскольку printf использует эллипс, я не думаю, что его можно скомпилировать в стиле Pascal).

C не гарантирует порядок вычисления параметров при вызове функции, поэтому при этом вы можете получить результаты «0 1» или «0 0».Порядок может меняться от компилятора к компилятору, и один и тот же компилятор может выбирать разные порядки на основе параметров оптимизации.

Безопаснее написать foo(test, test + 1), а затем выполнить ++test в следующей строке.В любом случае, компилятор должен оптимизировать его, если это возможно.

Компилятор может оценивать аргументы не в том порядке, в котором вы ожидаете.

Порядок вычисления аргументов функции не определен.В данном случае получается, что он делал их справа налево.

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

Хм, теперь, когда ФП был отредактирован для обеспечения единообразия, он не синхронизирован с ответами.Фундаментальный ответ о порядке оценки правильный.Однако конкретные возможные значения для foo(++test, test) различны;случай.

++тест воля перед передачей увеличивается, поэтому первый аргумент всегда будет равен 1.Второй аргумент будет равен 0 или 1 в зависимости от порядка вычисления.

Согласно стандарту C, неопределённым поведением является наличие более одной ссылки на переменную в одной точке последовательности (здесь вы можете думать об этом как об операторе или параметрах функции), где одна или несколько таких ссылок включают модификация до/после.Так:foo(f++,f) <--не определено, когда увеличивается f.И аналогично (я постоянно вижу это в пользовательском коде):*р = р++ + р;

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

Избегайте этого, включив предупреждения и обращая на них внимание.

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

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

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

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