LINUX.ORG.RU

рекурсия, не понял, как работает

 , ,


0

1

в общем, читал про рекурсию и запомнил, что в одном случае, если нам надо подсчитать кол-во чего-либо, то в одном случае мы передаем в ф-цию, какое-то значение, а в другом случае накапливаем счетчик как-то так return recursion() + 1; написал вот такую ф-цию, работает

int a_counter(void)
{
	char c;
	if ((c = getchar()) == '.') {
		return 0;
	} else if (c == 'a') {
		return a_counter() + 1;
	} else {
		a_counter();
	}
}

я не понимаю, вот чего: допустим мы ввели строку qaz. когда мы выходим из вызова, который получил a, то возвращаем 1, затем мы попадаем в вызов, который получил q, как из этого вызова возвращается 1 в main?



Последнее исправление: IvanRia (всего исправлений: 1)

В том коде никак не возвращается. Если оно возвращается в реале, то это наверняка UB. Лично я не знаю все UB «простой» сишки)

Да, должно быть в конце return a_counter();

goingUp ★★★★★
()
Последнее исправление: goingUp (всего исправлений: 1)

Никак. У тебя функция в принципе не может ничего вернут в main кроме нуля. upd. ну или навернуть стек при громадной длине строки без точек.

SkyMaverick ★★★★★
()
Последнее исправление: SkyMaverick (всего исправлений: 1)

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

mittorn ★★★★★
()
Ответ на: комментарий от IvanRia

Нет, эта ветка if выполнится, если символ не . и не а.

goingUp ★★★★★
()

Если можно сделать без рекурсии, то сделай без.

} else {
    a_counter();
}

Это wtf код. Даже если работает, то ревью не пройдёт.

ox55ff ★★★★★
()
Ответ на: комментарий от ox55ff

Если можно сделать без рекурсии, то сделай без.

это учебная задача, я не люблю рекурсию и никогда ее не использую

IvanRia
() автор топика
Ответ на: комментарий от IvanRia

я не люблю рекурсию и никогда ее не использую

антихриста на тебя нет… ))

yyk ★★★★★
()

рекурсия хороша при небольшой глубине - при парсинге деревьев, всяких там бисекций и проч.

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

alysnix ★★★
()
Последнее исправление: alysnix (всего исправлений: 1)
Ответ на: комментарий от alysnix

Рекурсивный код не обязательно должен/будет давать оверхед в рантайме

Интересующихся посылаю гуглить «рекурсивные схемы»

Но в теме про язык Си это оффтоп, тут лучше не мудрить

Crocodoom ★★★★★
()

я не понимаю, вот чего: допустим мы ввели строку qaz. когда мы выходим из вызова, который получил a, то возвращаем 1, затем мы попадаем в вызов, который получил q, как из этого вызова возвращается 1 в main?

а ты не пиши код который не понимаешь. там в последней ветке должно стоять return сounter_a();

это просто наведенный эффект, что все хорошо возвратилось. Возврат видимо был через регистр и там лежало значение от предыдущего возврата.

alysnix ★★★
()

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

return …

поскольку код может сложным и запутаным, то компилятору непросто установить, что ты возвращаешь результат. для этого компилятору приходится делать анализ графа исполнения кода функции и смотреть нет ли там выхода из функции без «return expression».

у тебя такой выход есть. это последняя ветка if. вот и правь ее, это и снимет твой вопрос.

у gcc варнинг -Wreturn-type дает предупреждение что нет возврата. кажется входит в группу варнингов -Wall. Всегда используй ее при компиляции

alysnix ★★★
()
Последнее исправление: alysnix (всего исправлений: 2)
Ответ на: комментарий от mittorn

Нет, это совсем даже не случайность. И хотя такой код действительно может создавать проблемы, но в большинстве случаев он как раз будет эквивалентен return a_counter(); по очевидной причине.

А именно: вложенный a_counter() уже сделал return и записал значение в то место, куда записывают возвращаемое значение. Этот a_counter() не сделал никакого return и соответственно оставил возвращённое значение как есть, передав его наружу как своё. Но рассчитывать на это, конечно, не стоит, по нормальному тут должна была бы быть ошибка компиляции, но компиляторы традиционно разрешают такое.

firkax ★★★★★
()
Ответ на: комментарий от firkax

но компиляторы традиционно разрешают такое

Я в своём проекте ловил краши из-за забытого return. С тех пор всегда добавляю опцию, чтобы компилятор ронял сборку в этом случае. Вообще непонятно почему это не по дефолту.

ox55ff ★★★★★
()
Ответ на: комментарий от ox55ff

Потому что раньше не додумались до атрибута noreturn (для функций типа exit), и вроде бы даже сейчас нет красивого способа сообщить компилятору про unreachable код явно (например после switch-а который перебрал все возможные варианты, а других быть не должно, по сути assert). А без этих вещей будут ложные срабатывания, мешающие программисту и вынуждающие его ставить лишние фейковые return-ы, замусоривая код.

firkax ★★★★★
()
Ответ на: комментарий от alysnix

вот теперь понял, как работает, спасибо

IvanRia
() автор топика
Ответ на: комментарий от alysnix

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

Не отрывают. Компиляторы давно могут в хвостовую рекурсию.

hateyoufeel ★★★★★
()
Ответ на: комментарий от hateyoufeel

Компиляторы давно могут в хвостовую рекурсию.

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

alysnix ★★★
()
Ответ на: комментарий от alysnix

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

hateyoufeel ★★★★★
()
Последнее исправление: hateyoufeel (всего исправлений: 1)
Ответ на: комментарий от beastie

Классика, моё увожение. «Чтобы понять рекурсию - нужно понять рекурсию» (c)

