Или в Java или в c я могу записать функцию как
fun(){
fun();
}
(игнорирование деталей синтаксиса)
В Java я получаю исключение OutOfMemory, но в C (и возможно некоторые другие языки) это, кажется, работает навсегда, как будто это был бесконечный цикл. Почему я не получаю ошибку OutOfMemory здесь также?
Поскольку ваша функция является примером хвостовой рекурсии , то, скорее всего, компилятор C оптимизирует рекурсию до итерации, заставляя ее зацикливаться бесконечно без сбоев.
Другие ответчики правы, что есть некоторая магия компилятора, которая преобразует хвостовую рекурсию в итерацию, хотя это зависит от оптимизации настройки компилятора. Например, в gcc, если мы скомпилируем с помощью gcc -S -O1 someFile.c
(учитывая ваш код), мы получим следующую сгенерированную сборку:
fun:
.LFB2:
pushq %rbp
.LCFI0:
movq %rsp, %rbp
.LCFI1:
movl $0, %eax
call fun
leave
ret
.LFE2:
.size fun, .-fun
Итак, вы можете видеть, он все еще использует call / leave / ret инструкции для выполнения фактического вызова функции, которая убьет процесс. Как только вы начнете оптимизацию с помощью gcc -S -O2 someFile.c
, мы начнем получать волшебство:
fun:
.LFB24:
.p2align 4,,10
.p2align 3
.L2:
jmp .L2
.LFE24:
.size fun, .-fun
.p2align 4,,15
Это зависит от вашего компилятора и настроек компилятора, поэтому дружить с ними помогает.
Причина в том, что компилятор C, вероятно, рассматривает это как хвостовой рекурсивный вызов и, следовательно, избегает построения стека для выполнения функции. Поскольку стек не создается для этого вызова, он превращается из рекурсии в простой бесконечный цикл. Вы можете заставить ее создавать стек, сделав ее головной рекурсивной
int fun() { 1 + fun(); }
Как отмечали другие, это оптимизация рекурсии хвостового вызова, выполненная компилятором языка Си. Как всегда, помогает конкретный пример. Без включенных оптимизаций gcc (v3.4.6) выдает следующий ассемблерный код x86:-
fun:
pushl %ebp
movl %esp, %ebp
call fun
leave
ret
.size fun, .-fun
Обратите внимание на рекурсивный вызов fun(). Если он будет выполнен, то в конце концов переполнится стек и произойдет крах, но при -O2 gcc выдает:-
fun:
pushl %ebp
movl %esp, %ebp
.L2:
jmp .L2
.size fun, .-fun
Обратите внимание на бесконечный цикл без инструкции возврата? Он просто будет выполняться вечно.
В C это можно было бы оптимизировать как хвостовой рекурсивный вызов. Таким образом, вызов fun ()
на самом деле не вызывает себя; он просто перезапускает функцию (как goto). Другими словами, компилятор рассматривает его так, как если бы он был написан следующим образом:
void fun() {
start:
goto start;
}
Итак, стек не будет увеличиваться.
Если реализация языка программирования имеет оптимизацию хвостовых вызовов, то она скомпилирует эту рекурсию в цикл. Текущая Java VM не имеет оптимизации хвостовых вызовов, поэтому это закончится java.lang.StackOverflowError.
Возможно, когда-нибудь в будущем Java VM будет иметь оптимизацию хвостовых вызовов, потому что функциональные языки программирования, которые работают на JVM (Scala, Clojure и т.д.), выиграют от этого (сейчас, по крайней мере, компилятор Scala делает свою собственную оптимизацию хвостовых вызовов для прямой рекурсии, но, AFAIK, не для косвенной рекурсии).
Ваш компилятор c, вероятно, использует хвостовую рекурсию. Каждый раз, когда вы входите в новую функцию, компьютер добавляет запись в стек. Эта запись указывает, куда ЦП должен вернуться после выполнения процедуры. Теперь в случае, приведенном выше, поскольку вызов fun () внутри fun () является последним вызовом в функции, компилятор c может оптимизировать выталкивание стека и вместо этого создавать хвостовой вызов. Фактически вы можете использовать это для создания цикла:
int foo(int from, int to)
{
if (from == to) return from;
dosomething();
from ++;
foo(from, to);
}
Многие языки (например, Erlang) вообще не имеют циклов и вместо этого используют описанный выше метод для создания циклов.
Java не поддерживает хвостовую рекурсию.
Вы получите аномальное завершение программы через некоторое время. Ваш код содержит неопределенную рекурсию, и каждый вызов fun() помещает в стек дополнительные байты. В зависимости от размера вашей памяти и ваших ограничений, приложение завершится после, по крайней мере, 500 вызовов. Это может занять некоторое время, но вы получите исключительное завершение.
ВМ java ограничивает глубину рекурсии до некоторого уровня, поэтому она завершится в ближайшее время.