Простые типы 12 страница

11.3.2 Запрос и освобождение памяти
В процедурных языках программирования есть явные выражения или операторы запроса и освобождения памяти. Язык С использует malloc, функцию весьма опасную, поскольку в ней никак не проверяется соответствие выделенного объема памяти размеру указуемого объекта. Следует использовать функцию sizeof, даже когда это явно не требуется:

int *p = (int *) malloc(1); /* Ошибка */

int *p = (int *) malloc(sizeof(int)); /* Этот вариант лучше */

malloc возвращает нетипизированный указатель, который должен быть явно преобразован к требуемому типу. При освобождении памяти задавать размер блока не нужно:

free(p);

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

Языки C++ и Ada используют нотацию, из которой ясно видно, что создается указуемый объект конкретного типа. При этом нет опасности несовместимости типа и размера объекта:

typedef Node *Node_Ptr;
Node_Ptr *p = new Node; // C++

type Node_Ptr is access Node;

P: Node_Ptr:= new Node; -- Ada

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

11.3.3 Повисшие ссылки
Серьезная опасность, связанная с указателями, — это возможность создания повисших ссылок (dangling pointers) при освобождении блока памяти:


int *ptr1 = new int; C++

int *ptr2;

ptr2 = ptr1; // Оба указывают на один и тот же блок

result = delete ptr1; // ptr2 теперь указывает на освобожденный блок

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

char *proc(int i) /* Возвращает указатель на тип char */

{

char с; /* Локальная переменная */
return &c; /* Указатель на локальную переменную типа char */

}

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

Ada пытается избежать повисших ссылок.

• Указатели на объекты (именованные переменные, константы и парамет
ры) запрещены в Ada 83; в Ada 95 они вводятся специальной конструк
цией alias, правила которой предотвращают возникновение повисших
ссылок.
• Явного выделения памяти избежать нельзя, поэтому применяемый метод
назван Unchecked Deallocation (освобождение без контроля) с целью
предупредить программиста об опасности.

11.4.1 Алгоритмы распределения динамической памяти

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

Рис.6. Фрагментация динамической памяти.

С распределением динамической области памяти связана проблема фрагментации. На рисунке 6. показана ситуация, когда сначала были выделены пять блоков памяти, а затем второй и четвертый освобождены. Теперь, хотя доступны 1000 байтов, невозможно выделить больше 600 байтов, потому что память раздроблена на небольшие блоки. Даже когда третий блок освободится, памяти будет достаточно только при условии, что менеджер кучи «умеет» сливать смежные свободные блоки.
В добавление к слияниям менеджер кучи может предупреждать фрагментацию, отыскивая блок подходящего размера, а не просто первый доступный, или выделяя большие блоки из одной области динамической памяти, а небольшие блоки — из другой. Существует очевидный компромисс между сложностью менеджера и издержками времени выполнения.
Программист должен знать используемые алгоритмы управления динамической памятью и писать программу с учетом этих знаний5.
Другая возможность ослабить зависимость от алгоритмов работы менеджера кучи — это завести кэш освобождаемых блоков. Когда блок освобождается, он просто подсоединяется к кэшу. Когда необходимо выделить блок, сначала проверяется кэш; это позволяет избежать издержек и фрагментации, возникающих при обращениях к менеджеру кучи.
В Ada есть средство, которое позволяет программисту задать несколько куч разного размера, по одной для каждого типа указателя. Это позволяет предотвратить фрагментацию, но повышает вероятность того, что в одной куче память будет исчерпана, в то время как в других останется много свободных блоков.

11.4.2 Виртуальная память

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

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

11.4.3 Сборка мусора
Последняя проблема, связанная с динамической памятью, — образование мусора (garbage), например:
int *ptr1 = new int; // Выделить первый блок
int *ptr2 = new int; // Выделить второй блок
ptr2 = ptrl; // Второй блок теперь недоступен
После оператора присваивания второй блок памяти доступен через любой из указателей, но нет никакого способа обратиться к первому блоку (см. рис.7). Это может и не быть ошибкой, потому что память, к которой нельзя обратиться, (называемая мусором) не может вам помешать. Однако, если продолжается утечка помяты, т. е. образуется мусор, в конечном счете программа выйдет из строя из-за недостатка памяти. Чрезвычайно трудно локализовать причину утечки памяти, потому что нет прямой связи между причиной и симптомом (недостатком памяти).

