stringtranslate.com

Функция (компьютерное программирование)

В компьютерном программировании функция (также процедура , метод , подпрограмма , рутина или подпрограмма ) — это вызываемая единица [1] программной логики , которая имеет четко определенный интерфейс и поведение и может быть вызвана несколько раз.

Вызываемые единицы предоставляют мощный инструмент программирования. [2] Основная цель — обеспечить разложение большой и/или сложной проблемы на части, которые имеют относительно низкую когнитивную нагрузку , и назначить частям осмысленные имена (если они не анонимны). Разумное применение может снизить стоимость разработки и поддержки программного обеспечения, одновременно повышая его качество и надежность. [3]

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

Терминология

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

Используемый термин, как правило, отражает контекст, в котором он используется – обычно на основе используемого языка. Например:

История

Идея вызываемого блока была первоначально задумана Джоном Мочли и Кэтлин Антонелли во время их работы над 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

Ранние варианты 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  =  Уильям .?-  родитель_ребенок ( Уильям ,  Гарри ). нет .?-  родитель_ребенок ( елизавета ,  чарльз ). да .

Смотрите также

Ссылки

  1. ^ "Terminology Glossary". nist.gov . NIST . Получено 9 февраля 2024 г. Вызываемая единица: (программы или логической конструкции) Функция, метод, операция, подпрограмма, процедура или аналогичная структурная единица, которая появляется внутри модуля.
  2. ^ Дональд Э. Кнут (1997). Искусство программирования, том I: Фундаментальные алгоритмы . Эддисон-Уэсли. ISBN 0-201-89683-4.
  3. ^ О.-Дж. Даль; Э. В. Дейкстра; К. А. Хоар (1972). Структурное программирование . Academic Press. ISBN 0-12-200550-3.
  4. ^ ab Mauchly, JW (1982). «Подготовка задач для машин типа EDVAC». В Randell, Brian (ред.). The Origins of Digital Computers . Springer. стр. 393–397. doi :10.1007/978-3-642-61812-3_31. ISBN 978-3-642-61814-7.
  5. ^ Уилер, DJ (1952). "Использование подпрограмм в программах" (PDF) . Труды национального собрания ACM 1952 года (Питтсбург) по - ACM '52 . стр. 235. doi : 10.1145/609784.609816 .
  6. ^ Уилкс, М. В.; Уилер, ДЖ.; Гилл, С. (1951). Подготовка программ для электронного цифрового компьютера . Эддисон-Уэсли.
  7. ^ Дайнит, Джон (2004). ""открытая подпрограмма". Словарь вычислений". Encyclopedia.com . Получено 14 января 2013 г.
  8. Тьюринг, Алан М. (1945), Отчет доктора А. М. Тьюринга о предложениях по разработке автоматической вычислительной машины (ACE): представлен Исполнительному комитету Национальной физической лаборатории в феврале 1946 г.перепечатано в Copeland, BJ , ed. (2005). Автоматическая вычислительная машина Алана Тьюринга . Оксфорд: Oxford University Press. стр. 383. ISBN 0-19-856593-3.
  9. Тьюринг, Алан Матисон (19 марта 1946 г.) [1945], Предложения по разработке в математическом отделе автоматической вычислительной машины (ACE)(Примечание. Представлено 19 марта 1946 г. Исполнительному комитету Национальной физической лаборатории (Великобритания).)
  10. Карпентер, Брайан Эдвард ; Доран, Роберт Уильям (1 января 1977 г.) [октябрь 1975 г.]. «Другая машина Тьюринга». The Computer Journal . 20 (3): 269–279. doi : 10.1093/comjnl/20.3.269 .(11 страниц)
  11. ^ ab Isaacson, Walter (18 сентября 2014 г.). «Walter Isaacson on the Women of ENIAC». Fortune . Архивировано из оригинала 12 декабря 2018 г. Получено 14 декабря 2018 г.
  12. ^ Герман Х. Голдстайн; Джон фон Нейман (1947). "Часть II, Том I-3, Планирование и кодирование проблем для электронного вычислительного прибора" (PDF) . Отчет о математических и логических аспектах электронного вычислительного прибора (технический отчет).(см. стр. 163 pdf-файла для соответствующей страницы)
  13. ^ Эксплуатационные характеристики процессоров для Burroughs B5000 (PDF) . Редакция A. Burroughs Corporation . 1963. 5000-21005 . Получено 8 февраля 2024 г.
  14. ^ "Push-Down Instructions" (PDF) . Programmed Data Processor 6 - Handbook (PDF) . стр. 37 . Получено 8 февраля 2024 г. .
  15. ^ Гай Льюис Стил-младший. Записка AI 443. «Развенчание мифа о «дорогом вызове процедур»; или Реализации вызовов процедур считаются вредными». Раздел «C. Почему вызовы процедур имеют плохую репутацию».
  16. ^ Фрэнк, Томас С. (1983). Введение в PDP-11 и его язык ассемблера. Серия программного обеспечения Prentice-Hall. Prentice-Hall. стр. 195. ISBN 9780134917047. Получено 6 июля 2016 г. Мы могли бы предоставить нашему сборщику копии исходного кода для всех наших полезных подпрограмм, а затем, предоставив ему основную программу для сборки, сообщить ему, какие подпрограммы будут вызываться в основной [...]
  17. ^ Баттлар, Дик; Фаррелл, Жаклин; Николс, Брэдфорд (1996). Программирование PThreads: стандарт POSIX для лучшей многопроцессорной обработки. "O'Reilly Media, Inc.". стр. 2–5. ISBN 978-1-4493-6475-5. OCLC  1036778036.
  18. ^ "ARM Information Center". Infocenter.arm.com . Получено 29 сентября 2013 г. .
  19. ^ "x64 stack usage". Microsoft Docs . Microsoft . Получено 5 августа 2019 .
  20. ^ "Типы функций". Msdn.microsoft.com . Получено 29 сентября 2013 г. .
  21. ^ Мартин, Роберт С. (1 августа 2008 г.). Чистый код: Справочник по гибкому программному мастерству (1-е изд.). Pearson . ISBN 9780132350884. Получено 19 мая 2024 г.
  22. ^ Verhoeff, Tom (2018). «Мастер-класс по рекурсии». В Böckenhauer, Hans-Joachim; Komm, Dennis; Unger, Walter (ред.). Adventures Between Lower Bounds and Higher Altitudes: Essays Dedicated to Juraj Hromkovič on the Occasion of His 60th Birthday . Springer. стр. 616. ISBN 978-3-319-98355-4. OCLC  1050567095.
  23. ^ "Встроенные функции". ibm.com . 9 марта 2017 г. . Получено 25 декабря 2023 г. .
  24. ^ Учебный материал Python. Апрель 2023 г. стр. 87. Получено 25 декабря 2023 г.
  25. ^ "Small Basic". Small Basic . Получено 8 февраля 2024 г. .
  26. ^ «Малое базовое руководство по началу работы: Глава 9: Подпрограммы». Microsoft. 17 января 2024 г.
  27. ^ "Процедуры в Visual Basic". Microsoft Learn . 15 сентября 2021 г. Получено 8 февраля 2024 г.
  28. ^ "Dim statement (Visual Basic)". Microsoft Learn . 15 сентября 2021 г. Получено 8 февраля 2024 г.
  29. ^ "что подразумевается под свободной функцией".
  30. ^ "4. Дополнительные инструменты потока управления — документация Python 3.9.7".