Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Скачиваний:
113
Добавлен:
02.05.2014
Размер:
93.7 Кб
Скачать

Лекция №5

Целые (рассмотрели на прошлой лекции)

Арифметические Вещественные (рассмотрели на прошлой лекции)

символьные (рассмотрели на прошлой лекции)

базисные логические

типы порядковые (диапазонный)

данных ссылочный (указатели)

процедурные (стоят немного особняком)

Логический тип данных. На данный момент он есть почти во всех ЯП, и это самый простой тип данных. Он сводится всего к двум константам true и false, и имеет базовый набор операций (это операции алгебры логики): and, or, not. В С++ логический тип данных появился совершенно недавно. А в С, который был основой С++, логический тип данных отсутствует, хотя операции логические есть. Программисты на С прекрасно живут без типа bool, а программисты на С++ прекрасно жили в течение долгого времени. В С++ есть возможность перегрузки или перекрытия операций, а если тип BOOL является базовым типом данных, то для него такая возможность должна реализовываться. Т.е.

int f(int);

int f(bool);

должны быть разными функциями, и если для базисных типов это не разрешается, то это неправильный тип данных. Именно по этой причине в С++ появляется тип данных BOOL, что упрощает переносимость с одной машины на другую. А как же быть с совместимостью программ на С и на С++ до введения типа BOOL? В стандарте было решено, что true имеет значение 1, а false – 0. И компилятор сам вставляет преобразование типа BOOL в любой целочисленный тип данных. Если у нас программа, где true – это любое ненулевое значение, то будет неявное преобразование его к 1. Все это сделано только для совместимости со старым кодом, написанном на С++. В других ЯП логический тип данных называется boolean. Там возможны преобразования из boolean в целый тип данных, но они должны быть явными. При этом true преобразуется в 1, false – в 0. Такие преобразования требуются редко. В первые такой тип появился в Алгло-60, хотя Фортран и развивался отдельно от Алгол, но в 64-м году там появляется тип данных logical. Не смотря на его внешнюю простоту иногда наличие данного типа данных значительно упрощает способ представления программ.

Порядковый или диапазонный тип.

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

0 1 N-1

Type T is (val1, val2, … , valN);

Появляется упорядоченность значений этого типа данных. Возникает естественный набор операций: succ – переход к следующему элементу, pred – взятие предыдущего элемента. Этот тип данных имел большой успех и после языка Паскаль перечислимый тип данных включается почти во все ЯП. Благодаря данному типу данных улучшается надежность и упрощается документация программ. Возникли проблемы, связанные с вводом-выводом. Во многих ЯП присутствует неявное преобразование в строковый вид не только при вводе, но и при выводе. Введена такая операция как (ord), допускается явное преобразование в целочисленные типы данных (int (T)), что по сути есть то же самое, что и (ord). Константы нумеруются от 0 до N-1, где N – мощность соответствующего типа. Когда мощность типа существенно меньше максимального целого числа перечислимый тип данных очень подходит для описания такого типа. Программа выглядит красивее, она становится более удобной для документации, более надежной, повышается читабельность программы.

Если есть преобразование из перечислимого типа данных в целый, то хотелось бы иметь преобразование из целого типа данных в перечислимый. В Модуле-2 есть псевдо-функция (псевдо потому что у нее аргумент переменного типа, это функция высшего типа, а таких функций в Модула-2 нет)

Val (Т e), где е – выражение целого типа, Т - соответствующий перечислимый тип. Она возвращает выражение соответствующего перечислимого типа. Аналогичные функции есть и языке Ада, можем просто написать, используя общий синтаксис приведения ,

INTEGER(e) , где е – выражение соответствующего перечислимого типа. Вот тут-то мы и сталкиваемся с квазистатической концепцией контроля типа, которая для порядкового типа работает очень хорошо, а именно порядковый тип ограничивает изменчивость данных, которые на внутреннем уровне представляются как целочисленные, уменьшает их изменчивость и улучшает документируемость за счет присваивания неявных имен. Так наш диапазон становится существенно меньше то возникает концепция квазистатического контроля, контроля, которая очень важна. Если преобразование (не важно, явное или неявное) является потенциально небезопасным, потому что может выводить за пределы диапазона, то преобразование а целый тип является безопасным, так как границы перечислимого типа обязательно меньше чем max_int, а вот обратное преобразование

