Подпрограммы - Язык Паскаль и начала программирования

Программирование: введение в профессию. 1: Азы программирования - 2016 год

Подпрограммы - Язык Паскаль и начала программирования

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

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

Опытные программисты знают, что экономия объёма кода — это не единственная причина для применения подпрограмм. Очень часто в программах встречаются такие подпрограммы, которые вызываются только один раз, что, конечно же, не только не сокращает объём написанного кода, но даже наоборот — увеличивает его, ведь оформление подпрограммы само по себе требует написания нескольких лишних строк. Такие “одноразовые” подпрограммы пишут, чтобы снизить сложность восприятия программы человеком. Правильным образом выделив фрагменты кода в подпрограммы и заменив их в основной программе их названиями, мы позволяем читателю нашей программы (и, кстати, в основном — самим себе) при работе с основной программой не думать о второстепенных деталях.

Паскаль предусматривает два вида подпрограмм: процедуры и функции. В принципе, такое деление во многом условно; в некоторых других языках программирования существуют только функции. Однако нам наличие процедур оказывается очень полезно: на их примере оказывается существенно проще объяснить общую идею подпрограмм для случая императивного программирования. Этим мы сейчас и займёмся.

2.4.1. Процедуры

Подпрограммы, будь то процедуры или функции, описываются в программе между её заголовком и главной программой, то есть в уже знакомом нам разделе описаний. Для создания первого впечатления о предмете приведём очень простой, хотя и странный пример: перепишем нашу самую первую программу hello, вынеся единственное действие, которое в ней есть, в процедуру. Выглядеть это будет так:

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

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

Вернёмся теперь к программе diamond из предыдущего параграфа и попробуем сделать её более понятной. Заметим для начала, что в программе несколько раз встречается цикл для печати заданного количества пробелов; вынесем это действие в процедуру. Поскольку процедура должна печатать любое количество пробелов, которое понадобится в программе, её придётся снабдить параметром; параметры подпрограмм в Паскале записываются в круглых скобках сразу после имени подпрограммы в виде списка, очень похожего на список описаний переменных. В нашем случае параметр будет всего один — количество пробелов, которое должно быть напечатано. Процедуру мы назовём PrintSpaces, а параметр — count; заголовок процедуры получится вот такой:

Вспомним теперь, что для печати заданного количества пробелов нам понадобится цикл for, а в нём будет нужна переменная типа integer в роли счётчика цикла. Про эту переменную можно точно сказать, что она никого и ничего не касается за пределами нашей процедуры, иначе говоря, это такая деталь реализации процедуры, которую не нужно знать, если только не писать и не редактировать саму нашу процедуру. Практически все существующие языки программирования, включая Паскаль, позволяют в таких случаях описывать локальные переменные, которые доступны (и вообще видны) только внутри одной подпрограммы. Для этого подпрограмма имеет свой собственный раздел описаний, который, как и раздел описаний главной программы, располагается между заголовком и основной частью, только теперь уже не программы, а подпрограммы.

Основная часть подпрограммы, как и основная часть главной программы, состоит из слова begin, последовательности операторов и слова end, только здесь после end ставится не точка, а точка с запятой. Целиком наша процедура будет выглядеть так:

Подчеркнём, что имена i и count в этой процедуре локальные, то есть они не оказывают никакого влияния на всю остальную программу: мы в любом месте можем, например, описывать переменные (и не только переменные) с такими же именами, причём, возможно, других типов, и ни к каким плохим последствиям это не приведёт.

Описав процедуру, мы можем теперь в любом месте программы вызвать её, написав что-то вроде PrintSpaces(k), и будет напечатано k пробелов.

Прежде чем переписывать программу diamond, припомним замечание, сделанное сразу после её написания — о том, что тела двух циклов в этой программе оказались совершенно одинаковыми и что делать так нельзя, но справиться с проблемой невозможно без подпрограмм. Подпрограммы теперь в нашем распоряжении есть, так что заодно исправим этот недостаток. Вспомним, что тела обоих циклов у нас печатали очередную, k-ю строку фигуры “полувысотой” n и делали это так: сначала напечатать n + 1 — k пробелов, потом звёздочку, далее, если k > 1, то напечатать 2k — 3 пробела и ещё одну звёздочку, и в конце перевести строку. Как видим, для выполнения этих шагов нужно знать две величины: k и n; их мы передадим в процедуру через параметры, а саму процедуру назовём PrintLineOfDiamond. После замены тел циклов вызовами этой процедуры главная программа станет совсем короткой; вся программа целиком будет выглядеть так:

