лекции (2003) (Глазкова) (1160821), страница 4
Текст из файла (страница 4)
Он содержал все простые типы данных, кроме ссылок и процедурных типов данных (из-за чего приходилось вводить дополнительно виды передачи параметров), хотя неявно ссылка вводилась (передача параметров по ссылке).
Пример 2.
Рассмотрим расширения языка Pascal – язык Turbo Pascal : там уже процедурные типы данных были.
Пример 3.
Рассмотрим развитие языка Pascal – язык Modula 2. Он совершенно четко содержал все перечисленные типы данных.
Пункт 1. Целые типы данных.
Они являются самыми базисными в ЯП, поскольку существуют машинные архитектуры, в которых единственным примитивным типом данных, представление которого допускается, является целый ТД.
Какие основные проблемы стоят перед разработчиками новых ЯП с точки зрения целых типов данных?
1.Знаковость/без знаковость.
Существует 2 набора операций над целым типом данных:
-
знаковые,
-
без знаковые.
Вспомним архитектуру Х-86 – базовую архитектуру процессоров INTEL.
Там были операции сложения (ADD) и вычитания (SUB).
Для хранения отрицательных чисел используется дополнительный код числа в двоичной системе, удобный для выполнения операций сложения и вычитания. В этом случае алгоритмы сложения и вычитания практически не отличаются за исключением алгоритма установления флажков.
Операции же умножения и деления появились как знаковые и без знаковые (MUL, DIV, IMUL, IDIV).
Известно, что смешение в знаковой и без знаковой арифметике, в общем случае, ведет к ошибкам.
Пример – 32000+6000.Если числа рассматривать как 16 битные целые, то результат некорректен (получаются отрицательные числа).
Решений этой проблемы несколько:
1.Запретить смешение знаковых и без знаковых типов данных.
Такой подход был реализован в языке Modula 2 : там есть 2 типа данных – INTEGER и CARDINAL. Смешивать знаковую и без знаковую арифметику нельзя. Есть явные преобразования из одного типа к другому, и либо мы выбираем знаковые операции, либо без знаковые. Другой пример : язык SPL (использовался Пентагоном) – там также требовались явные преобразования из одного типа в другой. Программировать при таком подходе иногда достаточно утомительно.
2.Игнорировать проблему знаковости/без знаковости.
Например, в языке Си допускаются знаковые и без знаковые типы данных и их можно смешивать совершенно произвольным образом. В соответствии с веяниями 70-ых
Бьерн Страуструп запретил смешивание знаковых и без знаковых ТД в языке Си с классами. В С++ можно смешивать. Почему? Программы на языке Си (фрагменты ОС UNIX) прогонялись через компилятор Си с классами. Ни одна из программ тест на смешение знаковой и без знаковой арифметики не выдержала. Даже компилятор Страуструпа языка Си с классами, написанный на этом же языке методом раскрутки, этого теста не выдержал.
3.Самый радикальный – убрать без знаковый ТД (т.к. более общим с абстрактной точки зрения является знаковый тип).
Потребность без знакового ТД обусловлена исключительно задачами низкоуровневого . эффективного программирования (адресная арифметика - арифметика машинных адресов - является без знаковой).
Еще одна потребность – это расширенный диапазон: в 16 битной архитектуре разница между диапазонами 0..65535 и 0..32767 была существенна.
Пример – язык Modula 2 был реализован на 16 битной архитектуре, и там были реализованы и знаковый, и без знаковый ТД.
В 32 битной арифметике необходимость в дополнительном диапазоне отпадает. В такой архитектуре наиболее простое решение – исключить без знаковый ТД.
Язык Oberon был реализован в 32 битной архитектуре, и поэтому без знаковый ТД CARDINAL там отсутствовал. В языке Java без знакового ТД также нет.
Рассмотрим язык с авторской позиции(язык – система компромиссов).
Исключение из языка Java без знаковых ТД приводит к упрощению компилятора,
и многих проблем, связанных с представлением знаковых и без знаковых чисел, удается избежать, т.е. программа становится более переносимой. Но на нижнем уровне становится трудно эффективно реализовывать низкоуровневые операции. Поэтому в языке Java появилась операция >>> - это без знаковый (логический) сдвиг вправо (биты слева заполняются нулями).
Рассмотрим язык С#.
Язык С#, так же как и Java, нельзя рассматривать как отдельно стоящий язык программирования. Относительно языка Java более правильно говорить о некотором комплексе Java-технологий, т.е. совокупности некоторых подходов к распределенным вычислениям, которые базируются именно на языке программирования. В языке C# есть система .NET. В этой системе присутствует базовый набор классов .NET Frame Work. ЕстьCLR (Common Language Run Time) – общая языковая система времени выполнения, которая включает в себя библиотеки NET Frame Work, единый менеджер динамической памяти и другие. Вся эта система базируется на общей спецификации языков CLS (Common Language Specification), часть которой CTS (Common Type System) – общая система типов. Именно на этой общей системе типов основан класс NET Frame Work. Система типов языка C# - это отражение CTS.
Сейчас все типы данных, которые можно представить в системе INTEL, представлены в CTS. В C# для любого арифметического типа данных существует знаковый и без знаковый аналог.
Знаковый Без знаковый
8 бит sbyte byte
16 бит short ushort
32 бита int uint
64 бита long ulong
Та же ситуация в языке С. Изначально для любого типа данных было принято решение представлять его знаковый и без знаковый аналог.
При этом в языке C# разрешены неявные преобразования, которые не приводят к потере точности. С этой точки зрения употреблять sbyte и byte, short и ushort, int и uint в одних и тех же выражениях нельзя, т.е. создатели языка C# запретили неконтролируемое смешение знаковой и без знаковой арифметики. Допустимы следующие преобразования byte -> short, ushort -> int, uint -> long, т.к. они не теряют информацию.
Таким образом, мы рассмотрели проблему знаковости/без знаковости в ЯП.
2. Теперь рассмотрим еще одну базовую проблему – проблему представления. Проблема такова : если мы зафиксируем представление целочисленных типов данных, то мы улучшим переносимость программ, но при этом потеряем эффективность, поскольку целый тип данных – наиболее базисный. Его базисность состоит в том, что большинство типов данных, представленных в машинном языке, сводится к целочисленным типам данных. С этой точки зрения желательно, чтобы система типов в языке была приближена к машинной архитектуре. Заметим, что в ЯП С, C#,C++ языковая система целочисленных типов данных сделана так, чтобы быть максимально приближенной к машинной архитектуре.
Проблема такова, что при переходе на другую архитектуру, представление базисного целочисленного ТД несколько меняется. Необходимо, чтобы целый ТД оптимальным образом представлялся в архитектуре. Сейчас все архитектуры регистровые. Поэтому, с точки зрения эффективности, важно учитывать размерность регистра и представление целочисленного ТД в регистровой архитектуре. Если мы зафиксируем представление, то сразу потеряем эффективность. Что нам важнее – эффективность или переносимость.
Вот исходя из этого и выбирают представление.
Для создателей языка Java переносимость была значительно важнее эффективности, и поэтому в языке Java представление всех чисел зафиксировано.
Аналогично зафиксировано представление всех чисел в языке C#. Почему там не встает проблема переносимости? Потому, что система Windows ориентирована прежде всего на архитектуру INTEL. А вот в языках С и С++ зафиксировать представление нельзя.
Т.о., мы можем
-
либо полностью зафиксировать представление и не обращать внимание на эффективность,
-
либо выбрать эффективность.
С этой точки зрения интересно посмотреть, как решается эта проблема в языке Ada.
Программы на языке Ada должны выполняться на как можно более широком спектре систем (т.к. некоторые редко встречающиеся системы могут быть критичны с военной точки зрения).Как переносимость, так и эффективность в языке Ada стояли очень остро. Создатели этого языка попытались «объять необъятное», т.е. сделать систему типов, которая была бы и эффективной, и переносимой.
Они предложили систему root_integer – это универсальный целый тип (идентификатора root_integer в языке Ada нет), который содержит все возможные целые числа. И, вообще говоря, есть подмножества root_integer, которые представимы на любой конкретной реализации.
Есть специальный пакет данных (аналог стандартной библиотеки языка Ada). Там описаны INTEGER, SHORT_INT, LONG_INT, которые являются подмножествами универсального целого типа root_integer. Возникает интересная концепция ЯП Ada, которая называется выведением типа. На этой концепции основана структура не только целочисленных, но и всех остальных ТД.
Общая идея выведения нового типа достаточно проста: пусть есть тип данных Т. Новый тип данных Т1 появляется с помощью генератора нового типа
type T1 is new T [ограничение]
При этом новый тип наследует все значения старого типа и все его операции. В языке Ada разные (т.е. разноименные) типы данных несовместимы по операциям.
Пусть
type Length is new Integer
Это означает, что тип данных Length полностью наследует свойства целочисленного ТД, наследует все операции, но несовместим с типом данных Integer (например, по присваиванию).
Пусть есть два объявления:
X: Length;
Y: Integer;
В этом случае присваивания X:=Y и Y:=X в языке Ada запрещены. Какой в этом смысл?
Мы говорим, что понятие типа данных должно отражать содержательную роль данных. Понятно, что содержательная роль типа данных Length – обозначать длины. Представим, что единица измерения длин целочисленная, тогда тип данных Square – наследник типа Integer.
Type Square is new Integer
Тогда Length*Length => Square.
С точки зрения содержательной роли данных, несовместимость разноименных типов по операциям позволяет ввести более надежную систему типов. При необходимости можно ввести ограничения, например, ограничение диапазона:
type T1 is new Integer range 0…128
В то же время, когда мы вводим различные диапазоны одного и того же типа, запрет смешения различных типов данных несколько мешает. В связи с этим создатели языка АДА ввели понятие подтипа. Подтип всегда имеет базовый тип данных. С точки зрения набора значений, он либо совпадает со своим базовым ТД, либо является его ограничением. По набору операций подтип совместим с базовым ТД. Т.е. различные подтипы одного и того же типа полностью совместимы между собой по операциям и могут смешиваться.
Например,
Subtype Height is Length range 0..MaxHeight
Базовые ТД включают:
-
INTEGER
-
SHORT_INT
-
LONG_INT
-
POSITIVE
-
NATURAL
Все они являются наследниками универсального целого ТД, но, с точки зрения множества значений, являются его ограничениями.
Диапазон значений этого типа , разумеется, зависит от реализации. В ряде приложений диапазоны нас не интересуют. Если надо, мы можем ввести свои типы или подтипы.
Например, мы можем описать константу MaxCard , и, если мы не хотим смешивать знаковую и без знаковую арифметику, мы можем ввести новый тип данных
type Cardinal is new Integer range 0..MaxCard
Т.е. мы сами устанавливаем диапазоны, а уже задача компилятора подобрать ту реализацию целочисленного ТД на данных архитектуре, которая бы максимально, с точки зрения эффективности, подходила бы к данной задаче. И данные типа Cardinal смешивать с данными типа Integer нельзя. Мы должны явным образом привести Cardinal к Integer.
Card:= Cardinal(i); i :=Integer(a);
Т.е. явным образом ввести преобразование типов.
Т.о., переносимость и гибкость обеспечивается за счет механизма типов и подтипов (где программист может указывать произвольные диапазоны), а эффективность реализуется компилятором.
В случае, если нас не интересует проблема знаковой/без знаковой арифметики, мы можем пользоваться вместо типа Cardinal подтипом Card
Subtype Card is Integer range 0..MaxCard
С этой точки зрения, т.к. речь идет о подтипе типа Integer, данные типа Card могут перемешиваться с данными типа Integer. В связи с этим в языке Ada появляется понятие квазистатический контроль.
Контроль на этапе трансляции или загрузки – это статический контроль (контроль соответствия типов).
Контроль подтипов – это контроль не статический. Пусть:
a : Card; i : Integer;
Поскольку Card - это подтип, а подтипы полностью совместимы с базовым типом и между собой, поэтому и присваивание a:=i; и присваивание i:=a; в общем случае допустимы. Но, в то же время, речь идет о разных подтипах.
Пусть
a : Card constant := 10;
y : Integer constant := -1;
Присваивание a:=y; ошибочно, поскольку различны диапазоны значений.
Квазистатическая проверка – это проверка, которую в состоянии выполнить компилятор, если он знает значения соответствующей переменной. Квазистатические проверки распространены в таких языках как АЛГОЛ-60 и Паскаль. Чаще всего квазистатические проверки ставятся при контроле диапазонов. Во многих ЯП, основанных на языке Паскаль, существует возможность эти проверки отключать (для компактности и эффективности программ).
В общем случае средства квазистатического контроля достаточно полезны. Квазистатический контроль – механизм проверки ограничений диапазонов.