Несомненно, все программисты знают что использование выражений, подобных тому что приведено в заглавии поста, не то что нежелательно, а строго противопоказано. Такие конструкции, поведение компилятора в которых не определено, могут принести множество трудноуловимых ошибок и нежелательных последствий. Но уверен, многим начинающим программистам хотелось бы по глубже понять эту проблему и, заглянув за ширму компилятора, узнать что же именно происходит в таких случаях. Изучению одного из примеров подобного кода я и посвящаю этот пост. Добро пожаловать под кат :)
Для примера, я разберу работу выражения "
Воспользовавшись дизассемблером, посмотрим что-то же в итоге сгенерировал компилятор Си:
Как видно из листинга, компилятор отобразил переменную
С помощью Ildasm посмотрим что скрывается за C#:
Для наглядности, я добавил комментарии к ассемблеру виртуальной стековой машины. Смотря на листинг, можно увидеть что для инкремента переменной
Вот такой вот, с виду одинаковый, код выполняется совершенно по разному в различных условиях. Избегайте подобного кода в своих программах.
Если тема вам показалась интересной — дайте знать, и я напишу еще пару занятных моментов из мира компиляции ;)
UPD. В комментариях подсказывают что в РНР, Java, Actionscript 3, JavaScript и TCL результат тоже равен 13, а вот в Perl, K++ и C GCC — 14 (но результат GCC может зависеть от настроек оптимизатора) :)
Отличились PL/SQL и Python — 10 (как подсказывает К.О. — из-за отсутствия инкрементов в языке), однако Bash — 12.
Так же есть компиляторы, непозволяющие написание подобного кода. Например Ruby (или вот так).
А каким будет результат в твоем, %username%, компиляторе?
UPD2. Ссылки по теме: точки следования Википедия, Алёна C++.
Объект исследования
Для примера, я разберу работу выражения "
++i + ++i
" на двух различных языках: С и С#. Первый, как известно, компилируется в нативный код процессора, а второй, если грубо, работает на основе виртуальной стековой машины. И так, рассмотрим сами примеры:Исходник на Си:
- #include <stdio.h>
- void main()
- {
- int i = 5;
- i = ++i + ++i;
- printf("%d\n",i);
- }
И исходник на C#:
- using System;
- public class Test
- {
- public static void Main()
- {
- int i = 5;
- i = ++i + ++i;
- Console.WriteLine(i);
- }
- }
За ширмой Си
Воспользовавшись дизассемблером, посмотрим что-то же в итоге сгенерировал компилятор Си:
#A#5: int i = 5; cs:0295 BE0500 mov si,0005 #A#6: i = ++i + ++i; cs:0298 46 inc si cs:0299 46 inc si cs:029A 8BC6 mov ax,si cs:029C 03C6 add ax,si cs:029E 8BF0 mov si,ax #A#7: printf("%d\n",i); cs:02A0 56 push si cs:02A1 B8AA00 mov ax,00AA cs:02A4 50 push ax cs:02A5 E8330C call _printf
Как видно из листинга, компилятор отобразил переменную
i
на регистр SI
процессора x86
. После чего, дважды инкрементировав этот регистр, прибавил его самого к себе через аккумулятор AX
. В результате, переменная i
становится равной 14.За ширмой C#
С помощью Ildasm посмотрим что скрывается за C#:
.method public hidebysig static void Main() cil managed { .entrypoint // Code size 21 (0x15) .maxstack 3 .locals init (int32 V_0) IL_0000: ldc.i4.5 // push 5 5 IL_0001: stloc.0 // i := pop() null IL_0002: ldloc.0 // push i i IL_0003: ldc.i4.1 // push 1 i, 1 IL_0004: add // push (pop() + pop()) (i+1) т.е. 6 IL_0005: dup // copy вершины стека 6, 6 IL_0006: stloc.0 // i := pop() // i := 6 6 IL_0007: ldloc.0 // push i 6, i IL_0008: ldc.i4.1 // push 1 6, i, 1 IL_0009: add // push (pop() + pop()) 6, (i+1) т.е. 7 IL_000a: dup // copy вершины стека 6, 7, 7 IL_000b: stloc.0 // i := pop() // i := 7 6, 7 IL_000c: add // push (pop() + pop()) 13 IL_000d: stloc.0 // i := pop() null IL_000e: ldloc.0 // push i i IL_000f: call void [mscorlib]System.Console::WriteLine(int32) IL_0014: ret } // end of method Test::Main
Для наглядности, я добавил комментарии к ассемблеру виртуальной стековой машины. Смотря на листинг, можно увидеть что для инкремента переменной
i
в стек помещается сама переменная и единица. Затем выполняется команда сложения которая, взяв два значения из стека и сложив, помещает результат опять в стек. Затем происходит дублирование вершины стека и запись значения обратно в переменную. Таким образом в стеке остается 5 + 1
, т.е. 6. Далее цикл повторяется для другого инкремена: заносится в стек переменная, вслед за ней — единица, происходит сложение, дублирование вершины и запись результата второго инкремента назад в переменную. Теперь уже в i
будет 7, а в стеке останутся 6 от первого случая и 7 от второго. Затем выполняется команда сложения и результат, равный теперь 13, заносится в переменную.Итоги
Вот такой вот, с виду одинаковый, код выполняется совершенно по разному в различных условиях. Избегайте подобного кода в своих программах.
Если тема вам показалась интересной — дайте знать, и я напишу еще пару занятных моментов из мира компиляции ;)
UPD. В комментариях подсказывают что в РНР, Java, Actionscript 3, JavaScript и TCL результат тоже равен 13, а вот в Perl, K++ и C GCC — 14 (но результат GCC может зависеть от настроек оптимизатора) :)
Отличились PL/SQL и Python — 10 (как подсказывает К.О. — из-за отсутствия инкрементов в языке), однако Bash — 12.
Так же есть компиляторы, непозволяющие написание подобного кода. Например Ruby (или вот так).
А каким будет результат в твоем, %username%, компиляторе?
UPD2. Ссылки по теме: точки следования Википедия, Алёна C++.