Типы данных в Delphi Обучающий материал. Особенности вещественных чисел в Delphi Пример четвёртый – вычитание в цикле
Неочевидные особенности вещественных чисел Взяться за эту статью меня побудили появляющиеся время от времени вопросы на Круглом Столе, вызванные непониманием внутреннего представления вещественных чисел. Когда-то описание внутреннего представления таких чисел было неотъемлемой частью любой сколь-нибудь серьёзной книги по программированию, но сейчас у авторов появились более интересные предметы для обсуждения: COM/DCOM, ActiveX, OLE и многое другое. На вещественные числа просто не хватает места. И люди, начавшие программирование с Delphi и не имеющие опыта работы в более старых средах, часто оказываются совершенно беспомощными перед непонятным поведением программы, содержащей дробные вычисления. Надеюсь, моя статья прольёт свет на эти вопросы и сделает поведение дробей более предсказуемым.
Двоичные дроби |
Вещественные типы Delphi |
|
Бесконечные дроби |
Пример первый – «неправильное значение»
Итак, напишем такой код: 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 сообщений
Порядковые типы
Из простых типов данных порядковые - самые простые.
В этих типах информация представляется в виде отдельных элементов. Связь
между отдельными элементами и их представлением в памяти определяет естественные
отношения порядка между этими элементами. Отсюда и название порядковые.
В 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
- Таблица 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 бит, без знака |
Диапазоны значений логических целых типов (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 нет.
Над целыми данными выполняются все операции, определенные
для порядковых типов, но с ними все же удобнее работать как с числами,
а не с "нечисленными порядковыми типами". Как и "живые" числа, данные целых
типов можно складывать (+), вычитать (-) и умножать (*). Однако некоторые
операции и функции, применяемые к данным целых типов, имеют несколько иной
смысл.
Символьные типы
Смысл символьных данных очевиден, когда они выводятся
на экран или принтер. Тем не менее, определение символьного типа может
зависеть от того, что подразумевать под словом символ. Обычно символьные
типы данных задают схему взаимодействия между участками памяти разного
объема и некоторым стандартным методом кодирования/декодирования для обмена
символьной информацией. В классическом языке 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 определены два физических символьных типа и один логический.
Физические типы перечислены ниже.
Логический символьный тип именуется 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) |
Переменным типа 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 |
Целые типы представляют целые числа, т.е. числа,
дробная часть которых равна нулю. Разница между двумя неодинаковыми целыми
числами не может быть меньше единицы. Именно благодаря этому целые числа
применяются для обозначения дискретных величин, независимо от того, имеют
ли реальные объекты какое-либо отношение к числам. Действительные типы
предназначены для представления чисел, которые могут иметь дробную часть,
поэтому они полезны для представления величин, которые могут быть довольно
близкими, почти непрерывными.
Заметьте, именно почти. Несмотря на название действительные,
переменные этих типов отличаются от математических действительных чисел.
В 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 возвращает целое |
Тип данных
Программа может
оперировать данными различных типов: целыми и дробными числами, символами,
строками символов, логическими величинами.
Целый тип
Язык Delphi
поддерживает семь целых типов данных: shortint, smailint, Longint, Int64, Byte,
word и Longword, описание которых приведено в табл. 1.1.
Таблица
1.1.
Целые типы
Тип |
Диапазон |
Формат |
||
Shortint |
128-127 |
8
битов |
||
Smallint |
32 768 - 32
767 |
16
битов |
||
Longint |
2 147 483 648 - 2 147 483
647 |
32
бита |
||
Int64 |
2 63 -
2 63 - 1 |
64
бита |
||
Byte |
0-255 |
8 битов,
беззнаковый |
||
Word |
0-65
535 |
16 битов,
беззнаковый |
||
Longword |
0 - 4 294 967
295 |
32 бита,
беззнаковый |
||
Object Pascal поддерживает и наиболее универсальный целый тип - Integer, который Эквивалентен Longint.
Вещественный тип
Язык Delphi
поддерживает шесть вещественных типов: Reai48, single, Double, Extended, comp,
Currency. Типы различаются между собой диапазо-ном допустимых значений,
количеством значащих цифр и количеством байтов, необходимых для хранения данных
в памяти компьютера (табл. 1.2).
Таблица
1.2.
Вещественные (дробные) типы
Тип |
Диапазон |
Значащих
цифр |
Байтов |
||
Real48 |
2.9x
10 -39 -1.7x10 38 |
11-12 |
06 |
||
Single |
1.5 x
10 -45 -3.4х 10 38 |
7-8 |
04 |
||
Double |
5.0x10- 324
-1.7x10 308 |
15-16 |
08 |
||
Extended |
3.6x10- 4951
-1.1 х10 4932 |
19-20 |
10 |
||
Comp |
2 63 +1 -
2 63 -1 |
19-20 |
08 |
||
Currency |
922 337 203 685 477.5808
--922 337 203 685 477.5807 |
19-20 |
08 |
||
Язык Delphi поддерживает и наиболее универсальный вещественный тип - Real, который э квивалентен Double.
Символьный тип
Язык Delphi
поддерживает два символьных типа: Ansichar и Widechar:
- тип Ansichar - это
символы в кодировке ANSI, которым соответствуют числа в диапазоне от 0 до
255;
- тип widechar - это
символы в кодировке Unicode, им соответствуют числа от 0 до 65
535.
Object Pascal поддерживает и наиболее универсальный символьный тип - Char, который эквивалентен Ansichar.
Строковый тип
Язык Delphi
поддерживает три строковых типа: shortstring, Longstring
- WideString:
- тип
shortstring представляет собой статически размещаемые в памяти компьютера строки
длиной от 0 до 255 символов;
- тип
Longstring представляет собой динамически размещаемые в памяти строки, длина
которых ограничена только объемом свободной памяти;
- тип
WideString представляет собой динамически размещаемые в памяти строки, длина
которых ограничена только объемом свободной памяти. Каждый символ строки типа
WideString является Unicode-символом.
В языке Delphi
для обозначения строкового типа допускается использование идентификатора string.
Тип string эквивалентен типу shortstring.
Логический тип
Логическая величина может принимать одно из двух значений True (истина) или False (ложь). В языке Delphi логические величины относят к типу Boolean.
Переменная
Переменная -
это область памяти, в которой находятся данные, которыми оперирует программа.
Когда программа манипулирует с данными, она, фактически, оперирует содержимым
ячеек памяти, т. е. переменными.
Чтобы программа
могла обратиться к переменной (области памяти), например, для того, чтобы
получить исходные данные для расчета по формуле или сохранить результат,
переменная должна иметь имя. Имя переменной придумывает
программист.
В качестве
имени переменной можно использовать последовательность из букв латинского
алфавита, цифр и некоторых специальных символов. Первым символом в имени
переменной должна быть буква. Пробел в имени переменной использовать
нельзя.
Следует
обратить внимание на то, что компилятор языка Delphi не различает прописные и
строчные буквы в именах переменных, поэтому имена SUMMA, Summa и summa
обозначают одну и ту же переменную.
Желательно,
чтобы имя переменной было логически связано с ее назначением. Например,
переменным, предназначенным для хранения коэффициентов и корней квадратного
уравнения, которое в общем виде традиционно записывают
ах2 + bх + с =
0
вполне логично
присвоить имена а, b, с, x1 и х2. Другой пример. Если в программе есть
переменные, предназначенные для хранения суммы покупки и величины скидки, то
этим переменным можно присвоить имена
TotalSumm и Discount или
ObSumma и Skidka.
В языке Delphi
каждая переменная перед использованием должна быть объявлена. С помощью
объявления устанавливается не только факт существования переменной, но и
задается ее тип, чем указывается и диапазон допустимых значений.
В общем виде
инструкция объявления переменной выглядит так:
Имя: тип;
где:
- имя - имя
переменной;
- тип - тип данных, для
хранения которых предназначена переменная.
Пример:
а: Real; b: Real; i:
Integer;
В приведенных
примерах объявлены две переменные типа real и одна переменная типа
integer.
В тексте
программы объявление каждой переменной, как правило, помещают на отдельной
строке.
Если в
программе имеется несколько переменных, относящихся к одному типу, то имена этих
переменных можно перечислить в одной строке через запятую, а тип переменных
указать после имени последней переменной через двоеточие,
например:
а,b,с: Real; x1,x2: Real;
Константы
В языке Delphi
существует два вида констант: обычные и именованные.
Обычная константа - это целое или дробное число, строка символов или отдельный символ, логическое значение.
Числовые константы
В тексте
программы числовые константы записываются обычным образом, т. е. так же, как
числа, например, при решении математических задач. При записи дробных чисел для
разделения целой и дробных частей используется точка. Если константа
отрицательная, то непосредственно перед первой цифрой ставится знак
"минус".
Ниже приведены
примеры числовых констант:
123 0.0
524.03 0
Дробные
константы могут изображаться в виде числа с плавающей точкой. Представление в
виде числа с плавающей точкой основано на том, что любое число может быть
записано в алгебраической форме как произведение числа, меньшего 10, которое
называется мантиссой, и степени десятки, именуемой порядком.
"2.4"
"Д"
Здесь следует обратить внимание на константу " 2.4". Это именно символьная константа, т. е. строка символов, которая изображает число "две целые четыре десятых", а не число 2,4.
Логические константы
Логическое высказывание (выражение) может быть либо истинно, либо ложно. Истине соответствует константа True, значению "ложь" - константа False.
Именованная
константа
Именованная
константа - это имя (идентификатор), которое в программе используется вместо
самой константы.
Именованная
константа, как и переменная, перед использованием должна быть объявлена. В общем
виде инструкция объявления именованной константы выглядит следующим
образом:
константа =
значение;
где:
- константа - имя
константы;
- значение - значение
константы.
Именованные
константы объявляются в программе в разделе объявления констант, который
начинается словом const. Ниже приведен пример объявления именованных констант
(целой, строковой и дробной).
const
Bound = 10;
Title = "Скорость
бега";
pi =
3.1415926;
После
объявления именованной константы в программе вместо самой константы можно
использовать ее имя.
В отличие от
переменной, при объявлении константы тип явно не указывают. Тип константы
определяется ее видом, например:
- 125 - константа целого
типа;
- 0.0 - константа
вещественного типа;
- " выполнить " - строковая
константа;
- " \" - символьная константа.
В 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 бита).
Числа с плавающей точкой (дробные) представлены в 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 байт.
Текстовую информацию(переменные) представляют строковые типы данных в Delphi. Различают 3 типа:
- Shortstring - длина строки может составлять максимально 255 символов и в памяти размещается статическим методом.
- Longstring - такой тип данных ограничен лишь объемом динамической памяти.
- WideString – аналогичен тип Longstring, но каждый символ представлен в Unicode.
Синтаксис указания типа переменной в delphi довольно просто. Ряд примеров подтверждает это утверждение:
Var Stroka: longstring; -Задаем переменной “Stroka” тип longstring.
var D: double; - вещественный тип данных.
var F: shortint; - целочисленный тип.
Язык Delphi является производным от низкоуровневого языка Object Pascal, что позволяет разрабатывать с использованием совместимых компиляторов программы под Linux. Такое положение обеспечивает написание программ, разработку графических интерфейсов, приложений, способных облегчить администрирование linux, насытить систему новым и удобным функционалом.
Данные в компьютере можно рассматривать как ячейки памяти, имеющие свои имена (идентификаторы). Все данные в программе на языке Delphi должны быть описаны до их первого использования. И компилятор следит, чтобы в программе они использовались в соответствии с этим описанием, что позволяет избежать ошибок.
Любая величина в Delphi может быть постоянной или переменной. Её имя (идентификатор) может состоять из комбинации латинских букв, цифр и знака подчёркивания, и начинаться не с цифры. При этом регистр символов значения не имеет.
Место описания данных в программе - вне логических блоков begin / end . В модуле перед ключевым словом implementation есть блок описания:
var
Form1: TForm1;
Именно здесь, начиная со следующей строки, удобно объявлять глобальные переменные и константы. Как видим, одна (Form1) уже есть!
Команда объявления переменных в языке Delphi :
var имя_переменной : тип_переменной ;
Слово var - ключевое. Именем может быть любой идентификатор, если он не был описан ранее и не является одним из ключевых или зарезервированных слов языка Delphi . Если нужно описать несколько переменных одного типа, то их перечисляют, отделяя запятой:
var A, B, C : Integer;
Если несколько описаний следуют друг за другом, то ключевое слово var повторно можно не указывать:
var
A, B : Integer;
C, D : String;
Постоянную величину иначе называют константой . Конечно, в программе можно использовать числа и строки непосредственно: 3.1415 или "Это значение числа пи" , но иногда удобнее присвоить их идентификатору. Описание констант аналогично описанию переменных, но используется ключевое слово const , за именем идентификатора следует тип, затем знак равенства и его значение. Причём тип константы допускается не указывать:
const
pi=
3.1415
;
ZnakPi : String =
"Это значение числа пи"
;
К слову, константа Pi встроенная в Delphi, то есть для того чтобы использовать в Delphi число 3,1415... в расчётах, нужно просто присвоить встроенную константу Pi переменной типа Real или просто использовать непосредственно в выражениях.
Теперь пришло время узнать о типах данных, используемых в Delphi . Прежде всего это строки и числа.
Строкой
называется последовательность символов, заключённая в одиночные кавычки:
"это текстовая строка"
Если текст должен содержать сам символ кавычки, то его надо повторить дважды:
"это "" - символ одиночной кавычки"
Строка может быть и пустой, не содержащей символов. Тогда она состоит из двух идущих друг за другом без пробела кавычек.
Естественно, строка может состоять и только из одних пробелов.
Самый популярный строковый тип - String
. Строка типа String
может содержать переменное количество символов
объёмом до 2 Гбайт. Если нужно ограничить размер строки фиксированным значением, то после ключевого слова String
в квадратных скобках
указывается число, определяющее количество символов в строке: String
. Более полно работа со строками Delphi описывается далее.
Одиночный символ имеет тип Char
и записывается в виде знака в одиночных кавычках: "a"
. Есть
символы, которые на экране отобразить невозможно, например, символ конца строки (равен #13), символ переноса строки (равен #10). Такие символы записываются в виде их числового кода (в кодировке ANSI
), перед которым стоит знак #
. Например, #0
.
Наконец, существуют так называемые нуль-терминированные строки. Отсчёт символов в таких строках начинается с нуля, а
заканчивается символом с кодом 0
(#0
). Такие строки имеют тип PChar
.
Числа
бывают целые
и дробные
.
В следующей таблице перечислены стандартные типы целых чисел и соответствующие им дипазоны допустимых значений.
Дробные числа имеют дробную часть, отделяемую десятичной точкой. Допускается использование символа e
(или E
),
за которым следует число, указывающее, что левую часть нужно умножить на 10 в соответствующей степени: 5e25
- пять умножить на десять в двадцать пятой степени.
Ниже приведены стандартные типы дробных чисел и соответствующие им диапазоны допустимых значений. Для большинства типов указан диапазон
положительных значений, однако допустимым является аналогичный диапазон отрицательных значений, а также число 0
.
Следующим типом данных является логический Boolean , состоящий всего из двух значений: True (Истина) и False (Ложь). При этом True > False .
Теперь, используя компоненты, их свойства и события, вводя собственные переменные, можно конструировать программы, содержащие
вычисления. Осталось узнать, как вычисленное значение вывести на экран.
Про консольные программы я здесь не говорю
! А в нормальных оконных Windows-приложениях это значение нужно поместить в какой-нибудь компонент, имеющий
свойства Text
или Caption
. Это, например, такие компоненты как Label
и Edit
, да и сама Форма имеет свойство
Caption
, куда тоже можно выводить информацию. Однако, в Delphi информацию перед выводом, как правило, необходимо преобразовывать. Так как присвоение возможно только между переменными одного типа, то такая программа (не пытайтесь её исполнять):
var
A, B, C: Integer ;
begin
A:= 5 ;
B:= 10 ;
C:= A+B ;
Label1.Caption:= C ;
end ;
Вызовет ошибку, так как свойство Caption имеет текстовый тип String , а использованные переменные - цифровой тип Integer . Значит, нужно преобразовать значение переменной C в текстовый тип. Для этого есть встроенная функция IntToStr . Строка в нашей "программе", вызывавшая ошибку, должна выглядеть так:
Label1.Caption:= IntToStr(C) ;
Такая программа, кроме показа числа 15 , ни на что не способна. Мы должны научиться вводить в программу другие числа. Используем компоненты Edit . Введённые числа будут содержаться в свойстве Text этих компонентов. Расположим на форме два компонента Edit , один компонент Label и кнопку Button , по нажатию на которую и будем проводить вычисления. В компоненты Edit1 и Edit2 будем вводить числа для суммирования. Чтобы переместиться в редактор кода, щёлкнем дважды по нашей кнопке Button1. Мы попадём прямо в сформированную для нас средой Delphi заготовку обработчика нажатия на кнопку, непосредственно между операторами begin и end . Напишем такой простой код:
procedure
TForm1.Button1Click(Sender: TObject);
var
A, B, C: Integer;//Не забудьте описание переменных
begin
//Начало кода:
A:= Edit1.Text;
B:= Edit2.Text;
C:= A+B;
Label1.Caption:= IntToStr(C);
//Конец кода
end ;
При попытке исполнить этот код Delphi покажет ошибки по аналогичной причине - переменные A и B имеют цифровой тип Integer , а свойство Text - текстовый тип String . Исправить ошибки поможет встроенная функция StrToInt , выполняющая обратное преобразование - текст в целое число. Операторы присвоения переменным A и B должны выглядеть так:
A:= StrToInt(Edit1.Text);
B:= StrToInt(Edit2.Text);
В данном случае переменные A, B, C использовались для наглядности. Можно обойтись одной строчкой:
Label1.Caption:=IntToStr(StrToInt(Edit1.Text)+StrToInt(Edit2.Text));
Аналогично, имеются функции и для преобразования в строку и обратно действительных чисел c плавающей (Float
ing англ.) запятой, имеющих тип
Real
. Для преобразования в строку - FloatToStr
, обратно - StrToFloat
.
Часто результаты вычислений, имеющие тип Delphi Real
, имеют после запятой длинный "хвост" цифр. При выводе такой переменной в текстовом виде необходимо ограничить количество цифр после запятой. Как это можно сделать, описывается также в Уроке Delphi