Целое число в delphi описывается типом. Неочевидные особенности вещественных чисел

24.11.2023
Любой реально существующий тип данных, каким бы сложным он ни казался на первый взгляд, представляет собой простые составляющие, которыми процессор может манипулировать. В Object Pascal эти простые типы данных разбиты на две группы: порядковые, представляющие данные разных объемов, которыми процессор может легко манипулировать, и действительные, представляющие приближенно математические действительные числа. Разделение типов на порядковые и действительные несколько условно. Точно так же простые данные можно было бы разделить на числа и не числа. Однако в языке Object Pascal порядковые и действительные данные трактуются по-разному, и такое разделение даже полезно.

Порядковые типы
Из простых типов данных порядковые - самые простые. В этих типах информация представляется в виде отдельных элементов. Связь между отдельными элементами и их представлением в памяти определяет естественные отношения порядка между этими элементами. Отсюда и название порядковые.
В Object Pascal определены три группы порядковых типов и два типа, определяемых пользователем. Группы - это целые, символьные и булевы типы. Порядковые типы, задаваемые пользователем, - это перечисления и поддиапазоны.
Все значения любого порядкового типа образуют упорядоченную последовательность, и значение переменной порядкового типа определяется его местом в этой последовательности. За исключением переменных целых типов, значения которых могут быть как положительными, так и отрицательными, первый элемент любого порядкового типа имеет номер 0, второй элемент - номер 1 и т.д. Порядковый номер целого значения равен самому значению. Отношение порядка определяет общие для данных всех порядковых типов операции. Некоторые стандартные функции такого вида встроены в Object Pascal. Они представлены в табл. 1.1.

Для всех порядковых типов в Object Pascal существует операция задания типа для преобразования целых значений в значения соответствующих порядковых типов. Если Т - имя порядкового типа, а Х - целое выражение, то Т (X) воз-вращает значение Т с порядковым номером X.

Совет: Программисты, работающие на С и C++, для приращения или уменьшения значений переменных привыкли заметку использовать операторы "++" и "--", возвращающие следующее и предыдущее значения. Программисты Delphi всегда разбивают эти операции на более простые составляющие с помощью функций Pred, Succ. Dec и Inc.

    Таблица 1.1. Операции над порядковыми типами
Операция Описание
Low (T) Минимальное значение порядкового типа Т
High(T) Максимальное значение порядкового типа Т
Ord(X) Порядковый номер значения выражения порядкового типа. Для целого выражения - просто его значение. Для остальных порядковых типов Ord возвращает физическое представление результата выражения, трактуемое как целое число. Возвращаемое значение всегда принадлежит одному из целых типов
Pred(X) Предыдущее по порядку значение. Для целых выражений эквивалентно Х-1
Succ(X) Следующее по порядку значение. Для целых выражений эквивалентно Х+1
Dec(V) Уменьшает значение переменной на 1. Эквивалентно V:= Pred(V)
Inc(V) Увеличивает значение переменной на 1. Эквивалентно V:= Succ(V)

Целые типы
В переменных целых типов информация представляется в виде целых чисел, т.е. чисел не имеющих дробной части. Определенные в Object Pascal целые типы подразделяются на физические (фундаментальные) и логические (общие). При программировании удобнее использовать логические целые типы, которые задают объем переменных в зависимости от типа микропроцессора и операционной среды таким образом, чтобы достигалась максимальная эффективность. Физические целые типы следует применять лишь в тех случаях, когда в первую очередь важны именно диапазон значений и физический объем переменной. В Object Pascal определены следующие целые типы.
    Integer
    Shortint
    Smallint
    Longint
    Byte
    Word
    Cardinal
Обратите внимание, что один из этих целых типов назван именно целым (integer). Это может иногда приводить к путанице, но мы легко сможем ее избежать, применяя термин целый к. группе типов, a integer - к конкретному типу, определяемому в программе этим ключевым словом. Переменные физических целых типов имеют разные диапазоны значений в зависимости от того, сколько байтов памяти они занимают (что равно значению, возвращаемому функцией SizeOf для данного типа). Диапазоны значений для всех физических типов перечислены в табл. 1.2.
    Таблица 1.2. Физические целые типы
    Тип Диапазон значении Физический формат
    Shortint -128-127 8 бит, со знаком
    Smallint -32 768-32 767 16 бит, со знаком
    Longint -2 147 483 648-2 147 483 647 32 бит, со знаком
    Byte 0-255 8 бит, без знака
    Word 0-65 535 16 бит, без знака
Диапазоны значений и форматы физических целых типов не зависят от микропроцессора и операционной системы, в которых выполняется программа. Они не меняются (или, по крайней мере, не должны меняться) с изменением реализации или версии Object Pascal.
Диапазоны значений логических целых типов (Integer и Cardinal) определяются совершенно иным образом. Как видно из табл. 1.3, они никак не связаны с диапазонами соответствующих физических типов. Обратите внимание, что в Delphi по умолчанию задано 32-разрядное представление.

Таблица 1.3. Логические целые типы

    Тип Диапазон значений Физический формат
    Integer -32 768-32 767 16 бит, со знаком (SmalIInt)
    Integer -2 147 483 648-2 147 483 647 32 бит, со знаком (Longint)
    Cardinal 0-65 535 16 бит, без знака (Word)
    Cardinal 0-2 147483647 32 бит, без знака (Longint)

Совет: В С и C++ для целых значений определены типы int, short int (или просто short) и long int (или просто long). Тип int из C/C++ соответствует типу Integer из Delphi, a long из C/C++ - Longint из Delphi. Однако Shortint из C/C++ соответствует в Delphi не Shortint, a Smalltlnt. Эквивалент Shortint из Delphi в C/C++- это signed char. Тип unsigned char в C/C++ соответствует типу Byte из Delphi. В C/C++ существует еще тип unsigned long, аналога которому в Delphi нет.

Над целыми данными выполняются все операции, определенные для порядковых типов, но с ними все же удобнее работать как с числами, а не с "нечисленными порядковыми типами". Как и "живые" числа, данные целых типов можно складывать (+), вычитать (-) и умножать (*). Однако некоторые операции и функции, применяемые к данным целых типов, имеют несколько иной смысл.

Совет: Будьте внимательны при перенесении численных выражений из одного языка в другой. В Basic, например,vфункция SQR вычисляет квадратный корень. В C/C++ целое деление обозначается косой чертой (/). В Delphi косая между двумя целыми даст действительный результат с плавающей запятой.

Символьные типы
Смысл символьных данных очевиден, когда они выводятся на экран или принтер. Тем не менее, определение символьного типа может зависеть от того, что подразумевать под словом символ. Обычно символьные типы данных задают схему взаимодействия между участками памяти разного объема и некоторым стандартным методом кодирования/декодирования для обмена символьной информацией. В классическом языке Pascal не задано никакой схемы, и в конкретных реализациях применялось то, что на том же компьютере мог использовать каждый.
В реализациях языка Pascal для первых микропроцессоров была применена 7-битовая схема, названная ASCII (American Standard Code for Information Interchange - Американский стандартный код для обмена информацией). Эта схема и поныне широко распространена, но информация хранится, как правило, в 8-битовых участках памяти. Дополнительный бит удваивает число возможных представлений символов, но реализации расширенного набора символов ASCII часто бывают далеки от стандарта. В данной версии Delphi определен набор 8-битовых символов, известный как расширенный (extended) ANSI (American National Standards Institute - Американский национальный институт стандартов). Как бы то ни было, символьную схему приходится воспринимать так, как ее воспринимает операционная система. Для оконных операционных систем фирмы Microsoft это схема ANSI, включающая ограниченное число предназначенных для вывода международных знаков. В стремлении же применить более обширный набор международных знаков весь компьютерный мир переходит к 16-битовой схеме, именуемой UNICODE, в которой первые 256 знаков совпадают с символами, определенными в схеме ANSI.
Для совместимости со всеми этими представлениями в Object Pascal определены два физических символьных типа и один логический.
Физические типы перечислены ниже.

