Скалярные типы данных Рассматривается семейство элементарных типов данных под общим названием скалярные типы: перечисления, целые и вещественные типы, десятичные числа. Вводится понятие поддиапазона, который ограничивает диапазон значений существующего базового типа, обсуждается логический тип и символьные типы. В завершение комментируется стиль программирования на языке С, основанный на неявном преобразовании типов. Дескриптор каждого объекта скалярного типа хранит лишь пару-тройку атрибутов. Например, для целочисленного объекта единственными атрибутами являются его имя и значение, а другой информации в этом объекте нет. Вообще специфика объектов данных скалярных типов во многом определяется возможностями аппаратуры компьютера. Перечисления Перечисление — это тип данных, представляемый конечной последовательностью имен, записанных внутри скобок. Объявление type день = (пон, втр, срд, чтв, птн, сбт, вск); вводит тип день как перечисление из семи элементов. В языке Pascal любое имя из перечисления (например, пон) считается константой (значением). Для каждого вхождения константы распознается ее принадлежность к типу день. Скалярный тип boolean рассматривается как предопределенное перечисление type boolean = (false, true); Аналогично, тип char является перечислением, которое определяется набором машинных символов. Широко используемый набор ASCII (American Standard Code for Information Interchange) включает 128 символов. Символьная константа заключается в одиночные кавычки (апострофы) '&'. Одиночные кавычки обозначаются '', поэтому символьная константа для одиночных кавычек записывается так ''''. Элементы перечисления упорядочены, это значит, что пон < втр < .. < вск К элементам перечисления (и к целым числам) применимы следующие операции языка Pascal: ‰. ord (x) — функция определяет для имени x его номер в последовательности, поэтому ord (вск) = 7; ‰. succ (x) — функция (successor) определяет для имени x следующее имя в последовательности, поэтому succ (втр) = срд; если x — последнее имя, то фиксируется ошибка; ‰. pred (x) — функция (predecessor) определяет для имени x предшествующее имя в последовательности, поэтому pred (втр) = пон; если x — первое имя, то фиксируется ошибка. Например, для дней может быть реализована функция завтра (x): if x = вск then завтра := пон else завтра := succ (x) В языке С перечисление объявляется следующим образом: enum day {пон, втр, срд, чтв, птн}; В отличие от языка Pascal, элементы перечисления здесь считаются именами целых констант (принадлежащих к целому типу). Иначе говоря, в примере элементам в фигурных скобках автоматически назначаются числовые значения от 0 до 4: пон = 0, втр = 1, срд = 2, чтв = 3, птн = 4 В языке C++ элементы перечисления тоже имеют порядковые номера, но эти номера (числа) не могут быть использованы как их эквиваленты (значения). Например, после объявления day today; присваивание today = 4; считается некорректным. Оно может быть разрешено только в том случае, если число из правой части будет явно преобразовано в тип day. Такой запрет предотвращает некоторые потенциальные ошибки. Скалярные типы данных 1 В языке C# перечисления подобны C++, за исключением того, что их элементы икогда не преобразуются в целые числа. Таким образом, здесь существенно ограничены как значения, так и операции над перечислениями. В 2004 году перечисления были добавлены в язык Java версии 5.0. Все перечисляемые типы здесь считаются неявными подклассами предопределенного класса Enum. Поскольку перечисляемые типы являются классами, они могут иметь поля данных, конструкторы и методы. Синтаксически определения перечисляемого типа в Java подобны C++ (если не учитывать поля, конструкторы и методы). Экземпляры перечислений являются экземплярами классов. Все перечисления наследуют toString, а также другие методы. Массив экземпляров перечисляемого типа может быть доступен с помощью статического метода, а внутреннее численное значение перечисляемой переменной — с помощью обычного метода. Перечисляемой переменной нельзя присвоить значение какого-либо другого типа. Кроме того, перечисляемая переменная никогда не преобразуется (неявно) в любой другой тип. Ни один из современных скриптовых языков не содержит перечислений. Перечисляемые типы улучшают как читабельность, так и надежность программ. Читабельность повышается непосредственно: в отличие от закодированных (числовых) значений, именованные значения легко распознаются и понимаются. С точки зрения надежности перечисления языки Ada, C#, F#, Java 5.0 и частично C++ предоставляют два преимущества: ‰. запрещают применение к перечисляемым переменным арифметических операций (например, нельзя складывать дни недели); ‰. запрещают присвоение значений, выходящих за пределы определенного диапазона. Этого нельзя сказать о перечислениях языка С, поскольку в нем элементы перечисления трактуются как целые числа. В языке C++ ситуация немного лучше. Числовые значения присваиваются перечисляемым переменным типа лишь тогда, когда они явно преобразуются в их тип. В ходе такого преобразования проверяется вхождение числа в диапазон номеров, которые определены для элементов перечисления. Целые и вещественные типы Почти в любом языке программирования присутствуют численные типы данных. Наиболее распространенными являются целый и вещественный типы, поскольку они непосредственно поддерживаются аппаратурой компьютера. Базовый целый тип в языке Pascal обозначается именем integer, в языке С — именем int. Множество значений целого типа образует ограниченное упорядоченное подмножество из бесконечного множества математических целых чисел. Максимальное значение иногда задается как именованная константа. Например, в языке Pascal имеется константа maxint. В таком случае все допустимые значения целых чисел находятся в диапазоне между -maxint и +maxint. В языке C существуют четыре разновидности целого типа: int, short, long и char, отличающиеся разрядностью чисел и диапазоном значений (например, для типа int — разрядность 16 битов [один разряд под знак и 15 разрядов под значение], диапазон значений –32767 ... +32767). Операции над целочисленными объектами данных обычно разделяются на следующие группы: ‰. Арифметические операции. Бинарные арифметические операции имеют сигнатуру: БинОп : целое × целое → целое Здесь БинОп может быть сложением (+), вычитанием (–), умножением (∗), делением (/ или div), а также делением по модулю (mod). Сигнатура унарных операций записывается в виде: УнарОп : целое → целое где УнарОп может быть, например, операцией смены знака (–). Обычно включаются и другие арифметические операции, часто в виде библиотечных функций (например, операция определения абсолютного значения). ‰. Операции сравнения. Сигнатура операции сравнения выглядит следующим образом: СравОп : целое × целое → булево Скалярные типы данных 2 Здесь под СравОп могут подразумеваться следующие операции: равно, не равно, больше чем, меньше чем, больше или равно, меньше или равно. Эти операции сравнивают значения двух операндов и в качестве результата возвращают логическое (булево) значение. ‰. Операции присваивания. Присваивание для целых чисел соответствует одному из двух способов: присваивание (:=) : целое × целое → пустой тип или присваивание (=): целое × целое → целое ‰. Битовые операции. В языках с небольшим набором элементарных типов целые числа нагружаются дополнительными ≪обязанностями≫. Например, в языке C они используются в качестве булевых величин. Поэтому битовые операции также определены через целые числа: БитОп : целое × целое → целое В C имеются операции для побитовых операций логического умножения (&), логического сложения (|), сдвига битов (<<) и т. д. ‰. Логические операции. В языке C для двух целых чисел, моделирующих значения ≪ложь≫ и ≪истина≫, заданы две бинарные и одна унарная логические операции: ЛогИ (&&) : целое × целое → целое ЛогИли (||) : целое × целое → целое ЛогНе (!) : целое → целое Как правило, целочисленный тип данных реализуется при помощи аппаратного представления целых чисел и набора машинных операций, встроенных в процессор. Базовый вещественный тип в языке Pascal обозначается именем real, в языке С — именем float. Как и в случае целого типа, допустимые вещественные значения формируют в диапазоне, обусловленном возможностями аппаратуры (от минимального отрицательного значения до максимального положительного). Правда, в отличие от целых чисел, вещественные числа распределены в этом диапазоне неравномерно. Хранение вещественных чисел с плавающей точкой в памяти компьютера также основано на их аппаратном представлении. Общепринятый формат вещественных чисел с плавающей точкой задает стандарт IEEE 754, этот формат для 32-разрядного числа имеет вид. Знак (S) Экспонента (Е) Мантисса (М) Число представляется с помощью трех полей: ‰. S — однобитовое поле, хранящее знак числа. Значение 0 соответствует положительным числам. ‰. E — экспонента со сдвигом на величину 127. Под это поле отведено 8 битов, таким образом, имеется 256 различных значений от 0 до 255, соответствующих степеням 2 в диапазоне от –127 до 128 (с учетом сдвига). ‰. M — мантисса, под которую отведено 23 бита. Так как в нормализованной форме представления вещественных чисел первый бит в мантиссе всегда равен 1, его можно опускать и вставлять автоматически аппаратными средствами, тем самым увеличивая точность до 24 битов. Формат обеспечивает диапазон значений чисел от 10–38 до 10+38. Для 64-разрядного числа поле под экспоненту расширяется до 11 бит, а поле под мантиссу — до 52 битов, что приводит к представлению чисел в диапазоне от 10–308 до 10+308. Для вещественных чисел обычно предусмотрены те же операции, что и для целых, хотя набор операций сравнения уменьшают. Из-за проблем округления проверку на равенство двух вещественных чисел выполнить не удается, поэтому такая операция обычно запрещается. Полный набор операций числовых типов для языка Pascal приведен в табл. 1, а для С — в табл. 2. Таблица 1. Набор операций в языке Pascal (целый, вещественный и логический типы) Скалярные типы данных 3 Из-за низкого приоритета операций отношения в составных условиях программ на языке Pascal достаточно часто приходится применять скобки. Например, в следующем выражении: (y >= 0) and (x <> M[i]) В языке С принята другая схема приоритетов операций. Благодаря этой схеме скобки в составных условиях здесь используются значительно реже. Например, предыдущее условие можно записать без применения скобок группировки: y >= 0 && x != M[i] Таблица 2. Набор операций в языке С Короткая схема вычисления, принятая в языке С, означает, что второй операнд обрабатывается только при необходимости: ‰. Если Е1 = истина, то Е1 ׀׀Е2 уже равно 1 (Е2 не вычисляется). ‰. Если Е1 = ложь, то Е1 && Е2 уже равно 0 (Е2 не вычисляется). Десятичные числа Аппаратура большинства компьютеров чаще всего обрабатывает двоичные целые числа и числа с плавающей точкой, однако во многих коммерческих приложениях требуется специальный вид десятичных чисел. Например, объекты данных, представляющие денежные суммы, должны содержать рубли и копейки, то есть выглядеть как десятичные числа с фиксированным количеством десятичных знаков и десятичной точкой. Эта точка располагается в определенном месте и отделяет целую часть числа от дробной. С помощью целых чисел такая запись невозможна, а использование чисел с плавающей точкой может привести к ошибкам округления. Для подобных целей удобны десятичные вещественные числа с фиксированной точкой. Типы данных для них предусмотрены в языках Ada, C# и F#. Десятичные числа способны представлять точные значения десятичных величин в некотором ограниченном диапазоне. Ограничение диапазона и повышенные затраты на сохранение в памяти считаются их главными недостатками. В памяти компьютера десятичные числа сохраняются в виде двоичных кодов десятичных цифр. Такие представления получили название двоично-кодированных десятичных чисел (BCD — Binary Coded Decimal). Обычно применяют один из двух вариантов упаковки десятичных цифр: 1) одна цифра на байт памяти; 2) две цифры на байт памяти. Скалярные типы данных 4 В любом варианте для запоминания числа требуется несколько больше памяти, чем при двоичном представлении. Например, для хранения шести десятичных цифр в BCD-формате (экономичный вариант) необходимы 24 бита памяти, а двоичном формате — 20 битов памяти. Операции над десятичными величинами легко реализуются современными процессорными средствами. Поддиапазоны Поддиапазоны представляют собой подтипы некоторых базовых типов, которые имеют счетное количество значений. Поддиапазоны ограничивают диапазон значений существующего базового типа: 0 .. 99 Значениями базового типа должны быть целые числа (или другие перечисляемые значения). Поддиапазоны вещественных чисел не допустимы. К поддиапазону применяются те же операции, что и к базовому типу. Достоинства поддиапазонов: 1. Уменьшаются затраты памяти. Поскольку при применении поддиапазона необходимо представлять лишь часть возможных значений базового типа, для хранения элементов этого подтипа требуется меньшая емкость памяти. Например, для представления целых чисел из поддиапазона 1..12 требуется только четыре бита, в то время как для стандартного представления целых чисел может потребоваться 16, 32 или более битов. Тем не менее, так как арифметические операции с укороченными целыми числами могут потребовать программных процедур (то есть будут замедлять вычисления), при реализации таких чисел ориентируются на минимальную разрядность операндов, для которой предусмотрена аппаратная поддержка арифметических операций. Обычно это восемь или шестнадцать разрядов. Например, в C символы хранятся как 8-разрядные целые числа, действия над которыми можно выполнить при помощи машинных команд. 2. Улучшается контроль переменных. Если переменная объявлена как экземпляр некоторого поддиапазона, то проверка значений, которые присваиваются этой переменной, становится более точной. Например, если переменная day_Of_Month объявлена как day_Of_Month: 1..31, то оператор присваивания day_Of_Month := 32; который хотел предложить барон Мюнхгаузен, будет признан ошибочным. Это обнаруживается во время компиляции. Если же переменная day_Of_Month объявлена просто как целая, то приведенный оператор присваивания формально оказывается правильным, и программисту придется отыскивать эту ошибку уже на этапе выполнения программы. Логический тип В большинстве языков предусмотрен логический (булев) тип данных для представления и обработки значений истина и ложь. Логический тип данных задает объекты, которые могут принимать два значения: истина (true) и ложь (false). В языке Pascal логический тип данных рассматривается как встроенное в язык перечисление, а именно: type Boolean = (false, true); В этом объявлении определены константы false и true, а также соотношение их величин false < true. В обычный набор операций с этим типом данных входят операция присваивания и следующие логические операции: and: Boolean × Boolean → Boolean (И, логическое умножение) or: Boolean × Boolean → Boolean (включающее Или) not: Boolean → Boolean (логическое отрицание или булево дополнение) Иногда добавляют и другие логические операции: эквивалентность, исключающее Или, импликация, not-and, not-or. Скалярные типы данных 5 Объекты логического типа могут представляться в памяти одним битом (если не учитывать дескриптор, описывающий тип). Поскольку один бит в памяти не имеет отдельного адреса, для представления логического объекта применяют слово или байт (иначе говоря, минимальную адресуемую область памяти). Тогда внутри такой области значения true и false могут быть обозначены двумя способами: 1) какой-то определенный бит внутри области памяти (например, бит знака числа) используется для представления булева значения (false = 0, true = 1), а остальные биты игнорируются; 2) значению false соответствуют нули во всех битах области, а любые другие комбинации считают истинным значением. В языке Java логический тип определен явно, а в языке C он моделируется целыми числами (истина соответствует любому ненулевому значению, а ложь — нулю). Если вы работаете с языком С, помните о практическом правиле обеспечения надежности. Для представления логического значения «истина» следует использовать целочисленную единицу и не стоит пытаться упаковать несколько булевых значений в одно многоразрядное целое число! Символьные типы Большая часть данных вводится и выводится из компьютера в символьной форме. Несмотря на то что затем эти данные переводятся в формат других типов, возможность обработки символьных данных в их исходной форме также весьма привлекательна. Символьный тип данных позволяет создавать объекты данных, значениями которых может быть какой-либо единственный символ. Как уже говорилось, множество возможных значений (символов) обычно является встроенным в язык перечислением. До недавнего времени наиболее распространенной системой кодировки символов считался 8-разрядный код ASCII (American Standard Code for Information Interchange — Американский стандартный код обмена информацией), в которой для 128 различных символов используется диапазон значений 0–127. Другой способ 8-разрядного кодирования задает стандарт ISO 8859-1, обеспечивающий 256 различных символов. Этот стандарт применяется в языке Ada. Впоследствии для поддержки современного информационного пространства был разработан 16-разрядный набор символов по имени Unicode. В этом наборе содержатся символы большинства естественных языков мира, а первые 128 символов идентичны символам ASCII. Первым стал использовать набор Unicode язык Java, сейчас он применяется в JavaScript, Python, Perl, C# и F#. Значения символьного типа упорядочены. Помимо операций отношения и присваивания, в символьных типах иногда применяют операции проверки принадлежности некоторого символа к какойлибо категории символов: буквам, цифрам или специальным символам. Символьный тип данных обычно прямо поддерживается аппаратурой компьютера, поскольку экземпляры этого типа используются для ввода-вывода. Если же набор символов языка отличается от набора символов для аппаратуры компьютера, то в языке предусматриваются способы взаимного преобразования. Стиль программирования в языке С. Преобразование типов Программа в этом языке оформляется как функция со стандартным именем main: # include <stdio.h> int main( ) { int c; c = getchar( ); while ( c != EOF ) { Скалярные типы данных 6 putchar(c); c = getchar( ); } } Первая строка — это директива на подключение библиотеки ввода-вывода из заголовочного файла stdio.h. Она адресована препроцессору. Цель программы — читать символы, вводимые с клавиатуры, и выводить их на экран. Для чтения используют функцию getchar( ), для вывода — функцию putchar( ). В ходе обработки выполняется неявное преобразование символа в целое число: c = getchar( ); Необходимость преобразования вызвана тем, что целочисленная константа EOF не принадлежит к символьному типу char. Обычно функция getchar( ) возвращает символьное значение, но при достижении маркера конца ввода EOF функция считывает целое число. В языке Pascal преобразование между символами и целыми числами должно задаваться явно: ‰. ord(c) — отображение символа c в число i. ‰. chr(i) — отображение числа i в символ c. Скалярные типы данных 7