В компьютерном программировании функция (также процедура , метод , подпрограмма , рутина или подпрограмма ) — это вызываемая единица [1] программной логики , которая имеет четко определенный интерфейс и поведение и может быть вызвана несколько раз.
Вызываемые единицы предоставляют мощный инструмент программирования. [2] Основная цель — обеспечить разложение большой и/или сложной проблемы на части, которые имеют относительно низкую когнитивную нагрузку , и назначить частям осмысленные имена (если они не анонимны). Разумное применение может снизить стоимость разработки и поддержки программного обеспечения, одновременно повышая его качество и надежность. [3]
Вызываемые единицы присутствуют на нескольких уровнях абстракции в среде программирования. Например, программист может написать функцию в исходном коде , которая компилируется в машинный код, реализующий схожую семантику . В исходном коде есть вызываемая единица и связанная с ней единица в машинном коде, но это разные виды вызываемых единиц — с разными последствиями и функциями.
Значение каждого вызываемого термина (функция, процедура, метод, ...) на самом деле различно. Они не являются синонимами . Тем не менее, каждый из них добавляет возможность к программированию, которая имеет общее.
Используемый термин, как правило, отражает контекст, в котором он используется – обычно на основе используемого языка. Например:
Sub
, сокращение от subroutine или subprocedure , является именем вызываемой функции, которая не возвращает значение, тогда как a Function
возвращает значение.Идея вызываемого блока была первоначально задумана Джоном Мочли и Кэтлин Антонелли во время их работы над ENIAC и зафиксирована на симпозиуме в Гарварде в январе 1947 года по теме «Подготовка задач для машин типа EDVAC ». [4] Морису Уилксу , Дэвиду Уилеру и Стэнли Гиллу обычно приписывают формальное изобретение этой концепции, которую они назвали закрытой подпрограммой , [5] [6] в отличие от открытой подпрограммы или макроса . [7] Однако Алан Тьюринг обсуждал подпрограммы в статье 1945 года о предложениях по проектированию для NPL ACE , зайдя так далеко, что изобрел концепцию стека обратного адреса . [8]
Идея подпрограммы была разработана после того, как вычислительные машины уже существовали некоторое время. Арифметические и условные инструкции перехода были запланированы заранее и изменились относительно мало, но специальные инструкции, используемые для вызовов процедур, сильно изменились за эти годы. Самые ранние компьютеры и микропроцессоры, такие как Manchester Baby и RCA 1802 , не имели единой инструкции вызова подпрограммы. Подпрограммы могли быть реализованы, но они требовали от программистов использования последовательности вызовов — серии инструкций — в каждом месте вызова .
Подпрограммы были реализованы в Z4 Конрада Цузе в 1945 году.
В 1945 году Алан М. Тьюринг использовал термины «захоронить» и «расхоронить» как средство вызова и возврата из подпрограмм. [9] [10]
В январе 1947 года Джон Мочли представил общие заметки на «Симпозиуме по крупномасштабным цифровым вычислительным машинам» при совместном спонсорстве Гарвардского университета и Бюро вооружений ВМС США. Здесь он обсуждает последовательную и параллельную работу, предлагая
...структура машины не должна быть ни на йоту усложненной. Поскольку все логические характеристики, необходимые для этой процедуры, доступны, возможно разработать инструкцию кодирования для размещения подпрограмм в памяти в местах, известных машине, и таким образом, чтобы их можно было легко вызвать к использованию.
Другими словами, можно обозначить подпрограмму A как деление, подпрограмму B как комплексное умножение, а подпрограмму C как оценку стандартной ошибки последовательности чисел и так далее по списку подпрограмм, необходимых для конкретной задачи. ... Все эти подпрограммы затем будут сохранены в машине, и все, что нужно сделать, это сделать краткую ссылку на них по номеру, как они указаны в кодировке. [4]
Кей МакНалти тесно сотрудничала с Джоном Мочли в команде ENIAC и разработала идею подпрограмм для компьютера ENIAC, который она программировала во время Второй мировой войны. [11] Она и другие программисты ENIAC использовали подпрограммы для расчета траекторий ракет. [11]
Голдстайн и фон Нейман написали статью от 16 августа 1948 года, в которой обсуждалось использование подпрограмм. [12]
Некоторые очень ранние компьютеры и микропроцессоры, такие как IBM 1620 , Intel 4004 и Intel 8008 , а также микроконтроллеры PIC , имеют вызов подпрограммы с одной инструкцией, который использует выделенный аппаратный стек для хранения адресов возврата — такое оборудование поддерживает только несколько уровней вложенности подпрограмм, но может поддерживать рекурсивные подпрограммы. Машины до середины 1960-х годов — такие как UNIVAC I , PDP-1 и IBM 1130 — обычно используют соглашение о вызовах , которое сохраняет счетчик инструкций в первой ячейке памяти вызываемой подпрограммы. Это допускает произвольно глубокие уровни вложенности подпрограмм, но не поддерживает рекурсивные подпрограммы. IBM System/360 имела инструкцию вызова подпрограммы, которая помещала сохраненное значение счетчика инструкций в регистр общего назначения; это можно использовать для поддержки произвольно глубокой вложенности подпрограмм и рекурсивных подпрограмм. Burroughs B5000 [13] (1961) — один из первых компьютеров, который хранил возвращаемые подпрограммой данные в стеке.
DEC PDP-6 [14] (1964) — одна из первых машин на основе аккумулятора, которая имела инструкцию вызова подпрограммы, которая сохраняла адрес возврата в стеке, адресуемом аккумулятором или индексным регистром. Более поздние линии PDP-10 (1966), PDP-11 (1970) и VAX-11 (1976) последовали этому примеру; эта функция также поддерживает как произвольно глубокую вложенность подпрограмм, так и рекурсивные подпрограммы. [15]
В самых ранних ассемблерах поддержка подпрограмм была ограничена. Подпрограммы не были явно отделены друг от друга или от основной программы, и, действительно, исходный код подпрограммы мог быть перемежен с кодом других подпрограмм. Некоторые ассемблеры предлагали предопределенные макросы для генерации последовательностей вызова и возврата. К 1960-м годам ассемблеры обычно имели гораздо более сложную поддержку как встроенных, так и отдельно собранных подпрограмм, которые могли быть связаны вместе.
Одним из первых языков программирования, поддерживающих пользовательские подпрограммы и функции, был FORTRAN II . Компилятор IBM FORTRAN II был выпущен в 1958 году. ALGOL 58 и другие ранние языки программирования также поддерживали процедурное программирование.
Даже при таком громоздком подходе подпрограммы оказались очень полезными. Они позволяли использовать один и тот же код во многих различных программах. Память была очень дефицитным ресурсом на ранних компьютерах, и подпрограммы позволяли значительно экономить размер программ.
Многие ранние компьютеры загружали инструкции программы в память с перфокарты . Каждая подпрограмма затем могла быть предоставлена отдельным куском ленты, загруженным или склеенным до или после основной программы (или «главной линии» [16] ); и та же лента подпрограммы затем могла использоваться многими различными программами. Похожий подход использовался в компьютерах, которые загружали инструкции программы с перфокарт . Название библиотека подпрограмм первоначально означало библиотеку, в буквальном смысле, которая хранила индексированные коллекции лент или колод карт для коллективного использования.
Чтобы устранить необходимость в самомодифицирующемся коде , разработчики компьютеров в конечном итоге предусмотрели косвенную инструкцию перехода , операндом которой вместо самого адреса возврата было местоположение переменной или регистра процессора , содержащего адрес возврата.
На этих компьютерах вместо изменения возвратного перехода функции вызывающая программа сохраняла адрес возврата в переменной, чтобы после завершения функции выполнить косвенный переход, который направлял бы выполнение в место, указанное предопределенной переменной.
Другим достижением стала инструкция перехода к подпрограмме , которая объединяла сохранение адреса возврата с вызовом перехода, тем самым значительно минимизируя накладные расходы .
В IBM System/360 , например, инструкции перехода BAL или BALR, разработанные для вызова процедур, сохраняли бы адрес возврата в регистре процессора, указанном в инструкции, по соглашению регистр 14. Для возврата подпрограмме нужно было бы только выполнить косвенную инструкцию перехода (BR) через этот регистр. Если подпрограмме нужен был этот регистр для какой-то другой цели (например, для вызова другой подпрограммы), она сохраняла бы содержимое регистра в частной ячейке памяти или в стеке регистров .
В таких системах, как HP 2100 , инструкция JSB выполняла бы похожую задачу, за исключением того, что адрес возврата сохранялся бы в ячейке памяти, которая была целью ветвления. Выполнение процедуры фактически начиналось бы в следующей ячейке памяти. На языке ассемблера HP 2100 можно было бы написать, например,
...JSB MYSUB (Вызывает подпрограмму MYSUB.)BB ... (Вернусь сюда после завершения MYSUB.)
для вызова подпрограммы с именем MYSUB из основной программы. Подпрограмма будет закодирована как
MYSUB NOP (Хранилище обратного адреса MYSUB.)АА ... (Начало тела MYSUB.)...JMP MYSUB,I (Возврат в вызывающую программу.)
Инструкция JSB помещала адрес инструкции NEXT (а именно, BB) в место, указанное в качестве ее операнда (а именно, MYSUB), а затем переходила в следующее место NEXT (а именно, AA = MYSUB + 1). Затем подпрограмма могла вернуться в основную программу, выполнив косвенный переход JMP MYSUB, I, который переходил в место, сохраненное в месте MYSUB.
Компиляторы для Fortran и других языков могли легко использовать эти инструкции, если они были доступны. Этот подход поддерживал несколько уровней вызовов; однако, поскольку адрес возврата, параметры и возвращаемые значения подпрограммы были назначены фиксированным ячейкам памяти, он не допускал рекурсивных вызовов.
Кстати, аналогичный метод использовался Lotus 1-2-3 в начале 1980-х годов для обнаружения зависимостей пересчета в электронной таблице. А именно, в каждой ячейке резервировалось место для хранения обратного адреса. Поскольку циклические ссылки не допускаются для естественного порядка пересчета, это позволяет выполнять обход дерева без резервирования места для стека в памяти, что было очень ограничено на небольших компьютерах, таких как IBM PC .
Большинство современных реализаций вызова функции используют стек вызовов , особый случай структуры данных стека , для реализации вызовов функций и возвратов. Каждый вызов процедуры создает новую запись, называемую кадром стека , наверху стека; когда процедура возвращается, ее кадр стека удаляется из стека, и его пространство может использоваться для других вызовов процедур. Каждый кадр стека содержит частные данные соответствующего вызова, которые обычно включают параметры процедуры и внутренние переменные, а также адрес возврата.
Последовательность вызовов может быть реализована с помощью последовательности обычных инструкций (подход, который до сих пор используется в архитектурах с сокращенным набором инструкций (RISC) и очень длинными командными словами (VLIW)), но многие традиционные машины, разработанные с конца 1960-х годов, включали специальные инструкции для этой цели.
Стек вызовов обычно реализуется как непрерывная область памяти. Это произвольный выбор дизайна, является ли нижняя часть стека самым низким или самым высоким адресом в этой области, так что стек может расти вперед или назад в памяти; однако, многие архитектуры выбрали последнее. [ необходима цитата ]
Некоторые проекты, в частности некоторые реализации Forth , использовали два отдельных стека, один в основном для управляющей информации (например, обратных адресов и счетчиков циклов), а другой для данных. Первый был или работал как стек вызовов и был доступен программисту только косвенно через другие языковые конструкции, в то время как последний был более доступен напрямую.
Когда впервые были введены вызовы процедур на основе стека, важной мотивацией была экономия драгоценной памяти. [ необходима цитата ] При такой схеме компилятору не нужно резервировать отдельное пространство в памяти для личных данных (параметров, адреса возврата и локальных переменных) каждой процедуры. В любой момент времени стек содержит только личные данные вызовов, которые в данный момент активны (а именно, которые были вызваны, но еще не вернулись). Из-за способов, которыми программы обычно собирались из библиотек, было (и до сих пор есть) не редкостью найти программы, которые включают тысячи функций, из которых только несколько активны в любой момент времени. [ необходима цитата ] Для таких программ механизм стека вызовов мог сэкономить значительные объемы памяти. Действительно, механизм стека вызовов можно рассматривать как самый ранний и простой метод автоматического управления памятью .
Однако еще одним преимуществом метода стека вызовов является то, что он допускает рекурсивные вызовы функций , поскольку каждый вложенный вызов одной и той же процедуры получает отдельный экземпляр ее частных данных.
В многопоточной среде обычно имеется более одного стека. [17] Среда, которая полностью поддерживает сопрограммы или ленивые вычисления, может использовать структуры данных, отличные от стеков, для хранения своих записей активации.
Одним из недостатков механизма стека вызовов является повышенная стоимость вызова процедуры и ее соответствующего возврата. [ необходимо разъяснение ] Дополнительные затраты включают в себя увеличение и уменьшение указателя стека (и, в некоторых архитектурах, проверку переполнения стека ), а также доступ к локальным переменным и параметрам по адресам относительно фрейма, а не по абсолютным адресам. Затраты могут быть реализованы в увеличенном времени выполнения или в увеличенной сложности процессора, или в том и другом.
Эти накладные расходы наиболее очевидны и нежелательны в листовых процедурах или листовых функциях , которые возвращаются без выполнения каких-либо вызовов процедур. [18] [19] [20] Чтобы уменьшить эти накладные расходы, многие современные компиляторы пытаются отложить использование стека вызовов до тех пор, пока это действительно не понадобится. [ требуется ссылка ] Например, вызов процедуры P может сохранять адрес возврата и параметры вызванной процедуры в определенных регистрах процессора и передавать управление телу процедуры простым переходом. Если процедура P возвращается без выполнения какого-либо другого вызова, стек вызовов вообще не используется. Если P необходимо вызвать другую процедуру Q , она затем будет использовать стек вызовов для сохранения содержимого любых регистров (например, адреса возврата), которые понадобятся после возврата Q.
В общем, вызываемый блок — это список инструкций, который, начиная с первой инструкции, выполняется последовательно, за исключением случаев, предписанных его внутренней логикой. Он может быть вызван (вызван) много раз во время выполнения программы. Выполнение продолжается со следующей инструкции после инструкции вызова, когда она возвращает управление.
Особенности реализаций вызываемых единиц развивались с течением времени и различаются в зависимости от контекста. В этом разделе описываются особенности различных распространенных реализаций.
Большинство современных языков программирования предоставляют возможности для определения и вызова функций, включая синтаксис для доступа к таким функциям, в том числе:
Некоторые языки, такие как Pascal , Fortran , Ada и многие диалекты BASIC , используют разные имена для вызываемых единиц, которые возвращают значение ( функция или подпрограмма ), и тех, которые не возвращают значение ( подпрограмма или процедура ). Другие языки, такие как C , C++ , C# и Lisp , используют только одно имя для вызываемых единиц, function . Языки семейства C используют ключевое слово для указания на отсутствие возвращаемого значения.void
Если объявлено, что возвращается значение, вызов может быть встроен в выражение , чтобы использовать возвращаемое значение. Например, вызываемая единица квадратного корня может быть вызвана как y = sqrt(x)
.
Вызываемая единица, которая не возвращает значение, вызывается как отдельный оператор , например print("hello")
. Этот синтаксис также может использоваться для вызываемой единицы, которая возвращает значение, но возвращаемое значение будет проигнорировано.
В некоторых старых языках для вызовов, которые не используют возвращаемое значение, требуется ключевое слово, например CALL print("hello")
.
Большинство реализаций, особенно в современных языках, поддерживают параметры , которые вызываемый объект объявляет как формальные параметры . Вызывающий объект передает фактические параметры , также известные как аргументы , для сопоставления. Различные языки программирования предоставляют различные соглашения для передачи аргументов.
В некоторых языках, таких как BASIC, вызываемый объект имеет разный синтаксис (т. е. ключевое слово) для вызываемого объекта, который возвращает значение, и для того, который не возвращает. В других языках синтаксис остается тем же. В некоторых из этих языков используется дополнительное ключевое слово для объявления отсутствия возвращаемого значения; например, void
в C, C++ и C#. В некоторых языках, таких как Python, разница заключается в том, содержит ли тело оператор return со значением, и конкретный вызываемый объект может возвращать значение или нет в зависимости от потока управления.
Во многих контекстах вызываемый объект может иметь побочные эффекты , такие как изменение переданных или глобальных данных, чтение с периферийного устройства или запись на него , доступ к файлу , остановка программы или машины или временная приостановка выполнения программы.
Побочные эффекты считаются нежелательными Робертом С. Мартином , который известен продвижением принципов дизайна. Мартин утверждает, что побочные эффекты могут привести к временной связи или зависимости порядка. [21]
В строго функциональных языках программирования, таких как Haskell , функция не может иметь побочных эффектов , что означает, что она не может изменить состояние программы. Функции всегда возвращают один и тот же результат для одного и того же ввода. Такие языки обычно поддерживают только функции, которые возвращают значение, поскольку в функции, которая не имеет ни возвращаемого значения, ни побочного эффекта, нет значения.
Большинство контекстов поддерживают локальные переменные – память, принадлежащая вызываемому объекту, для хранения промежуточных значений. Эти переменные обычно хранятся в записи активации вызова в стеке вызовов вместе с другой информацией, такой как адрес возврата .
Если поддерживается языком, вызываемый объект может вызывать сам себя, что приводит к приостановке его выполнения, пока выполняется другое вложенное выполнение того же вызываемого объекта. Рекурсия — это полезное средство для упрощения некоторых сложных алгоритмов и решения сложных проблем. Рекурсивные языки предоставляют новую копию локальных переменных при каждом вызове. Если программист хочет, чтобы рекурсивный вызываемый объект использовал те же переменные вместо использования локальных переменных, они обычно объявляют их в общем контексте, таком как статический или глобальный.
Языки, восходящие к ALGOL , PL/I и C , а также современные языки, почти всегда используют стек вызовов, обычно поддерживаемый наборами инструкций, чтобы обеспечить запись активации для каждого вызова. Таким образом, вложенный вызов может изменять свои локальные переменные, не влияя ни на одну из переменных приостановленных вызовов.
Рекурсия позволяет напрямую реализовать функциональность, определенную математической индукцией и рекурсивными алгоритмами «разделяй и властвуй» . Вот пример рекурсивной функции на C/C++ для поиска чисел Фибоначчи :
int Fib ( int n ) { if ( n <= 1 ) { return n ; } return Fib ( n - 1 ) + Fib ( n - 2 ); }
Ранние языки, такие как Fortran, изначально не поддерживали рекурсию, поскольку для каждого вызываемого объекта выделялся только один набор переменных и адрес возврата. [22] Ранние наборы компьютерных инструкций затрудняли хранение адресов возврата и переменных в стеке. Машины с индексными регистрами или регистрами общего назначения , например, серии CDC 6000 , PDP-6 , GE 635 , System/360 , UNIVAC 1100 , могли использовать один из этих регистров в качестве указателя стека .
Некоторые языки, например, Ada , Pascal , PL/I , Python , поддерживают объявление и определение функции внутри, например, тела функции, так что имя внутренней функции видно только внутри тела внешней функции.
Если вызываемый объект может быть выполнен правильно, даже если другое выполнение того же самого вызываемого объекта уже выполняется, то такой вызываемый объект называется реентерабельным . Реентерабельный вызываемый объект также полезен в многопоточных ситуациях, поскольку несколько потоков могут вызывать один и тот же вызываемый объект, не опасаясь мешать друг другу. В системе обработки транзакций IBM CICS квазиреентерабельность была немного менее строгим, но похожим требованием для прикладных программ, которые совместно использовались многими потоками.
Некоторые языки поддерживают перегрузку — позволяют использовать несколько вызываемых объектов с одинаковым именем в одной области действия, но работающих с разными типами входных данных. Рассмотрим функцию квадратного корня, применяемую к действительным числам, комплексным числам и матричному входному данным. Алгоритм для каждого типа входных данных отличается, а возвращаемое значение может иметь другой тип. Написав три отдельных вызываемых объекта с одинаковым именем, т. е. sqrt , можно упростить написание и поддержку результирующего кода, поскольку каждый из них имеет имя, которое относительно легко понять и запомнить, вместо того, чтобы давать более длинные и сложные имена, такие как sqrt_real , sqrt_complex , qrt_matrix .
Перегрузка поддерживается во многих языках, которые поддерживают строгую типизацию . Часто компилятор выбирает перегрузку для вызова на основе типа входных аргументов или терпит неудачу, если входные аргументы не выбирают перегрузку. Старые и слабо типизированные языки обычно не поддерживают перегрузку.
Вот пример перегрузки в C++ — две функции Area
, которые принимают разные типы:
// возвращает площадь прямоугольника, определяемую высотой и шириной double Area ( double h , double w ) { return h * w ; } // возвращает площадь круга, определяемую радиусом double Area ( double r ) { return r * r * 3.14 ; } int main () { double прямоугольник_область = Площадь ( 3 , 4 ); double круг_область = Площадь ( 5 ); }
PL/I имеет GENERIC
атрибут для определения общего имени для набора ссылок на записи, вызываемых с различными типами аргументов. Пример:
ОБЪЯВЛЯЕМ имя_гена GENERIC( имя КОГДА(ФИКСИРОВАННЫЙ ДВОИЧНЫЙ), пламя КОГДА(ПЛАВАЕТ), имя_пути (ИНАЧЕ);
Для каждой записи можно указать несколько определений аргументов. Вызов "gen_name" приведет к вызову "name", если аргумент - FIXED BINARY, "flame", если FLOAT" и т. д. Если аргумент не соответствует ни одному из вариантов, будет вызван "pathname".
Замыкание — это вызываемый объект плюс значения некоторых его переменных, захваченные из среды, в которой он был создан. Замыкания были заметной особенностью языка программирования Lisp, введенного Джоном Маккарти . В зависимости от реализации замыкания могут служить механизмом для побочных эффектов.
Помимо поведения счастливого пути , вызываемому объекту может потребоваться сообщить вызывающему объекту об исключительной ситуации, возникшей во время его выполнения.
Большинство современных языков поддерживают исключения, что позволяет реализовать исключительный поток управления, который извлекает стек вызовов до тех пор, пока не будет найден обработчик исключений для обработки условия.
Языки, не поддерживающие исключения, могут использовать возвращаемое значение для указания на успешность или неудачу вызова. Другой подход заключается в использовании известного местоположения, например глобальной переменной, для указания на успешность. Вызываемый объект записывает значение, а вызывающий объект считывает его после вызова.
В IBM System/360 , где ожидался код возврата от подпрограммы, возвращаемое значение часто проектировалось кратным 4, чтобы его можно было использовать как прямой индекс таблицы ветвлений в таблице ветвлений, часто расположенной сразу после инструкции вызова, чтобы избежать дополнительных условных проверок, что еще больше повышает эффективность. На языке ассемблера System/360 можно было бы написать, например:
BAL 14, SUBRTN01 перейти к подпрограмме, сохраняя адрес возврата в R14 B TABLE(15) использует возвращаемое значение в reg 15 для индексации таблицы ветвей,* переход к соответствующей ветке инстр.ТАБЛИЦА B Код возврата OK =00 GOOD } B BAD код возврата =04 Неверный ввод } Таблица ветвей B Код возврата ERROR =08 Неожиданное состояние }
Вызов имеет накладные расходы во время выполнения , которые могут включать, помимо прочего:
Для минимизации затрат времени на выполнение вызовов применяются различные методы.
Некоторые оптимизации для минимизации накладных расходов на вызовы могут показаться простыми, но их нельзя использовать, если вызываемый объект имеет побочные эффекты. Например, в выражении (f(x)-1)/(f(x)+1)
функция f
не может быть вызвана только один раз, а ее значение использовано дважды, поскольку два вызова могут вернуть разные результаты. Более того, в нескольких языках, которые определяют порядок оценки операндов оператора деления, значение x
должно быть извлечено снова перед вторым вызовом, поскольку первый вызов мог изменить его. Определить, имеет ли вызываемый объект побочный эффект, сложно — на самом деле, неразрешимо в силу теоремы Райса . Таким образом, хотя эта оптимизация безопасна в чисто функциональном языке программирования, компилятор для языка, не ограниченного функциональным, обычно предполагает наихудший случай, что каждый вызываемый объект может иметь побочные эффекты.
Встраивание устраняет вызовы для определенных вызываемых объектов. Компилятор заменяет каждый вызов скомпилированным кодом вызываемого объекта. Это не только позволяет избежать накладных расходов на вызов, но и позволяет компилятору более эффективно оптимизировать код вызывающего объекта, принимая во внимание контекст и аргументы при этом вызове. Однако встраивание обычно увеличивает размер скомпилированного кода, за исключением случаев, когда он вызывается только один раз или тело очень короткое, например , одна строка.
Вызываемые объекты могут быть определены внутри программы или отдельно в библиотеке , которая может использоваться несколькими программами.
Компилятор транслирует операторы вызова и возврата в машинные инструкции в соответствии с четко определенным соглашением о вызовах . Для кода , скомпилированного тем же или совместимым компилятором, функции могут быть скомпилированы отдельно от программ, которые их вызывают. Последовательности инструкций, соответствующие операторам вызова и возврата, называются прологом и эпилогом процедуры .
Встроенная функция , или встроенная функция , или внутренняя функция — это функция, для которой компилятор генерирует код во время компиляции или предоставляет его иным способом, чем для других функций. [23] Встроенную функцию не нужно определять, как другие функции, поскольку она встроена в язык программирования. [24]
Преимущества разбиения программы на функции включают в себя:
По сравнению с использованием встроенного кода, вызов функции влечет за собой некоторые вычислительные затраты в механизме вызова. [ необходима цитата ]
Обычно для функции требуется стандартный служебный код — как на входе, так и на выходе из функции ( пролог и эпилог функции — обычно сохраняющий как минимум регистры общего назначения и адрес возврата).
В отношении вызываемых функций разработано множество соглашений по программированию.
Что касается наименования, многие разработчики называют вызываемый объект фразой, начинающейся с глагола , когда он выполняет определенную задачу, прилагательным, когда он делает запрос, и существительным, когда он используется для подстановки переменных.
Некоторые программисты предполагают, что вызываемый объект должен выполнять ровно одну задачу, а если он выполняет более одной задачи, его следует разделить на несколько вызываемых объектов. Они утверждают, что вызываемые объекты являются ключевыми компонентами в обслуживании программного обеспечения , и их роли в программе должны оставаться различными.
Сторонники модульного программирования утверждают, что каждый вызываемый объект должен иметь минимальную зависимость от остальной части кодовой базы . Например, использование глобальных переменных обычно считается неразумным, поскольку оно добавляет связь между всеми вызываемыми объектами, которые используют глобальные переменные. Если такая связь не нужна, они советуют реорганизовать вызываемые объекты, чтобы вместо этого принимать переданные параметры .
Ранние варианты BASIC требуют, чтобы каждая строка имела уникальный номер ( номер строки ), который упорядочивает строки для выполнения, не обеспечивает разделения вызываемого кода, не обеспечивает механизма для передачи аргументов или возврата значения, и все переменные являются глобальными. Он предоставляет команду, GOSUB
где sub — это сокращение от sub procedure , subprocedure или subroutine . Управление переходит на указанный номер строки, а затем продолжается на следующей строке при возврате.
10 REM БАЗОВАЯ ПРОГРАММА 20 GOSUB 100 30 GOTO 20 100 ВВОД « ДАЙТЕ МНЕ ЧИСЛО » ; N 110 ПЕЧАТЬ « КВАДРАТНЫЙ КОРЕНЬ ИЗ » ; N ; 120 ПЕЧАТЬ « ЕСТЬ » ; КОРЕНЬ ( N ) 130 ВОЗВРАТ
Этот код многократно просит пользователя ввести число и сообщает квадратный корень значения. Строки 100-130 являются вызываемыми.
В Microsoft Small Basic , ориентированном на студентов, впервые изучающих программирование на текстовом языке, вызываемый блок называется подпрограммой . Ключевое Sub
слово обозначает начало подпрограммы и сопровождается идентификатором имени. Последующие строки представляют собой тело, которое заканчивается ключевым EndSub
словом. [25]
Sub SayHello TextWindow . WriteLine ( «Привет!» ) EndSub
Это можно назвать SayHello()
. [26]
В более поздних версиях Visual Basic (VB), включая последнюю линейку продуктов и VB6 , термин процедура используется для концепции вызываемого блока. Ключевое слово Sub
используется для возврата не значения и Function
для возврата значения. При использовании в контексте класса процедура является методом. [27]
Каждый параметр имеет тип данных , который можно указать, но если он не указан, то по умолчанию используется тип Object
для более поздних версий на основе .NET и вариант для VB6 . [28]
VB поддерживает соглашения о передаче параметров по значению и по ссылке через ключевые слова ByVal
и ByRef
соответственно. Если ByRef
не указано, передается аргумент ByVal
. Поэтому ByVal
редко указывается явно.
Для простого типа, такого как число, эти соглашения относительно ясны. Передача ByRef
позволяет процедуре изменять переданную переменную, тогда как передача ByVal
не позволяет. Для объекта семантика может сбивать программистов с толку, поскольку объект всегда рассматривается как ссылка. Передача объекта ByVal
копирует ссылку, а не состояние объекта. Вызванная процедура может изменять состояние объекта через свои методы, но не может изменять ссылку на объект фактического параметра.
Sub DoSomething () ' Здесь какой-то код End Sub
Не возвращает значение и должен вызываться отдельно, напримерDoSomething
Функция GiveMeFive () как целое число GiveMeFive = 5 Конец функции
Это возвращает значение 5, и вызов может быть частью выражения, напримерy = x + GiveMeFive()
Sub AddTwo ( ByRef intValue as Integer ) intValue = intValue + 2 End Sub
Это имеет побочный эффект – изменяет переменную, переданную по ссылке, и может быть вызвано для переменной v
типа AddTwo(v)
. Если v равно 5 до вызова, то после него будет 7.
В C и C++ вызываемая единица называется функцией . Определение функции начинается с имени типа возвращаемого ею значения или void
указания на то, что она не возвращает значение. Затем следует имя функции, формальные аргументы в скобках и строки тела в фигурных скобках.
В C++ функция, объявленная в классе (как нестатическая), называется функцией-членом или методом . Функция вне класса может быть названа свободной функцией, чтобы отличать ее от функции-члена. [29]
void doSomething () { /* какой-то код */ }
Эта функция не возвращает значение и всегда вызывается автономно, напримерdoSomething()
int giveMeFive () { return 5 ; }
Эта функция возвращает целочисленное значение 5. Вызов может быть автономным или в выражении, напримерy = x + giveMeFive()
void addTwo ( int * pi ) { * pi += 2 ; }
Эта функция имеет побочный эффект – изменяет значение, переданное по адресу, на входное значение плюс 2. Она может быть вызвана для переменной, v
как, addTwo(&v)
например, где амперсанд (&) говорит компилятору передать адрес переменной. Если указать v = 5 до вызова, то после него будет 7.
void addTwo ( int & i ) { i += 2 ; }
Эта функция требует C++ – не будет компилироваться как C. Она имеет то же поведение, что и предыдущий пример, но передает фактический параметр по ссылке, а не по адресу. Такой вызов addTwo(v)
не включает амперсанд, поскольку компилятор обрабатывает передачу по ссылке без синтаксиса в вызове.
В PL/I вызываемой процедуре может быть передан дескриптор, предоставляющий информацию об аргументе, например, длину строки и границы массива. Это позволяет процедуре быть более общей и устраняет необходимость для программиста передавать такую информацию. По умолчанию PL/I передает аргументы по ссылке. (Тривиальная) функция для изменения знака каждого элемента двумерного массива может выглядеть так:
изменение_знака: процедура(массив); объявить массив(*,*) float; массив = -массив;конец изменения_знака;
Это можно вызвать с различными массивами следующим образом:
/* первый массив ограничивается от -5 до +10 и от 3 до 9 */объявить массив1 (-5:10, 3:9)float;/* второй массив ограничивается от 1 до 16 и от 1 до 16 */объявить массив2 (16,16) float;вызов change_sign(array1);вызов change_sign(array2);
В Python ключевое слово def
обозначает начало определения функции. Операторы тела функции следуют с отступом на последующих строках и заканчиваются на строке, которая имеет такой же отступ, как и первая строка или конец файла. [30]
def format_greeting ( name ): return "Добро пожаловать" + name def greet_martin (): print ( format_greeting ( "Мартин" ))
Первая функция возвращает текст приветствия, который включает имя, переданное вызывающей стороной. Вторая функция вызывает первую и вызывается как greet_martin()
для записи "Welcome Martin" на консоль.
В процедурной интерпретации логических программ логические импликации ведут себя как процедуры сокращения цели. Правило (или предложение ) вида:
A :- B
что имеет логическое прочтение:
A if B
ведет себя как процедура, которая сводит цели, объединяемые с , A
к подцелям, которые являются экземплярами B
.
Рассмотрим, например, программу на Прологе:
мать_ребенок ( Элизабет , Чарльз ). отец_ребенок ( Чарльз , Уильям ). отец_ребенок ( Чарльз , Гарри ). родитель_ребенок ( X , Y ) :- мать_ребенок ( X , Y ). родитель_ребенок ( X , Y ) :- отец_ребенок ( X , Y ).
Обратите внимание, что функция материнства представлена отношением, как в реляционной базе данных . Однако отношения в Прологе функционируют как вызываемые единицы.X = mother(Y)
Например, вызов процедуры производит вывод . Но эту же процедуру можно вызвать и с другими шаблонами ввода-вывода. Например:?- parent_child(X, charles)
X = elizabeth
?- родитель_ребенок ( elizabeth , Y ). Y = charles .?- родитель_ребенок ( X , Y ). X = Элизабет , Y = Чарльз .X = Чарльз , Y = Гарри .X = Чарльз , Y = Уильям .?- родитель_ребенок ( Уильям , Гарри ). нет .?- родитель_ребенок ( елизавета , чарльз ). да .
Вызываемая единица: (программы или логической конструкции) Функция, метод, операция, подпрограмма, процедура или аналогичная структурная единица, которая появляется внутри модуля.
Мы могли бы предоставить нашему сборщику копии исходного кода для всех наших полезных подпрограмм, а затем, предоставив ему основную программу для сборки, сообщить ему, какие подпрограммы будут вызываться в основной [...]