Т(е) -> integer , где Т- перечислимый тип, а е – целое число потенциально небезопасное , так как диапазон INTEGER больше. Именно здесь компиляторы с квазистатическим контролем вставляют проверку типа. Все ЯП делятся на языки с квазистатическим контролем и без квазистатического контроля с точки зрения порядковых типов. Например, язык С++ никакого квазистатического контроля в принципе не имеет, этим он отличается от всех остальных языков, которые мы рассматриваем. То есть на месте преобразования никаких дополнительных проверок вставлено не будет. Все проверки только статические, то есть на стадии компиляции. Здесь С++ полностью соответствует идеологии языка С, где тоже изначально отказались от всяких квазистатических проверок. Эффективность программы существенно возрастает, потому что никакого неявного кода, не запланированного программистом, компилятор не вставляет. В С++ это можно объяснить тем, что программист должен пользоваться базовыми типами данных, а если он хочет использовать свой тип данных для ограничения диапазона изменения, он должен описать свой тип данных на базе понятия класса, где «зашьет» ограничение изменчивости данного типа. Утверждается, что можно писать надежные программы на С++, но при этом должна быть очень строгая дисциплина, которой , к сожалению, не все программисты придерживаются.

Перечислимые типы данных были придуманы профессором Виртом, появились во всех ЯП, естественно в Аде они тоже есть, но в Аде перечислимый тип появился позже. Здесь есть интересное расширение. В качестве объектов перечислимого типа могут выступать литералы.

‘A’  литерал перечисления.

Любая переменная, представимая как символьная (не строковая) переменная, может быть литералом перечисления, от сюда мы говорим не просто о константах перечисления, а о литералах перечисления. От сюда символический тип трактуется как частный тип перечислимого типа. Это оказалось очень удачным решением, так как мы не привязываемся к конкретной кодировке (не важно как у нас кодируется, например, символ ‘A’), и у нас есть преобразование, внутренняя кодировка-то есть (внутренняя кодировка такая, которую задает перечислимый тип данных), перечислимого типа данных в целый и обратно (т.е. связь с целым типом данных прослеживается). Следовательно символьные типы совершенно не зависимы от внешнего представления кодировки. Символьные значения упорядочены, имеются эффективные операции перехода от одного символьного значения к другому. Если нужно несколько наборов символов, то мы соответственно вводим char-set 1, char-set 2, и работаем с соответствующими char-set. Но некоторые литералы, например цифры или буквы латинские, они явным образом будут пересекаться. И вот тут язык Ада отступает от идеологии остальных ЯП, где идентификаторы перечислимых типов были именами равноправными с другими, следовательно соответствующие константы должны были быть уникальными. Ели принимать такую трактовку, то надо допустить, чтобы литералы перечислений в разных перечислимых типах пересекались. Например, можно ввести

  1. Цвета экрана type Screen Color is (…, Red , Green, Blue, White, Black)

  2. Цвети типа кожи человека type Skin Color is (White, Black, Yellow)

Теперь White (1) и White (2) различны. Если на Паскале нам надо было писать что-то типа Skin_white, то в Аде этого не требуется. Мы говорили, что перекрытие имен возможно только для операций и невозможно для имен констант и переменных. Вспомним дискретную математику, где ноль и единица трактовались как соответствующие нульместные функции, возвращающие значение константы 0 или 1. Здесь литералы тоже можно трактовать как нульместные функции, возвращающие значения соответствующего литерала. Поэтому перекрытие имен литералов есть перекрытие имен функций, и за рамки концепций языка выхода нет. С точки зрения языка С++ такое невозможно, потому что нульместные функции в С++ перекрывать нельзя (в прототип функции при перекрытии входят только типы параметров). В Аде это возможно, потому что есть понятие контекста перекрытия и фиксирован не только тип параметров, но и тип возвращаемого значения. В Аде понятие функции и процедуры не перемешано (функция обязательно имеет побочный эффект). Для вызова функции это есть контекст выражения, есть

V:=e, то, так как неявного преобразования типа не допускается, тип правой и левой частей должны совпадать. Тогда компилятор, видя контекст и само выражение, может догадаться какую именно функцию ему надо сюда подставить. Если компилятор видит нечто