Символьные типы объемом в двойное слово (32 бит) отсутствуют.
Логический символьный тип именуется char. В классическом языке Pascal char- единственный символьный тип. В Delphi char всегда соответствует физическому типу данных AnsiChar. У американских программистов ассоциация символа с однобайтовой ячейкой памяти укоренилась за долгие годы настолько, что им зачастую просто не приходит в голову, что можно использовать другие схемы кодирования. Однако дискуссии по интернационализации программ в Internet и World Wide Web могут существенно изменить их отношение к проблеме объема символьных данных. Применяя логический тип char, следует делать реализации для других микропроцессоров и операционных систем, в которых char может определяться как WideChar. При написании программ, которые могут обрабатывать строки любого размера, для указания этого размера рекомендуется применять функцию SizeOf, не задавая ее жестко постоянной. Функция Ord (С), где С - любая переменная символьного типа, возвращает целое значение, которым символ С представлен в памяти. Совет: Процессор не различает типы char, определенные в C/C++ и Delphi. Однако функционально каждый из этих языков трактует данный тип совершенно по-разному. В C/C++ это целый тип, переменной которого можно присваивать целые значения. Переменной int можно присвоить символьное значение, а переменной char - целое. В Delphi символьные типы жестко отделены от численных. Для присвоения численному значению символьного здесь необходимо воспользоваться функцией Ord. В языке Basic один символ представляется так же, как и строка символов. Функция Chr из Delphi эквивалентна функции CHR$ из Basic. Функция Ord из Delphi, возвращающая код ANSI символьной переменной, подобна функции A3 С из Basic, аргумент которой представляет односимвольную строку.

Булевы типы
На ранней стадии обучения программисты осваивают понятие бита, два состояния которого можно использовать для записи информации о чем-либо, представляющем собой одно из двух. Бит может обозначать 0 или 1, ДА или НЕТ, ВКЛЮЧЕНО или ВЫКЛЮЧЕНО, ВЕРХ или НИЗ, СТОЯТЬ или ИДТИ. В Object Pascal информация о чем-либо, что можно представить как ИСТИНА (True) или ЛОЖЬ (False), хранится в переменных булевых типов. Всего таких типов че-тыре, и они представлены в табл. 1.4.

    Таблица 1.4. Размеры переменных булевых типов
    Тип Размер
    Boolean 1 байт
    ByteBool 1 байт
    WordBool 2 байт (объем Word)
    LongBool 4 байт (объем Longint)
По аналогии с целыми и символьными типами, подразделяющимися на физические и логические, естественно предположить, что ByteBool, WordBool и LongBool - физические типы, Boolean - логический. Но в данном случае это не совсем так. Все четыре типа различны. Для Object Pascal предпочтителен тип Boolean, остальные определены для совместимости с другими языками программирования и операционными системами.
Переменным типа Boolean можно присваивать только значения True (истина) и False (ложь). Переменные ByteBool, WordBool и LongBool могут принимать и другие порядковые значения, интерпретируемые обычно как False в случае нуля и True - при любом ненулевом значении.

Совет: Булевы типы в Delphi можно сравнить с типом LOGICAL языка FORTRAN. В Basic, С и C++ булевы типы как таковые отсутствуют. Булевы выражения в этих языках применяются точно так же, как во всех остальных, однако результаты этих выражений интерпретируются не как значения отдельного типа, а как целые числа. Как в Basic, так и в C/C++ булевы выражения дают численные результаты, интерпретируемые как False в случае 0 и True - в случае любого ненулевого значения. Это совместимо с порядковыми значениями булевых выражений в Delphi. В C/C++ простые сравнения дают результат 1 (True) или 0 (False). Это эквивалентно булевым значениям Delphi. Только результат сравнения в Delphi выводится как булевый, а не целый. В большинстве случаев типу Boolean из Delphi соответствует тип char в C/C++. В Basic зарезервированы слова TRUE (эквивалентно константе -1) и FALSE (эквивалентно константе 0). В Basic TRUE меньше FALSE, в Delphi, наоборот, False меньше True.

Перечислимые типы
Type enum type = (first value, value2, value3, last value);
Обычно данные перечислимых типов содержат дискретные значения, представляемые не числами, а именами. Тип Boolean- простейший перечислимый тип в Object Pascal. Булевы переменные могут принимать два значения, выражаемые именами True и False, а сам тип определен в Object Pascal так, как будто он объявлен следующим образом:
Type Boolean = (False, True);
С помощью типа Boolean в Object Pascal выполняются сравнения, большинство же перечислимых типов - это просто списки уникальных имен или идентификаторов, зарезервированных с конкретной целью. Например, можно создать тип MyColor (мой цвет) со значениями myRed, myGreen и myBlue (мой красный, мой зеленый, мой синий). Это делается совсем просто:
Type MyColor = (myRed, myGreen, myBlue);
В этой строке объявлены четыре новых идентификатора: MyColor, myRed, myGreen и myBlue. идентификатором MyColor обозначен порядковый тип, следовательно, в синтаксисе Object Pascal можно применять этот идентификатор везде, где разрешены перечислимые типы. Остальные три идентификатора- это значения типа MyColor. Подобно символьным и булевым типам перечислимые не являются числами, и использовать их наподобие чисел не имеет смысла. Однако перечислимые типы относятся к порядковым, так что значения любого такого типа упорядочены. Идентификаторам в списке присваиваются в качестве порядковых номеров последовательные числа. Первому имени присваивается порядковый номер 0, второму - 1 и т.д.

Совет: В С и C++ есть тип enema, аналогичный перечислимому типу Delphi. Но в этих языках можно произвольно присваивать идентификаторам постоянные значения. В Delphi же соответствие имен и их значений фиксиро-вано: первому имени присваивается значение 0, каждому последующему - на единицу больше. В С тип enum применяется лишь как средство быстрого определения набора целых постоянных. В C++ объявленные в перечислимом типе идентификаторы можно присваивать только переменным того же типа.

Поддиапазонные типы
Переменные поддиапазонного типа содержат информацию, соответствующую некоторому pаданному диапазону значений исходного типа, представляющего любой порядковый тип, кроме поддиапазонного. Синтаксис определения поддиапазонного типа имеет следующий вид:

Type subrange type = low value...high value;

Поддиапазонные переменные сохраняют все особенности исходного типа. Единственное отличие состоит в том, что переменной поддиапазонного типа можно присваивать только значения, входящие в заданный поддиапазон. Контроль за соблюдением этого условия задается командой проверки диапазона (range checking).
Необходимость явно определять поддиапазонный тип возникает нечасто, но все программисты неявно применяют эту конструкцию при определении массивов. Именно в форме поддиапазонной конструкции задается схема нумерации элементов массива.

Действительные типы
В переменных действительных типов содержатся числа, состоящие из целой и дробной частей. В Object Pascal определено шесть действительных типов. Все типы могут представлять число 0, однако они различаются пороговым (минимальным положительным) и максимальным значениями, которые могут представлять, а также точностью (количеством значащих цифр) и объемом. Действительные типы описываются в табл. 1.5.

    Таблица 1.5. Действительные типы.
    Тип Порог Максимальное значение Количество значащих цифр Объем (байт)
    Real 2.9E-39 1.7Е38 11-12 6
    Single 1.5E-45 3.4Е38 7-8 4
    Double 5.0E-324 1.7Е308 15-16 8
    Extended 3.4E-4932 1.IE4932 19-20 10
    Comp 1.0 9.2Е18 19-20 8
    Currency 0.0001 9.2Е14 19-20 8
Совет: Тип Real предназначен для совместимости с ранними версиями Delphi и Borland Pascal. Формат этого типа неудобен для семейства процессоров Intel, поэтому операции с типом Real выполняются несколько медленнее операций над остальными действительными типами.