Несмотря на обилие служебных строк (при описании каждой процедуры мы тратим минимум три лишние строки — на заголовок, на слово begin и на слово end) и пустых строк, которые мы для наглядности вставляем между подпрограммами, новый вариант программы всё же получился на четыре строки короче предыдущего, если же мы сравним длину их текстов в байтах, окажется, что мы сэкономили почти четверть объёма. Впрочем, столь скромная экономия обусловлена лишь примитивностью решаемой задачи; в более сложных случаях экономия за счёт использования подпрограмм может достигать десятков, сотен, тысяч раз, так что создание серьёзных программ без их разбиения на подпрограммы оказывается просто немыслимо.

Прежде чем двигаться дальше, отметим один технический момент. Как и при описании обычных переменных, в описании параметров подпрограммы можно параметры одного типа перечислить через запятую, указав для них тип один раз; именно так мы поступили при описании процедуры PrintLineOfDiamond. Если же нам нужны параметры различных типов, то тип указывается для них раздельно, а между описаниями параметров разных типов ставится точка с запятой. Например, если мы захотим усовершенствовать процедуру PrintSpaces так, чтобы она могла печатать не только пробелы, но и любые другие символы, мы можем нужный символ передать через параметр:

Здесь параметры ch и count имеют разные типы, так что в заголовке между их описаниями появилась точка с запятой.

2.4.2. Функции

Кроме процедур, Паскаль предусматривает также другой вид подпрограмм — так называемые функции; на идейном уровне они отличаются от процедур тем, что основной их задачей является (по крайней мере, исходно предполагается) вычисление некоторого значения. В остальном функции очень похожи на процедуры: они тоже состоят из заголовка, секции описаний и основной части, точно так же в них можно описывать локальные переменные и т. д.

Рассмотрим простейший пример — функцию, которая возводит число с плавающей точкой в куб. Выглядит это так:

Как видим, заголовок функции начинается с ключевого слова function, дальше, как и для процедуры, записывается имя и список параметров; после него для функции нужно (через двоеточие) указать тип возвращаемого значения, то есть, собственно говоря, тип того значения, для вычисления которого предназначена наша функция. В данном случае функция вычисляет число типа real.

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

где а и b — переменные типа real; в ходе вычисления выражения в правой части присваивания управление будет временно отдано функции Cube, причём параметр, который в ней называется х, получит значением результат вычисления b+3.5; в свою очередь, результат работы функции Cube будет использован в роли уменьшаемого при выполнении вычитания, а результат вычитания окажется занесён в переменную а.

Как и процедура, функция является подпрограммой, то есть фактически представляет собой обособленный фрагмент кода, которому при вызове временно передаётся управление; в принципе, никто не запрещает ей выполнять совершенно произвольные действия. Значение, которое функция в итоге “вернёт” (то самое значение, ради вычисления которого её вызывают), задаётся в коде функции оператором присваивания специального вида, у которого в левой части вместо имени переменной записано имя самой функции. Наша функция Cube из одного этого оператора и состоит, но бывают случаи более сложные. Например, хорошо известна последовательность чисел Фибоначчи, в которой первые два элемента равны единицам, а каждый последующий равен сумме двух предыдущих: 1, 1, 2, 3, 5, 8, 13, 21, 34,... Функция, вычисляющая число Фибоначчи по его номеру, могла бы выглядеть, например, так (учитывая быстрый рост этих чисел, мы будем использовать для их нумерации числа типа integer, а для работы с самими числами Фибоначчи — числа типа longint):