Pinkbyte ★★★★★
()
Ответ на: комментарий от X512

Современные компиляторы любят начинать выполнять следующую функцию если return забыли.

Это если для компиляции C++-компилятор использовать, а не чисто C-шный.

EXL ★★★★★
()
Ответ на: комментарий от ox55ff

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

firkax ★★★★★
()
Ответ на: комментарий от firkax

Я уже писал выше поо первый регистр. Но то, что регистр не перезаписан чем-то ещё - случайность. Могла быть другая архитектура с другим соглашением вызова

mittorn ★★★★★
()
Ответ на: комментарий от mittorn

Нет, не так.

  1. Регистр может быть и не первым а любым, главное чтобы он был одинаковым (а он тут обязательно будет одинаковым: одинаковый компилятор, тем более одинаковый тип возврата, и ещё тем более одна и та же функция).
  2. Перезаписывать его некому, потому что после вызова рекурсивной функции там ничего нет, остаться может только ассемблерный эпилог;
  • на современных компиляторах его тут скорее всего не будет;
  • эпилог (если он есть) обычно рассчитан на то, что он будет вызываться после подготовки return value, и соответственно он его портить не будет, а тем более в этой функции, у которой return value есть, эпилог будет именно с учётом его (если они вдруг разные для int и void функций, в чём я сомневаюсь);
  • эпилог на x86 (пусть это будет частный случай для наглядного примера, хотя я склонен считать это дефолтом) это что-то из POP reg (не EAX) / MOV ESP,EBP / ADD ESP,imm и опять же трогать другие регистры ему незачем; хотя я видел проги где какая-то валидация/обфускация стека включена и там эпилог сложнее, но EAX там тоже не трогался, по причине что return value где-то всё-таки есть и делать разные эпилоги для функций с ним и без него незачем.

Итого, вероятность того, что return value будет испорчено, около нуля, случайностью это никак не назвать. Можно лишь сказать что никто совершенно не гарантирует что всё будет хорошо, но обычно так (всё хорошо) и есть.

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 1)
Ответ на: комментарий от firkax

Итого, вероятность того, что return value будет испорчено, около нуля, случайностью это никак не назвать.

назвать.

у него просто один неверный возврат (без результата) сразу следует за верным возвратом с результатом в регистре. но если между этими возвратами будет еще какой-то его код, использующий этот регистр - то результат будет испорчен. короче, у него просто код удачный, что не успел испортить результат предыдущего возврата.

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

alysnix ★★★
()
Ответ на: комментарий от alysnix

у него просто один неверный возврат (без результата) сразу следует за верным возвратом с результатом в регистре. но если между этими возвратами будет еще какой-то его код, использующий этот регистр - то результат будет испорчен.

Эм, разумеется. Только это не «код удачный», а все мои рассуждения касались именно случая «вызов в конце с забытым return». Если ты забудешь return в середине то там гарантированно будет не то что нужно - ведь код дальше вообще не должен был выполняться, и ассемблерные фокусы тут будут уже ни при чём.

опять же результат можно возвращать и через стек например(теоретически)

Я почти уверен что платформ (не самодельных одного анонимуса), где int возвращается через стек, не существует.

----------- Кстати, пример можно расширить и передавать вовращаемое значение через протекающую абстракцию void-функции:

#include <stdio.h>

int f1(void) {
  return 42;
}

void f2(void) {
  f1();
}

int f3(void) {
  if(0) return 123;
  f2();
}

int main(void) {
  printf("f3 = %d\n", f3());
  return 1;
}

Правда если включить хотя бы -O1 то компилятор начинает тут всё инлайнить и портит фокус (причём с int f2() тоже портит). Но это из-за того что функции слишком простые.

Вот такое работает даже с -O2

#include <stdio.h>

void a_wrapper(void) { a_counter(); }
int a_counter(void)
{
    char c;
    if ((c = getchar()) == '.') {
	return 0;
    } else if (c == 'a') {
	return a_counter() + 1;
    } else {
	a_wrapper();
    }
}

int main(void) {
  printf("%d\n", a_counter());
  return 0;
}

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 3)
Ответ на: комментарий от firkax

Вот и подтверждение тому, что это случайность. Оптимизация может просто выкинуть возврат.
Никогда не полагайся на UB
Вот пример, к чему оно может привести в результате оптимизаций:
https://habr.com/ru/company/infopulse/blog/338812/

mittorn ★★★★★
()
Ответ на: комментарий от mittorn

Я не пойму, ты споришь со мной или что? Я в каждом своём сообщении так или иначе уточнял, что не стоит на это полагаться, и что от этого могут быть проблемы. И нигде не писал, что оно работает на 100%. Но всё же, то, что оно работает - это не случайное совпадение (как когда у меня одна прога много лет правильно работала, несмотря на наличие 0*strlen(NULL) в ней, с выкинутым оптимизатором strlen-ом) , а закономерность, хоть и ненадёжная.

firkax ★★★★★
()
Ответ на: комментарий от ox55ff

Если можно сделать без рекурсии, то сделай без.

Любую рекурсивную функцию можно реализовать итерационно и наоборот.

yurikoles ★★★
()
Ответ на: комментарий от firkax

Согласен. Так оно и есть. Требует понимания ABI.

ПыСы: меня тут как-то, не то чтобы загнобили, но потрепали так скажем, за истинное утверждение о том что в C89 return; из функции возвращающей int вполне себе legit, и понятно почему.

bugfixer ★★★★★
()
Ответ на: комментарий от IvanRia

Можно конечно. Рекурсия - это просто цикл с сохранением некоторой части локальных переменных в стеке.

khrundel ★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.