М. Бен-Ари - Языки программирования. Практический сравнительный анализ (2000) (1160781), страница 11
Текст из файла (страница 11)
Подтипы важны для определения массивов, как это будет рассмотрено в разделе 5.4. Кроме того, именованный подтип можно использовать для упрощения многих операторов:
if С in Upper-Case then ... - Проверка диапазона
for C1 in Upper-Case loop ... — Границы цикла
4.6. Производные типы
Вторая интерпретация отношения между двумя аналогичными типами состоит в том, что они представляют разные типы, которые не могут использоваться вместе. В языке Ada такие типы называются производными (derived) типами и обозначаются в определении словом new:
type Derived_Dharacter is new Character;
C: Character;
D: Derived_Character;
С := D: -- Ошибка, типы разные
Когда один тип получен из другого типа, называемого родительским (parent) типом, он наследует копию набора значений и копию набора операций, но типы остаются разными. Однако всегда допустимо явное преобразование между типами, полученными друга из друга:
D := Derived_Character(C); -- Преобразование типов
С := Character(D); -- Преобразование типов
Можно даже задать другое представление для производного типа; преобразование типов будет тогда преобразованием между двумя представлениями (см. раздел 5.8).
Производный тип может включать ограничение на диапазон значений родительского типа:
type Upper_Case is new Character range 'A'.. 'Z';
U: Upper_Case;
C: Character;
С := Character(U); -- Всегда правильно
U := Upper_Case(C); -- Может привести к ошибке
Производные типы в языке Ada 83 реализуют слабую версию наследования (weak version of inheritance), которая является центральным понятием объектно-ориентированных языков (см. гл. 14). Пересмотренный язык Ada 95 реализует истинное наследование (true inheritance), расширяя понятие производных типов; мы еще вернемся к их изучению.
Целочисленные типы
Предположим, что мы определили следующий тип:
type Altitudes is new Integer range 0 .. 60000;
Это определение работает правильно, когда мы программируем моделирование полета на 32-разрядной рабочей станции. Что случается, когда мы передадим программу на 16-разрядный контроллер, входящий в состав бортовой электроники нашего самолета? Шестнадцать битов могут представлять целые числа со знаком только до значения 32767. Таким образом, использование производного типа было бы ошибкой (так же, как подтипа или непосредственно Integer) и нарушило бы программную переносимость, которая является основной целью языка Ada.
Чтобы решать эту проблему, можно задать производный целый тип без явного указания базового родительского типа:
type Altitudes is range 0 .. 60000;
Компилятор должен выбрать представление, которое соответствует требуемому диапазону — integer на 32-разрядном компьютере и Long_integer на 16-разрядном компьютере. Это уникальное свойство позволяет легко писать на языке Ada переносимые программы для компьютеров с различными длинами слова.
Недостаток целочисленных типов состоит в том, что каждое определение создает новый тип, и нельзя писать вычисления, которые используют разные типы без преобразования типа:
I: Integer;
A: Altitude;
А := I; -- Ошибка, разные типы
А := Altitude(l); -- Правильно, преобразование типов
Таким образом, существует неизбежный конфликт:
• Подтипы потенциально ненадежны из-за возможности писать смешанные выражения и из-за проблем с переносимостью.
• Производные типы безопасны и переносимы, но могут сделать программу трудной для чтения из-за многочисленных преобразований типов.
4.7. Выражения
Выражение может быть очень простым, состоящим только из литерала (24, V, True) или переменной, но может быть и сложной комбинацией, включающей операции (в том числе вызовы системных или пользовательских функций). В результате вычисления выражения получается значение.
Выражения могут находиться во многих местах программы: в операторах присваивания, в булевых выражениях условных операторов, в границах for-циклов, параметрах процедур и т. д. Сначала мы обсудим само выражение, а затем операторы присваивания.
Значение литерала — это то, что он обозначает; например, значение 24 — целое число, представляемое строкой битов 0001 1000. Значение переменной V — содержимое ячейки памяти, которую она обозначает. Обратите внимание на возможную путаницу в операторе:
V1 :=V2;
V2 — выражение, значение которого является содержимым некоторой ячейки памяти. V1 — адрес ячейки памяти, в которую будет помещено значение V2.
Более сложные выражения содержат функцию с набором параметров или операцию с операндами. Различие, в основном, в синтаксисе: функция с параметрами пишется в префиксной нотации sin (x), тогда как операция с операндами пишется в инфиксной нотации а + b. Поскольку операнды сами могут быть выражениями, можно создавать выражения какой угодно сложности:
a + sin(b)*((c-d)/(e+34))
В префиксной нотации порядок вычисления точно определен за исключением порядка вычисления параметров отдельной функции:
max (sin (cos (x)), cos (sin (y)))
Можно написать программы, результат которых зависит от порядка вычисления параметров функции (см. раздел 7.3), но такой зависимости от порядка вычисления следует избегать любой ценой, потому что она является источником скрытых ошибок при переносе программы и даже при ее изменении.
Инфиксной нотации присущи свои проблемы, а именно проблемы старшинства и ассоциативности. Почти все языки программирования придерживаются математического стандарта назначения мультипликативным операциям («*», «/») более высокого старшинства, чем операциям аддитивным («+», «-»), старшинство других операций определяется языком. Крайности реализованы в таких языках, как АР L, в котором старшинство вообще не определено (даже для арифметических операций), и С, где определено 15 уровней старшинства! Частично трудность изучения языка программирования связана с необходимостью привыкнуть к стилю, который следует из правил старшинства.
Примером неинтуитивного назначения старшинства служит язык- Pascal. Булева операция and рассматривается как операция умножения с высоким старшинством, тогда как в большинстве других языков, аналогичных С, ее приоритет ниже, чем у операций отношения. Следующий оператор:
pascal |
if а > b and b > с then ...
является ошибочным, потому что это выражение интерпретируется
Pascal |
и синтаксис оказывается неверен.
Значение инфиксного выражения зависит также от ассоциативности операций, т. е. от того, как группируются операции одинакового старшинства: слева направо или справа налево. В большинстве случаев, но не всегда, это не имеет значения (кроме возможного переполнения, как рассмотрено в разделе 4.1). Однако значение выражения, включающего целочисленное деление, может зависеть от ассоциативности из-за усечения:
C |
i = i * j / k; /* результат равен 1 2 или 1 4? */
В целом, бинарные операции группируются слева направо, так что рассмотренный пример компилируется как:
C |
в то время как унарные операции группируются справа налево: !++i в языке С вычисляется, как ! (++i).
Все проблемы старшинства и ассоциативности можно легко решить с помощью круглых скобок; их использование ничего не стоит, поэтому применяйте их при малейшем намеке на неоднозначность интерпретации выражения.
В то время как старшинство и ассоциативность определяются языком, порядок вычисления обычно отдается реализаторам для оптимизации. Например, в следующем выражении:
(а + Ь) + с + (d + е)
не определено, вычисляется а + b раньше или позже d + е, хотя с будет просуммировано с результатом а + b раньше, чем с результатом d + е. Порядок может играть существенную роль, если выражение вызывает побочные эффекты, т. е. если при вычислении подвыражения происходит обращение к функции, которая изменяет глобальную переменную.
Реализация
Реализация выражения, конечно, зависит от реализации операций, используемых в выражении. Однако стоит обсудить некоторые общие принципы.
Выражения вычисляются изнутри наружу; например, а * (b + с) вычисляется так:
load R1,b
load R2, с
add R1 , R2 Сложить b и с, результат занести в R1
load R2, а
mult R1.R2 Умножить а на b + с, результат занести в R1
Можно написать выражение в форме, которая делает порядок вычисления явным:
явным:
bс + а
Читаем слева направо: имя операнда означает загрузку операнда, а знак операции означает применение операции к двум самым последним операндам и замену всех трех (двух операндов и операции) результатом. В этом случае складываются b и с; затем результат умножается на а.
Эта форма называется польской инверсной записью (reverse polish notation — RPN) и может использоваться компилятором. Выражение переводится в RPN, и затем компилятор вырабатывает команды для каждого операнда и операции, читая RPN слева направо..
Для более сложного выражения, скажем:
(а + b) * (с + d) * (е + f)
понадобилось бы большее количество регистров для хранения промежуточных результатов: а + b, с + d и т. д. При увеличении сложности регистров не хватит, и компилятору придется выделить неименованные временные пере менные для сохранения промежуточных результатов. Что касается эффектив ности, то до определенной точки увеличение сложности выражения дает лучший результат, чем использование последовательности операторов присваивания, так как позволяет избежать ненужного сохранения промежуточных результатов в памяти. Однако такое улучшение быстро сходит на нет из-за необходимости заводить временные переменные, и в некоторой точке компилятор, возможно, вообще не сможет обработать сложное выражение.
Оптимизирующий компилятор сможет определить, что подвыражение а+b в выражении
(а + b) * с + d * (а + b)
нужно вычислить только один раз, но сомнительно, что он сможет распознать это, если задано
(а + b) * с + d * (b + а)
Если общее подвыражение сложное, возможно, полезнее явно присвоить его переменной, чем полагаться на оптимизатор.
Другой вид оптимизации — свертка констант. В выражении:
2.0* 3.14159* Radius
компилятор сделает умножение один раз во время компиляции и сохранит результат. Нет смысла снижать читаемость программы, производя свертку констант вручную, хотя при этом можно дать имя вычисленному значению:
C |
Two_PI: constant := 2.0 * PI;
Circumference: Float := Two_PI * Radius;
4.8. Операторы присваивания
Смысл оператора присваивания:
переменная := выражение;
состоит в том, что значение выражения должно быть помещено по адресу памяти, обозначенному как переменная. Обратите внимание, что левая часть оператора также может быть выражением, если это выражение можно вычислить как адрес:
Ada |
Выражение, которое может появиться в левой части оператора присваивания, называется l-значением; константа, конечно, не является 1-значением. Все выражения дают значение и поэтому могут появиться в правой части оператора присваивания; они называются r-значениями. В языке обычно не определяется порядок вычисления выражений слева и справа от знака присваивания. Если порядок влияет на результат, программа не будет переносимой.
В языке С само присваивание определено как выражение. Значение конструкции
переменная = выражение;
такое же, как значение выражения в правой части. Таким образом,
C |
int v1 , v2, v3;
v1 = v2 = v3 = e;
означает присвоить (значение) е переменной v3, затем присвоить результат переменной v2, затем присвоить результат переменной v1 и игнорировать конечный результат.
В Ada присваивание является оператором, а не выражением, и многократные присваивания не допускаются. Многократное объявление
V1.V2.V3: Integer :=Е;
рассматривается как сокращенная запись для
Ada |
V2: Integer := Е;
V3: Integer := Е;
а не как многократное присваивание.
Хотя стиль программирования языка С использует тот факт, что присваивание является выражением, этого, вероятно, следует избегать как источник скрытых ошибок программирования. Весьма распространенный класс ошибок вызван тем, что присваивание («=») путают с операцией равенства («==»). В следующем операторе:
C |
программист, возможно, хотел просто сравнить i и j, не обратив внимания, что значение i изменяется оператором присваивания. Некоторые С-компиляторы расценивают это как столь плохой стиль программирования, что выдают предупреждающее сообщение.
Полезным свойством языка С является комбинация операции и присваивания:
C |
v+=e; /* Это краткая запись для... */
v = v + е; /* такого оператора. */