Следует отметить, что в Delphi не обязательно напрямую использовать WinSock API, чтобы работать с сокетами, т.к. VCL содержит компоненты для этого. Прежде всего это TServerSocket и TClientSocket, использующие асинхронные сокеты, основанные на оконных сообщениях. Начиная с Delphi 7, к ним добавились компоненты TTCPServer, TTCPClient и TUDPSocket, использующие блокирующие или неблокирующие сокеты. Кроме того, с Delphi поставляется библиотека Indy, которая тоже содержит компоненты для работы с сокетами. Но практика показывает, что освоить эти компоненты без знания особенностей WinSock API очень сложно, так что даже если вы никогда не будете вызывать функции WinSock API явно, а ограничитесь компонентами. информация, изложенная в этой главе, вам все равно пригодится.
Примечание
Начиная с Delphi 7, компоненты TClientSocket и TServerSocket в поставке присутствуют, но в палитру компонентов по умолчанию не устанавливаются. Чтобы работать с этими компонентами, их нужно установить самостоятельно. Для этого в меню Component следует выбрать пункт Install Packages, в открывшемся диалоговом окне нажать кнопку Add и добавить нужный пакет. Этот пакет находится в папке $(DELPHI)/Bin, а название его зависит от версии Delphi. Для Delphi 7 это будет dclsockets70.bpl, для BDS 2005 — dclsockets90.bpl, для BDS 2006, Turbo Delphi и Delphi 2007 — dclsockets100.bpl.
Настоятельно рекомендуем прочитать книгу [3]. Несмотря на незначительные недостатки, она является наиболее полным из изданных на данный момент на русском языке руководством по использованию сокетов в Windows. В крайнем случае рекомендуем хотя бы посмотреть ее содержание в Интернете, чтобы представлять себе, сколько различных возможностей WinSock API остались здесь не упомянутыми.
Данная глава посвящена "подводным камням"— ситуациям, в которых ошибки или неожиданное поведение программы наиболее вероятны. Другими словами, подводные камни — это то, на чем раз за разом спотыкаются многие начинающие программисты. Не претендуя на описание всех подобных случаев, мы, тем не менее, разберем несколько достаточно характерных примеров. Более полный список можно посмотреть в разделе "Подводные камни" сайта "Королевство Delphi" (см. приложение 1).
Подводные камни можно классифицировать по причинам, вызывающим повышенную вероятность ошибок, следующим образом:
□ Аппаратные "камни" — проблемы, вызванные некорректной работой аппаратуры. Наиболее известная из таких проблем — неправильная работа операции деления в блоке FPU первых версий процессора Pentium (в настройках компилятора Delphi можно увидеть опцию Pentium-safe FDIV — при ее включении генерируется более медленный, но правильно работающий на (очень) старых процессорах код для вещественного деления). Но подобные проблемы, к счастью, редки, поэтому мы не будем рассматривать их здесь.
□ Системные "камни" — проблемы, вызванные тем, что системные функции, которые использует программа, работают не так, как описано в документации, или же у этих функций обнаруживаются особенности работы, вообще не упомянутые в документации.
□ "Камни" компилятора — проблемы, вызванные ошибками компиляторе Delphi.
□ "Камни" VCL — ошибки, содержащиеся в библиотеке VCL. Ранее мы уже упоминали о некоторых из них. Далее мы рассмотрим еще несколько имеющихся в ней ошибок.
□ И последний класс "камней" — ошибки, связанные с тем, что программист — человек. Здесь объединены ситуации, когда документация даёт исчерпывающее описание проблемы, аппаратура и программные средства работают безукоризненно, но все новые и новые поколения программистов совершают одни и те же ошибки, потому что ситуация кажется им слишком простой и очевидной, чтобы изучать документацию. (Заметим, что это не говорит плохо о таких программистах — человеческая психология имеет свои законы, столь же объективные, как и законы в естественных науках.) Но компьютер — лишь имитация реального мира, и нередко он не оправдывает наших интуитивных ожиданий. Пункты приведенной классификации не являются взаимоисключающими: далее мы увидим, что некоторые ситуации попадают одновременно под несколько пунктов.
Данная глава посвящена детальному разбору некоторых из подобных ситуаций. Она состоит из четырех разделов. Первый раздел посвящен неочевидным проблемам при работе с целыми числами, второй — при работе с вещественными, в третьем описываются неочевидные моменты использования строк, а в четвертом собрана небольшая коллекция не связанных между собой "подводных камней", с которыми пришлось столкнуться автору книги. Всем ситуациям дано подробное объяснение, чтобы читатель не только запомнил, как делать нельзя, но и понял, почему.
Описание каждого из подводных камней будет сопровождаться примером, который можно найти на прилагаемом компакт-диске. Все примеры (за исключением специально оговоренных случаев) построены следующим образом: на главную (и единственную) форму программы помещаются компоненты Button1: TButton и Label1: TLabel. Событию Button1.OnClick назначается код. демонстрирующий проблему, результат работы кода отображается в Label1.Caption. В тексте книги приводится только код этого обработчика.
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 это типы Integer (знаковое целое) и Cardinal (беззнаковое целое. В имеющейся реализации они имеют 32 разряда, но при переходе на 64-разрядные компиляторы следует ожидать что эти типы также станут 64-разрядными. В частности, в 16-разрядном Turbo Pascal тип Integer был 16-разрядным а типа Cardinal там не было).
Знаковые числа устроены несколько сложнее. Старший из N бит, отводящихся на такое число, служит для хранения знака (этот бит называется знаковым). Если этот бит равен нулю, число считается положительным, а оставшиеся N-1 разрядов используются для хранения числа так же, как в случае беззнакового целого (эти разряды мы будем называть беззнаковой частью). В этом случае знаковое число ничем не отличается от беззнакового. Отрицательные значения кодируются несколько сложнее. Когда все разряды (включая знаковый бит) равны единице, это соответствует значению -1. Рассмотрим это на примере однобайтного знакового числа. Числу -1 в данном случае соответствует комбинация 1 1111111 (знаковый бит мы будем отделять от остальных пробелом), т.е. беззнаковая часть числа содержит максимально возможное значение -127. Числу -2 соответствует комбинация 1 1111110, т.е. в беззнаковой части содержится 126. В общем случае отрицательное число, хранящееся в N разрядах равно X-2N-1, где X — положительное число, хранящееся в беззнаковой части. Таким образом, N разрядов позволяют представить знаковое целое в диапазоне -2N-1..2N-1-1, причем значению -2N-1 соответствует ситуация, когда все биты, кроме знакового равны нулю.
Такая на первый взгляд не очень удобная система позволяет унифицировать операции для знаковых и беззнаковых чисел. Для примера рассмотрим число 11111110. Если его рассматривать как беззнаковое, оно равно 254, если как знаковое, то -2. Вычитая из него, например, 3, мы должны получить 251 и -5 соответственно. Как нетрудно убедиться, в беззнаковой форме 251 — это 11111011. И число -5 в знаковой форме — это тоже 11111011, т.е. результирующее состояние разрядов зависит только от начального состояния этих разрядов и вычитаемого числа и не зависит от того, знаковое или беззнаковое число представляют эти разряды. И это утверждение справедливо не только для выбранных чисел, но и вообще для любых чисел, если ни они, ни результат операции не выходят за пределы допустимого диапазона. То же самое верно для операции сложения. Поэтому в системе команд процессора нет отдельно команд знакового и беззнакового сложения и вычитания — форматы чисел таковы, что можно обойтись одной парой команд (для умножения и деления это неверно, поэтому существуют отдельно команды знакового и беззнакового умножения и деления).