Целые типы представляют целые числа, т.е. числа, дробная часть которых равна нулю. Разница между двумя неодинаковыми целыми числами не может быть меньше единицы. Именно благодаря этому целые числа применяются для обозначения дискретных величин, независимо от того, имеют ли реальные объекты какое-либо отношение к числам. Действительные типы предназначены для представления чисел, которые могут иметь дробную часть, поэтому они полезны для представления величин, которые могут быть довольно близкими, почти непрерывными.
Заметьте, именно почти. Несмотря на название действительные, переменные этих типов отличаются от математических действительных чисел. В Object Pascal действительный тип - это подмножество математических действительных чисел, которые можно представить в формате с плавающей запятой и фиксированным числом цифр. Для невнимательных программистов ситуация усугубляется тем, что в стандартных форматах IEEE (Institute of Electrical and Electronic Engi-neers - Институт инженеров- электриков и электронщиков), применяемых в программах Delphi и вообще в большинстве программ для Windows, возможно точное представление только чисел с фиксированным числом бит в дробной части. Удивительно, но такое простое число, как 0,1, записывается в расширенном формате IEEE с некоторой погрешностью, пусть очень небольшой. Из-за этого представление с плавающей запятой оказывается несколько неудобным для программ, в которых сохраняется и выводится фиксированное число десятичных разрядов численных значений. Это относится и к программам, работающим с ""живыми" деньгами.
Для частичного решения этой проблемы в Object Pascal определены два формата с фиксированной запятой. Тип Comp (computational - вычислительный) содержит только целые числа в диапазоне от -2 63 +1 до 2 63 -1, что примерно соответствует диапазону от -9,2х10 18 до 9,2х10 18 . При программировании операций с американской валютой разработчикам обычно приходится искать естественный способ записи денежных сумм, в котором целая часть числа определяет количество долларов, дробная - центов. Если такие значения записывать в переменные типа Comp, придется представлять их в виде целого числа центов. В этом случае следует умножать значение на 100 для обращения центов в доллары, а затем делить на 100, чтобы снова получить центы.
Этих забот можно избежать, если воспользоваться типом Currency. В этом случае задачу выбора масштаба возьмет на себя компилятор. Физически значения Currency записываются в память того же объема, что и Comp, как целые числа, однако компилятор не забывает вовремя разделить значение на 10 000 (не на 100!) для его приведения в соответствие с денежным знаком и умножить на 10 000 перед записью в память. Это обеспечивает абсолютную точность в четыре десятичных знака после запятой.
В Delphi есть модуль System, содержащий ряд процедур обработки данных действительных типов. Наиболее распространенные из них перечислены в табл. 1.6. Много полезных процедур содержится также в модулях SysUtils и Math.

    Таблица 1.6. Функции действительных типов
    Функция Возвращаемое значение
    Abs (x) Абсолютная величина х
    АгсТаn(х) Арктангенс х
    Cos (х) Косинус х (х выражается в радианах, а не в градусах)
    Ехр (х) Экспоненциальная функция от х
    Frac(x) Дробная часть х
    Int (х) Целая часть х. Несмотря на название, возвращает действительное значение (с плавающей запятой), т.е. просто устанавливает нуль в дробной части
    Ln (х) Натуральный логарифм от х
    Pi Число Пи (3.1416...)
    Round (х) Ближайшее к х целое значение. Возвращает значение целого типа. Условие "ближайшее к х" не работает, если верхнее и нижнее значения оказываются равноудаленными (например, ес-ли дробная часть точно равна 0,5). В этих случаях Delphi перекладывает решение на опера-ционную систему. Обычно процессоры Intel решают эту задачу в соответствии с рекоменда-цией IEEE округлять в сторону ближайшего четного целого числа. Иногда такой подход на-зывают "банкирским округлением"
    Sin(x) Синус х
    Sqr(x) Квадрат х, т.е. X*X
    Sqrt (х) Квадратный корень от х
    Тrunc (х) Целая часть х. В отличие от Int, возвращающей действительное значение, Trunc возвращает целое