Очевидное решение состоит в том, чтобы не создавать мусор, прежде всего тщательно заботясь об освобождении каждого блока до того, как он станет недоступен. Кроме того, исполняющая система языка программирования может содержать сборщик мусора (garbage collector). Задача сборщика мусора состоит в том, чтобы «повторно использовать» мусор, идентифицируя недоступные блоки памяти и возвращая их менеджеру динамической памяти. Существует два основных алгоритма сборки мусора: один из них для каждого блока

Рис. 7. Образование мусора.

ведет счетчик текущего числа указателей, ссылающихся на этот блок, и автоматически освобождает блок, когда счетчик доходит до нуля. Другой алгоритм отмечает все доступные блоки и затем собирает немаркированные (и, следовательно, недоступные) блоки. Первый алгоритм проблематичен, потому что группа блоков, каждый из которых является мусором, могут указывать друг на друга так, что счетчик никогда не сможет уменьшиться до нуля. Второй алгоритм требует прерывания вычислений на длительные периоды времени, чтобы маркировку и сбор можно было выполнить без влияния вычислений. Это, конечно, недопустимо в интерактивных системах.
Сборка мусора традиционно выполняется в таких языках, как Lisp и Icon, которые создают большое число временных структур данных, быстро становящихся мусором. Проведены обширные исследования по сборке мусора; особое внимание в них уделено параллельным и пошаговым методам, которые не будут нарушать интерактивные вычисления или вычисления в реальном масштабе времени. Eiffel — один из немногих процедурных языков, которые включают сборщики мусора в свои исполняющие системы.


12. Полиморфизм

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


12.1. Преобразование типов
Преобразование типов — это операция преобразования значения одного типа к значению другого типа. Существуют два варианта преобразования типов:
1) перевод значения одного типа к допустимому значению другого типа
2) пересылка значения как неинтерпретируемой строки битов.
Преобразование числовых значений, скажем, значений с плавающей точкой, к целочисленным включает выполнение команд преобразования битов значения с плавающей точкой так, чтобы они представили соответствующее целое число. Фактически, преобразование типов делается функцией, получающей параметр одного типа и возвращающей результат другого типа. Синтаксис языка Ada для преобразования типов такой же, как у функции:

I: Integer:= 5; [Ada

F: Float:= Float(I);
в то время как синтаксис языка С может показаться странным, особенно в сложном выражении:
int i = 5;

float f = (float) i;

В C++ для совместимости сохранен синтаксис С, но для улучшения читаемости программы также введен и функциональный синтаксис, как в Ada.
Кроме того, и С, и C++ включают неявные преобразования между типами, прежде всего числовыми:

int i;

float f = i;
Явные преобразования типов безопасны, потому что они являются всего лишь функциями: если не существует встроенное преобразование типа, вы всегда можете написать свое собственное. Неявные преобразования типов более проблематичны, потому что читатель программы никогда не знает, было преобразование преднамеренным или это просто оплошность. Использование целочисленных значений в сложном выражении с плавающей точкой не должно вызывать никаких проблем, но другие преобразования следует указывать явно.
Вторая форма преобразования типов просто разрешает программе использовать одну и ту же строку битов двумя разными способами. В языке С используется один и тот же синтаксис для обеих форм преобразования: если преобразование типов имеет смысл, например между числовыми типами или указательными типами, то оно выполняется; иначе строка битов передается, как есть.
В языке Ada можно между любыми двумя типами осуществить не-контролируемое преобразование (unchecked conversion), при котором значение трактуется как неинтерпретируемая строка битов. Поскольку это небезопасно по самой сути и разрушает все с таким трудом добытые преимущества контроля типов, неконтролируемые преобразования не поощряются, и синтаксис языка спроектирован так, чтобы такие преобразования бросались в глаза.
Хотя для совместимости в C++ сохранено такое же преобразование типов, как в С, в нем определен новый набор операций преобразования типов:
• dynamic_cast.
• static_cast. Выражение типа Т1 может статически приводиться к типу Т2,
если Т1 может быть неявно преобразовано к Т2 или обратно; static_cast
следует использовать для безопасных преобразований типов, как, например, float к int или обратно.
• reinterpret_cast. Небезопасные преобразования типов.
• const_cast. Используется, чтобы разрешить делать присваивания кон-
стантным объектам.

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

int i =abs(25); [с]

double d=fabs(1.57);

long I =labs(-25L);

В Ada и в C++ одно и то же имя может быть у двух или нескольких разных подпрограмм при условии, что сигнатуры параметров разные. Пока число и/или типы (а не только имена или режимы) формальных параметров различны, компилятор будет в состоянии запрограммировать вызов правильной подпрограммы, проверяя число и типы фактических параметров:

function Sin(X: in Float) return Float;

function Sin(X: in Long_Float) return Long_Float;

F1,F2: Float;

L1,L2: Long_Float:

F1:=Sin(F2);

L1:=Sin(L2);

Различие между двумя языками состоит в том, что Ada принимает во внимание тип результата функции, в то время как C++ ограничивается формальными параметрами:
float sin(float);

double sin(double); // Перегрузка sin

double sin(float); // Ошибка, переопределение в области действия

Особый интерес представляет возможность перегрузки стандартных операций, таких как + и * в Ada:

Ada

function "+" (V1, V2: Vector) return Vector;
Надо представить саму функцию, реализующую перегруженную операцию для новых типов. Обратите внимание, что синтаксические свойства операций, в частности старшинство, не изменяются. В C++ есть аналогичное средство перегрузки:

Vector operator + (const Vector &, const Vector &);
Это совершенно аналогично объявлению функции, за исключением зарезервированного ключевого слова operator. Перегружать операции имеет смысл только в том случае, если вновь вводимые операции аналогичны предопределенным, иначе можно запутать тех, кто будет сопровождать программу.
При аккуратном использовании перегрузка позволяет уменьшить длины имен и обеспечить переносимость программы. Она может даже увеличить прозрачность программы, поскольку такие искусственные имена, как fabs, больше не нужны. С другой стороны, перегрузка без разбора может легко нарушить читаемость программы (если одному и тому же имени будет присваиваться слишком много значений). Перегрузка должна быть ограничена подпрограммами, выполняющими аналогичные вычисления, чтобы читатель программы мог понять смысл уже по самому имени подпрограммы.

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

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

type lnt_Array is array(integer range <>) of Integer;
procedure Sort(A: lnt_Array) is
Temp, Min: Integer;
begin
for I in A'First..A'Last-1 loop
Min:= I;
for J in I+1.. A'Last loop
if A(J) < A(Min) then Min:= J; end if;
-- Сравнить элементы, используя "<"
end loop;
Temp:= A(l); A(l):= A(Min); A(Min):= Temp;
-- Переставить элементы, используя ":="
end loop;
end Sort;
На самом деле даже тип индекса не существенен при программировании этой процедуры, лишь бы он был дискретным типом (например, символьным или целым).
Чтобы получить процедуру Sort для некоторого другого типа элемента, например Character, можно было бы физически скопировать код и сделать необходимые изменения, но это могло бы привести к дополнительным ошибкам. Если надо было бы изменить алгоритм, то пришлось бы сделать эти изменения отдельно в каждой копии. В Ada определено средство, называемое родовыми сегментами (generics), которое позволяет программисту задать шаблон подпрограммы, а затем создавать конкретные экземпляры подпрограммы для нескольких разных типов. Хотя в С нет подобного средства, его отсутствие не так серьезно, потому что указатели void, оператор sizeof и указатели на функции позволяют легко запрограммировать «обобщенные», пусть и не такие надежные, подпрограммы. Применение родовых сегментов не гарантирует, что конкретные экземпляры одной родовой подпрограммы будут иметь общий объектный код; фактически, при реализации может быть выбран независимый объектный код для каждого конкретного случая.
Ниже приведено объявление родовой подпрограммы с двумя родовыми формальными параметрами:

Ada

generic
type Item is (<>);

type ltem_Array is array(lnteger range <>) of Item;

procedure Sort(A: ltem_Array);
Это обобщенное объявление на самом деле объявляет не процедуру, а только шаблон процедуры. Необходимо обеспечить тело процедуры: оно будет написано в терминах родовых параметров:

procedure Sort(A: ltem_Array) is [Ada

Temp, Min: Item;

begin

… -- Полностью совпадает с вышеприведенным

end Sort;

Чтобы получить (подлежащую вызову) процедуру, необходимо конкретизировать родовое объявление, т. е. создать экземпляр, задав родовые фактические параметры:
type lnt_Array is array(lnteger range <>) of Integer;

type Char_Array is array(lnteger range <>) of Character;

procedure lnt_Sort(A: lnt_Array) is new Sort(lnteger, lnt_Array);

procedure Char_Sort(A: Char_Array) is new Sort(Character, Char_Array);

Это реальные объявления процедур; вместо тела процедуры после объявления следует ключевое слово is, и тем самым запрашивается новая копия обобщенного шаблона.
Родовые параметры — это параметры этапа компиляции, и используются они компилятором, чтобы сгенерировать правильный код для конкретного экземпляра. Параметры образуют контракт между кодом родовой процедуры и ее конкретизацией. Первый параметр Item объявлен с записью (<>). Это означает, что конкретизация программы обещает применить дискретный тип, такой как Integer или Character, а код обещает использовать только операции, допустимые на таких типах. Так как на дискретных типах определены операции отношения, процедура Sort уверена, что «<» допустима. Второй обобщенный параметр ltem_Array — это предложение контракта, которое говорит: какой бы тип ни был задан для первого параметра, второй параметр должен быть массивом элементов этого типа с целочисленным индексом.
Модель контракта работает в обе стороны. Попытка выполнить арифметическую операцию «+» на значениях типа Item в родовом теле процедуры является ошибкой компиляции, так как существуют такие дискретные типы, как Boolean, для которых арифметические операции не определены. И обратно, родовая процедура не может быть конкретизирована с элементом массива типа запись, потому что операция «<» для записей не определена.
Цель создания модели контракта заключается в том, чтобы позволить программистам многократно применять родовые модули и избавить их от необходимости знать, как реализовано родовое тело процедуры. Уж если родовое тело процедуры скомпилировано, конкретизация может завершиться неуспешно, только если фактические параметры не удовлетворяют контракту. Конкретизация не может быть причиной ошибки компиляции в теле процедуры.

12.4 Шаблоны в C++

В языке C++ обобщения реализованы с помощью специального средства — шаблона (template):
template <class ltem_Array> void Sort(ltem_Array parm)
{

}
Здесь нет необходимости в явной конкретизации: подпрограмма создается неявно, когда она используется:
typedef int l_Array[100];
typedef char C_Array[100];
I_Array a;
C_Array c;

Sort(a); // Конкретизировать для целочисленных массивов

Sort(c); // Конкретизировать для символьных массивов

Явная конкретизация — это оптимизация, задаваемая программистом по желанию; в противном случае, компилятор сам решает, какие конкретизации необходимо сделать. Шаблоны могут быть конкретизированы только по типам и значениям, или, в более общем случае, по классам.
Язык C++ не использует модель контракта, поэтому конкретизация может закончиться неуспешно, вызвав ошибку компиляции в определении шаблона. Это затрудняет производство и поставку шаблонов как самостоятельных компонентов программного обеспечения.

12.5 Родовые параметры-подпрограммы в языке Ada
В Ada допускается, чтобы родовые параметры были подпрограммами. Пример программы сортировки может быть написан так:
generic

type Item is private;
type ltem_Array is array(lnteger range <>) of Item;

with function "<"(X, Y: in Item) return Boolean;
procedure Sort(A: ltem_Array);
Контракт теперь расширен тем, что для реализации операции «<» должна быть предоставлена булева функция. А поскольку операция сравнения обеспечена, Item больше не нужно ограничивать дискретными типами, для которых эта операция является встроенной. Ключевое слово private означает, что любой тип, на котором определено присваивание и сравнение на равенство, может применяться при реализации:
type Rec is record... end record;
type Rec_Array is array(lnteger range <>) of Rec;

function "<"(R1, R2: in Rec) return Boolean;
procedure Rec_Sort(A: Rec_Array) is newSort(Rec, Rec_Array, "<");
Внутри подпрограммы Sort присваивание является обычным поразрядным присваиванием для записей, а когда нужно сравнить две записи, вызывается функция «<». Эта обеспеченная программистом функция решит, является ли одна запись меньше другой.
Модель контракта в языке Ada очень мощная: типы, константы, переменные, указатели, массивы, подпрограммы и пакеты (в Ada 95) могут использоваться как родовые параметры.

12.6 Вариантные записи
Вариантные записи используются, когда во время выполнения необходимо интерпретировать значение несколькими разными способами. Ниже перечислены распространенные примеры.
• Сообщения в системе связи и блоках параметров в вызовах операцион-
ной системы. Обычно первое поле записи является кодом, значение ко-
торого определяет количество и типы остальных полей в записи.
• Разнородные структуры данных, такие как дерево, которое может содер-
жать узлы разных типов.
Чтобы решать проблемы такого рода, языки программирования представляют новый класс типов, называемый вариантными записями, которые имеют альтернативные списки полей. Такая переменная может первоначально содержать значение одного варианта, а позже ей может быть присвоено значение другого варианта с совершенно другим набором полей. Помимо альтернативных могут присутствовать поля, которые являются общими для всех записей этого типа; такие поля обычно содержат код, с помощью которого программа определяет, какой вариант используется на самом деле. Предположим, что мы хотим создать вариантную запись, поля которой могут быть или массивом, или записью:

typedef int Arr[10]; \C\

typedef struct {

float f1;

int i1;

} Rec;

Сначала определим тип, который кодирует вариант:

typedef enum {Record_Code, Array_Code} Codes;
Теперь с помощью типа union (объединение) в С можно создать вариантную запись, которая сама может быть вложена в структуру, включающую общее поле тега, характеризующего вариант:
typedef struct {

/* Общее поле тега */

/* Объединение с альтернативными полями*/

/* Вариант массива */

/* Вариант записи */

Codes code;

union {

Arr a;

Rec r;

} data;

} S_Type;

S_Type s;

С точки зрения синтаксиса это всего лишь обычная вложенность записей и массивов внутри других записей. Различие состоит в реализации: полю data выделяется объем памяти, достаточный для самого большого поля массива а или поля записи r (см. рис. 1). Поскольку выделяемая память рассчитана на самое большое возможное поле, вариантные записи могут быть чрезвычайно неэкономны по памяти, если один вариант,

Рис.1. Вариантные записи.

если один вариант очень большой, а другие маленькие:

union { [С

int a[1000];

float f;

char c;

}

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

if (s.code == Array_Code)

i = s.data.a[4]; /* Выбор первого варианта */

else
i = s.data.r.i1; /* Выбор второго варианта */
Основная проблема с вариантными записями состоит в том, что они потенциально могут вызывать серьезные ошибки. Так как конструкция union позволяет программе обращаться к той же самой строке битов различными способами, то возможна обработка значения одного типа, как если бы это было значение какого-либо другого типа (скажем, обращение к числу с плавающей точкой, как к целому). Программисты, пишущие на языке Pascal, используют вариантные записи, чтобы делать преобразование типов, которое в языке непосредственно не поддерживается.
В вышеупомянутом примере ситуация еще хуже, потому что возможно обращение к ячейкам памяти, которые вообще не содержат никакого значения: поле s.data.r могло бы иметь длину 8 байт для размещения двух чисел, а поле s.data.а — 20 байт для размещения десяти целых чисел. Если в поле s.data.r в данный момент находится запись, то s.data.a[4] не имеет смысла.

В Ada не разрешено использовать вариантные записи, чтобы не разрушать контроль соответствия типов. Поле code теперь является обязательным полем, и называется дискриминантом, а при обращении к вариантным полям проверяется корректность значения дискриминанта. Дискриминант выполняет роль «параметра» типа:

type Codes is (Record_Code, Array_Code); |Ada

type S_Type(Code: Codes)is
record

case Code is

when Record_Code => R: Rec;

when Array_Code => A: Arr;

end case;

end record;

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

S1: S_Type(Record_Code);

S2: S_Type(Array_Code);

Другая возможность — объявить указатель на вариантную запись и проверять дискриминант во время выполнения:

type Ptr is access S_Type;

P: Ptr:= new S_Type(Record_Code);

I:=P.R.I1; --Правильно
I:=P.A(5); -- Ошибка

Первый оператор присваивания правильный, поскольку дискриминант записи P.all — это Record_Code, который гарантирует, что поле R существует; в то же время второй оператор приводит к исключительной ситуации при работе программы, так как дискриминант не соответствует запрошенному полю.
Основное правило для дискриминантов в языке Ada заключается в том, что их можно читать, но не писать, так что нельзя обойти контроль соответствия типов. Это также означает, что память может выделяться в точном соответствии с выбранным вариантом, в отличие от обычного выделения для самого большого варианта.


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: