Я задавал некоторые хитрые вопросы о C ++, и я работал над кодом, похожим на этот, затем я изменил его, чтобы посмотреть, что произойдет.
Я не понимаю, почему сначала эта рекурсия работает (печатает значения от 2 до 4764), а потом неожиданно выдает исключение.
Я также не понимаю, почему я могу сказать return in void function и фактически вернуть что-то отличное от return;
Кто-нибудь может объяснить это две проблемы?
#include<iostream>
using namespace std;
void function(int& a){
a++;
cout << a << endl;
return function(a);
}
void main() {
int b = 2;
function(b);
system("pause>0");
}
2 ответа
Комментарии правильно определили, что ваша бесконечная рекурсия вызывает переполнение стека - каждый новый вызов одной и той же функции занимает больше оперативной памяти, пока вы не израсходуете объем, выделенный для программы (размер стека C ++ по умолчанию сильно зависит от среды, и где-то от 10 кБ в старых системах до 10+ МБ в верхнем конце). Комментарии правильно определили, что ваша бесконечная рекурсия вызывает переполнение стека - объем пространства, выделенный для этой цели (размер стека C ++ по умолчанию сильно зависит от среды, и от 10 кБ на старых системах до 10+ МБ на верхнем конце). Хотя сама функция делает очень мало с точки зрения памяти, кадры стека (которые отслеживают, какая функция вызывала, какая другая текущая функция с какими параметрами) могут занимать довольно много.
Хотя рекурсивные программы полезны для определенных структур данных, они не должны проходить несколько тысяч уровней и обычно добавлять условие остановки (в данном случае даже проверяя, a > some_limit
) ли это, чтобы определить точку, в которой они углубились и нуждаются перестать добавлять больше вещей в стек (обычный return;
).
В этом случае точно такой же результат может быть достигнут с помощью простого цикла for
, поэтому я думаю, что эти хитрые вопросы являются чисто экспериментальными.
На платформах x86-64, таких как ваш ноутбук или настольный компьютер, функции вызываются одним из двух способов:
- с
call
инструкцией по сборке - с
jmp
инструкцией по сборке
В чем разница? Инструкция по сборке call
содержит дополнительные инструкции: после вызова функции код вернется в то место, откуда он был вызван. Чтобы отслеживать, где она находится, функция использует память в стеке. Если рекурсивная функция вызывает себя, используя call
, то при повторном обращении она будет использовать все больше и больше стека, что в конечном итоге приведет к переполнению стека.
С другой стороны, инструкция jmp
просто указывает процессору перейти к той части кода, где хранится другая функция. Если функция вызывает себя сама, то процессор просто jmp
вернется к началу функции и запустит ее заново с обновленными параметрами. Это называется оптимизацией хвостового вызова и во многих распространенных случаях полностью предотвращает переполнение стека, поскольку стек не растет.
Если вы скомпилируете свой код на более высоком уровне оптимизации (скажем, -O2
в GCC), тогда компилятор будет использовать оптимизацию хвостового вызова, и у вашего кода не будет переполнения стека.
Новые вопросы
c++
C ++ - это язык программирования общего назначения. Первоначально он был разработан как расширение C и имеет аналогичный синтаксис, но теперь это совершенно другой язык. Используйте этот тег для вопросов о коде (который будет скомпилирован с помощью компилятора C ++). Используйте тег, зависящий от версии, для вопросов, связанных с конкретной редакцией стандарта [C ++ 11], [C ++ 14], [C ++ 17] или [C ++ 20] и т. Д.