Здесь будут уместны некоторые пояснения. Основной алгоритм, реализованный в нашей функции, действует при условии, что номер, переданный в функцию через параметр n, не меньше единицы. На каждом шаге в переменных р, q и r хранятся два предыдущих числа Фибоначчи и текущее. Перед началом работы в q заносится 0, а в r — число 1, что соответствует числам Фибоначчи с номерами 0 и 1. Следует обратить внимание, что в переменной r теперь находится текущее число Фибоначчи, а номер текущего числа считается первым. Расположенный в последующих строках цикл “сдвигает” переменные р, q и r на одну позицию вдоль последовательности, для этого в р заносится то, что было в q (предпоследнее число становится предпредпоследним), в q заносится то, что было в r (последнее число становится предпоследним), а в r заносится сумма р и q, т. е. происходит вычисление очередного числа Фибоначчи. Иначе говоря, каждая итерация цикла увеличивает номер текущего числа на единицу, а само текущее число каждый раз оказывается в переменной r. Цикл выполняется столько раз, сколько нужно, чтобы номер текущего числа сравнялся со значением параметра n: если n равно единице, цикл не выполняется вообще, если двойке — цикл отрабатывает одну итерацию, и т. д. Полученное в итоге число г возвращается в качестве итогового значения функции.

Легко видеть, что всё это может работать лишь для значений параметра (номера числа) от единицы и более, поэтому случай n = 0 нам пришлось рассмотреть отдельно; именно для этого предусмотрен оператор if. В итоге наш пример позволяет проиллюстрировать ещё один важный момент: в теле функции может быть больше одного оператора присваивания, задающего значение, которое функция вернёт, но за каждый вызов функции сработать должен один и только один такой оператор.

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

Например, в программе рисования “алмаза” из звёздочек мы могли бы ради дальнейшего упрощения главной части программы вынести в функцию организацию диалога с пользователем. Напомним, что нам в итоге нужно число п, представляющее собой полувысоту фигуры, но от пользователя требуется ввести высоту целиком, причём так, чтобы это было положительное нечётное число. Напишем функцию, которая проведёт весь диалог, а вернёт искомую полувысоту:

Теперь первые шесть строк главной части программы можно заменить одним присваиванием:

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

2.4.3. Параметры-переменные

Параметры, которые мы применяли в наших подпрограммах до сих пор, иногда называют параметрами-значениями. Имя такого параметра, данное ему в заголовке подпрограммы, представляет собой фактически локальную переменную, в которую при вызове подпрограммы заносится значение, указанное в вызове (отсюда название “параметр-значение”). Например, если у нас есть процедура р с параметром х:

мы можем вызвать её, указав в точке вызова в качестве значения параметра произвольное выражение, лишь бы оно дало в результате целое число (любого целочисленного типа), например:

При таком вызове сначала будет вычислено значение выражения 2*а + 7; полученное число 37 будет занесено в локальную переменную (параметр) х процедуры р, после чего будет выполнено тело процедуры. Очевидно, что изнутри процедуры мы никак не можем повлиять на значение выражения 2*а + 7 и тем более на значение переменной а, о которой мы вообще ничего не знаем, находясь внутри процедуры. Мы можем, в принципе, присваивать новые значения переменной х (хотя некоторые программисты считают это плохим стилем), но по окончании выполнения процедуры переменная х просто исчезнет вместе со всей информацией, которую мы в неё записали.

Некоторую путаницу у начинающих вызывает тот факт, что в качестве значения параметра можно (то есть никто не мешает) указать имя переменной соответствующего типа:

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

Между тем в некоторых случаях удобно иметь возможность именно что изнутри подпрограммы изменить значение (значения) одной или нескольких переменных, находящихся в точке вызова. Например, это может потребоваться, если наша подпрограмма вычисляет больше одного значения, и все эти значения нужны вызывающему. Для передачи из подпрограммы к вызывающему одного значения мы можем описать функцию, как, например, мы это сделали для возведения в куб; но что делать, если нам хочется, к примеру, написать подпрограмму, которая для заданного числа вычисляет сразу его квадрат, куб, четвёртую и пятую степень? В этом простом примере можно указать “лобовое” решение проблемы: написать не одну функцию, а четыре; но если мы их все последовательно вызовем, в итоге будет выполнено десять умножений, тогда как для решения задачи их достаточно сделать всего четыре. В более сложных случаях “лобового” решения проблемы может не оказаться.

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

