Несмотря на проблему, подход Фортрана имел свои положительные моменты. Главный из них – тот факт, что мы не должны поддерживать множество механизмов. Та же самая схема передачи адреса аргумента работает для всех случаев, включая массивы. Так что размер компилятора может быть сокращен.
Частично из-за этого подводного камня Фортрана и частично просто из-за уменьшенной связи, современные языки типа C, Pascal, Ada и Modula 2 в основном передают скаляры по значению.
Это означает, что значение скаляра копируется в отдельное значение, используемое только для вызова. Так как передаваемое значение – копия, вызываемая процедура может использовать его как локальную переменную и изменять ее любым способом, каким нравится. Значение в вызывающей программе не будет изменено.
Сначала может показаться, что это немного неэффективно из-за необходимости копировать параметр. Но запомните, что мы в любом случае окажемся перед необходимостью выбирать какое-то значение, является ли оно непосредственно параметром или его адресом. Внутри подпрограммы, использование передачи по значению определенно более эффективно, так как мы устраняем один уровень косвенности. Наконец, мы видели раньше, что в Фортране часто было необходимо в любом случае делать копии внутри подпрограммы, так что передача по значению уменьшает количество локальных переменных. В целом, передача по значению лучше.
Исключая одну маленькую деталь: если все параметры передаются по значению, у вызванной процедуры нет никакого способа возвратить результат в вызвавшую! Переданный параметр не изменяется в вызвавшей подпрограмме а только в вызванной. Ясно, что так работы не сделать.
Существуют два эквивалентных ответа на эту проблему. В Паскале Вирт предусмотрел параметры-переменные, которые передаются по ссылке. VAR параметр не что иное как наш старый друг параметр Фортрана с новым именем и расцветкой для маскировки. Вирт аккуратно обходит проблему «изменения литерала» так же как проблему «адрес выражения» с помощью простого средства, разрешая использовать в качестве фактических параметров только переменные. Другими словами, это тоже самое ограничение, которое накладывали самые ранние версии Фортрана.
Си делает ту же самую вещъ, но явно. В C все параметры передаются по значению. Однако одним из видов переменных, которые поддерживает С, является указатель. Так передавая указатель по значению, вы в действительности передаете то, на что он указывает по ссылке. В некоторых случаях это работает даже еще лучше, потому что даже хотя вы и можете изменить указываемую переменную на все, что хотите, вы все же не сможете изменить сам указатель. В функции типа strcpy, к примеру, где указатель увеличивается при копировании строки, мы в действительности увеличиваем только копии указателей, так что значение указателей в вызвавшей процедуре все еще остается каким было. Чтобы изменить указатель вы должны передавать указатель на указатель.
Так как мы просто проводим эксперименты, мы рассмотрим и передачу по значению и передачу по ссылке. Таким образом у нас будет возможность использовать любой из них как нам нужно. Стоит упомянуть, что было бы тяжело использовать здесь подход С, так как указатель это другой тип а типы мы еще не изучали!
Передача по значению
Давайте просто попробуем некоторые нехитрые вещи и посмотрим, куда они нас приведут. Давайте начнем со случая передачи по значению. Рассмотрим вызов процедуры:
FOO(X, Y)
Почти единственным приемлемым способом передачи данных является передача через стек ЦПУ. Поэтому, код который мы бы хотели видеть сгенерированным мог бы выглядеть так:
MOVE X(PC),-(SP) ; Push X
MOVE Y(PC),-(SP) ; Push Y
BSR FOO ; Call FOO
Это конечно не выглядит слишком сложным!
Когда BSR выполнен центральный процессор помещает адрес возврата в стек и переходит к FOO. В этой точке стек будет выглядеть следующим образом:
.
.
Значение X (2 bytes)
Значение Y (2 bytes)
SP –> Адрес возврата (4 bytes)
Так что значения параметров имеют адреса с фиксированными смещениями от указателя стека. В этом примере адреса такие:
X: 6(SP)
Y: 4(SP)
Теперь рассмотрим, на что могла бы походить вызываемая процедура:
PROCEDURE FOO(A, B)
BEGIN
A = B
END
(Помните, что имена формальных параметров произвольные... учитываются только позиции).
Желаемый код мог бы выглядеть так:
FOO: MOVE 4(SP),D0
MOVE D0,6(SP)
RTS
Обратите внимание, что для адресации формальных параметров нам будет необходимо знать, какую позицию они занимают в списке параметров. Это подразумевает некоторые изменения в содержимом таблицы идентификаторов. Фактически, в нашем односимвольном случае лучше всего просто создать новую таблицу идентификаторов для формальных параметров.
Давайте начнем с объявления новой таблицы:
var Params: Array['A'..'Z'] of integer;
Нам также необходимо отслеживать, сколько параметров имеет данная процедура:
var NumParams: integer;
И мы должны инициализировать новую таблицу. Теперь, не забудьте, что список формальных параметров будет различным для каждой процедуры, которые мы обрабатываем, так что мы будем должны инициализировать эту таблицу заново для каждой процедуры. Вот инициализатор:
{–}
{ Initialize Parameter Table to Null }
procedure ClearParams;
var i: char;
begin
for i := 'A' to 'Z' do
Params[i] := 0;
NumParams := 0;
end;
{–}
Мы поместим обращение к этой процедуре в Init и также в конец DoProc:
{–}
{ Initialize }
procedure Init;
var i: char;
begin
GetChar;
SkipWhite;
for i := 'A' to 'Z' do
ST[i] := ' ';
ClearParams;
end;
{–}
.
.
.
{–}
{ Parse and Translate a Procedure Declaration }
procedure DoProc;
var N: char;
begin
Match('p');
N := GetName;
FormalList;
Fin;
if InTable(N) then Duplicate(N);
ST[N] := 'p';
PostLabel(N);
BeginBlock;
Return;
ClearParams;
end;
{–}
Обратите внимание, что вызов внутри DoProc гарантирует, что таблица будет чиста, когда мы в основной программе.
Хорошо, теперь нам нужны несколько процедур для работы с таблицей. Следующие несколько функций являются по существу копиями InTable, TypeOf и т.д.:
{–}
{ Find the Parameter Number }
function ParamNumber(N: char): integer;
begin
ParamNumber := Params[N];
end;
{–}
{ See if an Identifier is a Parameter }
function IsParam(N: char): boolean;
begin
IsParam := Params[N] <> 0;
end;
{–}
{ Add a New Parameter to Table }
procedure AddParam(Name: char);
begin
if IsParam(Name) then Duplicate(Name);
Inc(NumParams);
Params[Name] := NumParams;
end;
{–}
Наконец, нам понадобятся некоторые подпрограммы генерации кода:
{–}
{ Load a Parameter to the Primary Register }
procedure LoadParam(N: integer);
var Offset: integer;
begin
Offset := 4 + 2 * (NumParams – N);
Emit('MOVE ');
WriteLn(Offset, '(SP),D0');
end;
{–}
{ Store a Parameter from the Primary Register }
procedure StoreParam(N: integer);
var Offset: integer;
begin
Offset := 4 + 2 * (NumParams – N);
Emit('MOVE D0,');
WriteLn(Offset, '(SP)');
end;
{–}
{ Push The Primary Register to the Stack }
procedure Push;
begin
EmitLn('MOVE D0,-(SP)');
end;
{–}
(Последнюю подпрограмму мы уже видели прежде, но ее не было в этой остаточной версии программы.)
После этих приготовлений мы готовы работать с семантикой процедур со списками вызовов (помните, что код для работы с синтаксисом уже на месте).