Совет: Будьте внимательны при переносе численных выражений из одного языка в другой. В Basic функция SQR вычисляет квадратный корень, а функция Sqr из Delphi - квадрат числа. Для вычисления квадратного корня в Delphi применяется функция Sqrt.
Неочевидные особенности вещественных чисел Взяться за эту статью меня побудили появляющиеся время от времени вопросы на Круглом Столе, вызванные непониманием внутреннего представления вещественных чисел. Когда-то описание внутреннего представления таких чисел было неотъемлемой частью любой сколь-нибудь серьёзной книги по программированию, но сейчас у авторов появились более интересные предметы для обсуждения: COM/DCOM, ActiveX, OLE и многое другое. На вещественные числа просто не хватает места. И люди, начавшие программирование с Delphi и не имеющие опыта работы в более старых средах, часто оказываются совершенно беспомощными перед непонятным поведением программы, содержащей дробные вычисления. Надеюсь, моя статья прольёт свет на эти вопросы и сделает поведение дробей более предсказуемым.
Двоичные дроби
Для начала – немного математики. В школе мы проходим два вида дробей – простые и десятичные. Десятичные дроби, по сути дела, представляют собой разложение числа по степеням десяти. Так, запись 13.6704 означает число, равное 1*10 1 +3*10 0 +6*10 -1 +7*10 -2 +0*10 -3 +4*10 -4 . Но внутреннее представление всех чисел в компьютере, в том числе и вещественных – не десятичное, а двоичное. Поэтому используются двоичные дроби. Они во многом похожи на десятичные, но основание степени у них двойка. Так, число 101.1101=1*2 2 +0*2 1 +1*2 0 +1*2 -1 +1*2 -2 +0*2 -3 +1*2 -4 . То есть в десятичном представлении это число равно 5.8125, в чём нетрудно убедиться с помощью любого калькулятора. Теперь вспомним научный формат записи десятичного числа. Первым в этой записи идёт знак числа – плюс или минус. Дальше идёт так называемая мантисса – число от 1 до 10. Затем идёт экспонента – степень десяти, на которую надо умножить мантиссу, чтобы получить нужное число. Итак, уже упоминавшееся число 13.6704 запишется в этом формате как 1.36704*10 1 (или 1.36704E1 по принятым в компьютере правилам). Если записываемое число меньше единицы, экспонента будет отрицательной. Аналогичная запись существует и в двоичной системе. Так, 101.1101 запишется в виде 1.011101*10 10 (Везде использована двоичная форма записи, так что 10 10 означает 2 2). Именно такое представление используется в компьютере. Двоичная точка в такой записи не остаётся на одном месте, а сдвигается на величину, указанную в экспоненте, поэтому такие числа называются числами с плавающей точкой (floating point numbers).
Вещественные типы Delphi
В Delphi существует четыре вещественных типа: Single, Double, Extended и Real. Их общий формат одинаков: Знак Экспонента Мантисса Знак – это всегда один бит. Он равен нулю для положительных чисел и единице для отрицательных. Что же касается размеров мантиссы и экспоненты, то именно в них и заключается различие между типами. Прежде чем перейти к конкретным цифрам, рассмотрим подробнее тип Real, сделав для этого небольшой экскурс в историю. Real – это стандартный тип языка Паскаль, присутствовавший там изначально. Когда создавался Паскаль, процессоры ещё не имели встроенной поддержки вещественных чисел, поэтому все операции над этим типом сводились к операциям с целыми числами. Соответственно, размер полей в типе Real был подобран так, чтобы оптимизировать эти операции. Микропроцессор Intel 8086/88 и его улучшенные варианты – 80286 и 80386 – также не имели аппаратной поддержки вещественных чисел. Но системы на базе этих процессоров имели возможность подключения так называемого сопроцессора. Эта микросхема работала с памятью через шины основного процессора и обеспечивала аппаратную поддержку вещественных чисел. В системах средней руки гнездо сопроцессора обычно было пустым, так как это удешевляло систему (разумеется, вставить туда сопроцессор не было проблемой). Для каждого центрального процессора выпускались свои сопроцессоры, маркировавшиеся Intel 8087, 80287 и 80387 соответственно. Были даже сопроцессоры, выпускаемые другими фирмами. Они работали быстрее, чем Intel"овские, но появлялись на рынке позже. Тип вещественных чисел, поддерживаемый сопроцессорами, не совпадает с Real. Чтобы обеспечить в своих системах поддержку сопроцессорных типов, Borland вводит в Turbo Pascal типы Single, Double и Extended. Extended – это родной для сопроцессора тип, а типы Single и Double получаются из него очень простым усечением. При загрузке числа типа Single или Double во внутренний регистр сопроцессора последний конвертирует их в Extended. Напротив, при выгрузке чисел этих типов из регистра в память сопроцессор усекает их до нужного размера. Внутренние же операции всегда выполняются с данными типа Extended (впрочем, из этого правила есть исключение, на котором мы остановимся позже, после детального рассмотрения формата различных типов). Single и Double используются для экономии памяти. Ни один из них также не совпадает с типом Real. В системах с сопроцессорами новые типы обрабатываются заметно (в 2-3 раза) быстрее, чем Real (это с учётом того, что тип Real после соответствующего преобразования также обрабатывался сопроцессором; если же сравнивать обработку типа Extended на машине с сопроцессором и Real на машине без сопроцессора, то там на отдельных операциях достигалась разница примерно в 100 раз). Чтобы программы с этими типами можно было выполнять и в системах без сопроцессора, была возможность подключать к ним программный эмулятор сопроцессора. Обработка этих типов эмулятором была медленнее, чем обработка Real. Начиная с 486-ого процессора Intel берёт курс на интеграцию процессора и сопроцессора в одной микросхеме. Процент брака в микросхемах слишком велик, поэтому Intel идёт на хитрость: если у микросхемы брак только в сопроцессорной части, то на этой микросхеме прожигаются перемычки, блокирующие сопроцессор, и микросхема продаётся как процессор 80486SX, не имеющий встроенного сопроцессора (в отличие от полноценной версии, которую назвали 80486DX). Бывали и обратные ситуации, когда сопроцессор повреждений не имел, зато процессор был неработоспособен. Такие микросхемы превращали в «сопроцессор 80487». Но это уже из области экзотики, и, насколько мне известно, до России этот сопроцессор не дошёл. Процессор Pentium во всех своих вариантах имел встроенный сопроцессор. Таким образом, с приходом этого процессора тип Real стал как бы обузой, а на передний план вышли Single, Double и Extended. Чтобы свести к минимуму необходимые переделки программ, Borland ввела новую директиву компилятора: {$REALCOMPATIBILITY ON/OFF}. По умолчанию стоит OFF, что означает отсутствие полной совместимости. В этом случае тип Real в Delphi совпадает с типом Double. Если же совместимость включена, тип Real совпадает со своим прообразом из Паскаля. Существует ещё тип Real48, который всегда, вне зависимости от настроек, совпадает со старым Real. Далее в этой статье под словом “Real” я всегда буду подразумевать старый тип. Отмечу, что всё это появилось только в Delphi 4, в более ранних версиях тип Real48 отсутствовал, а тип Real был всегда старым, шестибайтным. Итак, теперь можно, наконец, добраться до размеров полей.
Тип Размер типа, байт Размер мантиссы, бит Размер экспоненты, бит
Single 4 23 8
Double 8 52 11
Extended 10 64 15
Real 6 40 7
Другие параметры вещественных типов, такие как диапазон и точность, есть в справке Delphi, в которую я рекомендую почаще заглядывать. Рассмотрим тип Single, так как он является самым коротким и, следовательно, самым простым для понимания. Остальные типы отличаются от него только количественно. В дальнейшем числа в формате Single мы будем записывать как s eeeeeeee mmmmmmmmmmmmmmmmmmmmmmm, где s означает знаковый бит, e – бит экспоненты, m – бит мантиссы. Именно в таком порядке эти биты хранятся в четырёхбайтном значении (здесь учтена перестановка байтов; напоминаю, что в процессорах Intel байты в многобайтных значениях переставляются так, что младший байт идёт первым, а старший – последним). В мантиссе хранится двоичное число. Чтобы получить истинное значение мантиссы, к ней надо мысленно добавить слева единицу с точкой (то есть, например, мантисса 1010000000000000000000 означает двоичную дробь 1.101). Таким образом, имея 23 двоичных разряда, мы записываем числа с точностью до 24-ёх двоичных разрядов. Такая запись числа называется нормализованной. Экспонента по определению всегда целое число. Но способ записи экспоненты в вещественных числах не совпадает с обычным способом записи чисел со знаком. Ноль в этом представлении записывается как 01111111. В обычном представлении это равно 127. Соответственно, 10000000 (128 в обычном представлении) означает единицу, а 01111110 (126) означает –1, и так далее (то есть из обычного беззнакового числа надо вычесть 127, и получится число, закодированное в экспоненте). Из описанных выше правил есть исключения. Так, если все биты экспоненты равны нулю (то есть там стоит число –127), то к мантиссе перед её началом надо добавлять не “1.”, а “0.” (денормализованная запись). Это позволяет увеличить диапазон вещественных чисел. Если бы этого исключения не было бы, минимально возможное положительное число типа Single было бы равно примерно 5.9*10 -39 . А так появляется возможность использовать числа до 1.4*10 -45 . Побочным эффектом этого является то, что числа, меньшие, чем 1.17*10 -38 , представляются с меньшей, чем 24 двоичных разряда, точностью. Если все биты в экспоненте равны единице, а в матрице – нулю, то мы получаем комбинацию, известную как INF (от английского Infinity – бесконечность). Эта комбинация используется тогда, когда результат вычислений превышает максимально допустимое форматом число. В зависимости от значения бита s бесконечность может быть положительной или отрицательной. Если же при такой экспоненте в мантиссе хоть один бит не равен нулю, такая комбинация называется NAN (Not A Number – не число). Попытки использования комбинаций NAN или INF приводят к ошибке времени выполнения. Для задания нуля все биты мантиссы и экспоненты должны быть равны нулю (формально это означает 0*10 -127). С учётом описанных выше правил если хотя бы один бит экспоненты не будет равен нулю (т.е. экспонента будет больше -127), запись будет считаться нормализованной, и нулевая мантисса будет рассматриваться как единица. Поэтому никакие другие комбинации значений мантиссы и экспоненты не могут дать ноль. Тип Double устроен точно так же, разница только в количестве разрядов и в том, какое значение экспоненты берётся за ноль. Итак, мы имеем 11 разрядов для экспоненты. За ноль берётся значение 1023. Несколько иначе устроен Extended. Кроме количественных отличий добавляется ещё и одно качественное: в мантиссе явно указывается первый разряд. То есть, мантисса 1010... интерпретируется как 1.01, а не как 1.101, как это было в типах Single и Float. Поэтому если 23-битная мантисса типа Single обеспечивает 24-знаковую точность, а 52-битная мантисса Double – 53-битную, то 64-битная мантисса Extended обеспечивает 64-, а не 65-битную точность. Соответственно, при денормализованной форме записи первый разряд мантиссы явно содержит 0. За ноль экспоненты принимается значение 16383. Тип Real, как уже упоминалось, стоит особняком. Во-первых, в нём используется другой порядок следования битов, а, во-вторых, не используется денормализованная форма. Я не стал детально разбираться с типом Real, потому что сейчас это нужно разве что историкам, но никак не программистам. Напомню, чуть выше я сказал, что сопроцессор всегда выполняет все операции в формате Extended, оговорившись при этом, что есть исключение, к которому я вернусь чуть позже. Если вы прочитали всё, что было изложено выше, теперь у вас достаточно знаний, чтобы понять, что это за исключение. У сопроцессора есть специальный двухбайтный регистр, называемый управляющим словом. Установка отдельных битов этого регистра диктует сопроцессору то или иное поведение. Прежде всего, это связано с тем, какие исключения может возбуждать сопроцессор. Другие биты этого регистра отвечают за то, как будут округляться числа, как сопроцессор понимает бесконечность – всё это можно при необходимости узнать из документации Intel. Нас же будут интересовать только два бита из этого слова – восьмой и девятый. Именно они определяют, как будут обрабатываться числа внутри сопроцессора. Если восьмой бит содержит единицу (так установлено по умолчанию), то десять байт внутренних регистров сопроцессора будут использоваться полностью, и мы получим «полноценный» Extended. Если же этот бит равен нулю, то всё определяется значением бита 9. Если он равен единице, то используются только 53 разряда мантиссы (остальные всегда равны нулю). Если же этот бит равен нулю – только 24 разряда мантиссы. Это увеличивает скорость вычислений, но уменьшает точность. Другими словами, точность работы сопроцессора может быть понижена до типа Double или даже Single. Но это касается только мантиссы, экспонента в любом случае будет содержать 15 бит, так что диапазон типа Extended сохраняется в любом случае. Для работы с управляющим словом сопроцессора в модуле System описана переменная Default8087CW:Word и процедура Set8087CW(CW:Word). При запуске программы в переменную Default8087CW записывается то управляющее слово, которое установила система при запуске программы. Функция Set8087CW записывает новое значение в управляющее слово. Одновременно это новое значение записывается в переменную Default8087CW . Такое поведение этой функции не всегда удобно – иногда бывает нужно сохранить старое значение переменной Default8087CW (впрочем, это несложно сделать, заведя дополнительную переменную). С другой стороны, если значение управляющего слова изменить, не используя Set8087CW (а в дальнейшем мы увидим, что такие изменения могут происходить помимо нашей воли), то с помощью функции Default8087CW просто нет возможности узнать текущее значение управляющего слова. В Delphi 6 и выше появилась функция Get8087CW , позволяющая узнать значение именно контрольного слова, а не переменной Default8087CW . В более ранних версиях единственный способ получить значение этого слова – использование ассемблера, тем более что в Delphi нет проблем с ассемблерными вставками. Установить значение управляющего слова можно с помощью команды FLDCW, прочитать – с помощью FNSTCW. Обе эти команды имеют один аргумент – переменную типа Word. Чтобы, например, установить 53-значную точность, не изменив при этом другие биты управляющего слова, надо выполнить такую последовательность команд: asm FNSTCW MyCW AND MyCW,0FCFFh OR MyCW,200h FLDCW MyCW end; Современные сопроцессоры обрабатывают числа с такой скоростью, что при обычных вычислениях вряд ли может возникнуть необходимость в ускорении за счёт точности - выигрыш будет ничтожен. Эта возможность используется, в основном, в тех случаях, когда вычисления с плавающей точкой составляют значительную часть программы, а высокая точность не имеет принципиального значения (например, в движках для 3D-игр). Однако забывать об этой особенности работы сопроцессора не стоит, потому что она может преподнести один неприятный сюрприз, о котором чуть позже.
Бесконечные дроби
Из школы мы все помним, что не каждое число может быть записано конечной десятичной дробью. Бесконечные же дроби бывают двух видов: периодичные и непериодичные. Примером непериодичной дроби является число «пи», периодичной – число 1/3 или любая другая простая дробь, не представимая в виде конечной десятичной дроби. Для тех, кто забыл математику, напомню, что периодичные дроби – это такие дроби, которые содержат бесконечно повторяющуюся последовательность цифр. Например, 1/9=0.11111…, 1/12=0.08333333…, 1/7=0.142857142857… Для записи таких чисел используют скобки – в них заключают повторяющуюся часть. Те же числа должны быть записаны так: 1/9=0.1(1), 1/12=0.08(3), 1/7=0.1(428571). Впрочем, это было небольшое отступление. Нас сейчас не интересует вопрос о периодичности или непериодичности числа, нам достаточно знать, что не все числа можно представить в виде конечной десятичной дроби. При работе с такими числами мы всегда используем не точное, а приближённое значение, поэтому ответ получается тоже приближённым. Это нужно учитывать в своих расчётах. До сих пор мы говорили о только о десятичных бесконечных дробях. Но двоичные дроби тоже могут быть бесконечными. Даже более того, любое число, выражаемое конечной двоичной дробью, может быть также выражено и десятичной конечной дробью. Но существуют числа (например, 1/5) которые выражаются конечной десятичной дробью, но не могут быть выражены конечной двоичной дробью. Это сильно усложняет жизнь программистам. Далее мы рассмотрим примеры, в которых вещественные типы ведут себя необъяснимо для человека, не знакомого с внутренним форматом их записи. К каждому такому примеру будет дано объяснение. Надеюсь, что изучение этих примеров поможет понять, какие подводные камни подстерегают программиста, использующего вещественные типы, и как эти камни обойти. Все примеры построены одинаково: на форму надо кинуть два компонента – метку (TLabel) и кнопку (TButton). Так как это только примеры, я не стал придумывать имена для этих компонентов, пусть называются Button1 и Label1. Обработчик Button1Click содержит некоторый код, результаты работы которого выводятся на форму через Label1. Таким образом, нужно запустить программу, нажать на кнопку и посмотреть, что будет написано в метке. Я буду приводить только код обработчика Button1Click, так как всё остальное тривиально. Напомню, что в Паскале допускается не ставить точку с запятой перед end"ом (за исключением end"а в описании класса) и перед until"ом. Я предпочитаю пользоваться этой возможностью, так что не надо тыкать в меня пальцем, что я забываю ставить точки с запятой.

