2008-09-19 10 views
15

Можно создать дубликат:
Pre & post increment operator behavior in C, C++, Java, & C#Сообщение прибавка поведение оператора

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


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

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

+0

Возможно, ваше описание/пример должно полностью включать вопрос названия? – 2008-09-19 00:18:52

+0

Неверный заголовок и пример кода – itj 2008-09-19 08:02:24

+0

Вопрос путается между заголовком и примером кода. title имеет ++ n пример имеет тест ++ – itj 2008-09-19 08:05:16

ответ

47

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

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

В целом, выполнение побочных эффектов в аргументах - плохая практика программирования.

Вместо foo (test ++, test); вы должны написать foo (test, test + 1); тест ++;

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

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

+0

В качестве дополнительного акцента, чтобы избежать подобных проблем, у меня всегда есть приращения как отдельное утверждение. – 2008-09-19 01:17:18

1

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

14

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

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

Here - хорошее описание точек последовательности и неуказанного поведения.

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

"++" бит "тест ++" запускается на выполнение после вызова 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()

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

1

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

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

2

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

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

6

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

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

Возьмите PRINTF в:

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

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

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

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

29

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

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

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

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

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

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

int dummy; 
dummy=test++,test; 

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

1

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

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

1

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

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

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

1

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

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

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