О чём не пишут в книгах по Delphi
Шрифт:
□ И последний класс "камней" — ошибки, связанные с тем, что программист — человек. Здесь объединены ситуации, когда документация даёт исчерпывающее описание проблемы, аппаратура и программные средства работают безукоризненно, но все новые и новые поколения программистов совершают одни и те же ошибки, потому что ситуация кажется им слишком простой и очевидной, чтобы изучать документацию. (Заметим, что это не говорит плохо о таких программистах — человеческая психология имеет свои законы, столь же объективные, как и законы в естественных науках.) Но компьютер — лишь имитация реального мира, и нередко он не оправдывает наших интуитивных ожиданий. Пункты приведенной классификации не являются взаимоисключающими: далее мы увидим, что некоторые ситуации попадают одновременно
Данная глава посвящена детальному разбору некоторых из подобных ситуаций. Она состоит из четырех разделов. Первый раздел посвящен неочевидным проблемам при работе с целыми числами, второй — при работе с вещественными, в третьем описываются неочевидные моменты использования строк, а в четвертом собрана небольшая коллекция не связанных между собой "подводных камней", с которыми пришлось столкнуться автору книги. Всем ситуациям дано подробное объяснение, чтобы читатель не только запомнил, как делать нельзя, но и понял, почему.
Описание каждого из подводных камней будет сопровождаться примером, который можно найти на прилагаемом компакт-диске. Все примеры (за исключением специально оговоренных случаев) построены следующим образом: на главную (и единственную) форму программы помещаются компоненты
3.1. Неочевидные особенности целых чисел
Аппаратная реализация целочисленной арифметики достаточно очевидна и в большинстве случаев не приносит неожиданностей. К тому же возможные проблемы в том или ином виде упомянуты во многих книгах по Delphi, поэтому даже начинающий программист обычно готов к ним. В этом разделе мы компактно изложим эти проблемы и объясним причины их появления.
3.1.1. Аппаратное представление целых чисел
Delphi относится к языкам, в которых целые типы данных максимально приближены к аппаратной реализации целых чисел процессором. Это позволяет выполнять операции с целочисленными данными максимально быстро, но заставляет программиста учитывать аппаратные ограничения.
Такая реализация целых чисел может также приводить к проблемам при переносе языка на другую аппаратную платформу, но для Delphi это, видимо, не очень актуально.
Целые числа могут быть знаковыми и беззнаковыми. Сначала рассмотрим формат более простых беззнаковых чисел. Если у нас есть N двоичных разрядов для хранения такого числа, то мы можем представить любое число от 0 до 2N– 1. В Delphi беззнаковые целые представлены фундаментальными типами Byte (N=8, диапазон 0..255), Word (N=16, диапазон 0..65 535) и LongWord (N=32, диапазон 0..4 294 967 295).
Фундаментальными называются те типы данных, разрядность которых не зависит от аппаратной платформы. Кроме них существуют еще общие (generic) типы, разрядность которых определяется разрядностью платформы. В Delphi это типы
Знаковые числа устроены несколько сложнее. Старший из N бит, отводящихся на такое число, служит для хранения
Такая на первый взгляд не очень удобная система позволяет унифицировать операции для знаковых и беззнаковых чисел. Для примера рассмотрим число 11111110. Если его рассматривать как беззнаковое, оно равно 254, если как знаковое, то -2. Вычитая из него, например, 3, мы должны получить 251 и -5 соответственно. Как нетрудно убедиться, в беззнаковой форме 251 — это 11111011. И число -5 в знаковой форме — это тоже 11111011, т.е. результирующее состояние разрядов зависит только от начального состояния этих разрядов и вычитаемого числа и не зависит от того, знаковое или беззнаковое число представляют эти разряды. И это утверждение справедливо не только для выбранных чисел, но и вообще для любых чисел, если ни они, ни результат операции не выходят за пределы допустимого диапазона. То же самое верно для операции сложения. Поэтому в системе команд процессора нет отдельно команд знакового и беззнакового сложения и вычитания — форматы чисел таковы, что можно обойтись одной парой команд (для умножения и деления это неверно, поэтому существуют отдельно команды знакового и беззнакового умножения и деления).
Ранее мы специально оговорили, что такое удобное правило действует только до тех пор, пока аргументы и результат остаются в рамках допустимого диапазона. Рассмотрим, что произойдет, если мы выйдем за его пределы. Пусть в беззнаковой записи нам нужно из 130 вычесть 10. 130 — это 10000010, после вычитания получим 01111000 (120). Но если попытаться интерпретировать эти двоичные значения как знаковые числа, получится, что из -126 мы вычитаем 10 и получаем 120. Такими парадоксальными результатами приходится расплачиваться за унификацию операций со знаковыми и беззнаковыми числами.
Рассмотрим другой пример: из пяти (в двоичном представлении 00000101) вычесть десять (00001010). Здесь уместно вспомнить вычитание в столбик, которое изучается в школе: если в разряде уменьшаемого стоит цифра, большая, чем в соответствующем разряде вычитаемого, то из старшего разряда уменьшаемого приходится занимать единицу. То же самое и здесь: чтобы вычесть большее число из меньшего, как бы занимается единица из несуществующего девятого разряда. Это можно представить так: из числа (1)00000101 вычитается (0)00001010 и получается (0)11111011 (несуществующий девятый разряд показан в скобках: после получения результата мы про него снова забываем). Если интерпретировать полученный результат как знаковое целое, то он равен -5, т.е. именно тому, что и должно быть. Но с точки зрения беззнаковой арифметики получается, что 5-10=251.