М. Бен-Ари - Языки программирования. Практический сравнительный анализ (2000) (1160781), страница 20
Текст из файла (страница 20)
Однако если вы используете именованные параметры, то, возможно, вам придется сильно изменить свою программу, чтобы установить соответствие новым именам параметров:
Ada |
7.3. Передача параметров подпрограмме
Описание механизма передачи параметров — один из наиболее тонких и важных аспектов спецификации языка программирования. Неверная передача параметров — главный источник серьезных ошибок, поэтому мы подробно рассмотрим этот вопрос.
Давайте начнем с данного выше определения: значение фактического параметра передается формальному параметру. Формальный параметр — это просто переменная, которая объявлена внутри подпрограммы, поэтому, очевидно, нужно копировать значение фактического параметра в то место памяти, которое выделено для формального параметра. Этот механизм называется
«семантикой copy-in » («копирование в») или «вызовом по значению» (call-by-value). На рисунке 7.1 показана семантика copy-in для процедуры:
procedure Proc(F: in Integer) is
begin
Ada |
end Proc;
и вызова:
Ada |
Proc(2+3*4);
Преимущества семантики copy-in:
• Copy-in является самым надежным механизмом передачи параметров. Поскольку передается только копия фактического параметра, подпрограмма никак не может испортить фактический параметр, который, несомненно, «принадлежит» вызывающей программе. Если подпрограмма изменяет формальный параметр, изменяется только копия, а не оригинал.
• Фактические параметры могут быть константами, переменными или выражениями.
• Механизм copy-in может быть очень эффективным, потому что начальные затраты на копирование делаются один раз, а все остальные обращения к формальному параметру на самом деле являются обращениями к локальной копии. Как мы увидим в разделе 7.7, обращение к локальным переменным чрезвычайно эффективно.
Если семантика copy-in настолько хороша, то почему существуют другие механизмы? дело в том, что часто мы хотим изменить фактический параметр, несмотря на тот факт, что такое изменение «небезопасно»:
• Функция возвращает только один результат, но, если результат вычисления достаточно сложен, может возникнуть желание вернуть несколько значений. Чтобы сделать это, необходимо задать в процедуре несколько фактических параметров, которым могут быть присвоены результаты вычисления. Обратите внимание, что этого часто можно избежать, определив функцию, которая возвращает в качестве результата запись.
• Кроме того, цель выполнения подпрограммы может состоять в модификации данных, которые ей передаются, а не в их вычислении. Обычно это происходит, когда подпрограмма обрабатывает структуру данных. Например, подпрограмма, сортирующая массив, не вычисляет значение; ее цель состоит только в том, чтобы изменить фактический параметр. Нет никакого смысла сортировать копию массива!
• Параметр может быть настолько большим, что копировать его неэффективно. Если copy-in используется для массива из 50000 целых чисел, может просто не хватить памяти, чтобы сделать копию, или затраты на копирование будут чересчур большими.
Первые две ситуации легко разрешить с помощью семантики copy-out («копирование из»). Фактический параметр должен быть переменной, а подпрограмме передается адрес фактического параметра, который она сохраняет. Для формального параметра используется временная локальная переменная, и значение должно быть присвоено формальному параметру, по крайней мере, один раз во время выполнения подпрограммы. Когда выполнение подпрограммы завершено, значение копируется в переменную, на которою указывает сохраненный адрес. На рисунке 7.2 показана семантика copy-out для следующей подпрограммы:
procedure Proc(F: out Integer) is
begin
Ada |
end Proc;
A: Integer;
Proc(A); -- Вызов процедуры с переменной
Когда нужно модифицировать фактический параметр, как, например, в sort, можно использовать семантику copy-in/out фактический параметр копирует-
ся в подпрограмму, когда она вызывается, а результирующее значение копируется обратно после ее завершения.
Однако механизмы передачи параметров на основе копирования не могут решить проблему эффективности, связанную с «большими» параметрами. Решение, которое известно как «вызов по ссылке» (call-by-reference) или «семантика ссылки» (reference cemantics), состоит в том, чтобы передать адрес фактического параметра и обращаться к параметру косвенно (см. рис. 7.3). Вызов подпрограммы эффективен, потому что для каждого параметра передается только указатель небольшого, фиксированного размера; однако обращение к параметру может оказаться неэффективным из-за косвенности.
Чтобы получить доступ к фактическому параметру, нужно загрузить его адрес, а затем выполнить дополнительную команду для загрузки значения. Обратите внимание, что при использовании семантики ссылки (или copy-out), фактический параметр должен быть переменной, а не выражением, так как ему будет присвоено значение.
Другая проблема, связанная с вызовом по ссылке, состоит в том, что может возникнуть совмещение имен (aliasing), т. е. может возникнуть ситуация, в которой одна и та же переменная известна под несколькими именами.
В следующем примере внутри функции f переменная global получает алиас (т. е. альтернативное имя) *parm:
C |
inta[10];
int f(int *parm)
{
*parm = 5: /* Та же переменная, что и "global" */
return 6;
}
х = a[global] + f(&global);
В этом примере, если выражение вычисляется в том порядке, в котором оно записано, его значение равно а[4] + 6, но из-за совмещения имен значение выражения может быть 6 + а[5], если компилятор при вычислении выражения выберет порядок, при котором вызов функции предшествует индексации массива. Совмещение имен часто приводит к непереносимости программ.
Реальный недостаток «вызова по ссылке» состоит в том, что этот механизм по сути своей ненадежен. Предположим, что по некоторым причинам подпрограмма считает, что фактический параметр — массив, тогда как реально это всего лишь одно целое число. Это может привести к тому, что будет затерта некоторая произвольная область памяти, так как подпрограмма работает с фактическим параметром, а не просто с локальной копией. Этот тип ошибки встречается очень часто, потому что подпрограмма обычно пишется не тем программистом, который разрабатывает вызывающую программу, и всегда возможно некоторое недопонимание.
Безопасность передачи параметров можно повысить с помощью строгого контроля соответствия типов, который гарантирует, что типы формальных и фактических параметров совместимы. Однако все еще остается возможность недопонимания между тем программистом, кто написал подпрограмму, и тем, чьи данные модифицируются. Таким образом, мы имеем превосходный механизм передачи параметров, который не всегда достаточно эффективен (семантика copy-in), а также необходимые, но ненадежные механизмы (семантика copy-out и семантика ссылки). Выбор усложняется ограничениями, которые накладывают на программиста различные языки программирования. Теперь мы подробно опишем механизмы передачи параметров для нескольких языков.
Параметры в языках С и C++
В языке С есть только один механизм передачи параметров — copy-in:
int i = 4; /* Глобальная переменная */
C |
void proc(int i, float f)
{
i=i+(int) f; /* Локальная переменная "i" */
}
proc(j, 45.0); /* Вызов функции */
В ргос изменяемая переменная i является локальной копией, а не глобальной переменной i.
Чтобы получить функциональные возможности семантики ссылки или copy-out, пишущий на С программист должен прибегать к явному использованию указателей:
int i = 4; /* Глобальная переменная */ [с]
void proc(int *i, float f)
{
*i = *i+ (int) f; /* Косвенный доступ */
}
proc(&i, 45.0); /* Понадобилась операция получения адреса */
После выполнения ргос значение глобальной переменной i изменится. Необходимость пользоваться указателями для реализации ссылочной семантики следует отнести к неудачным решениям в языке С, потому что начинающим программистам приходится изучать это относительно сложное понятие в начале курса.
В языке C++ этот недостаток устранен, поскольку в нем есть возможность задавать параметры специального ссылочного типа (reference parameters):
int i = 4; // Глобальная переменная
C++ |
void proc(int & i, float f)
{
i = i + (int) f; // Доступ по ссылке
}
proc(i, 45.0); // He нужна операция получения адреса
Обратите внимание на естественность стиля программирования, при котором нет неестественного использования указателей. Это усовершенствование механизма передачи параметров настолько важно, что оправдывает использование C++ в качестве замены С.
Вам часто придется применять указатели в С или ссылки в C++ для передачи больших структур данных. Конечно, в отличие от копирования параметров (copy-in), существует опасность случайного изменения фактического параметра. Можно задать для параметра доступ только для чтения, объявив его константой:
void proc(const Car_Data & d)
{
d.fuel = 25; // Ошибка, нельзя изменять константу
}
Объявления const следует использовать возможно шире, как для того, чтобы сделать смысл параметров более прозрачным для читателей программы, так и для того, чтобы отлавливать возможные ошибки.
Другая проблема, связанная с параметрами в языке С, состоит в том, что массивы не могут быть параметрами. Если нужно передать массив, передается адрес первого элемента массива, а процедура отвечает за правильный доступ к массиву. Для удобства имя массива в качестве параметра автоматически рассматривается как указатель на первый элемент:
intb[50]; /* Переменная типа массив */
C |
{
а[100] = а[200]; /* Сколько элементов? */
}
proc(&b[0]); /* Адрес первого элемента */
proc(b); /* Адрес первого элемента */
Программисты, пишущие на С, быстро к этому привыкают, но, все равно, это является источником недоразумений и ошибок. Проблема состоит в том, что, поскольку параметр — это, фактически, указатель на отдельный элемент, то допустим любой указатель на переменную того же типа:
int i;
void proc(int a[ ]); /* "Параметр-массив" */
proc(&i); /* Допустим любой указатель на целое число!! */
Наконец, в языке С контроль соответствия типов никак не действует между файлами, поэтому можно в одном файле поместить
C |
а в другом файле —
C |
а затем месяцами искать ошибку.