Не могу понять, почему эта простая рекурсия работает и падает

Я задавал несколько каверзных вопросов о C++ и запускал аналогичный код, а затем модифицировал его, чтобы посмотреть, что произойдет.

Я не понимаю, почему сначала эта рекурсия работает (она печатает значения от 2 до 4764), а потом вдруг выдает исключение.

Я также не понимаю, почему я могу сказать «возврат» в функции void и на самом деле вернуть что-то, кроме «возврата»;

Кто-нибудь может объяснить эти две проблемы?

#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");
}

Переполнение стека;

Amadeus 31.05.2019 00:19

С++ не использует хвостовую рекурсию, поэтому каждый вызов представляет собой совершенно новый кадр стека, помещаемый в стек, в конечном итоге который выйдет из строя после того, как стек вырастет до некоторой точки, где произойдет что-то плохое, зависящее от реализации.

Grady Player 31.05.2019 00:21

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

Havenard 31.05.2019 00:21
system("pause>0"); выглядит ущербно
Stack Danny 31.05.2019 00:23

Вы не добавили условие остановки рекурсии. Вот почему стек будет переполняться.

Cristián Ormazábal 31.05.2019 00:24

По второму вопросу: en.cppreference.com/w/cpp/language/returnIn a function returning void, the return statement with expression can be used, if the expression type is void.

user2956272 31.05.2019 00:27

"Я также не понимаю, почему я могу сказать «возврат» в функции void и на самом деле вернуть что-то, кроме «возврата»;" См. этот вопрос: Могу ли я вернуться в недействительной функции?

Yksisarvinen 31.05.2019 00:27

Какое исключение выдает?

Eljay 31.05.2019 00:33

Если вы скомпилируете его с -O2 (или выше) на любом современном компиляторе, он, вероятно, выполнит оптимизацию хвостового вызова и превратит ваш рекурсивный вызов в цикл. Затем он должен работать вечно без переполнения стека.

HAL9000 31.05.2019 00:37

@GradyPlayer решает компилятор, выполнять ли оптимизацию хвостовой рекурсии. Стандарт этого не запрещает.

M.M 31.05.2019 03:22

Даже без переполнения стека программа вызывает неопределенное поведение из-за переполнения целого числа со знаком.

M.M 31.05.2019 03:23
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
11
216
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

В комментариях правильно указано, что ваша бесконечная рекурсия вызывает переполнение стека - каждый новый вызов одной и той же функции занимает больше оперативной памяти, пока вы не израсходуете объем, выделенный для программы (размер стека С++ по умолчанию сильно зависит от среды, и где-то от 10 кБ в старых системах до 10+ МБ на верхнем конце). В комментариях правильно указано, что ваша бесконечная рекурсия вызывает переполнение стека - объем пространства, выделенный для этой цели (размер стека С++ по умолчанию сильно зависит от среды, и где-то от 10 кБ на старых системах до 10+ МБ на верхнем конце). В то время как сама функция использует очень мало памяти, кадры стека (которые отслеживают, какая функция вызвала какую другую текущую функцию с какими параметрами) могут занимать довольно много.

Хотя это полезно для определенных структур данных, рекурсивные программы не должны углубляться в несколько тысяч слоев и обычно добавляют условие остановки (в данном случае даже проверку того, является ли a > some_limit), чтобы определить точку, в которой они ушли вглубь и нужно прекратить добавлять больше. вещи в стек (обычный return;).

В этом случае точно такой же результат можно получить с помощью простого цикла for, поэтому я думаю, что эти каверзные вопросы чисто экспериментальные.

На платформах x86-64, таких как ваш ноутбук или настольный компьютер, функции вызываются одним из двух способов:

  • с call инструкцией по сборке
  • с jmp инструкцией по сборке

Какая разница? После ассемблерной инструкции call идут дополнительные инструкции: при вызове функции код вернется туда, откуда был вызван. Чтобы отслеживать, где он находится, функция использует память в стеке. Если рекурсивная функция вызывает себя с помощью call, то по мере рекурсии она будет использовать все больше и больше стека, что в конечном итоге приведет к переполнению стека.

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

Если вы скомпилируете свой код на более высоком уровне оптимизации (скажем, -O2 в GCC), то компилятор будет использовать оптимизацию хвостового вызова, и в вашем коде не будет переполнения стека.

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