Пример первый – «неправильное значение»

Итак, напишем такой код: var R:Single; begin R:=0.1; Label1.Caption:=FloatToStr(R) end; Что мы увидим, когда нажмём кнопку? Разумеется, не «0.1», иначе не было бы смысла писать этот пример. Мы увидим «0.100000001490116». То есть расхождение в девятой значащей цифре. Ну, из справки по Delphi мы знаем, что точность типа Single – 7-8 десятичных разрядов, так что нас, по крайней мере, никто не обманывает. В чём же причина? Просто число 0.1 не представимо в виде конечной двоичной дроби, оно равно 0.0(0011). И эта бесконечная двоичная дробь обрубается на 24-ёх знаках; мы получаем не 0.1, а некоторое приближённое число (какое именно – см. выше). А если мы присвоим переменной R не 0.1, а 0.5? Тогда мы получим на экране 0.5, потому что 0.5 представляется в виде конечной двоичной дроби. Немного поэкспериментировав с различными числами, мы заметим, что точно представляются те числа, которые выражаются в виде m/2 n , где m, n – некоторые целые числа (разумеется, n не должно превышать 24, а то нам не хватит точности типа Single). В качестве упражнения предлагаю доказать, что любое целое число, для записи которого хватает 24-ёх двоичных разрядов, может быть точно передано типом Single.

Пример второй – сравнение

Теперь изменим код так: var R:Single; begin R:=0.1; if R=0.1 then Label1.Caption:="Равно" else Label1.Caption:="Не равно" end; При нажатии кнопки мы увидим надпись «Не равно». На первый взгляд это кажется абсурдом. Действительно, мы уже знаем, что переменная R получает значение 0.100000001490116 вместо 0.1. Но ведь «0.1» в правой части равенства тоже должно преобразоваться по тем же законам, ведь в компьютере всё предопределено. Тут самое время вспомнить, что процессоры Intel работают только с 10-байтным типом Extended, поэтому и левая, и правая часть равенства сначала преобразуется в этот тип, и лишь потом производится сравнение. То корявое число, которое оказалось в переменной R вместо 0.1, хоть и выглядит страшно, но зато представляется в виде конечной двоичной дроби. Информация же о том, что это на самом деле должно означать «0.1», нигде не сохранилось. При преобразовании этого числа в Extended младшие, избыточные по сравнению с типом Single разряды мантиссы просто заполняются нулями, и мы снова получим то же самое число, только записанное в формате Extended. А «0.1» из правой части равенства преобразуется в Extended без промежуточного превращения в Single. А 0.1 – бесконечная в двоичном представлении дробь. Поэтому некоторые из младших разрядов мантиссы будут содержать единицы. Другими словами, мы получим хоть и не точное представление числа 0.1, но всё же более близкое к истине, чем 0.100000001490116. Из-за таких хитрых преобразований оказывается, что мы сравниваем два близких, но всё же не равных числа. Отсюда – закономерный результат в виде надписи «Не равно». Тут уместна аналогия с десятичными дробями. Допустим, в одном случае мы делим 1 на три с точностью до трёх знаков, и получаем 0.333. Потом мы делим 1 на три с точностью то четырёх знаков, и получаем 0.3333. Теперь мы хотим сравнить эти два числа. Для этого приводим их к точности в четыре разряда. Получается, что мы сравниваем 0.3330 и 0.3333. Очевидно, что это разные числа. Если попробовать заменить число 0.1 на 0.5, то мы получим «Равно». Думаю, вы уже знаете почему, но для полноты текста объясню. 0.5 – это конечная двоичная дробь. При прямом приведении её к типу Extended в младших разрядах оказываются нули. Точно такие же нули оказываются в этих разрядах при превращении числа 0.5 типа Single в тип Extended. Поэтому в результате мы сравниваем два числа. Это похоже, как если бы мы делили 1 на 4 с точностью до трёх и до четырёх значащих цифр. В первом случае получили бы 0.250, во втором – 0.2500. Приведя их оба к точности в четыре знака, получим сравнение 0.2500 и 0.2500. Очевидно, что эти цифры равны.

Пример третий – сравнение разных типов

Немного усложним наш пример: var R1:Single; R2:Double; begin R1:=0.1; R2:=0.1; if R1=R2 then Label1.Caption:="Равно" else Label1.Caption:="Не равно" end; Наученные горьким опытом, вы, наверное, ожидаете увидеть надпись «Не равно». Что ж, жизнь вас не разочарует, именно это вы и увидите. Тип Double точнее, чем Single (хотя его точности тоже не хватает для представления бесконечной дроби). В R2 мы получим не 0.100000001490116, а другое число, с точностью 15-16 десятичных знаков. Я не могу назвать точно это число, потому что FloatToStr воспринимает его как 0.1, так что, заменив в первом примере Single на Double, вы увидите 0.1 (только не надо обольщаться, всё равно это не 0.1, просто функция FloatToStr имеет такую особенность работы). Числа в обеих переменных приводятся к типу Extended, но при этом они не меняются и, как были не равны, так и остаются неравными. Это напоминает ситуацию, когда мы сравниваем 0.333 и 0.3333, приводя их к точности в пять знаков: числа 0.33300 и 0.33330 не равны. Мне уже неловко надоедать вам такими очевидными замечаниями, но всё-таки: если в этом примере заменить 0.1 на 0.5, мы увидим «Равно».

Пример четвёртый – вычитание в цикле