А := White , тип А известен, то компилятор знает, какой именно White сюда вставить. В случае, когда невозможно сразу определить тип выражения, то вставляется конструкция Т'е, которая является не приведением типа, а просто подсказкой компилятору. Это статически атрибутная функция.

Например в цикле

For i in Black .. Yellow loop …

Не понятно, что такое Black .. Yellow, i – переменная, неявно описанная в теле цикла. Компилятор в этом случае не будет угадывать, к чему относятся Black .. Yellow к Screen Color или к Skin Color, он просто скажет, что не может определить тип выражения и вот тут надо уточнять

For i in Skin Color’Black .. Yellow loop …

Теперь понятно, что и i, и Yellow есть константы типа Skin Color, так как это и есть тот самый контекст. Это есть некоторое усложнение языка не являющиеся необходимым, но очень полезное. Вспомним, что язык Ада проектировался по принципу сундука. В смысле перечислений язык Ада стоит некоторым особняком в отличие от остальных ЯП.

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

Enum Color { Red Green Blue}

Везде в программе, где появляются данные типа еnum Color они трактуются как данные целого типа. При этом никакого контроля нет.

Int i= Red; корректно

Enum Color c = i+5; Мы можем переменной типа еnum Color присвоить произвольное целое значение. Одно полезное свойство в С от еnum есть, это облегчается способ документирования программ. Страуструпу, когда он должен был обеспечивать преемственность старых программ на С, никак не мог отказаться от еnum, но коль уж это некоторый тип данных, то ему надо было обеспечивать перекрытие. Страуструпу все-таки удалось это сделать за счет частичной потери совместимости со старыми программами на языке С. У компилятора есть возможность сделать неявное преобразование из типа данных enum в тип данных int. А вот неявного обратное преобразование уже не пройдет. Такое преобразование может быть только явным. Точно так же, как есть неявное преобразование из типа char в int, а неявного обратного нет.

Enum Color : byte {

…………………….

};

Делаем явное преобразование Color c = (color) (i + 5). Такое преобразование от аналогичного преобразования в языке Ада отличается тем, что никакого квазистатического преобразования в языке С++ нет. Компилятор никогда не вставит за Вас в период выполнения никакие проверки : все проверки только в период компиляции. Вся ответственность за некорректные операции и возможности выхода за пределы диапазона полностью ложиться на программиста. Здесь проскакивает некоторая ненадежность языка С++, но все-таки enum – это есть базисный тип данных. Здесь, в отличие от других ЯП, мы можем давать произвольное значение соответствующим константам. В случае с цветами очень удобно дать представление RGB.

Enum Color RGB {

Red = 0xFF0000

Green = 0x00FF00

Blue = 0x0000FF

};

Такого рода возможности еще больше увеличивают приятность документирования программ. И все-таки enum больше похоже на обычный тип перечисления. Отличие подхода C++ в том, что неявное преобразование из перечислимого типа в целый тип данных существует.

Все хорошо и сточки зрения удобности документирования, и сточки зрения удобность применения. Но вот появляется язык Оберон, тот самый минимальный язык. Считается, что если без того что есть в языке Оберон, по крайней мере с точки зрения ООП, обойтись нельзя. Перечислимого типа данных в языке Оберон нет. Получается ситуация на уровне Тараса Бульбы : «Я тебя породил, я тебя и убью». Интересно, что включение перечислимого типа данных в языки программирования никем и никогда не оспаривалось, потому что, во-первых, перечислимый тип данных – это красиво ( никаких недостатков не было названо). Почему же Вирт исключил перечислимый тип данных из языка Оберон? Причин было две. Во-первых, с точки зрения модульной структуры возникали некоторые проблемы с неявным экспортом имен.

Есть модуль М1 и М

М1 М Т

Он экспортирует тип данных Т и, следовательно, неявно за собой тянет целую кучу имен констант. Это усложняет компиляцию. В языках Оберон, Модула-2, Delphi нет неявного импорта. Если Вы импортируете все имена из модуля М, а он в свою очередь импортирует все имена из модуля М1, это не значит, что у Вас включаются и все имена из модуля М1. А вторая причина, она же и основная, это то, что основная концепция Оберона – это расширение типа, то есть наследование, выведение одного типа из другого с добавлением в него новых свойств и операций. Перечислимый тип данных с самого начала противоречит концепции расширения, потому что при задании он слишком строго задает диапазон изменения.