В заголовке подпрограммы перед описаниями параметров- переменных ставится слово var, которое действует до точки с запятой или закрывающей скобки; например, для процедуры с заголовком

параметры x, у и k будут параметрами-переменными, a z — параметром-значением.

Задачу с возведением в четыре степени можно с использованием параметров-переменных решить следующим образом:

Процедура Powers имеет пять параметров; при её вызове в качестве первого параметра можно указать произвольное выражение типа real21, а вот остальные четыре параметра требуют указания переменной, притом переменной типа real и никакого иного. Это ограничение вполне понятно: внутри процедуры Powers идентификаторы quad, cube, fourth и fifth будут синонимами того, что мы укажем в точке вызова соответствующими параметрами, а работа с ними внутри процедуры ведётся как с переменными типа real; если попытаться в таком качестве использовать переменную любого другого типа (то есть использующую другое машинное представление для хранения значения), на выходе мы получим полный хаос.

Правильный вызов такой подпрограммы мог бы выглядеть так:

В результате работы такого вызова в переменные р, q, r и t будут занесены вторая, третья, четвёртая и пятая степени числа 17.5.

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

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

2.4.4. Локальные и глобальные переменные

Как мы уже отмечали, переменная, описанная в секции описаний подпрограммы, видна только в самой этой подпрограмме и больше нигде; такие переменные называются локальными в подпрограммах. В противоположность этому переменные, описанные в программе вне подпрограмм, видны начиная от того места, где они описаны, и до конца текста программы. Если ваша переменная будет использоваться только в главной части программы, лучше всего описать её непосредственно перед началом главной части22, чтобы только там она и была видна; такие переменные можно с некоторой натяжкой назвать локальными переменными главной части программы.

image14 Если переменную описать раньше, то она будет видна во всех подпрограммах, описанных после неё. Такие переменные называются глобальными. Теоретически можно использовать глобальные переменные для передачи информации в подпрограмму и из неё: например, мы можем перед вызовом подпрограммы присвоить значение какой-нибудь глобальной переменной, а подпрограмма это значение использует, и наоборот, подпрограмма может занести значение в глобальную переменную, а тот, кто подпрограмму вызвал, это значение из переменной извлечёт. Больше того, через глобальные переменные можно (теоретически) организовать связь между разными подпрограммами: к примеру, мы вызываем сначала одну подпрограмму, потом другую, и первая что-то заносит в глобальные переменные, а вторая этим пользуется. Так вот, делать так не следует. Насколько это возможно, всю связь с подпрограммами нужно осуществлять через параметры: передавать информацию в подпрограммы через параметры-значения, а из подпрограмм “наружу” всю информацию передавать в виде значений, возвращаемых функциями, и при необходимости — через параметры-переменные.

Основная причина тут кроется, как это часто бывает, в особенностях восприятия программы человеком. Если работа процедуры или функции зависит только от её параметров, в целом гораздо легче себе представить работу программы и понять, что происходит; если же в дело вмешиваются значения глобальных переменных, то о них придётся помнить, то есть каждый раз, глядя на вызов подпрограммы с какими-либо параметрами, нужно будет принимать во внимание, что её работа будет зависеть ещё и от значений в каких-то глобальных переменных; параметры в точке вызова чётко видны, чего о глобальных переменных никак не скажешь. Кроме того, к глобальным переменным доступ может оказаться возможен из самых разных мест программы, так что, например, обнаружив во время отладки, что кто-то когда-то успел “загнать” в глобальную переменную такое значение, которого там никто не ожидал, вы можете потратить очень много времени, выясняя, в каком же месте вашей программы это произошло; в особенности это оказывается актуально при создании больших программ, над которыми работают одновременно несколько программистов.