Рассмотрим ещё один пример, иллюстрирующий ситуацию, которая часто озадачивает начинающего программиста var R:Single; I:Integer; begin R:=1; for I:=1 to 10 do R:=R-0.1; Label1.Caption:=FloatToStr(R) end; Конечно, если бы в результате выполнения этого примера вы увидели бы ноль, я бы не стал тратить на него время. Но на экране появится -7.3015691270939E-8. Думаю, такой оборот дела уже никого не удивляет. Мы уже знаем про то, что число 0.1 не может быть передано точно ни в одном из вещественных типов, и про преобразования Single в Extended и обратно. При этом постоянно происходят округления, и эти округления приводят к тому, что мы получаем в результате не ноль, а «почти ноль».

Пример пятый – сюрпириз от Microsoft

Изменим в предыдущем примере тип переменной R с Single на Double. Значение, выводимое программой, станет 1.44327637948555E-16. Вполне логичный и предсказуемый результат, так как тип Double точнее, чем Single и, следовательно, все вычисления более точны, мы просто обязаны получить более точный результат. Хотя, разумеется, абсолютная точность (то есть ноль), для нас остаётся недостижимым идеалом. А теперь – вопрос на засыпку. Изменится ли результат, если мы заменим Double на более точный Extended? Ответ не такой однозначный, каким его хотелось бы видеть. В принципе, после такой замены вы должны получить -6.7762635780344E-20. Но в некоторых случаях от замены Double на Extended результат не изменится, и вы снова получите 1.44327637948555E-16. Это зависит от операционной системы. Всё дело в использовании «неполноценного» Extended. При запуске программы любая система устанавливает такое управляющее слово сопроцессора, чтобы Extended был полноценным. Но затем программа вызывает много разных функций Windows API. Какая-то (или какие-то) из этих многочисленных функций в некорректно работают с управляющим словом, меняя его значение и не восстанавливая при выходе. Такая проблема встречается, в основном, в Windows 95 и старых версиях Windows 98. Также имеются сведения о том, что управляющее слово может портиться и в Windows NT, причём эффект наблюдался не сразу после установки системы, а лишь через некоторое время, после доустановки других программ. Проблема именно в некорректности поведения системных функций; значение управляющего слова, устанавливаемое системой при запуске программы, всегда одинаково. Эта проблема известна: например, в исходных кодах VCL можно найти сохранение управляющего слова сопроцессра перед вызовом некоторых API-функций с последующим его восстановлением. Комментарии сообщают, что функция может изменить значение управляющего слова, поэтому необходимо его сохранение и восстановление. Таким образом, приходим к неутешительному выводу: к тем проблемам с вещественными числами, которые обусловлены особенностями их аппаратной реализации, добавляются ещё и баги Windows. Правда, радует то, что в последнее время эти баги встречаются крайне редко - видимо, новые версии системы ведут себя более ответственно. Тем не менее, полностью исключать такую возможность нельзя, особенно если ваша программа будет использоваться на устаревшей технике с устаревшими системами (например, в образовательных учреждениях, финансирование которых оставляет желать лучшего). Чтобы наш пример всегда выдавал правильное значение -6.7762635780344E-20, достаточно поставить в начале нашей процедуры Set8087CW(Get8087CW or $0100), и программа в любой системе будет использовать сопроцессор в режиме максимальной точности. (Если вы используете старые версии Delphi, эту строку можно заменить на Set8087CW(Default8087CW), если, конечно, значения по умолчанию прочих флагов управляющего слова вас устраивают.) Раз уж мы заговорили об управляющем слове, давайте немного поэкспериментируем с ним. Изменим первую строчку на Set8087CW(Get8087CW and $FCFF or $0200). Тем самым мы переведём сопроцессор в режим 53-ёхразрядной точности представления мантиссы. Теперь в любой системе мы увидим 1.44327637948555E-16, несмотря на использование Extended. Если же мы изменим первую строчку на Set8087CW(Get8087CW and $FCFF), то будем работать в режиме 24-ёхразрядной точности. Соответственно, в любой системе будет результат -7.3015691270939E-8. Заметим, что при загрузке в 10-байтный регистр сопроцессора числа типа Extended в режиме пониженной точности «лишние» биты не обнуляются. Только результаты математических операций представляются с пониженной точностью. Кроме того, при сравнении двух чисел также учитываются все биты, независимо от точности. Поэтому код var R:Double; // или Single begin R:=0.1; if R=0.1 then Label1.Caption:="Равно" else Label1.Caption:="Не равно" end; при выборе любой точности даст «Не равно».

Пример шестой – машинное эпсилон

Когда мы имеем дело с вычислениями с ограниченной точностью, возникает такой парадокс. Пусть, например, мы считаем с точностью до трёх значащих цифр. Прибавим к числу 1.00 число 1.00*10 -4 . Если бы всё было честно, мы получили бы 1.0001. Но у нас ограничена точность, поэтому мы вынуждены округлять до трёх значащих цифр. В результате получается 1.00. Другими словами, мы прибавляем к единице некоторое число, большее нуля, а в результате из-за ограниченной точности получаем снова единицу. Наименьшее положительное число, которое при добавлении его к единице даёт результат, не равный единице, называется машинным эпсилон. Понятие машинного эпсилон у новичков нередко путается с понятием наименьшего числа, которое может быть записано в выбранном формате. Это неправильно. Машинное эпсилон определяется только размером мантиссы, а минимально возможное число оказывается существенно меньше из-за сдвига плавающей двоичной точки с помощью экспоненты. Прежде чем искать машинное эпсилон программно, попытаемся найти его из теоретических соображений. Итак, мантисса типа Extended содержит 64 разряда. Чтобы закодировать единицу, старший бит мантиссы должен быть равен 1 (денормализованная запись), остальные биты - нулю. Очевидно, что при такой записи наименьшее из чисел, для которых вполняется условие x>1, получается, когда самый младший бит мантиссы тоже будет равен единице, т.е. x=1.00...001 (в двоичном представлении; между точкой и младшей единицей 62 нуля). Таким образом, машинное эпсилон равно x-1, т.е. 0.00...001. В более привычной десятичной форме записи это будет 2 -63 , т.е. примерно 1.084*10 -19 . Теперь напишем программу для отыскания машинного эпсилон. var R:Extended; begin R:=1; while 1+R/2>1 do R:=R/2; Label1.Caption:=FloatToStr(R) end; В результате на экране появится число 1.0842021724855E-19 в полном соответствии с теоретическими выкладками (если в вашей системе присутствует описанный выше баг с переводом процессора в режим пониженной точности, вместо этого числа вы получите 2.22044604925031E-16, т.е. 2 -52 . Чтобы этого не происходило, исправьте значение управляющего слова). А теперь заменим тип Extended на Double. Результат не изменится. На Single – опять не изменится. Но такое поведение лишь на первый взгляд может показаться странным. Давайте подробнее рассмотрим выражение 1+R/2>1. Итак, все вычисления (в том числе и сравнение) сопроцессор выполняет с данными типа Extended. Последовательность действий такова: число R загружается в регистр сопроцессора, преобразуясь при этом к типу Extended. Дальше оно делится на 2, а затем к результату прибавляется 1, и всё это в Extended, никакого обратного преобразования в Single или Double не происходит. Затем это число сравнивается с единицей. Очевидно, что результат сравнения не должен зависеть от исходного типа R. В этой статье я постарался объяснить внутреннее устройство вещественных чисел с точки зрения процессоров Intel и упомянуть некоторые проблемы, которые с ними связаны. На самом деле все проблемы сводятся к двум: во-первых, не всякое вещественное число может быть представлено точно, и, во-вторых, не всякое вещественное число, представимое в виде конечной десятичной дроби, представимо в виде конечной двоичной дроби. Вторая проблема, наверное, приносит больше неприятностей начинающим пользователям, так как она менее очевидна. Рецепты преодоления этих проблем я сознательно не излагаю, так как оптимальный вариант очень сильно зависит от конкретной задачи. Человеку же, понявшему причины появления проблем, не составит труда в каждом конкретном случае подобрать наиболее приемлемое решение. В этом, собственно, и заключается разница между программистом и ламером: первый разбирается в задаче и находит для неё решение, второй умеет только кидать на форму готовые компоненты и передирать куски чужого кода. А эту статью я писал для начинающих программистов, а не для начинающих ламеров, отсюда и такой стиль.

Огромное спасибо Елене Филипповой за помощь в поиске информации.

Обсуждение материала [ 01-07-2019 03:46 ] 77 сообщений

Численные переменные и типы данных

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

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

Вы уже знаете, как объявляются переменные целого и вещественного типа. Кроме этих переменных в системе Delphi существует еще несколько типов данных. Они отличаются друг от друга только размером памяти, необходимым для их хранения, и, соответственно, диапазоном чисел, которые можно хранить в этих переменных. Таким образом программист может выбрать необходимый численный тип, и рационально использовать память. Так что же такое типы данных.