Представим себе тип данных Т и функцию f, которая делает f(T) => enum, которая возвращает значение какого-то перечислимого типа данных enum, мы выводим тип данных Т1, для которого переопределяем функцию f(T1) => enum, которая тоже возвращает enum, таким образам расширяется диапазон входных значений, функциональность функции возрастает, а возвращаемый тип никак не меняется. Полезность понятия наследования становится несколько ограниченной. Если проанализировать наследование классов в языках, где есть перечислимый тип данных, то можно заметить тенденцию отказа от перечислимого типа данных в пользу целочисленного с явным перечислением соответствующего набора констант. Никто еще не придумал способ надежного и эффективного расширения перечислимого типа данных. Таким образом в основную концепцию языка Оберон перечислимый тип не вписывается , и Вирт от него отказался.

В Delphi перечислимый тип данных есть – он остался от языка Паскаль. В С++ перечислимый тип данных есть – он остался от языка С, к тому же С++ нам не навязывает явной необходимости ООП, как впрочем и Delphi.

В языке JAVA, который появился в 1995 году тоже нет перечислимого типа данных. Там есть понятие констант и, если хотим использовать перечисления, то возвращаемся к доброму старому стилю, где мы фактически вводим набор констант. Там нет ключевого типа const, но используем конструкцию

static final int Red = 0xFF0000.

Посмотрим язык С# это 1999 год. Там перечислимый тип данных опять появляется. Зачем, если это противоречит концепции ООП? В описании языка С# авторы особо подчеркнули, что Главная причина – это то, что перечислимый тип данных хорошо сочетается и интегрированными средами программирования. Современные ЯП проектируются с учетом некоторой средой программирования (не обязательно интегрируемой).

Если раньше некоторые вещи было необходимо вводить с клавиатуры, то здесь на уровне языка вводить не надо – пусть этим занимаются интегрированные среды.

Табличка:

Имя свойства Значения

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

Пример.

Выравнивание текста

No aline

Left

Right

Center

Вместо того, что бы, например, ставить 3, значение которой не понятно, и надо лезть в «помощь», что бы определить его значение, значительно удобнее ставить соответствующую константу (Right).

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

IDL для описания прототипов, интерфейсов взаимодействия компонент. В ней есть перечислимые типы данных.

Если для С++ зафиксировано, что внутреннее представление перечислимых типов данных – это всегда int, то в С# есть некая оптимизация представления: мы можем указывать базисный тип данных (любой из целочисленных), который будет служить для внутреннего хранения.

Enum Color: byte {

……………………..

};

Можно предавать конкретные значения. Можно делать преобразования из перечислимого типа в базовый (если базовый не указан явно, то по умолчанию – это int). Все преобразования из базового типа в перечислимый только явные. Это касается всех значений за исключением константы 0.

Можно писать (Color != 0), и тут уже никаких преобразований уже не требуется.

Есть еще один интересный наворот. Это уже некоторые «дебри» языка. Атрибуты в С# -- это совсем не то, что в других ЯП (вообще, программисты фирмы Micrifoft любят давать свои трактовки понятиям, которые уже определены). То, что они называют атрибутами, было ьы удобнее назвать прагматами (это слово появилось давно). Прагматы – это средства, которые служат для управления компилятором и средой времени выполнения.

Структура на С

Char c;

Int i;

с

i Зазор, если машинное слово 4 байта, а chаr – 1.

Можно написать #pragma … , где опишет, что данную структуру не надо выравнивать, т.е. паковать. Набор прагматов колеблется от реализации к реализации.

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

Права доступа на чтение READ 0x1

Права доступа на запись WRITE 0x2

Права на чтение и запись будет объединение с помощью операции OR

READ|WRITE 0x3

Здесь некоторые противоречия с концепцией перечислимого типа. Перечислимый тип хорош тем, что мы даем каждому значению свое уникальное имя. В данном случае хотелось бы несколько расширить набор операций, а именно допустить побитовые операции OR и AND. В С++ эта проблема решается с помощью неявного преобразования в целый тип и применения уже к нему логических операций. Язык С# поступает по-другому: там есть специальный атрибут, а мы говорили, что атрибут это есть фактически прагмат, т.е. способ управления интегрированной средой или компилятором. Прагмат обычно записывается в квадратные скобки, т.е. [текст]. Есть специальный атрибут [Flags], который может стоять перед значениями перечислимого типа

