Как предполагается в этом ответе , это вопрос аппаратной поддержки, хотя традиции в языковой разработке также играют роль.
когда функция возвращает, она оставляет указатель на возвращаемый объект в определенном регистре
Из трех первых языков, Fortran, Lisp и COBOL, первый использовал одно возвращаемое значение, которое было смоделировано по математике. Второе возвращало произвольное количество параметров так же, как оно получило их: в виде списка (можно также утверждать, что он только передал и возвратил единственный параметр: адрес списка). Третье возвращает ноль или одно значение.
Эти первые языки сильно повлияли на дизайн последующих языков, хотя единственный, который возвращал множество значений, Lisp, никогда не пользовался большой популярностью.
Когда появился C, находясь под влиянием предшествующих языков, он уделял большое внимание эффективному использованию аппаратного ресурса, сохраняя тесную связь между тем, что делал язык C, и машинным кодом, который его реализовывал. Некоторые из его самых старых функций, таких как переменные «auto» и «register», являются результатом этой философии проектирования.
Следует также отметить, что ассемблер был широко популярен до 80-х годов, когда он, наконец, начал постепенно вытесняться из основного развития. Люди, которые писали компиляторы и создавали языки, были знакомы с ассемблером и, по большей части, придерживались того, что там работало лучше всего.
Большинство языков, которые отклонялись от этой нормы, никогда не находили большой популярности и, следовательно, никогда не играли сильную роль, влияющую на решения языковых дизайнеров (которые, конечно, были вдохновлены тем, что они знали).
Итак, давайте рассмотрим ассемблер. Давайте сначала посмотрим на 6502 , микропроцессор 1975 года, который классно использовался в микрокомпьютерах Apple II и VIC-20. Он был очень слабым по сравнению с тем, что использовалось в мэйнфреймах и миникомпьютерах того времени, хотя и был мощным по сравнению с первыми компьютерами 20, 30 лет назад, на заре языков программирования.
Если вы посмотрите на техническое описание, у него есть 5 регистров плюс несколько однобитовых флагов. Единственным «полным» регистром был счетчик программ (ПК) - этот регистр указывает на следующую команду, которая должна быть выполнена. Другие регистры, где аккумулятор (A), два «индексных» регистра (X и Y) и указатель стека (SP).
Вызов подпрограммы помещает ПК в память, указанную SP, а затем уменьшает SP. Возврат из подпрограммы работает в обратном порядке. Можно помещать и извлекать другие значения из стека, но сложно обратиться к памяти относительно SP, поэтому написание подпрограмм с повторным входом было затруднено. То, что мы считаем само собой разумеющимся, вызывая подпрограмму в любое время, когда мы чувствуем, было не так распространено в этой архитектуре. Часто создается отдельный «стек», чтобы параметры и адрес возврата подпрограммы оставались раздельными.
Если вы посмотрите на процессор, который вдохновил 6502, 6800 , у него был дополнительный регистр, регистр индекса (IX), такой же широкий, как SP, который мог принимать значение от SP.
На компьютере вызов подпрограммы с повторным входом состоял из помещения параметров в стек, нажатия ПК, смены ПК на новый адрес, а затем подпрограмма помещала бы свои локальные переменные в стек . Поскольку число локальных переменных и параметров известно, их адресация может выполняться относительно стека. Например, функция, получающая два параметра и имеющая две локальные переменные, будет выглядеть так:
SP + 8: param 2
SP + 6: param 1
SP + 4: return address
SP + 2: local 2
SP + 0: local 1
Его можно вызывать любое количество раз, потому что все временное пространство находится в стеке.
8080 , используемый на ТРС-80 и множество CP / M микрокомпьютеров на основе могли бы сделать что - то похожее на 6800, нажав SP на стек , а затем выскакивают его на косвенном регистре, HL.
Это очень распространенный способ реализации, и он получил еще большую поддержку на более современных процессорах, с базовым указателем, который упрощает вывод всех локальных переменных перед возвратом.
Проблема в том, как вы что-то возвращаете ? Регистры процессора были не очень многочисленными на раннем этапе, и часто приходилось использовать некоторые из них, даже чтобы выяснить, к какой части памяти обращаться. Возврат вещей в стеке будет сложным: вам нужно будет выгрузить все, сохранить ПК, передать возвращаемые параметры (которые будут храниться где-то еще?), Затем снова нажать ПК и вернуться.
Так что обычно делали резервирование одного регистра для возвращаемого значения. Вызывающий код знал, что возвращаемое значение будет в конкретном регистре, который должен быть сохранен до тех пор, пока не будет сохранен или использован.
Давайте посмотрим на язык, который допускает множественные возвращаемые значения: Forth. То, что делает Форт, - это хранение отдельного стека возврата (RP) и стека данных (SP), так что все, что нужно было сделать функции, - это вытолкнуть все ее параметры и оставить возвращаемые значения в стеке. Поскольку стек возвратов был отдельным, он не мешал.
Как человек, который выучил ассемблер и Форт за первые шесть месяцев работы с компьютерами, множественные возвращаемые значения выглядят для меня совершенно нормально. Такие операторы, как операторы Форта /mod
, которые возвращают целочисленное деление, а остальные - кажутся очевидными. С другой стороны, я легко вижу, как кто-то, чей ранний опыт был С-умом, находит эту концепцию странной: она идет вразрез с их укоренившимися ожиданиями о том, что такое «функция».
Что касается математики ... ну, я программировал компьютеры задолго до того, как начал заниматься функциями на уроках математики. Там есть целый раздел CS и языков программирования , который под влиянием математики, но, опять же , есть целый раздел , который не является.
Таким образом, у нас есть стечение факторов, где математика влияла на ранний языковой дизайн, где аппаратные ограничения диктовали то, что было легко реализовано, и где популярные языки влияли на то, как развивалось оборудование (машины Лиспа и процессоры Форта в этом процессе были путаницами).