Типы данных

Любые данные, т.е. константы, переменные, свойства, значения функций или выражения в Delphi характеризуются своими типами. Тип определяет множество допустимых значений, которые может иметь тот или иной объект, а также множество допустимых операций, которые применимы к нему. Кроме того, тип определяет также и формат внутреннего представления данных в памяти ПК.

Delphi характеризуется разветвленной структурой типов данных:

Мы рассмотрим с вами пока только простые типы.

Порядковые типы

Порядковые типы отличаются тем, что каждый из них имеет конечное количество возможных значений. Эти значения можно определенным образом упорядочить (отсюда и название типов) и, следовательно, с каждым из них можно сопоставить некоторое целое число - порядковый номер значения.

К порядковым типам относятся целые, логические, символьный, перечисляемый и тип-диапазон. К любому из них применима функция Ord(х), которая возвращает порядковый номер значения выражения Х.

Целые типы

Название

Длина,байт

Диапазон значений

Cardinal

0. .. 2 147 483 647

Byte

0...255

Shortint

128...+127

Smallint

32 768...+32 767

Word

0...65 535

Integer

Longint

2 147 483 648...+2 147 483 647

Int64

9*10 18 . ..+9*10 18

LongWord

0. . .4 294 967 295

Типы LongWord и Int64 впервые введены в версии 4, а типы Smallint и Cardinal отсутствуют в Delphi 1. Тип integer для этой версии занимает 2 байта и имеет диапазон значений от -32768 до +32767, т. е. совпадает с Smallint.

Стандартные процедуры и функции, применимые к целым типам

Обращение

Тип результата

Действие

abs (x)

Возвращает модуль x

chr(b)

Char

Возвращает символ по его коду

dec (vx [, i])

Уменьшает значение vx на i, а при отсутствии i - на 1

inc(vx[,i])

Увеличивает значение vx на i, а при отсутствии i -на 1

Hi(w)

Byte

Возвращает старший бант аргумента

Hi(I)

То же

Возвращает третий по счету байт

Lo(i)

Возвращает младший байт аргумента

Lo(w)

То же

odd(l)

Boolean

Возвращает True, если аргумент-нечетное число

Random(w)

Как у параметра

Возвращает псевдослучайное число, равномерно распределенное в диапазоне 0...(w-l)

sqr(x)

Возвращает квадрат аргумента

swap(i)

Integer

Меняет местами байты в слове

swap (w)

Word

Тоже

Также помимо известных вам арифметических операций для целых чисел существуют еще две: div и mod.

d iv - деление целых чисел с отбросом дробной части. Например:

a div b если а=10 и b=3, то результат равен 3.

mod - остаток от деления целых чисел. Например:

a mod b если а=10 и b=3, то результат равен 1

Представление целых чисел

В изображении целых чисел присутствуют лишь знак и цифры (знак + можно не указывать). Предполагается десятичная система счисления, однако можно использовать и шестнадцатеричную. В этом случае перед числом следует поставить знак $.

Тип диапазон

Все целые типы относятся к так называемым перечисляемым или порядковым типам. В любом порядковом типе можно выделить подмножество значений, в которое входят все значения исходного типа. Тип-диапазон задается в разделе объявления типов указанием минимального и максимального значений, разделенный двумя точками. Раздел объявления типов всегда начинается служебным словом Type. Например:

Type pir = [ 1..2006];

Перечисляемый тип

Перечисляемый тип задается перечислением тех значений, которые он может получать. Каждое значение именуется некоторым идентификатором и располагается в списке, обрамленном круглыми скобками, например:

Typecolors = (red, white, blue);

Символьный тип

Значениями символьного типа является множество всех символов ПК. Каждому символу приписывается целое число в диапазоне О...255. Это число служит кодом внутреннего представления символа, его возвращает функция ord.

Для кодировки в Windows используется код ANSI (назван по имени American National Standard Institute - американского института стандартизации, предложившего этот код). Первая половина символов ПК с кодами 0... 127 соответствует таблице 7.3. Вторая половина символов с кодами 128...255 меняется для различных шрифтов. Стандартные Windows-шрифты Arial Cyr, Courier New Cyr и Times New Roman для представления символов кириллицы (без букв “ё” и “Ё”) используют последние 64 кода (от 192 до 256): “А”... “Я” кодируются значениями 192..223, “а”... “я” - 224...255. Символы “Ё” и “ё” имеют соответственно коды 168 и 184.

Кодировка символов в соответствии со стандартом ANSI

Код

Символ

Код.

Символ

Код.

Символ

Код

Символ

ЗОН

ЕТХ

f

-

/

DC 4

f

Символы с кодами 0...31 относятся к служебным кодам. Если эти коды используются в символьном тексте программы, они считаются пробелами.

К типу char применимы операции отношения, а также встроенные функции:

Сhr (в) - функция типа char; преобразует выражение в типа Byte в символ и возвращает его своим значением;

UpCase(CH) - функция типа char; возвращает прописную букву, если сн - строчная латинская буква, в противном случае возвращает сам символ сн (для кириллицы возвращает исходный символ).

Вещественные типы

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

Вещественные типы

Длина, байт

Название

Количество значащих цифр

Диапазон значений

8

4

8

10

8

8

Real

Single

Double

Extended

Comp

Currency

15…16

7…8

15…16

19…20

19…20

19…20

5.0*10e-324…1.7*10e308

1.5*10e-45…3.4*10e38

5.0*10e324…1.7*10e308

3.4*10-4951…1.1*10e4932

2e63…+2e63-1

+/-922 337 203 685477,5807

В предыдущих версиях Delphi 1...3 тип Real занимал 6 байт и имел диапазон значений от2, 9*10 -39 до 1,7*10 38 . В версиях 4 и 5 этот тип эквивалентен типу Double. Если требуется (в целях совместимости) использовать 6-байтньш Real, нужно указать директиву компилятора {SREALCOMPATIBILITY ON}.

Как видно из предыдущей табл. вещественное число Delphi занимает от 4 до 10 смежных байт и имеет следующую структуру в памяти ПК:

S

е

M

Здесь s - знаковый разряд числа; е - экспоненциальная часть; содержит двоичный порядок; m - мантисса числа.

Мантисса m имеет длину от 23 (для single) до 63 (для Extended) двоичных разрядов, что и обеспечивает точность 7...8 для single и 19...20 для Extended десятичных цифр. Десятичная точка (запятая) подразумевается перед левым (старшим) разрядом мантиссы, но при действиях с числом ее положение сдвигается влево или вправо в соответствии с двоичным порядком числа, хранящимся в экспоненциальной части, поэтому действия над вещественными числами называют арифметикой с плавающей точкой (запятой).

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

Для работы с вещественными данными могут использоваться встроенные математические функции, представленные в следующей табл. в этой таблице Real означает любой вещественный тип, integer - любой целый тип.

Стандартные математические функции Delphi

Обращение

Тип параметра

Тип результата

Примечание

abs (x)

Real, Integer

Тип аргумента Real

Модуль аргумента

П =3.141592653...

ArcTan(x)

Арктангенс (значение в радианах)

cos (x)

To же <<

To же <<

Косинус, угол в радианах

exp(x)

Экспонента

frac(x)

Дробная часть числа

int(x)

Целая часть числа

ln(x)

Логарифм натуральный

Random

Псевдослучайное число, равномерно распределенное в диапазоне 0...

Random.fx)

Integer

Integer

Псевдослучайное целое число, равномерно распределенное в диапазоне 0...(х-1)

Randomize

Инициация генератора псевдослучайных чисел

sin (x)

Real

Real

Синус, угол в радианах

sqr(x)

To же

To же

Квадрат аргумента

sqrt(x)

Корень квадратный

На заметку Генератор псевдо случайных чисел представляет собой функцию которая берет некоторое целое число, называемое базовым, изменяет, его разряды По определенному алгоритму и выдает новое число результат. Одновременно с этим новое число становится базовым при следующем обращении к функций; и т. д. (Так как алгоритм процедуры не меняется: в ходе ее работы, числа называются псевдослучайными.) В системном модуле System, который автоматически доступен любой программе, базовое число хранится в переменной с именем RandSeek и всегда имеет начальное значение 0. Это означает, при последовательном обращении к Random в разных, программах (или при нескольких прогонах одной программы) будет всегда выпадать одна и та же последовательность псевдослучайных чисел.

Представление вещественных чисел

Вещественные числа содержат в своем изображении точку и/или показатель степени (символ Е или е), например:

X = -0.5;

Y = 1.34;

Z = -1 E -5;

W = 7.5 e +15;

Функции преобразования типов данных

Round (x ) - округление вещественного числа до ближайшего целого. Аргумент функции - величина вещественного типа, а результат - округленная до ближайшего целого числа величина типа Longint. Если результат выходит за диапазон значений Longint, то при выполнении программы произойдет ошибка.

Trunc (x ) - получение целой части вещественного числа. Тип результата - Longint. Если результат выходит за диапазон значений Longint, то при выполнении программы произойдет ошибка.

Тип дата-время

Тип дата-время определяется стандартным идентификатором TDateTime и предназначен для одновременного хранения и даты, и времени. Во внутреннем представлении он занимает 8 байт и подобно currency представляет собой вещественное число с фиксированной дробной частью: в целой части числа хранится дата, в дробной - время. Дата определяется как количество суток, прошедших с 30 декабря 1899 года, а время - как часть суток, прошедших с 0 часов, так что значение 36444,837 соответствует дате 11.10.1999 и времени 20:05. Количество суток может быть и отрицательным, однако значения меньшие -693594 (соответствует дате 00.00.0000 от Рождества Христова) игнорируются функциями преобразования даты к строковому типу.

Над данными типа TDateTime определены те же операции, что и над вещественными числами, а в выражениях этого типа могут участвовать константы и переменные целого и вещественного типов.

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

Подпрограммы для работы с датой и временем

Function Date: TDateTime;

Function DateToStr(D: TDateTime): String;

Function DateTimeToStr(D: TDateTime): String ;

Function FormatDateTime (Format: String ;

Value(TDateTime): String;

Function Now : TDateTime;

Function Time: TDateTime;

Function TimeToStrfT(TDateTime): String;

Возвращает текущую дату

Преобразует дату в строку символов

Преобразует дату и время в строку символов

Преобразует дату и время из параметра value в строку символов в соответствии со спецификаторами параметра Format (см. пояснения в п. 7.3.)

Возвращает текущую дату и время

Возвращает текущее время

Преобразует время в строку

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

Только обязательное назначение типа переменной обеспечит эффективную работу приложения с минимальной нагрузкой на компьютерную систему.

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

Целочисленный тип данных в Delphi представлен:

  • Shortint - занимает в памяти 8 битов и имеет числовой диапазон от -127 до 128.
  • Smallint - числовой интервал находится в пределах -32 768 - 32 767 (16 битов).
  • Longint – диапазон чисел от -2 147 483 648 до 2 147 483 647 (32 бита).
  • Int64- наибольший интервал от – 263 до 263-1 (64 бита).
  • Byte- интервал значений от 0 до 255 (8 бит).
  • Word- числовой диапазон от 0 до 65535 (16 бит).
  • Longword –интервал составляет 0 - 4 294 967 295 (32 бита).
Следует заметить, что последние 3 типа называются беззнаковыми так, как имеют в своем числовом интервале только положительные числа(нет отрицательных значений). К тому же можно использовать и тип “Integer”, который соответствует “Longint”. К тому же следует знать, что значения в типах имеют строгий порядок. Такое положение позволяет использовать по отношению к значениям различные процедуры и функции. Не относится к вещественному типу данных в Delphi(не упорядочен).

Числа с плавающей точкой (дробные) представлены в delphi вещественным типом. Вещественный тип данных делится на 6 типов, которые отличаются числовым диапазоном, количеством значащих цифр и занимаемой памятью.

  • Single- число может находиться в интервале 1.5 x 1045-3.4х 1038.Объем занимаемой памяти 4 байта.
  • Real48 - числовой диапазон 2.9x-39-1.7x1038 (6 байт).
  • Double - интервал составляет 5.0x10-324 -1.7x10308 (8 байт).
  • Extended - 3.6x10-4951 -1.1 х104932 (10 байт).
  • Comp - диапазон чисел 263+1 – 263-1, занимаемая память 8 байт.
Currency – этот вещественный тип данных называют еще денежным. С его помощью осуществляется реализация различных приложений финансовой тематики. Имеет 53 бита точности и поддержку 4 десятичных мест.

Текстовую информацию(переменные) представляют строковые типы данных в Delphi. Различают 3 типа:

  • Shortstring - длина строки может составлять максимально 255 символов и в памяти размещается статическим методом.
  • Longstring - такой тип данных ограничен лишь объемом динамической памяти.
  • WideString – аналогичен тип Longstring, но каждый символ представлен в Unicode.
В delphi строковые типы данных допускается обозначать типом string, который аналогичен shortstring.

Синтаксис указания типа переменной в delphi довольно просто. Ряд примеров подтверждает это утверждение:

Var Stroka: longstring; -Задаем переменной “Stroka” тип longstring. var D: double; - вещественный тип данных. var F: shortint; - целочисленный тип.
Язык Delphi является производным от низкоуровневого языка Object Pascal, что позволяет разрабатывать с использованием совместимых компиляторов программы под Linux. Такое положение обеспечивает написание программ, разработку графических интерфейсов, приложений, способных облегчить администрирование linux, насытить систему новым и удобным функционалом.

К встроенным типам данных в языке Delphi относятся типы целые, действительные, символы, строки, указатели, булевы.

Порядковые типы. Порядковыми (ordinal) типами называются те, в которых значения упорядочены, и для каждого из них можно указать предшествующее и последующее значения.

Структурные типы. К структурным типам относятся множества, массивы, записи, файлы, классы, интерфейсы.

Целые типы данных. В переменных целых типов информация представляется в виде целых чисел, т.е. чисел не имеющих дробной части.

Таблица 1 Операции над порядковыми типами

Операция

Описание

Минимальное значение порядкового типа Т

Максимальное значение порядкового типа Т

Порядковый номер значения выражения порядкового типа. Для целого выражения - просто его значение. Для остальных порядковых типов Ord возвращает физическое представление результата выражения, трактуемое как целое число. Возвращаемое значение всегда принадлежит одному из целых типов

Предыдущее по порядку значение. Для целых выражений эквивалентно Х-1

Следующее по порядку значение. Для целых выражений эквивалентно Х+1

Уменьшает значение переменной на 1. Эквивалентно V:= Pred(V)

Увеличивает значение переменной на 1. Эквивалентно V:= Succ(V)

Таблица 2

Диапазон значений

2147483648 -- 2147483647

8 битов, беззнаковый

16 битов, беззнаковый

32 бита, беззнаковый

Также существует такой тип, как Integer, который эквивалентен типу LongInt. Его диапазон от -2147483648 до 21474836478. Занимает 4 байта в пямяти. Основными являются Integer и Cardinal, так что в большинстве случаев желательно использовать эти типы.

Над целыми данными выполняются все операции, определенные для порядковых типов. Операции над целыми типами:

Таблица 3

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

Таблица 4

Основным, обеспечивающим максимальную производительность, является тип Real, который в настоящий момент эквивалентен типу Double.

Таблица 5 Функции действительных типов

Возвращаемое значение

Абсолютная величина х

Арктангенс х

Косинус х (х выражается в радианах, а не в градусах)

Экспоненциальная функция от х

Дробная часть х

Целая часть х. Несмотря на название, возвращает действительное значение (с плавающей запятой), т.е. просто устанавливает нуль в дробной части

Натуральный логарифм от х

Число Пи (3.1416...)

Ближайшее к х целое значение. Возвращает значение целого типа. Условие "ближайшее к х" не работает, если верхнее и нижнее значения оказываются равноудаленными (например, если дробная часть точно равна 0,5). В этих случаях Delphi перекладывает решение на операционную систему. Обычно процессоры Intel решают эту задачу в соответствии с рекомендацией IEEE округлять в сторону ближайшего четного целого числа. Иногда такой подход называют "банкирским округлением"

Квадрат х, т.е. X*X

Квадратный корень от х

Целая часть х. В отличие от Int, возвращающей

Символьные типы данных. Символьные типы предназначены для хранения одного символа.

Таблица 6

Булевы типы данных. Переменные булевых типов данных представляют логические значения, например, true (истина) и false (ложь).

Таблица 7 Размеры переменных булевых типов

2 байт (объем Word)

4 байт (объем Longint)

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

Объявление массива

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

Имя: [нижний_индекс..верхний_индекс] of тип

где: имя -- имя массива;

array -- зарезервированное слово языка Delphi, обозначающее, что объявляемое имя является именем массива;

нижний_индекс и верхний_индекс -- целые константы, определяющие диапазон изменения индекса элементов массива и, неявно, количество элементов (размер) массива;

тип -- тип элементов массива.