Enum File access Rights {

WRITE = 2

READ = 1

READ_WRITE = READ or WRITE

};

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

Еще один порядковый тип данных, а именно ДИАПАЗОНЫ.

Диапазоны тоже впервые появились именно в языке Паскаль.

Type T = 0..N; где тип Т являлся контролируемым, т.е. был квазистатический контроль. Диапазонным может быть только целый и перечислимый типы данных, если таковой имеется в ЯП. Базисный тип всегда является целым.

Х: Т;

Х:= е (здесь будет вставлена проверка).

Перечислимый тип наследуется от целочисленного типа данных. Если проверку, вставляемую компилятором, можно сразу выполнить, то она выполняется, если нет – то она остается на выполнение. То же самое, что было при переводе из перечислимого тапа данных в целочисленный (классический квазистатический контроль). После Паскаля диапазоны с удовольствием стали вставлять в другие ЯП. Но тенденция современных ЯП диапазоны не использовать. Вирт придумал и перечислимый тип данных, и диапазоны, но в Оберон он не включил ни тот , ни другой тип данных. Почему? Диапазон не расширяем, у него слишком жесткие ограничения. Диапазонов нет ни в JAVA, ни в C#, ни в других ЯП, которые появились в 90-е годы. Это еще и потому, что если нужен контроль ограничения, его можно описать на программном уровне с помощью класса.

Интересен подход к диапазонам языка Ада. Диапазоны всегда имеют базовый класс. В языке Паскаль базовый Тип никогда не указывался, потому что там по имени константы всегда можно было безошибочно определить, к какому типу данных относится данный тип данных. Ели определьть неля, то уже надо явно указывать. В Модула-2 был изменен немного синтаксис. Появился новый без знаковый тип данных CARDINAL. И появилась путаница с именами констант.

Если писать T=CARDINAL [0..N], то N обозначала, что базовый тип – без знаковый, а если T=[0..N] – то знаковый.

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

Type POSITIVE is new INTEGER range 1.. MAXINT;

Мы показали, что базовый тип INTEGER, также указали диапазон. Данные типа INTEGER и типа POSITEVE в одном выражение смешивать нельзя.

INTEGER POSITIVE

i := p; нельзя, компилятор не пропустит.

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

i := INTEGER (p);

p := POSITIVVE (i);

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

Мы можем обеспечить и другую семантику (Паскалевскую). С помощью подтипа.

Subtype CARDINAL is INTEGER range 0..MAXINT.

c := i;

i := c;

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

Чаще всего диапазоны используют в индексах массивов. А во всех современных ООЯП длина массива является свойством самого массива, и компилятор сам вставляет все необходимые проверки.

Ссылки и указатели.

Указатель – языковая абстракция понятия адреса. С точки зрения указателей все ЯП делятся на те, в которых есть понятие указателя, а на те, в которых данное понятие отсутствует. Из тех ЯП, которые мы рассматриваем, указатели полностью отсутствуют в языке JAVA, и они отсутствуют в C# (указатели присутствуют в неуправляемой части кода, но мы ее рассматривать не будем). Понятие указателя заменено на понятие ссылка. Во всех остальных ЯП, которые мы сейчас рассматриваем понятие указателя есть. Но опять же есть строгие и нестрогие языки.

Строгие языки, их представителями являются Паскаль, Оберон, Модула-2 и Ада. Там понятие указателя используется только для объектов из динамической памяти.

В Паскале инициализируем New(p),

А в Аде

type PT in access T;

X : PT;

X := new T;

Это говорит о том, что у нас в динамической памяти будет расположен объект типа Т, и на него будет ссылаться указатель Х. Не поощряется смешивание указателей, полученных с помощью операции new, и обычных адресов памяти. Кроме как через указатели на объекты динамической памяти мы сослаться больше никак не можем. Здесь мы можем инициализовать указатель, присвоить один указатель другому, разыменовать указатель. В Обероне указатель служит для моделирования динамических объектов данных. По этому явной операции разыменования не требуется. Мы можем просто писать p.i, где i – соответствующее поле записи.

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

UNCHECKED_DEALLOCATION (p); -- аналог освобождения динамической памяти.

Нестрогие языки – это все производные от языка С.

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

  1. мусор

  2. «висячие» ссылки

Соседние файлы в папке Лекции по программированию на ЯВУ