Можно назвать ещё одну причину, по которой глобальных переменных следует по возможности избегать. Всегда существует вероятность того, что объект, который в настоящее время в вашей программе один, потребуется “размножить”. Например, если вы реализуете игру и в вашей реализации имеется игровое поле, то весьма и весьма вероятно, что в будущем вам может понадобиться два игровых поля. Если ваша программа работает с базой данных, то можно (и нужно) предположить, что рано или поздно потребуется открыть одновременно две или больше таких баз данных (например, для изменения формата представления данных). Ряд примеров можно продолжать бесконечно. Если теперь предположить, что информация, критичная для работы с вашей базой данных (или игровым полем, или любым другим объектом) хранится в глобальной переменной и все подпрограммы завязаны на использование этой переменной, то совершить “метапереход” от одного экземпляра объекта к нескольким у вас не получится.

2.4.5. Рекурсия

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

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

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

Мы приведём совсем простой пример рекурсии. В § 2.4.1 мы написали процедуру PrintChars, которая печатала заданное количество одинаковых символов; сам символ и нужное их количество передавалось через параметры. Эту процедуру можно реализовать с использованием рекурсии вместо цикла. Для этого необходимо заметить, что, во-первых, случай, когда нужное количество символов равно нулю — вырожденный, при котором делать ничего не надо; во-вторых, если случай попался не вырожденный, то напечатать n символов — это то же самое, что напечатать сначала один символ, а потом (n — 1) символов, при этом задача “напечатать (n — 1) символов” вполне годится в качестве “чуть-чуть более простого” случая той же самой задачи. Рекурсивная реализация будет выглядеть так:

Как видим, для случая, когда печатать нечего, наша процедура ничего и не делает, а для всех остальных случаев она печатает один символ, так что остаётся напечатать на один символ меньше; для печати оставшихся символов процедура использует сама себя. Например, если сделать вызов PrintChars(’*’, 3), то процедура напечатает звёздочку и сделает вызов PrintChars(’*’, 2); “новая ипостась” процедуры напечатает звёздочку и вызовет PrintChars(’*’, 1), которая напечатает звёздочку и вызовет PrintChars(’*’, 0); этот последний вызов ничего делать не станет и завершится, предыдущий тоже завершится, и тот, что перед ним, тоже, и, наконец, завершится наш исходный вызов. Звёздочка, как легко видеть, будет напечатана трижды.

Часто оказывается полезным использование обратного хода рекурсии, когда подпрограмма сначала делает рекурсивный вызов, а потом выполняет ещё какие-то действия. Пусть, например, у нас имеется задача напечатать (разделив для наглядности пробелами) цифры, составляющие десятичное представление заданного числа. Отщепить от числа младшую цифру проблем не составляет: это остаток от деления на 10. Все остальные цифры числа можно извлечь, если повторить тот же процесс для исходного числа, разделённого на десять с отбрасыванием остатка. Базисом рекурсии может выступить случай нуля: мы в этом случае не будем ничего печатать. Если нужно обязательно напечатать ноль, получив число 0, то этот особый случай можно рассмотреть, написав другую процедуру, которая для нулевого аргумента будет печатать ноль, а для любого другого числа — вызывать нашу процедуру. Если написать что-то вроде

и вызвать её, например, так: PrintDigitsOfNumber(7583), то напечатано будет не совсем то, чего мы хотим: “3 8 7 5”. Цифры-то правильные, только порядок обратный. Оно и понятно, мы же самой первой “отщепили” младшую цифру и сразу же её напечатали, и так далее, вот они и получились напечатанными справа налево. Но проблема решается всего одним небольшим изменением:

Здесь мы поменяли местами рекурсивный вызов и оператор печати. Поскольку возвраты из рекурсии происходят в порядке, обратном заходу в рекурсию, теперь цифры будут напечатаны в порядке, обратном тому, в котором их “отщепляли”, то есть для того же самого вызова напечатано будет “5 7 8 3”, что нам и требовалось.

Рекурсивными бывают не только процедуры, но и функции. Например, в следующем примере функция ReverseNumber вычисляет число, полученное из исходного “переворотом задом наперёд” его десятичной записи, а сама рекурсия происходит во вспомогательной функции DoReverseNumber:

Например, ReverseNumber(752) вернёт число 257, a ReverseNumber(12345) вернёт 54321. Как это работает, предлагаем читателю разобраться самостоятельно.






Для любых предложений по сайту: [email protected]