Ссылочные типы в языке Ада

Тема не самая простая, поэтому начну с общей информации. Как следует из названия, ссылочная переменная - это переменная, которая на что-то ссылается (указывает).

Пусть у нас есть ящики A, B, C, D (переменные), причём, ящики одинаковые (одного типа). Подвесим над ящиками на верёвке стрелку Ref, которая может перемещаться по верёвке и указывать (ссылаться) на любой из ящиков:

Рис. 1

Здесь стрелка Ref ссылается (указывает) на ящик A, но её можно с таким же успехом переместить к ящику B, C, или D. Важно отметить, что в любой из ящиков можно что-то положить или что-то взять (присвоить какое-нибудь значение (1, 2, 3, -1 и т.д.)) только тогда, когда на ящик указывает стрелка. На рисунке 1 стрелка указывает на ящик A, следовательно именно в этот ящик можно что-то положить (присвоить значение переменной). Чтобы работать с содержимым ящика, например, D, нужно сначала перетащить стрелку к ящику D.

Если быть точным, то ссылка Ref ссылается на участок памяти, которому присвоено имя A. То есть, Ref содержит адрес участка памяти с именем A, и кроме адреса ссылочная переменная больше ничего содержать не может.

Если попроще... Пусть есть Оля (переменная). Оля одета в платье (ей присвоено значение). И пусть есть Коля (ссылка), который смотрит на Олю (ссылается). При этом Коля:

  1. Платье надеть не может (мужик такое не носит 🙂 Т.е. ссылке значение присвоить нельзя).
  2. Может вместо платья подарить Оле юбку (переменной, на которую указывает ссылочная переменная, можно присвоить новое значение).
  3. Когда Оля надоест Коле, он может переключить внимание на Машу (будет ссылаться на другую переменную того же типа).

Вот такая вот музыка... Направлением Колиного взгляда (ссылкой) можно управлять, Коля тот ещё Кобелино 🙂

В С/С++ ссылочные переменные называются указателями. В С++ дополнительно есть ещё и ссылки. Ссылки в Аде почти аналоги указателей в С, но адресную арифметику не поддерживают. Думаю, что те, кто знаком с С/С++, меня поняли, а те, кто не знаком - не забивайте голову.

Для чего это нужно. Ну, например, в одной из тем мы моделировали очередь в магазине и для моделирования использовали массив из 20 элементов. То есть, в памяти было выделено место для 20 человек (переменных), которые теоретически могут стоять в очереди. Даже если очередь состоит из 2 человек, место выделяется для 20, а это неоправданные потери (особенно, если массив не из 20, а из нескольких тысяч элементов). А что делать, если в очередь хочет встать двадцать первый покупатель? Мест-то всего 20. Вот здесь на помощь приходят ссылки.

Ещё одно преимущество ссылок в том, что массив занимает монолитный участок памяти, и если массив объявлен достаточно большим, например, из 1_000_000 элементов, то для его создания нужно ещё найти участок памяти, который мог бы вместить такой монолит. При использовании ссылок каждая переменная занимает ровно столько памяти, сколько ей надо, и переменных создаётся именно столько, сколько нужно в текущий момент времени. То есть, если в очередь пришли 5 человек, то место будет выделено для 5, а если 100 - то для 100. При этом, если кто-то уходит из очереди, то его место сразу же освобождается (память возвращается программе и вновь может быть использована). Переменные могут быть разбросаны по памяти хаотично, и монолитный кусок тут просто не нужен:

Рис. 2

На рисунке 2 видно, что все объекты в памяти разбросаны хаотично (где было место, там переменная и была создана), однако между ними есть связи (веревки, по которым перемещаются стрелки-ссылки). Также есть две ссылочные переменные - Head (ссылается на первый элемент очереди) и Tail (ссылается на последний элемент очереди). Недостатки по сравнению с массивом тоже есть. Например, если нам нужно достучаться до переменной D, то в массиве мы просто укажем индекс. Здесь же нам придётся пройти последовательно весь путь от E (Head) до D (ну или от Tail до D).

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

Пусть у нас есть запись Person. Она описывает одного человека, стоящего в очереди. Для создания ссылочного типа, переменные которого ("стрелки") смогут ссылаться (указывать) на нашу запись Person, нужно использовать такую конструкцию:

type Ref_Person is access Person; --ссылочный тип Ref_Person на переменные типа Person.

Далее мы просто создаём переменные типа Ref_Person:

ref_A : Ref_Person;

