Трудности понятия ссылки в языках программирования

Владимир Лидовский

Аннотация

Исследуется понятие «ссылка» и смежные понятия «указатель», «параметры подпрограмм», «переменные» в разных языках программирования (C++, Pascal, Lisp, Java, PHP и др.). Устанавливается, что ряд терминов в литературе на русском языке переведены не совсем корректно. Дается определение ссылки, общее для почти всех языков программирования. Устанавливается, что ссылка в C++ — это фактически не спецификатор типа, а спецификатор класса памяти. Делается предположение о полезности введения термина "ссылочное присваивание" в некоторые языки программирования.

The difficulties with the term "a reference" in programming languages

Vladimir Lidovski

Abstract

The term «a reference» and related terms «a pointer», «a subroutine's parameter», «a variable» are explored in the different programming languages (C++, Pascal, Lisp, Java, PHP, ...). It is discovered that some terms are not completely correct in its general Russian translations. The definition of the term «a reference» suitable for almost all programming languages is given. It is asserted that the references in C++ are more like to be storage class specifiers than specifiers of the type. There is also assumption that the introduction of the term "an assignment by reference" will be useful for some programmining languages.

Постановка вопроса

Одно из самых неоднозначных понятий языков программирования — это понятие ссылки. Действительно, что такое ссылка? Это тип, переменная, параметр подпрограмы или не то, и не другое? Почему в одних языках ссылки противопоставляются указателях, в других мирно с ними сосуществуют, в третьих их трудно отличить друг от друга, а четвертых они даже не называются ссылками? Языки си++, паскаль, Perl и Java вводят разные парадигмы для использования этого термина. Другие языки также разбиваются по этому критерию, как минимум, на четыре группы. Например, ссылочный тип в смысле паскаля имеется в модуле, обероне, аде1, а ссылки в смысле Java — в лиспе2, PHP, современном бейсике и других. Запутанности добавляет то, что ссылочный тип в смысле паскаля в си/си++ так никогда не называется. Последнее, а также сам факт наличия «ссылочного типа» в паскале или аде связаны с особенностями перевода документации по языкам программирования на русский язык. Почему-то при переводе материалов о паскале иногда слова pointer type переводятся как «ссылочный тип», а при переводе этих же слов в материалах по си/си++/Java/... используется слово указатель. Еще большая путаница возникает из-за того, что в материалах по паскалю передача параметров по ссылке (by reference) переводится именно как передача по ссылке, что приводит к отсутствующей в исходных англоязычных материалах коллизии между понятием типа указателей и такой передаче параметров.

Почему с переводом слова ссылка возникает столько проблем? Если отвлечься от относящегося к пенитенциарной системе смысла, то существительное ссылка, как производное от глаголов сослаться-ссылаться, весьма точно соответствует английскому существительному a reference, связанному с глаголом to refer. Однако в отличие от английского языка в русском языке это слово не полностью формально определено в словарях. В ссылочной теории смысла3 рассматривается регулярный процесс появления идей одних сущностей при наблюдении за другими сущностями. Например, при виде дыма естественно подумать об огне. То, что наблюдается, в разных контекстах называют знаком, символом, ссылкой, означающим, денотатом. То, о чем можно подумать, — смыслом, значением, означаемым, десигнатом. В приведенном примере дым является ссылкой на огонь, также как подпись является ссылкой на подписавшегося. Проблема в том, что в русском, в отличие от английского, слово ссылка почти всегда выпадает из первого списка и рассматривается только в частном значении книжной ссылки (см. словарь С. Ю. Ожегова и Н. В. Шведовой). Таким образом, ссылка в отличие от указателя уже обладает ассоциированным значением, а указатель можно использовать для доступа к этому значению, применяя соответствующие средства. Очевидно, что «смысловая дистанция» между этими понятиями совсем небольшая. Однако, в текстах на русском языке, не относящихся к программированию, ее почти всегда сохраняют. Англоязычные тексты сохраняют эту дистанцию в большинстве случаев и в литературе о программировании. Хотя есть исключения. В стандарте языка ада [12] о типах для косвенного доступа несколько двусмысленно говорится, как о том, что «в некоторых других языках зовется указателями или ссылками». Если понимать это буквально, то можно подумать о синонимичности этих терминов, но если иметь в виду только назначение этих типов, то это сохранит означенную дистанцию. В языке Perl его автор и автор основного руководства к нему (Larry Wall), а также другие, кто пишет книги, в частности, на русском [4] по Perl, установили несколько нетиповое обращение с терминами... Есть еще сложность, связанная с операцией взятия значения по указателю или ссылке. При работе со ссылками в отличие от указателей эта операция, как правило, используется неявно. В английском языке эта операция называется a dereference — ее часто несколько усложненно переводят как «разыменование» или более удачно как «косвенное обращение». И ее применяют как к ссылкам, так и к указателям. Сложностей для перевода добавляет также слово link (связь), которое в компьютерном контексте всегда переводят как «ссылка», что приводит к неточностям, в частности, при описании работы с HTML-документами.

Итак, если отбросить проблемы с переводом, то остается только три способа работать с понятием ссылки: способ, принятый в си++ и паскале, способ, принятый в Perl, и способ, используемый практически во всех других языках, где можно работать со ссылками. Более того, способ си++ — это скорее частный случай от общего случая использования ссылок в Java, PHP и др., чем что-то совершенно особенное.

Для того, чтобы четко определить понятие ссылки, нужно начать с определения понятия переменная. Но прежде необходимы некоторые предварительные замечания.

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

Компьютерные термины, в своем большинстве, не появились вместе с компьтерами, а были взяты из естественных языков. Эти термины естественно унаследовали свои прежние значения. Более того, часто, используя эти прежние докомпьютерные значения, удобнее объяснять те или иные аспекты функционирования вычислительных машин для достаточно широкой аудитории непрофессионалов. Например, компьютерный термин переменная часто удобнее представлять в общематематическом смысле, как некое название, связанное со значением, которое можно менять. В однозначной же интерпретации на уровне аппаратуры переменная — это ресурс, для существования которого необходима компьютерная память, т. е. некоторое известное, определяемое типом и состоянием переменной количество бит, у каждого из которых в каждый момент есть известный адрес. Как правило, память для переменной выделяется в виде последовательности байт, а в случае переменных, представляющих сложные структуры (сети, например), когда вся память для переменной обычно не является непрерывной, всегда есть связывающий разрозненные участки памяти центр, адрес которого может считаться адресом переменной. Для однонаправленного списка таким «центром» может считаться адрес структуры, содержащей указатель или ссылку на первый элемент списка.

Иногда при введении компьютерных терминов их авторы сознательно стараются сделать их более абстрактными и оторванными от аппаратуры. Это делает их более понятными для непрофессионалов и позволяет вводить искусственные ограничения, в частности, способные повысить надежность программ. Для любого достаточно квалифицированного программиста, типы указателей в си/си++, типы указателей и подпрограмм в паскале/обероне, типы доступов в аде (список можно продолжить) различаются только набором таких ограничений: в си их практически нет, в си++ и Borland Pascal есть незначительные, в нерасширенном паскале и обероне — значительно большие, но в больше всего их в аде. Такие ограничения можно рассматривать как дополнительные атрибуты базового машинного представления. Такой подход ни в коей мере не сможет нарушить уникальность и полезность исходных терминов, абсолютно также, как понятие «млекопитающее» никак не мешает работать с понятием «тигр», хотя оно и позволяет объединить тигра с кроликом.

Целью статьи не является всестроннее описание всех используемых в ней компьютерных терминов. Поэтому некоторые атрибуты понятий переменная, указатель и т. п., которые автор не считает напрямую связанными с темой статьи, останутся незатронутыми.

Переменные

Понятие переменная является базовым для всех языков программирования, хотя, в некоторых из них, например, в PostScript, вообще нет явных переменных, а работа с переменными в прологе мало похожа на работу с ними в большинстве других языков. У переменной должен быть адрес в памяти — номер байта в последовательности занумерованных с нуля всех байт памяти компьютера. Имеется в виду абсолютный, уникальный адрес байта из всей памяти (в общем случае, виртуальной) компьютера. Переменная также должна иметь размер — некоторое количество последовательных байт, начиная с ее адреса. Этот размер определяется типом переменной. Если переменная представляет сложную структуру, то ее размером часто считают размер «центра» (см. выше) такой структуры. В некоторых языках (си, си++, паскаль, Java, ...) тип переменной устанавливается заранее и его впоследствии нельзя менять. В языке бейсик тип можно также жестко установить, но есть еще возможность при использовании типа Variant предоствить переменной иметь значения любого типа. В «бестиповых» языках, например, лиспе, или в языках сценариев PHP, JavaScript, AWK, TCL, BASH и др. тип переменной может почти свободно меняться при исполнении программы.

Как правило (исключение составляют лишь некоторые языки сценариев PHP, Perl, BASH, но не JavaScript, TCL, AWK или VBScript), при обращении к имени переменной происходит обращение по связанному с ней адресу. В «языках-исключениях» (Perl, PHP, ...) работа с переменными организована также как в PostScript, через словари (таблицы или деревья имен, ассоциативные массивы), поэтому при обращении к переменной нужно указывать словарь. В Perl для этого используют значки * (словарная статья), $ (скаляр), % (хэш), @ (массив) и & (подпрограмма), в PHP и BASH — $.

Некоторые языки предоставляют возможность явно создавать переменные в процессе исполнения программы, что называется динамической работой с памятью. Созданные таким образом переменные также называются динамическими, для их использования необходимы ссылки или указатели. Динамические переменные в свою очередь необходимы для возможности работать со сложными структурами данных (деревьями, множествами, сетями, списками и т. п.). Не все языки программирования предоставляют средства для явного уничтожения таких переменных. Во время своего существования динамические переменные практически ничем не отличаются от других переменных (могут быть некоторые небольшие различия в синтаксисе). Рассмотрим пример на си++.


  int *const &a1 = new int[10], a2[10], *a3 = new int [10];

Дальнейшая работа с a1, a2 и a3 будет протекать практически идентично. Память для a1 и a3 выделена динамически, но только для a3 ее размер можно будет потом менять (кстати, a1 — это ссылка типа указатель). В JavaScript (как и в Java) все массивы — динамические, например,

  var a3 = new Array(10);

создаст массив, размер которого затем можно будет изменить. В языках PHP, AWK, Perl, C++ (со стандартной библиотекой) и других, где есть ассоциативные массивы, все такие массивы неявно являются динамическими в силу своей природы. В некоторых языках (PostScript, Python) ассоциативные массивы называют словарями, а в Perl — хэшами (hash).

Очевидно, что при смене типа переменной может измениться ее размер. В случае уменьшения размера в памяти образуется бесполезный «зазор», что неявно требует освобождения всей занимаемой переменной памяти и выделения новой, а случай увеличения размера приводит уже к явной необходимости смены адреса переменной. Смена адреса при смене типа, как правило, жестко контролируется транслятором. В языках программирования, где тип переменных фиксируется, обычные переменные не могут менять свой адрес.

Для гибкой работы с адресами данных могут использоваться как переменные-указатели, так и переменные-ссылки. Кроме того, необходимость настраиваться на разные адреса в памяти возникает при передаче параметров по ссылке.

Указатели

Указатель — это переменная, предназначенная для указания на переменные и другие объекты в памяти компьютера (например, подпрограммы). В каком-то смысле они похожи на дорожные указатели — они указывают на место в памяти и часто содержат информацию о типе или размере этого места. Появление типа указатель было весьма скандальным. Впервые такой тип был независимо введен в языки си и паскаль. Известный теоретик программирования Хоар встретил это фразой: «Их введение в высокоуровневые языки было шагом назад, от которого мы никогда не оправимся». Написано множество работ, в которых обсуждаются недостатки указателей. Главным образом критикуется возможность появление «висячих указателей», указывающих по неверным адресам, например, на уже удаленный объект. Например, в следующем фрагменте программы на паскале образуется висячий указатель p.


  var 
    p: ^integer;
  begin
    new(p);
    p^ := 11;
    dispose(p);

Целой переменой p^ можно после этого присвоить некоторое число, что является очевидно семантически некорректным действием. Кроме того, использование указателей может приводить к потере связи с объектом в памяти, что превращает такой объект в «мусор», который нельзя даже явно удалить (его может удалить только специальный процесс уборки мусора, которого в паскале или C/C++ нет). Например, в коде на си++

  int *p = new int;
  p = new int;

созданная первой динамическая переменная превращается именно в такой мусор. Борьба с этими недостатками привела к появлению неэффективного и несколько жутковатого по названию метода «надгробий», а также к более благозвучному методу «ключей и замков». Если вернуться к аналогии с дорожными указателями, то все сводится к желанию сделать невозможными неправильные указатели на дорогах.

Активное неприятие указателей дало свои результаты: в большинстве языков, созданных на основе си (Java, C#, PHP, JavaScript, ...), указателей нет. Хотя в Perl, PHP или BASH нет явных указателей, но синтаксически работа с переменными организуется, как если бы все переменные были символическими указателями. Разница между обычными и такими указателями, в точности такая же, как между индексами обычных и ассоциативных массивов. Например, исполнение фрагмента кода PHP


  $c = "z"; 
  $b = "c";
  $a = "b";
  echo $$$a;

приведет к печати символа z. Знак доллара означает обратиться к указываемому значению — именно так и работают с указателями! Кстати, здесь опять сталкиваемся с «трудностями перевода» — в отечественной литературе эти фактические символические указатели обычно переводят как «символические ссылки», хотя в исходных материалах случай повторного использования знака доллар называется переменная от переменной (variable variable). Можно продолжить тему тем, что этот запутывающий суть перевод вызван другой трудностью в переводе: в литературе по файловым системам есть термины «символическая ссылка» и «жесткая ссылка». Похоже, что из-за того, что связанные с этими терминами понятия отдаленно похожи на используемые в PHP ссылки (просто references без какой-либо «жесткости») и обычные переменные, эти термины переводятся как «жесткие» и «символические ссылки». В ангоязычной литературе по файловым системам используется другое слово — link, в смысле близком, но не идентичном понятию ссылка в языках программирования. Действительно, при работе с файловыми символическими ссылками (ярлыками) не нужно отслеживать всю возможную цепочку связей, как в примере выше, — значение у всех ссылок в такой цепочке одно. Есть еще один фактор, возможно решающий, приводящий к неточному переводу — это Perl. Там, действительно, используют термины a hard reference, a symbolic reference и даже, вместо последнего, a soft reference. Но, правильно ли при переводе терминологию из одного языка волюнтаристки переносить на другой? Тем более, что работа со ссылками в PHP и жесткими ссылками в Perl происходит принципиально по-разному?

Сравним приведенный код PHP с аналогичным кодом для си.


  char *c = "z",
    **b = &c, 
    ***a = &b;
  printf("%c", ***a);

Разница только в том, что в си нужно каждый раз точно указывать тип переменной. Это был искусственный пример — в общем случае для описания работы с символьным указателем нужен ассоциативный массив. Рассмотрим точный аналог приведенного кода PHP с использованием такого массива на языке AWK (именно там впервые были введены ассоциативные массивы) — нужно лишь везде знак $ заменить на обращение к массиву s.


  s["c"] = "z"
  s["b"] = "c" 
  s["a"] = "b"
  print s[s[s["a"]]]

Подобные длинные обращения (в случае, если значения образуют цикл, то эти обращения могут быть неограниченной длины) очень естественно реализованы в лиспе, где нет ни явных указателей, ни специального понятия ссылка. В частности, рассматриваемый пример в переводе на лисп выглядит так.


  (set 'c 'z)
  (set 'b 'c) 
  (set 'a 'b)
  (print (eval (eval (eval 'a))))

Фактически обычные указатели — это индексы к массиву байт всей памяти компьютера. Можно еще вспомнить, что в си работа с массивами естественно сводится к алгебре указателей: в следующем примере строки два и три эквивалентны.


  int a[4];
  a[1] = 8;
  *(a+1) = 8;

В языке си++ есть еще указатели на компоненты структур, которые являются адресами (номерами, с 0) байт памяти для указываемых компонент относительно начала этих структур. Например,


  struct T {
    int i;
  } t;
  int T::*i_off = &T::i;
  void f() {t.*i_off = 7;}

Здесь i_off — это относительное смещение любого целого числа относительно начала любого объекта типа T, например, смещение поля i в классе t.

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

Указатели размещаются в памяти, как правило, также как и переменные целого типа. Указатели позволяют явно работать с переменными и другими объектами с адресом через их адреса.

В паскале или аде указатели можно только сравнивать на равенство или неравенство. В си (и си++) к ним можно применять любые сравнения, а также операции + и -, что дает большую гибкость, особенно при работе с массивами или строками (строки в си, как и в паскале, — это массивы символов). Рассмотрим пример


  char s[] = "abcde", *p = s + 1;
  int i = p - s;

Здесь p устанавливается значением строки "bcde", а i — значением числа 1: s[3] и p[2] — это один и тот же символ 'd'. Разница между s и p в том, что s имеет фиксированную привязку к памяти, хранящей строку "abcde", а p может указывать на любой символ как отдельный, так и в составе любой строки.

В силу очевидного по смыслу соответствия указателей и индексов массива, проблемы висячих указателей и выход индекса неассоциативного массива за диапазон допустимых значений являются родственными. Хотя технически вторая проблема, из-за отсутствия сплошного диапазона допустимых значений для указателей, определяется проще.

В языках, созданых на основе паскаля, модуле и обероне, указатели сохранились, но в обероне их можно использовать только для указания на массивы и записи. В обероне при обращении к полю data записи, указываемой указателем p, можно использовать как обычный синтаксис p^.data, так и сокращенный p.data. Этот сокращенный синтаксис фактичекски превращает указатель в ссылку, что нашло свое отражение в новейшем паскалеподобном языке Zonnon, где то, что прежде называлось указателями уже явно называется ссылками, а значок ^ стал необязательным элементом языка, который всегда можно опускать.

Параметры и возвращаемые значения

При передаче параметров используют два основных способа: по значению или по ссылке (это четкое разделение впервые появилось в паскале). Формальным параметрам-значениям не нужно иметь какой-либо привязки к памяти, занимаемой соответствующими им фактическими параметрами, — значения фактических параметров просто копируются в формальные. В случае передачи параметров по ссылке такая привязка необходима — формальные параметры должны получить адреса соответствующих фактических параметров. В этом случае происходит отождествление формальных и фактических параметров — формальные параметры становятся синонимами фактических. Рассмотрим пример на паскале.


  procedure t(var p: integer);
    begin
      p := 25
    end;
  var
    v: integer;
  begin
    v := 52;
    t(v);
    writeln(v)
  end.

Его выполнение приведет к печати числа 25, т. к. внутри процедуры t происходило отождествление формального параметра p с фактическим параметром v.

Результаты функций также могут передаваться по значению или по ссылке. В первом случае создается временное значение-результат, которое после его использования в выражении автоматически уничтожается. Во втором случае результат записывается в определенное место в памяти. Если функция возвращает результат-ссылку, то она допустима, в частности, в левой части оператора присваивания, например, код си++


  int& f(int &p) {return p;};
  main () {
    int v;
    f(v) = 7;

установит значение переменной v в 7.

Параметры и результаты функций — это неявные локальные переменные, в частности, параметры, передаваемые по ссылке, и результаты функций, возвращаемые по ссылке, — это переменные-ссылки, локальные для подпрограммы, где они используются. В некоторых языках, в частности, лисп или AWK, локальные переменные подпрограмм объявляются через неиспользуемые формальные параметры. В языке паскаль Вирта есть ссылки-параметры, но явных ссылок нет. В литературе по паскалю параметры-ссылки часто называются параметрами-переменными, что не совсем удачно, т. к. любые формальные параметры — это обычные переменные, открытые для взаимодействия с другими компонентами программы. В языке ада есть даже специальные средства для регулирования характера такого взаимодействия. С другой стороны, если называть параметры-ссылки параметрами-переменными по причине того, что им при вызове подпрограммы должны сопоставляться переменные, то это формально вполне корректно, но ограничивает общее понятие передачи по ссылке рамками языка паскаль. В си++, например, по ссылке можно передавать и значения выражений, например, исполнение


  int f(const int &p) {return p;};
  main () {
    cout << f(1+1) << endl;
  }

приведет к выводу 2. В последних версиях паскаля фирмы Borland можно написать абсолютно аналогичный код

function f(const p: integer):integer;
begin f:=p end;
begin
  writeln(1+1)
end.

Однако, здесь параметр p называется параметром-константой, хотя он и передается по ссылке.

Используя указатели, можно неявно производить передачу параметров по ссылке через явное использование передачи параметров по значению — так это принято в языке си, где все параметры могут передаваться только по значению. Например, исполнение


  int f(int *p) {return (*p)++;};
  main () {
    int v = 7;
    printf("%d\n", f(&v));
  }

приведет к печати 7 и установки v в 8. Аналогичный пример на си++ с явным использованием передачи параметра по ссылке выглядит чуть более элегантным.

  int f(int &p) {return p++;};
  main () {
    int v = 7;
    printf("%d\n", f(v));
  }

Ссылки

Кроме ссылок, формальных параметров подпрограмм, могут быть еще ссылки, работа с которыми практически ничем не отличается от работы с обычными переменными. Языки си++, Java, лисп и некоторые другие предлагают возможности для такой работы. Сначала надо четко определить это понятие.

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

Обращение по указателю можно рассматривать как ссылку, например, *p в си или p^ в паскале, можно рассматривать как ссылки, что похоже на ссылки на символы в HTML, где, например, &amp; рассматривается как ссылка на знак &.

В расширение паскаля фирмы Borland (и в Delphi) можно ввести ссылку при помощи конструкции со служебным словом absolute, хотя в этом расширении j называется не ссылкой, а переменной с абсолютным адресом. Например,


  i: integer;
  j: integer absolute i;

вводит ссылку j. Аналогичный пример на си++ выглядит так.


  int i;
  int &j = i;

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


  (setq x '(a b))
  (setf (car x) (cdr x)) ;сделает x равным ((b) b)
  (setf (cadr x) 5) ;сделает x равным ((5) 5)
  

В Java и JavaScript ссылки — это переменные-массивы и переменные-объекты. В бейсике — только объекты. В этих языках нет явного типа ссылка. В языках Java и C# со строгой проверкой типов ссылки призваны полностью заменить указатели. Кстати, в C# не полностью отказались от указателей — их там можно использовать в так называемых небезопасных (unsafe) контекстах.

Присваивание в этих языках во многих случаях передает не значение, а ссылку. Передача параметра по ссылке — это неявное ссылочное присваивание. В современном бейсике существует как присваивание значений со словом LET, обычно игнорируемым, так и ссылочное присваивание SET. В PHP можно заменить присваивание значений ссылочным присваиванием, записывая =&, вместо =. Например, в


  $i = 7;
  $j = $i;
  $k =& $i;

присваивание $j=$i просто установит $j в 7, а присваивание $k=&$i еще и установит адрес значения $k таким же как и у $i. Присваивание, подобное последнему, возможно и в си/си++, но там переменная в левой части должна быть указателем. В лиспе вместо присваивания значения, SET, можно использовать присваивание, которое может реализовывать возможности похожие на присваивание по ссылке, SETF. Кроме того, есть еще функции RPLACA, RPLACD, NCONC для частных случаев такого особого ссылочного присваивания. К сожалению, явного понятия ссылочное присваивание в большинстве современных языках программирования нет. Обычно, при использовании такого присваивания говорят о создании ссылки. В современном Фортране есть особая операция присваивание указателю (pointer assignment), =>, и, возможно, родственный термин (assignment by reference) будет рано или поздно востребован не только в PHP.

Такая работа с переменными постоянно приводит к тому, что старые значения переменных становятся невозможными для дальнейшего использования, т. е. превращаются в «мусор», что приводит к необходимости организации автоматической его уборки. Такая уборка, которая запускается специальными стандартными средствами является необходимой частью любого транслятора с лиспа, Perl, PHP, Java и др. В этих языках нет необходимости в явном удалении динамических объектов из памяти и не может возникнуть проблемы «висячих указателей». Рассмотрим несколько примеров.


  (set 'v '(a b c))
  (set 'v 4)
В этом фрагменте программы на лиспе переменной v сначала был присвоен список (a b c), а затем число 4. Список после второго присваивания превращается в мусор.

  int [] a1 = new int[100];
  int [] a2;
  a2 = new int[200];
  a1 = a2;

Здесь в коде на Java переменная a1 становится последовательно массивом из 100 и 200 целых чисел. После ссылочного присваивания a1=a2 память, выделенная для массива из 100 чисел также превращается в мусор.

Ссылки в расширенном паскале, кроме параметров подпрограмм, являются несколько искусственной возможностью и хотя они не явлются отдельным типом, но фактически почти во всем совпадают со ссылками си++. Ссылки в си++ являются на взгляд автора чрезвычайно неудачно определенными — ссылки здесь это особый тип, когда по сути они являются спецификатором класса памяти, таким как static или extern. Действительно переменные типов int и int& невозможно различать в практически любых контекстах — ссылки лишь имеют заранее известный адрес. Конкретно, переменные i и j типов int и int& из примера выше всегда взаимозаменяемы — какой же это тип?! Или нужно признать, что понятие типа данных, которое, как правило, фиксируется на совместимости операндов при операциях присваивания и, в частности, при передаче параметров в подпрограмму в си++ понимается совершенно иначе. Действительно, классическое определение типа данных основывается на диапазоне допустимых значений величин типа и на наборе применимых к ним операций — для данных типов T и T& (T — любой тип) обе эти характеристики идентичны, даже typeid этих «типов» совпадает. Однако, пересмотренного понятия тип данных стандарт си++ не вводит.

В си++, в отличие от Java, адрес ссылки фиксируется при ее создании и не может потом меняться, например,


  int &a = *new int;

определяет целую ссылку, изменить адрес которой впоследствии уже невозможно. Динамическую память выделенную для ссылки в таких условиях освобождать бессмысленно, хотя и возможно явным обращением к delete (это приведет к возникновению того, что можно назвать висячей ссылкой, объекта даже более потенциально опасного, чем висячий указатель). Полная поддержка ссылок в си++ могла бы быть реализована отказом от искусственного ссылочного типа и введением дополнительной операции, ссылочного присваивания, готовый синтаксис которой уже есть в языке (&i=j), но это потребовало бы введение поддержки механизма автоматической уборки мусора.

Ссылки в Perl могут быть двух разновидностей: символические (мягкие) и жесткие. Символические ссылки — это то, что в PHP называется переменные от переменных, т. е. это просто значения переменных, используемые как имена при доступе к словарю переменных. Жесткие ссылки — это весьма любопытный объект. Они похожи на ссылки в PHP, например,


  $ref=\$var;

установит адрес $ref таким же как и у $var. Однако, в Perl можно написать также

  $ref=\\$var; 

— это приведет к установке адреса $ref таким же как и у специально для этого случая создаваемой анонимной переменной-ссылки, адрес которой $var. Такой сверхгибкости для работы со ссылками нет, похоже, больше нигде.

Ссылки в Perl можно рассматривать как некоторое исключение прежде всего потому, что для доступа к их значениям нужно использовать специальные средства, т. е. со ссылками здесь работают как с указателями. Более того, эти средства одинаковы для жестких и символических ссылок, что создает значительные сложности, для преодоления которых в языке есть возможность запрещать символические ссылки в определенных местах кода.

Ссылки против указателей

Здесь речь пойдет только о ссылках в смысле Java, т. к. ссылки в смысле си++ не противопоставляются указателям, а дополняют их. Лисп неявно использует ссылки, но в отличие от Java они не появились в результате выбора между ними и указателями. Ссылки Perl фактически являются таковыми только по названию.

Ссылки требуют меньше синтаксических правил и значков для работы с ними. Работа с переменными-ссылками часто ничем синтаксически не отличается от работы с переменными-нессылками.

С другой стороны, ссылки требуют понимания того, как с ними работать, т. е. понимания того, что ссылки — это «замаскированные указатели». В отличной книге по Java [10] ссылки именно так и объясняются. Поэтому утверждение, что ссылки проще, очень похоже на спорное утверждение о том, что рекурсивные подпрограммы проще своих итерационных аналогов. Действительно, рекурсивные подпрограммы, как правило, выглядят красивее и менее технично, но это ценой скрытия механизма рекурсии, который необходимо, в общем случае, четко представлять. Получается, что синтаксическое упрощение достигается семантическим усложнением, дополнительным требованиям по пониманию синтаксиса конструкций. В частности, требуется понимать разницу между часто внешне идентичными присваиваниями по значению и присваиваниями по ссылке. Например, рассмотрим две команды языка лисп, производящие на первый взгляд совершенно одинаковые действия, дающие в результате объединяющий список (a b c d).


  (append '(a b) '(c d))
  (nconc '(a b) '(c d))

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

Следующий пример на си++ также иллюстрирует путаность понятия ссылки.


struct X {
  char &i, j;
  X(): i(j) {}
};
Здесь ссылка i реализуектся через неявный указатель. Поэтому если поменять, например, &i и j местами, то размер переменной типа X может измениться из-за выравнивания адреса по размеру кратному стандартному для выбранной аппаратуры целому типу!

Рассмотрим еще два явных и синтаксически идентичных присваивания на Java.


  int [] a = new int[10], b = new int [5];
  int i, j = 7;
  i = j;
  a = b;

Присваивание a=b ссылочное, а i=j по значению. Эти проблемы Java несколько необязательные и легко устраняются введением дополнительной операции ссылочного присваивания. Возможно авторы языка отказались от этого, из-за того, что это усложнило бы язык и лишило бы ссылки простоты и элегантности. Есть еще и чисто практический аспект: издержки по автоматической уборке мусора для простых скалярных типов (как в лиспе) могут в несколько раз замедлить присваивания с данными таких типов, эти же издержки при использовании объектых типов (а массивы во многих языках рассматриваются как объекты) незначительны по сравнению с издержками при альтернативном присваивании по значению.

Автоматическая уборка мусора не дает программисту возможности, «открыв скобку», поставить затем парную «закрывающую скобку»: после создания объекта его нельзя явно уничтожить. В частности, использование ссылок вместо указателей при работе со структурами данных (деревьями, множествами, сетями и проч.) приводит невозможности явной операции «уничтожить» при уничтожении элемента структуры. Подобные возможности всегда считались отличительной особенностью средств для быстрой разработки небольших программ и критиковались в составе средств для разработки больших проектов.

Процесс автоматической уборки мусора требует времени и приводит к некоторому замедлению работы основного процесса. Такая уборка мусора более естественна для преимущественно интерпретирующих систем, таких как лисп, PHP или Java. Автоматическая уборка мусора создает непредсказуемые задержки по ходу исполнения программы, что делает ее почти непригодной для систем реального времени.

Фактически ссылки отличаются от указателей только неявностью прямой работы с адресами памяти компьютера. И как следует из существующей практики появления новых языков программирования для быстрого создания программ, ссылки требуют меньшей подготовленности программиста для работы с ними.

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

Литература

  1. Е. А. Зуев Язык программирования Turbo Pascal — М.: Унитех, 1992. — 298 с.
  2. Б. В. Керниган, Р. Пайк Unix — универсальная среда программирования — М.: Финансы и статистика, 1992. — 304 с.
  3. Д. Котеров, А. Костарев PHP 5 — СПб.: БХВ-Петербург, 2007. — 1120 с.
  4. В. В. Маслов Основы программирования на языке Перл — М.: Радио и связь, 1999. — 144 с.
  5. Т. Пауэлл, Ф. Шнайдер Полный справочник по JavaScript — М.: Издательский дом «Вильямс», 2007. — 960 с.
  6. Р. Н. Михеев VBA и программирование в MS Office для пользователей — СПб.: БХВ-Петербург, 2006. — 384 с.
  7. Р. У. Собеста Основные концепции языков программирования — М.: Издательский дом «Вильямс», 2001. — 672 с.
  8. Л. Стерлинг, Э. Шапиро Искусство программирования на языке пролог — М.: Мир, 1990. — 235 c.
  9. Б. Страуструп Язык программирования C++ — СПб., М.: «Невский диалект» — «Издательство БИНОМ», 1999. — 991 с.
  10. К. С. Хорстманн, Г. Корнелл Библиотека профессионала. Java 2. Том 1. Основы — М.: Издательский дом «Вильямс», 2004. — 848 с.
  11. Э. Хювёнен, И. Сеппянен Мир лиспа — М.: Мир, 1990. — 766 с.
  12. Ada Reference Manual ISO/IEC 8652:1995(E) with Technical Corrigendum 1 — The MITRE Corporation, 2000. — 582 p.
  13. C# Language Specification. Standard ECMA-334 — Ecma International, June 2005. — 543 p.
  14. Delphi Language Guide — Borland Software Corporation, 2004. — 249 p.
  15. Jürg Gutknecht, Eugene Zueff Zonnon Language Report /Editors: Brian Kirk and David Lightfoot — Institute of Computer Systems, ETH Zentrum, Zürich, Switzerland, December 2005. — 57 p.
  16. H. Mössenböck, N. Wirth The Programming Language Oberon-2 — Institut für Computersysteme, ETH Zürich, March 1995. — 20 p.
  17. Perl Programmers Reference Guide — manual (man) pages. (Компьютерный документ)
  18. PostScript language reference manual /Adobe Systems Incorporated. — Addison-Wesley Publishing Company, 1999. — 910 p.
  19. Programming languages — C++. ISO/IEC 14882:1998(E) — ANSI, New York 10036, 1998. — 776 p.
  20. Python Documentation, Release 2.4.3, 29 March 2006 — http://www.python.org/doc/ (Компьютерный документ)

1 — в аде используется несколько иная терминология. Там есть типы для косвенного доступа (access) к другим программным объектам — и эти типы для доступа по своей семантике очень близки указателям паскаля.

2 — здесь и далее имеется ввиду наиболее используемый вариант лиспа — Common Lisp.

3 — The referential theory of meaning.


Вариант статьи опубликован в журнале Магия ПК, № 5, 2008.