лекции (2003) (Глазкова) (1160821), страница 7
Текст из файла (страница 7)
Для чего необходим такой переход?
Например, для ввода-вывода очень удобно, когда константы нумеруются соответствующими числами.
В некоторых языках есть возможность вводить константы перечислимых ТД, если они заданы своими идентификаторами. Например, Type Color is (Red, Green, Blue). Это означает, что будут введены три константы перечислимого ТД.
Перечислимые ТД стали настолько популярны, что они были включены даже в язык С.
Но в языке С перечислимый ТД не является самостоятельным типом данных.
Например, пусть есть перечисление еnum E {…,C,…}, то переменная перечислимого типа является по сути целой ( т.е. вполне допустимо int I=C; что не так уж плохо).
С другой стороны, если enum E {…,x,…}, то допустимо x=i; (и никакого контроля попадания в соответствующий диапазон не производится).
Зачем же в языке С перечислимые ТД? Это очень быстрый и удобный способ описания целочисленных констант.
Почему такой подход не годился для языка С++?
Одна из идей языка С++ состоит в том, что все ТД должны быть равноправны.
В частности, в языке С++ есть понятие перекрытия функций, когда одной и той же функции может соответствовать несколько тел.
Например, в языке С++ допустимо:
int f();
int f(int);
int f(bool);
Тогда, если нет никакой разницы между перечислимым ТД и целым, то перечислимый ТД является неравноправным ТД. Например, мы хотим описать int f(enum E);
Чтобы это было возможно, перечислимый ТД должен быть отдельным ТД, следовательно, нужно было запретить часть преобразований.
Совместимость между перечислимым и целым осталась, т.е. перечисление осталось удобным способом задания целочисленных констант.
В чем отличие перечислимого ТД в языке С и в языке Аda?
В языке Аda можно описать:
i=INTEGER(X);
X=T(i);
В языке Аda работает квазистатический контроль, т.е. если компилятор знает значение переменной i, он сразу проверит , попадает ли она в диапазон для перечисления Т, если же он не знает, он ставит квазистатическую проверку.
В языке С++, как и в языке С, никакого квазистатического контроля ( кроме того, что вставляет сам программист) нет.
Интересно, что в 70-80 годы перечислимые ТД появились практически во всех ЯП. Но в 1988 появился язык Oberon (продолжатель Algol-Modula2 –Pascal), и перечислимые ТД в нем отсутствовали.
В чем причина?
Вирт явным образом объяснил по крайней мере две причины, по которым он не включил перечислимый ТД в множество языковых конструкций языка Oberon:
-
усложнение компилятора за счет неявного импорта. Вся программа на языке Oberon разбита на модули, и пусть какой-нибудь модуль М импортирует перечислимый ТД T(C1,…,Cn). Тогда, как следствие, мы должны импортировать и множество имен C1,…,Cn, т.е. константы перечислимого ТД. Кроме этого, возрастает возможность конфликта соответствующих имен. А если же явным образом импортировать соответствующие константы, то смысл использования перечислимого типа теряется.
-
самое главное – несовместимость с объектно-ориентированным стилем программирования. В объектно-ориентированном стиле программирования наследование – одна из основных операций с типами данных.
Пусть ТД Т1 наследуется из ТД Т.
Пусть наследуется некоторая функция, которая имеет аргумент перечислимого ТД:
f(enum E);
Если мы наследуем ТД и для него переопределяем функцию f, то хотим обогатить функциональность соответствующего ТД. Нет смысла полностью заменять функцию f. Мы хотим обогатить функциональность Т и в тоже время имеем жестко определенное множество значений.
В качестве примера можно привести абстрактную иерархию файлов.
-
file – open (r,w) – функция открытия: режимы открытия (чтение, запись) – константы перечислимого типа данных.
-
iofile +rw – режим открытия на чтение и запись одновременно.
-
textfile + текстовый и бинарный режимы открытия.
Здесь наследуется функция open, и значения ее аргумента (режим открытия) все время обогащаются. С этой точки зрения, перечислимые ТД не являются расширяемыми.
С другой стороны, в iostream константы описываются с помощью перечислимого ТД. Т.е. для каждого потокового ТД описывается свое перечисление, в котором указаны константы режимов открытия.
Но вспомним особенность языка С++. В языке С++ перечисления неявным образом приводимы к целочисленному типу данных. Режим открытия в iostream описывается не в терминах перечислимых типов, а в терминах целых. Следовательно, в объектно-ориентированном стиле программирования перечислимый ТД нужен только для быстрого задания списка перечислений.
Не случайно в языке Oberon отказались от диапазонов и перечислений. Вместо этого Вирт рекомендует просто явным образом описывать соответствующие константы: например,
CONST ReadMode = 1,
WriteMode = 2.
Точно такого же подхода придерживаются и в языке Java, который появился в 1995 году.
Static final int ReadMode = 1;
ТД имя константы
В языке Java, как и в C#, не существует ни глобальной функции, ни глобальной переменной, т.е. все функции и переменные являются членами какого-либо класса. Константы же не стоит хранить как члены класса (иначе значение константы будет храниться в каждом экземпляре класса). Поэтому константу описывают как статическую (static). Ключевое слово final говорит о том, что соответствующее значение будет константой, т.е. оно получает значение один раз и больше меняться не будет.
Т.е. в языке Java перечислимые ТД сочтены излишними.
Интересно, что в языке C# (1999) перечислимые ТД явным образом есть. Например,
enum Color {Red, Green, Blue};
Почему?
Особенность перечислимых ТД: они очень хорошо совместимы с интегрированными средами. Мы ранее обсуждали концепцию свойства в визуальном программировании. Пусть есть некоторая компонента, которая представлена своим набором свойств. Существуют визуальные средства, с помощью которых можно задавать свойства компоненты. У нас есть браузер компонент (инспектор свойств).
Надо указать имя свойства : тип и должно быть некоторое окно, чтобы показать значение соответствующего свойства или редактировать соответствующее значение. Пусть тип - целый, тогда мы должны понимать, что означает соответствующее значение. Если речь идет о положении пикселя – это положение пикселя относительно левого верхнего угла окна. А если речь идет о цвете, задавать конкретный цвет в виде целого числа неинтересно. Задавать цвет перечислимым ТД более выгодно: во-первых, мы легче можем подобрать соответствующее значение, а, во-вторых, мы не можем ввести неподходящее значение.
Т.о., с точки зрения языкового дизайна, перечислимые ТД хороши тем, что если свойства имеют перечислимый ТД, то они будут наглядны.
В конце 80-х годов проявилась очень интересная тенденция: язык проектируется с учетом интегрированных сред. С этой точки зрения, необходимость введения перечислимых ТД обосновывалась исключительно потребностью создания интегрированных сред . Еще один аргумент связан с моделями взаимодействия компонент между собой. Одно из главных свойств программирования на современных языках : языки используются для написания компонент. И автоматизация проектирования и использования компонент, с этой точки зрения, является очень важной.
Взаимодействие компонент между собой описывается на языке IDL (Interface Definition Language). В IDL описываются интерфейсы и перечисления. Но принцип остается тот же, что и в С++ : существуют неявные преобразования enum -> int, и явные преобразования
Enum Color{ }; int i;
Color C=(Color)i;
Но здесь в отличие от языка С++ будет квазистатическая проверка. С этой точки зрения, C# надежнее.
Все, что говорилось о неприменимости перечислимых ТД к объектно-ориентированному стилю, разумеется, относится и к языку C#. Перечислимые ТД нужны для описания готовых компонент. Как правило, компоненты, которые дистрибьютируются по сети, предназначены не для того, чтобы от них что-то наследовать, а для того, чтобы использоваться в готовом виде.
С этой точки зрения, перечислимые ТД ничему не противоречат. Но как только речь начинает идти о некоторой иерархии, то роль перечислимых ТД сводится только к удобному описанию целочисленных констант.
Особенности перечислимых ТД.
-
в языке Аda в качестве литералов перечисления допустимы любые символы (символьный ТД можно описывать как частный случай перечислимого ТД)
-
литералы и перечисления в языке Ада имеют ту же область видимости, что и соответствующий тип. Если мы пишем enum Color{Red,Green,Blue}; то тип данных Color и константы Red,Green,Blue имеют одну и ту же область видимости. Недостаток, который отметил Вирт, состоит в том, что совпадение областей видимости ведет к неявному импорту имен(это усложняет компилятор и провоцирует конфликты в именах).
В C# сделали более оригинально. Пусть есть enum Color {Red, Green, Blue}.Color – имя типа, Red, Green, Blue - константы, область видимости которых ограничивается перечислением. Т.е. использовать эти константы можно только следующим образом.
Color C;
C=Color. Red;
Присваивание C=Red; запрещено, т.к. Red не имеет область видимости, совпадающую с C. Т.е. обращаться к константам перечисления можно только через имя соответствующего типа. Это уменьшает вероятность конфликта имен. В C#, по сравнению с С++, сделано еще одно расширение – в С++ уже можно было задавать соответствующие значения констант.
enum Color {Red=0xFF0000,
Green=0x00FF00,
Blue=0x0000FF};
В некоторых случаях удобно задавать конкретные значения констант. Здесь мы снова видим роль перечислимого типа как компактного способа задания констант.
В C# можно выделять базовый тип соответствующего перечисления. Если количество констант в перечислении невелико, то можно записать enum Mode : byte{…}, т.е. в качестве базового типа берется не целый тип, а ТД byte (вместо преобразования к целому теперь перечислимый ТД неявно приводится к ТД byte)
Еще одна особенность перечислимых ТД – это атрибут флаги. Здесь мы сталкиваемся с интересным понятием атрибута. Атрибут в языке C# определён очень расплывчато.
Мы уже говорили, что все данные имеют атрибуты (любой объект данных имеет 6 основных атрибутов). В C# есть множество предопределенных атрибутов, которое управляет работой компилятора и интегрированной средой.
Атрибуты – это некое средство управления работой компилятора. Атрибут задан в следующем виде: [атрибут].
Пусть есть перечисление enum Mode :byte {Read=1,Write=2,Binary=4};
Тогда если перед перечислимым ТД указать атрибуты флаги [flags], то к соответствующим константам этого типа можно применять логические операции
( and,or,not), и они будут трактоваться как побитовые.
Поэтому можно записать ReadWrite = Read or Write.
Пункт 6 Указатели и ссылки.
Указатели и ссылки – это абстракция понятия адреса. Во всех языках, где есть указатели, эти указатели используются для доступа к объектам динамической памяти.
Языки делятся на 2 группы: строгие и нестрогие.
Строгие языки – это языки, в которых указатели служат только для работы с объектами динамической памяти (к этим языкам относятся Pascal, Modula2, Ada). Пусть Т – тип данных. Тогда type P is T access; означает, что Р- указатель на тип данных Т.
Для чего нужны указательные типы и объекты динамической памяти?
Для моделирования рекурсивных структур данных. Рекурсивная структура данных – это структура, которая ссылается сама на себя, посредством указателя на другой экземпляр. Сам указатель определяется в терминах структуры. Возникает вопрос , что должно быть описано первым – структура или указатель? В языке Pascal разрешено употребление неопределенного типа только в следующих значениях:
type PT=^T;
Здесь допускается, что Т – еще не определенный тип. Та же ситуация в языках С и С++.
В Аda несколько другая ситуация. В начале надо описать, что Р – это просто указательный тип:
type P is access;
После этого описать базовый тип:
type T is …;
И после этого еще раз написать:
type P is T access;
Долее, пусть X: P.
Тогда порождать объекты можно с помощью оператора new следующим образом:
X:=new P;
В строгих ЯП работа через указатели – единственный способ ссылки на динамические структуры данных. Кроме того, указатели не могут ссылаться на нединамические объекты.
Операции с указателями:
-
порождение нового объекта динамической памяти X:=new P;
-
операции разыменовывания - *p (в С,С++) ; если p указывает на структуру, то в С p->f – это операция доступа по указателю к полю; p^ (в Pascal и Modula2); в Ada, если p – это указатель на структуру, p.f – это указатель на элемент структуры, p.all – ссылка на весь объект в целом. (аналог p^ в Pascal и *p в С и С++).
-
присваивание одного указателя другому - p1:=p2;
В строгих ЯП больше ничего с указателями делать нельзя. Еще можно делать процедуру, обратную процедуре new – процедуру delete, т.е. явное удаление объекта из памяти ( но это, в общем, зависит от языка).
Проблемы работы с указателями.
1.Мусор
T* p; T* p1;
p=new T;
p= new T;
Объект Т остается висеть в динамической памяти. Файл – это системный ресурс. Аксиома работы с системными ресурсами : если ты забрал какой-то ресурс, ты должен его явно или неявно освободить до конца программы. Чем отличается файл от динамической памяти?