В Аде переменные ссылочного типа условно можно разделить на две категории: ссылочные типы для динамической памяти и обобщённые ссылочные типы (которые могут ссылаться на обычные переменные).

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

Для выделения памяти используется оператор new. Причём, если для переменной ссылочного типа выделена память, то по умолчанию ей присваивается значение NULL. Освобождение памяти тоже происходит вручную (если честно, то стандарт допускает возможность создания и использования сборщика мусора, но реально в компиляторе GNAT его нет, может, в других компиляторах он есть, но я с ними не сталкивался). Для освобождения памяти используется шаблон (дженерик) Unchecked_Deallocation, т.е развивая наш пример:

--в шаблон "забрасываем" исходный тип Person и ссылочный тип Ref_Person и создаём
--процедуру для уничтожения объекта типа Person
procedure Free is new Unchecked_Deallocation(Person, Ref_Person);
...
ref_A := new Person'(<Здесь можно присвоить значения полям записи Person>);
...
Free(ref_A); --Уничтожение объекта и освобождение памяти

Например, пусть нам нужно обрабатывать строки, вводимые пользователем с клавиатуры. Строки всегда будут разной длины и максимально возможная длина нам неизвестна. Для наглядности забудем про существование Unbounded_String (да и в стандарте 83 года его не было, однако как-то обходились). Одно из решений данной задачи выгладит так:

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Unchecked_Deallocation;
 
procedure main is
    --Ссылочный тип, переменные которого будут ссылаться на переменные типа String
    type Ref_String is access String;
    ref_Str : Ref_String; --Ссылка на переменные типа String
    --Процедура освобождения памяти от неактуальных динамических переменных
    procedure Free is new Ada.Unchecked_Deallocation(String, Ref_String);
begin
    loop
        --"Налету" выделяем под вводимую пользователем строку (Get_Line)
        --необходимое количество памяти и "направляем" ссылку на этот участок
        --памяти 
        ref_Str := new String'(Get_Line);
        Put_Line(ref_Str.all); --Вывод строки на экран. Что такое .all см. ниже
        --Условие выхода из цикла
        if ref_Str.all = "exit" then
            Free(ref_Str); --Освобождение памяти, т.к. строка больше не нужна
            exit; --Выход из цикла
        end if;
        --Если условие выхода не выполнено:
        Free(ref_Str); --Освобождение памяти, т.к. строка больше не нужна
    end loop;
end main;

Для доступа к содержимому переменной через ссылку используется ключевое слово 'all'. В приведённом выше примере использование all позволило осуществить вывод строк, размещающихся в динамической памяти (каждый раз в новом месте при вызове new), с помощью ссылочной переменной ref_Str.

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

type Person is
    Record
        Name : Unbounded_String; --Содержит имя человека в очереди
        Next : Ref_Person; --Ссылается на человека, стоящего в очереди следующим
    end Record;

Для использования типа Reference_Person в объявлении записи необходимо, чтобы он был описан до записи Person. То есть, строку

type Ref_Person is access Person;

нужно поместить до type Person is... Но тогда получается, что теперь строка type Reference_Person is access Person; ссылается на тип Person, а он объявлен после ссылочного типа. Имеем рекурсивное обращение типов друг к другу. Чтобы избежать этой неприятности в Аде предусмотрено использование неполного описания типа:

type Person; --неполное описание типа. Так мы сообщаем компилятору, что тип Person будет описан позднее

Тогда полная запись будет выглядеть так:

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
with Ada.Unchecked_Deallocation;
with Ada.Strings.Unbounded.Text_IO; use Ada.Strings.Unbounded.Text_IO;
 
procedure main is
 
    type Person; --Неполное объявление типа
    type Ref_Person is access Person; --Ссылочный тип
    type Person is --Полное объявление типа
        Record
            Name : Unbounded_String; --Содержит имя человека в очереди
            Next : Ref_Person; --Ссылается на человека, стоящего в очереди следующим
        end Record;
    procedure Free is new Ada.Unchecked_Deallocation(Person, Ref_Person);
    Head, Tail : Ref_Person; --Ссылочные переменные, Голова и Хвост очереди, по умолчанию равны NULL
begin
    Head := new Person; --Создаем в памяти участок для человека в очереди и устанавливаем на него ссылку
    Head.Name := To_Unbounded_String("Иванов"); --Записываем в созданный участок памяти имя.
    Tail := new Person; --Создаем в памяти участок для человека в очереди и устанавливаем на него ссылку
    Tail.Name := To_Unbounded_String("Петров"); --Записываем в созданный участок памяти имя.
    Head.Next := Tail; --Указываем, что Петров стоит в очереди после Иванова.
 
    --Теперь у нас в очереди два человека. На первого ссылается переменная Head, а на последнего - Tail.
    --При этом так как после Tail (Петрова) в очереди никто не стоит, то Tail.Next равна NULL (NULL
    --присваивается по умолчанию при создании ссылочной переменной). Значит, если Tail.Next будет
    --не NULL, это будет означать, что к очереди ещё кто-то пристроился:
    Tail.Next := new Person; --Создаем в памяти участок для человека в очереди и устанавливаем на него ссылку
    Tail.Next.Name := To_Unbounded_String("Сидоров"); --Записываем в созданный участок памяти имя.
 
    --Теперь на конец очереди указывает Tail.Next, а должен Tail, поэтому:
    Tail := Tail.Next; --Смещаем хвост очереди, т.к. добавился ещё один человек
 
    --Повторим эту процедуру ещё раз:
    Tail.Next := new Person;
    Tail.Next.Name := To_Unbounded_String("Смирнов");
    Tail := Tail.Next;
 
    --Итого в очереди 4 человека. Память выделена под 4 переменных
    --Выведем всю очередь на экран:
    Put("Вся очередь: ");
    Tail := Head; --Хвост и голова указывают на начало очереди
    loop
        Put(Tail.Name & " -> ");
        exit when Tail.Next = NULL; --Когда достигнут конец
        Tail := Tail.Next; --Смещаем хвост на одну позицию в направлении конца очереди
    end loop;
    New_Line;
 
    --Теперь будем считать, что очередь продвигается вперёд и на каждом шаге
    --из неё выходит один человек:
    loop
        Put(Head.Name & " ушёл. Остались: ");
        Tail := Head.Next; --Tail указывает на второго в очереди
        Free(Head); --Первый ушел => освобождаем занимаемую им память.
        exit when Tail = NULL;
        Head := Tail; --Второй стал первым
 
        --Вывод остатка очереди
        Tail := Head;
        loop
            Put(Tail.Name & " -> ");
            exit when Tail.Next = NULL;
            Tail := Tail.Next;
        end loop;
        New_Line;
 
    end loop;
end main;

И ещё одно: если у нас в памяти лежат две записи Person и на них указывают две разные ссылки ref_Pers_1 и ref_Pers_2, то для полного копирования одной записи в другую необходимо задействовать ключевое слово all:

ref_Pers_1.all := ref_Pers_2.all;

после этой операции участок памяти, на который ссылалась переменная ref_Pers_1 будет перезаписан информацией, на которую ссылается ref_Pers_2.

Кроме рассмотренных ссылок на объекты в динамической памяти Ада позволяет создавать ссылки на статические переменные. Такие ссылки называются обобщёнными ссылками.

Если предполагается, что со статической переменной в паре будет использоваться ссылочная переменная, то:

1. Сами статические переменные объявляются с использованием ключевого слова aliased:

Val_1 : aliased Integer; --Если обычная переменная
Val_2 : aliased constant Integer := 100; --Если константа. Вместо 100 можно взять любое число.

2. Ссылочный тип объявляется с использованием слов all и constant. Причём, если ссылочный тип объявляется как const (на константу), то это означает, что переменные этого типа могут только читать значение константы (менять не могут):

type ref_Val_1 is access all Integer;
type ref_Val_2 is access constant Integer;

3. Привязка ссылки к переменной осуществляется с помощью атрибута Access.

Рассмотрим пример:

with Ada.Text_IO; use Ada.Text_IO;
 
procedure main is
    A, B : aliased Integer;
 
    type Ref_Integer is access all Integer;
    ref_Int : Ref_Integer;
begin
    A := 5;
    B := 10;
    Put_Line("A = " & Integer'Image(A) & ", B = " & Integer'Image(B));
    ref_Int := A'Access; --Устанавливаем ссылку на статическую переменную
    Put_Line("ref_Int = " & Integer'Image(ref_Int.all));
    ref_Int.all := B; --Копируем переменную B в A с помощью ссылки
    Put_Line("ref_Int = " & Integer'Image(ref_Int.all));
    Put_Line("A = " & Integer'Image(A) & ", B = " & Integer'Image(B));
end main;

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

Обычно изучение ссылок и указателей доставляет немало "весёлых" минут и здорово портит нервы 🙂 , поэтому пока ограничимся изложенным материалом.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *