Author: Хэзфилд Р. Кирби Л.
Tags: общие вопросы горного дела программирование искусство программирования язык программирования c
ISBN: 966=7393-82-8
Year: 2001
Text
Ричард Хэзфилд, Лоуренс Кирби Искусство программирования на С Реализация на С самых современных, быстрых и эффективных алгоритмов! Фундаментальные алгоритмы, структуры данных и примеры приложений Подробно рассмотрены издательство DiaSoft Фундаментальные концепции программирования Широкий спектр реальных бизнес-приложений Полное описание последнего ANSI- стандарта языка Управление прои зводител ьностью приложений и теория оптимизации Стандарты программирования Абстрактные структуры данных Реализация алгоритмов сортировки, поиска и обработки древовидных структур Создание инструментальных средств для разработчиков Организация параллельной обработки данных Концепции и реализация CGI-приложений SAMS PUBLISHING и книга-почтой интернет-магазин
Richard Heathfield Lawrence Kirby etal. SAMS UNLEASHED
Искусство программирования на С Фундаментальные алгоритмы, структуры данных и примеры приложений Ричард Хэзфилд, Лоуренс Кирби и др. ЭНЦИКЛОПЕДИЯ -----
ББК 33.114 Хэзфилд Ричард, Кирби Лоуренс и др. X 82 Искусство программирования на С. Фундаментальные алгоритмы, структуры данных и примеры приложений. Энциклопедия программиста: Пер. с англ./Ричард Хэзфилд, Лоуренс Кирби и др. — К.: Издательство «ДиаСофт», 2001. — 736 с. ISBN 966-7393-82-8 Эта книга посвящена искусству программирования на одном из самых популярных и мощных языков. Значительное внимание уделено таким актуальным вопросам, как обработка данных, работа с битами и байтами, отладка программ, управление памятью, моделирование, рекурсия, а также темам, не часто встре- чающимся в литературе, но играющим важную роль при разработке коммерческих приложений. Описание самых разнообразных алгоритмов в книге гармонично сочетается с вопросами их практической реализации. Приведено большое количество фрагментов кода и целых программ, которые непосредственно можно применять в сложных приложениях. Книга предназначена для опытных программистов, а также читателей, имеющих базовые знания по язы- ку С и желающих повысить свою квалификацию до профессионального уровня. Научное издание Хэзфилд Ричард, Кирби Лоуренс и др. ИСКУССТВО ПРОГРАММИРОВАНИЯ НА С. ФУНДАМЕНТАЛЬНЫЕ АЛГОРИТМЫ, СТРУКТУРЫ ДАННЫХ И ПРИМЕРЫ ПРИЛОЖЕНИЙ. ЭНЦИКЛОПЕДИЯ ПРОГРАММИСТА Заведующий редакцией С. Н. Козлов Главный редактор Ю.Н.Артеменко Научный редактор к.т.н. С.Л.Попов Верстка А,А. Коптюх Главный дизайнер О.А.Шадрин Н/К Сдано в набор 10.03.2001. Подписано в печать 02.04.2001. Формат 84x108/16. Бумага типографская. Гарнитура Таймс. Печать офсетная. Усл.печ.л. 55,20. Усл.кр.отт. 55,20 Тираж 3000 экз. Заказ № 265 Издательство «ДиаСофт», 04053, Киев-53, а/я 100, тел./факс (044) 212-1254. e-mail: books@diasoft.kiev.ua, http://www.diasoft.kiev.ua. Отпечатано с готовых диапозитивов в ордена Трудового Красного Знамени ФГУП «Техническая книга» Министерства РФ по делам печати, телерадиовещамия и средств массовых коммуникаций 198005, Санкт-Петербург, Измайловский пр., 29. Authorized translation from the English language edition published by Sams Copyright © 2000 All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from the Publisher. Russian language edition published by DiaSoft Publishing. Copyright © 2001 Лицензия предоставлена издательством Sams Corporation. Все права зарезервированы, включая право на полное или частичное воспроизведение в какой бы то ни было форме. ISBN 966-7393-82-8 (рус.) © Перевод на русский язык. Издательство «ДиаСофт», 2001 ISBN 0-672-31896-2 (англ.) © Sams Corporation, 2000 © Оформление. Издательство «ДиаСофт», 2001 Свидетельство о регистрации 24729912 от 11.03.97. Гигиеническое заключение № 77.99.6.953.П.439.2.99 от 04.02.1999
Оглавление Часть I. Пересмотренный язык С...........................................25 ГЛАВА 1. Энциклопедия С-программиста.....................................26 Для кого предназначена эта книга.........................................27 Какие знания вы должны иметь.............................................27 Как наиболее эффективно использовать эту книгу...........................28 Для чего нужна еще одна книга по С.......................................29 Почему используется стандарт ANSI С......................................29 Какие платформы охватывает эта книга.....................................30 Об исходном коде.........................................................30 Качество программного кода ................................................30 Как организована эта книга.................................................30 Проблемы авторских прав..................................................32 Общество С-программистов.................................................32 Резюме...................................................................34 ГЛАВА 2. Войны стандартов программирования: причины и пути перемирия................................................................35 Стили расстановки фигурных скобок........................................36 Стиль 1TBS............................................................36 Стиль Алмена..........................................................36 Стиль Whitesmith......................................................36 Стиль GNU.............................................................36 Использование пробелов...................................................37 Отступы...............................................................37 Табуляторы и мэйнфреймовские компиляторы..............................38 Пробелы вокруг символов...............................................38 Косметические исправления кода........................................39 Структурное программирование........................................... 39 Оператор goto.........................................................39 Оператор break........................................................39 Оператор continue.....................................................40 Цикл while(l).........................................................40 Оператор return.......................................................41 Инициализация............................................................42 Множественные определения в одной строке..............................42 Инициализация в определении...........................................42 Статические и глобальные объекты.........................................43 Проблемы с повторным использованием...................................44 Имена идентификаторов....................................................45 Длина.................................................................45 Ясность...............................................................45 Зарезервированные имена...............................................46 Префиксы: трансильванская ересь........................................ 47 Именование переменных.................................................47 Именование констант...................................................48 Именование типов....................................................... 48
Искусство программирования на С 6 Именование макросов....................................................48 Именование функций.....................................................49 Написание полезных комментариев..........................................49 Стили размещения комментариев..........................................49 Когда комментарии излишни..............................................50 Описание выполняемых кодом действий.................................. 50 Комментирование сложного кода..........................................51 Комментирование закрывающих скобок.....................................51 Не "закомментируйте" код...............................................51 Общие ошибки и недоразумения.............................................52 Ересь void main ().....................................................52 Количество аргументов функции main ()..................................53 Целочисленная математика против математики плавающей точки.............53 Обработка сигналов...................................................... 54 Передача по значению...................................................54 Проблемы с включающим ИЛИ..............................................55 Оператор sizeof................А.......................................56 Ключевое слово return................................................ 56 Объявления, определения и прототипы......................................56 Объявления.............................................................56 Определения............................................................57 Прототипы..............................................................57 Важность переносимости программ..........................................57 Поразрядное дополнение до единицы и до двух............................57 Определение неопределенного поведения..................................57 Подавление буферов ввода............................................58 Функции is () и to ()...............................................58 Аннулирование строки................................................59 Размеры целых типов....................................................59 Заполнение структур.................................................. 60 Макросы..................................................................60 С против C+ +............................................................61 Резюме...................................................................61 ГЛАВА 3. Оптимизация................................................... 62 Привлекательность оптимизации............................................62 Не тратьте времени даром...............................................62 С — быстродействующий язык.............................................62 Важность измерений.....................................................63 Размышления об эффективности.............................................63 Смысл перспективы......................................................63 Иерархия памяти и ее влияние на эффективность.......................64 Виртуальная память..................................................65 О-нотация..............................................................65 Профайлинг...............................................................67 Плоский профиль..........*........................................... 68 Графические профили....................................................69 Другие методы профайлинга..............................................70 Алгоритмические стратегии оптимизации....................................71 Реализация стратегий оптимизации, или микрооптимизация.................72 Инлайнинг (встраивание).............................................72 Разворачивание цикла................................................73
Оглавление 7 Сжатие цикла.........................................................73 Обращение цикла.....................................................74 Редукция строгости выражения........................................74 Инвариантные вычисления цикла.......................................74 Группировка выражений...............................................74 Удаление хвоста рекурсии............................................75 Табличный поиск.....................................................76 Строковые операции..................................................76 Переменные..........................................................77 Использование плавающей точки.......................................77 Локальность ссылок.................................................. 77 Адресация главной строки............................................ 78 Некоторые дополнительные стратегии оптимизации.........................78 Кооперативная и параллельная оптимизация.................................79 Некоторые новые величины эффективности.................................79 Клиент-сервер и параллелизм............................................80 Неявный параллелизм....................................................80 Пользовательский интерфейс для медлительных алгоритмов.................81 Когда оптимизация не нужна...............................................82 Корень зла — в преждевременной оптимизации.............................82 Легче сделать корректную программу быстрой, чем быструю программу корректной.............................................................83 Не выполняйте оптимизацию без учета переносимости......................83 Позвольте делать это компилятору.......................................83 Резюме...................................................................83 ГЛАВА 4. Работа с датами.................................................84 Функции даты и времени...................................................84 Основные функции даты и времени........................................ 84 Полезные формы представления даты и времени............................ 85 Простое строковое форматирование времени...............................86 Сложное форматирование времени.........................................86 Считывание даты и времени и манипулирование ими........................88 Определение времени выполнения программ................................90 Ошибка тысячелетия: проблемы, связанные с датами.........................91 Полезные мелочи..........................................................92 Високосные годы........................................................92 Стандарт ISO 8601: форматы дат и номера недель.........................92 Резюме...................................................................94 ГЛАВА 5. Игры с битами и байтами.........................................95 Представление величин в С................................................95 Представление целых величин..............................................96 Использование величин без знака в битовых операциях......................97 Битовый едвиг............................................................98 Другие битовые операторы.................................................98 Битовые массивы” (битовые карты)........................................ 100 Подсчет битов.......................................................... 102 Зеркальное отражение битов..............................................105 Битовые поля........................................................... 106 Переносимость программ................................................. 107 Резюме................................................................. 110
Искусство программирования на С 8 ГЛАВА 6. Хранение и извлечение данных....................................111 Цели и приложения....................................................... 111 Текстовые и двоичные форматы........................................ 112 Вопросы структурирования............................................. 112 Вопросы разработки форматов........................................... ИЗ Основные методы......................................................... 114 Текстовые файлы...................................................... 114 Двоичные файлы....................................................... 119 Общие форматы........................................................... 126 Файлы, в качестве разделителей использующие символы пробела или табуляции... 126 Величины, разделенные запятыми....................................... 128 Файлы .ini........................................................... 130 Усовершенствованные методы.............................................. 130 Обновление записей................................................... 130 Индексная адресация.................................................. 132 Смежные вопросы......................................................... 133 Резюме.................................................................. 134 ГЛАВА 7. Исправление кода программ.......................................135 Обратимся к диагностике................................................. 135 Как нужно использовать предупреждения................................ 136 Метод "сверху-в низ"................................................. 138 Ваш друг Lint........................................................ 139 Поиск и исправление распространенных ошибок............................. 140 Ошибки завышения (или занижения) значения на единицу................. 142 Ошибки нарушения границы............................................. 142 Бесконечные циклы.................................................... 143 Присваивание вместо сравнения........................................ 143 Переполнение буфера.................................................. 144 Нарушение границ массива............................................. 144 Недостающие аргументы................................................ 145 Указатели............................................................ 145 Программные средства отладки,........................................... 149 Коммерческие отладчики............................................... 149 Макросы трассировки................................................. 149 Планируем успех, предвидя неудачи..................................... 156 Отладочный код....................................................... 156 Использование операторов контроля.................................... 157 Использование операторов контроля в процессе компиляции.............. 159 Процесс отладки......................................................... 159 Что должно происходить............................................... 159 Что происходит на самом деле......................................... 159 Определение места поломки............................................ 159 Типы ошибок...........................................................160 Ошибки Бора (Bohr Bugs) ............................................. 161 Ошибки Гейзенберга (Heisenbugs)...................................... 161 Ошибка Мандельброта (Mandelbugs)..................................... 162 Ошибка Шредингера (Schroedinbugs).................................... 163 Кошмар программиста................................................... 163 Резюме.................................................................. 165
Оглавление 9 ГЛАВА 8. Управление памятью..............................................166 Управление памятью...............................;;..................... 166 Общие ошибки использования памяти....................................... 166 Использование незаказанной памяти.................................... 166 Рассматриваем функцию gets() как вредную............................. 167 Ошибки при сохранении адреса......................................... 168 Отсутствие проверки возвращаемых значений............................ 168 Отсутствие запасного указателя для realloc........................... 168 Использование памяти, которая не выделена............................ 169 Восстановление памяти операционной системой...........................170 Сбои функции выделения памяти........................................... 171 Проанализируйте требования памяти.................................... 171 Используйте меньше памяти.......................................... 171 Используйте буфер фиксированной длины................................ 172 Выделяйте резерв на случай аварийной ситуации........................ 172 Использование дискового пространства................................. 173 Успешное выполнение функции calloc...................................... 173 Занимаемся контролем.................................................... 174 Заголовочный файл для осуществления контроля над памятью............. 175 Реализация библиотеки контроля памяти................................ 178 Перепроектирование функции realloc................................. 179 Возвращение к предыдущему проекту realloc............................ 179 Проектирование хеш-ключей............................................ 180 Сообщения о текущих заказах памяти................................... 181 Резюме.................................................................. 184 ГЛАВА 9. Моделирование и Контроллеры.....................................185 Общее представление о конечных автоматах................................ 185 Пример выключателя света................................................ 186 Превращение конечного автомата в код.................................... 186 Применение моделирования и контроллеров................................. 187 Важные аспекты безопасности в использовании контроллеров................ 187 Обычные ошибки программирования...................................... 187 Рассказ о Therac-25.................................................. 188 Мораль этой истории.................................................. 188 Моделирование простого компьютера....................................... 188 Память................................................................188 Регистры............................................................. 189 Построение С-кода.................................................... 190 Считывание ассемблерного кода........................................ 190 Выполнение программы................................................. 192 Пошаговое выполнение команд.......................................... 192 Проверка содержимого регистров....................................... 192 Проверка содержимого памяти.......................................... 193 Дисассемблирование содержимого памяти................................ 193 Собираем все вместе.................................................. 193 Резюме................................................................. 193 ГЛАВА 10. Рекурсия.......................................................194 Что такое рекурсия...................................................... 194 Факториалы: традиционный пример...................................... 195 Числа Фибоначчи: другой традиционный пример.......................... 196
Искусство программирования на С 10 Как использовать рекурсию............................................. 196 Пример использования: двоичный поиск................................ 197 Как не следует использовать рекурсию..................................199 О рекурсии подробнее.....................................................199 Еще один пример: Евклидов алгоритм....................................200 "Хвостовая рекурсия”................................................ 200 Непрямая рекурсия................................................... 203 Рекурсия и время существования данных.................................203 Практическое применение рекурсии.........................................206 Резюме................................................................. 207 Часть II. Организация данных.............................................208 ГЛАВА 11. Простые абстрактные структуры данных...........................209 Массивы..................................................................210 "Обычные" массивы.....................................................210 Массивы переменного размера...........................................213 Массивы указателей....................................................222 Массивы указателей на функции.........................................222 Массивы разнородных объектов..........................................225 Односвязные списки.......................................................227 Добавление элементов..................................................229 Обновление элемента списка............................................231 Отыскание данных......................................................231 Удаление элемента.....................................................232 Уничтожение списка....................................................232 Проход по списку.............................-........................232 Тестовый драйвер......................................................233 Двусвязные списки........................................................235 Создание двусвязного списка...........................................235 Вставка элемента в начало списка......................................237 Вставка элемента в конец списка.......................................237 Вставка элемента внутрь списка........................................237 Обновление и поиск данных.............................................237 Извлечение элемента из списка.........................................237 Удаление элемента списка..............................................238 Как поменять элементы местами.........................................238 Подсчет числа элементов...............................................238 Вырезание и вставка...................................................239 Уничтожение всего списка..............................................239 Проход по списку......................................................239 Тестовый драйвер......................................................239 Циклические списки.......................................................242 Заглавный узел........................................................242 Вставка первого узла..................................................243 Вставка последующих узлов.............................................243 Восстановление и обновление данных....................................244 Вращение списка.......................................................244 Удаление узлов .......................................................244 Проход по списку.........................................................245 Решение задачи Иосифа.................................................245 Стеки....................................................................247
Оглавление 11 Создание стека........................................................248 Занесение элементов...................................................249 Извлечение элементов..................................................249 Обращение к первому элементу..........................................250 Подсчет числа элементов в стеке.......................................250 Сохранение природы стека............................................ 250 Пример стека: программа проверки синтаксиса HTML......................250 Очереди..................................................................253 Создание очереди......................................................254 Добавление элементов в очередь........................................254 Удаление элементов из очереди.........................................254 Сохранение природы очереди............................................255 Прикладная библиотека работы с очередями.........................*....255 Очереди по приоритету....................................................255 Создание очереди по приоритету........................................256 Добавление элементов в очередь по приоритету..........................256 Удаление элементов из очереди по приоритету...........................257 Приложение с очередью по приоритету...................................259 Двусторонние очереди (деки)-».......................................... 260 Добавление элемента в начало дека........У............................261 Добавление элементов в конец дека.....................................261 Удаление элементов из начала дека.....................................261 Удаление элементов из конца дека......................................262 Сохранение природы дека...............................................262 Дек автомобилей..................................................... 262 Разнородные структуры и объектные деревья................................265 Резюме................................................................. 265 ГЛАВА 12. Поиск по двоичному дереву.................................. 266 Анализ алгоритмов поиска.................................................266 Двоичный поиск........................................................266 Добавление и удаление элементов из отсортированной таблицы............267 Двоичные деревья.........................................................267 Структура для узла в языке С..........................................268 Структура в языке С для дерева........................................268 Операции..............................................................268 Создание...........................................................268 Поиск..............................................................269 Вставка............................................................269 Удаление...........................................................270 Упорядоченное рекурсивное прохождение..............................272 Упорядоченное итеративное прохождение..............................273 Уничтожение дерева.................................................274 Счетчик.......................................................... 274 Анализ................................................................275 Формирование двоичного дерева случайным образом....................275 Формирование двоичного дерева не случайным образом.................275 Передовые методы............................:.........................776 Указатели на родительские узлы.....................................276 Ссылки.............................................................277 Сбалансированные двоичные деревья........................................278
Искусство программирования на С 12 AVL-дерево...................................................... 278 Дерево red-black.................................................. 284 Сравнение AVL-дерева и дерева red-black............................. 289 Резюме.......................................................... 290 ГЛАВА 13. Методы быстрой сортировки................................. 291 Классификация данных.............................................. 291 Типы алгоритмов сортировки........................................ 292 Когда выполнять сортировку.......................................... 292 Основы сортировки................................................. 292 Алгоритмы порядка О(п2)........................................... 295 Алгоритмы, которых следует избегать...................................295 Сортировка методом выбора..........................................295 Пузырьковая сортировка....................................... 295 Эффективные методы сортировки....................................... 296 Сортировка методом вставок............................................296 Сортировка методом Шелла.......................................... 300 Быстрая сортировка.......................................... 300 Сортировка методом Синглтона..........................................302 Пирамидальная сортировка..............................................303 Сортировка подсчетом................................................ 304 Восходящая поразрядная сортировка................................... 306 Нисходящая поразрядная сортировка............................... 307 Методы сортировки слиянием............................................ 310 Сортировка двоичным слиянием..........................................310 Сортировка слиянием с делением на секции.......................... 310 Сортировка слиянием по принципу "нарезания печенья"...................312 Резюме ........................................................... 330 ГЛАВА 14. Деревья.......................................................331 Структура данных типа дерево............................................331 В каких случаях применяются деревья...................................332 Использование деревьев.......................................... 332 Создание и разрушение............................................. 333 . Выделение битов................................................. 333 Поиск............................................................... 335 Вставка..................................................... 335 Удаление........................................................ 338 Возможные модификации структуры типа дерево...........................340 Сравнение деревьев с двоичными древовидными структурами и хеш-таблицами.............................................342 Резюме............................................................ 342 ГЛАВА 15. Разреженная матрица...........................................343 Что такое разреженная матрица......................................... 343 Это не просто массив другого типа................................. 343 Метод физического хранения.........................................344 Метод доступа........................................ 345 Скорость доступа................................................. 345 Заголовочные списки............................................... 345 Узлы матрицы.................................................... 346 Размерность................................................... 346
Оглавление 13 Почему данные могут быть разреженными....................................347 Что такое разреженные данные...........................................347 Сложность кода...................................................... 348 Экономия памяти.......................................................348 Когда используется разреженная матрица.......................... 349 Типы задач.......................................................... 349 Направленный граф..................................................349 Сгруппированные данные........................................... 350 Многосвязные узлы.............................................. 350 Целесообразные операции...............................................351 Обход графов.......................................................351 Параллельные операции над узлами................................. 351 Мономиальные матрицы...............................................351 Простые операции над матрицами.....................................351 Операции, которых следует избегать....................................351 Сложные операции над матрицами.....................................351 Обращение матрицы..................................................352 Поиск "нулевых" значений......................................... 352 Построение разреженной матрицы........................................ 352 Построение заголовочных списков.......................................353 Добавление элементов заголовочного списка.............................357 Удаление элемента заголовочного списка................................358 Построение списка узлов матрицы.......................................359 Вставка узлов матрицы.................................................361 Удаление узлов матрицы................................................369 Прохождение разреженной матрицы....................................... 370 Перемещение по строкам................................................370 Перемещение по столбцам...............................................370 Перемещение по упорядоченным парам....................................371 Резюме..................................................................371 ГЛАВА 16. Работа с графами ....................................... 372 Определение графов.................................................. 372 Ориентированные графы.................................................373 Неориентированный граф................................................373 Связанные графы.......................................................373 Насыщенные и разреженные графы........................................374 Циклические и ациклические графы........................................374 Представление графов....................................................374 Матрицы смежности.....................................................375 Списки смежных вершин.........................................’.......375 Топологическая сортировка.............................................377 Паросочетание.........................................................379 Глубинный поиск..................................................... 383 Нерекурсивный глубинный поиск....................................... 386 Сильно связанные компоненты...........................................387 Определение путей и контуров Эйлера...................................388 Задача коммивояжера...................................................389 Алгоритмы поиска кратчайшего пути.......................................391 Алгоритм Дийкстры: единственный источник..............................392 Алгоритм Беллмана-Форда: централизованные ребра с отрицательными затратами.............................................................397
Искусство программирования на С 14 Алгоритм Флойда: все пары вершин......................................398 Минимальные остовные деревья............................................402 Алгоритм Крускала.....................................................402 Алгоритм Прима........................................................405 Оптимизация: последнее замечание.................................... 408 Резюме........................................................ 409 Часть III. Дополнительные тематические разделы..........................410 ГЛАВА 17. Матричная арифметика........................................ 411 Что такое матрица................................................... 411 Простые операции матричной арифметики............................... 412 Реализация матричной структуры в языке С.......................... 413 Арифметика указателей и индексы массива...............................413 Что такое начальный индекс.......................................... 414 Структура MATRIX_T матричного типа....................................414 Обработка ошибок......................................................416 Другие принципы проектирования матричной библиотеки...................416 Инициализация матриц из массивов...................................... 417 Извлечение матрицы из файла........................................... 418 Запись объектов MATRIX_T в stdout или в другой файл.....................420 Полная реализация суммирования и транспонирования..................... 421 Сложные концепции матриц........................................ 422 Сложная математика матриц........................................... 422 Умножение матриц.................................................. 423 Единичная матрица............................................. 423 Определители и нормы Евклида........................................ 424 Обратная матрица.................................................... 428 Решение линейных уравнений..............................................430 Распространение ошибок при матричных вычислениях.................... 431 Исправление ошибок при решении систем линейных уравнений..............432 Дальнейшие направления работы..................................... 434 Другие подходы........................................................ 435 Резюме................................................................ 435 ГЛАВА 18. Обработка цифровых сигналов...................................436 Применение С к явлениям реального мира............................ 436 Сжатие данных...........................................................437 Типы сжатия....................................................... 437 Наиболее часто используемые алгоритмы сжатия........................ 438 Факсимильное изображение........................................ 438 Преобразование изображения в закодированный поток................... 439 Программа сжатия Т.4: encode.c.................................. 443 Функция main()................................................. 444 Функция EncodePage()...............................................445 Функция EncodeLineQ................................................445 Функция CountPixelRun()............................................445 Функция OutputCodeWordQ........................................ 448 Преобразование закодированного потока в изображение................. 451 Программа распаковки Т.4 decode.c .................................. 452 Функция main()............................................... 453 Функция DecodePageQ........................................ 453
15 Функция GetPixelRunQ...............................................456 Функция T4Compare()................................................458 Функция GetNext Bit()............................................ 458 Функция OutputPixels()........................................... 459 Генерирование символов................................................459 Выявление и исправление ошибок..........................................461 Борьба с хаосом.......................................................461 Избыточность................................................ 461 Четность.......................................................... 462 Контрольные суммы................................................... 464 Контроль, осуществляемый с помощью избыточного циклического кода... 465 Функция CRCCCITTO..................................................466 Функция main()................................................... 467 Исправление ошибок: коды Гамминга.....................................467 Технология RAID..................................................... 469 Алгоритмы управления PID............................................ 471 Программа pidloop.c............................................. 474 Структура PID__PARAMS............................................ 474 Переменные области видимости файла.................................475 Функция ComputePIDQ............................................ 475 Функция GetCurrentPV()........................................... 475 Пропорциональное управление: коэффициент Р............................478 Интегральное управление: коэффициент 1................................478 Производный член: коэффициент D.......................................479 Объединение всех коэффициентов, PID............................... 479 Профили......................................................... 479 Упреждающие поправки................................................ 480 Прочие модификации и эксперименты с PID...............................481 Специализированные процессоры цифровых сигналов....................... 481 Инструкция МАС.................................................... 482 Суммирование квадратов................................................482 Индексация массивов...................................................482 Кольцевые буферы....................... ?..........................482 Арифметические операции с насыщением................................ 482 Циклы переполнения нуля............................................. 482 Множество адресных областей и шин данных..............................482 Большие внутренние области памяти............................... 482 Почему в настольном компьютере не используется DSP....................482 Расширения DSP в процессорах общего назначения........................483 Резюме..................................................................483 ГЛАВА 19. Синтаксический анализ и вычисление выражений..................485 Постановка задачи................................................. 485 Формулировка решения................................................ 486 Синтаксическая нотация................................................486 О понимании синтаксиса................................................487 Правила синтаксиса.................................................. 487 Правила старшинства и порядок вычисления..............................492 Польская нотация........................................................492 Краткая историческая справка..........................................492 Примеры обычной и обратной польской нотации...........................493
Искусство программирования на С 16 Преимущества польской нотации........................................493 Преобразование из обычной формы в форму обратной польской нотации......493 Упрощенный вариант................................................. 494 Полная версия.................................................... 521 Преобразование из формы польской нотации в оценочную функцию Описание процесса обычным языком........................... Пример кода для некоторого процесса...................... Синтаксический анализ ошибочного ввода..................... Резюме................................................................... ГЛАВА 20. Создание программных инструментальных средств.................. Характеристики хороших программных средств............................... Интерфейс пользователя ................................................ Ошибкоустойчивость.................................................... Гибкость............................................................... Простота............................................................... Переносимость.......................................................... Библиотеки кодов......................................................... Фильтры и инструменты общего применения.................................. Преобразование символов табуляции в символы пробела.................... Преобразование из EBCDIC в ASCII....................................... Просмотр выходных данных............................................ Простой разделитель строк.............................................. Поиск и замена байтов.................................................. Шестнадцатиричные данные............................................... Автоматическое генерирование тестовых данных............................. Написание вспомогательных приложений для тестирования.................. Разработка тестовых данных............................................. Написание кода генерирования тестовых данных........................... Генераторы кода.......................................................... Квины.................................................................. Когда есть смысл генерировать код...................................... Разработка грамматики и синтаксиса входных данных...................... Простой синтаксический анализатор...................................... Образец вывода генератора кодов........................................ Управление сопровождением.............................................. Простой генератор кода................................................. Резюме................................................................... ГЛАВА 21. Генетические алгоритмы ........................................ Понятие генетического алгоритма.......................................... Генетическая структура................................................... Операции мутации......................................................... Рекомбинация............................................................. Щдинственные родители.................................................. Скрещивание генов...................................................... Скрещивание последовательностей генов.................................. Объединение генов...................................................... Отбор.................................................................... Вскрытие “черного ящика"................................................ Оптимизация.............................................................. Параллелизм............................................................
Оглавление Поиск эффективных генетических операторов........................565 Разделение проблемной области....................................566 Отклонение произошедших неудач................................. 566 Исправление ошибок...............................................566 Неполные решения и изменение ограничений на ресурсы..............566 Использование метафор............................................567 Пример приложения: генетический биржевой консультант...............567 Анализ проблемы..................................................567 Генетическая структура....................................... 567 Определение пригодности..........................................568 Процесс выбора...................................................568 Инициализация популяции..........................................569 Стратегия мутации................................................570 Стратегия рекомбинации...........................................570 Результаты и умозаключения.......................................572 Резюме.............................................................572 ГЛАВА 22. Межплатформенная разработка: программирование коммуникационных средств...........................................573 Планирование переноса..............................................573 Абстрактные слои...................................................574 Сокеты.............................................................574 Сокеты, использующие TCP под управлением UNIX....................576 Сокеты, использующие TCP под управлением Windows.................581 Межплатформенное приложение........................................585 Использование препроцессора в качестве инструмента переноса......585 Написание абстрактного слоя......................................589 Резюме.............................................................603 ГЛАВА 23. Написание CGI-приложений на С ...........................604 Что такое CGI......................................................604 Основы CGI.........................................................605 Методы ввода данных в HTML.........................................605 Тег <FORM>.......................................................605 Тег <INPUT>......................................................606 Теги <SELECT> и <OPTION>.........................................607 Тег <TEXTAREA>...................................................608 Пример HTML-формы входных данных.................................608 Среда CGI..........................................................608 AUTH_TYPE........................................................609 CONTENT-LENGTH...................................................609 . CONTENT_TYPE...................................................609 GATEWAY-INTERFACE................................................609 HTTP—ACCEPT......................................................609 HTTP—CONNECTION..................................................609 HTTP_HOST.................................................... 609 HTTP—REFERER................................................... 609 HTTP-USER-AGENT..................................................609 PATH-INFO........................................................610 PATH-TRANSLATED..................................................610 QUERY-STRING.....................................................610 2 Зак . 265
Искусство ^ми.^^нияна С REMOTE_ADDR............................................................610 REMOTE_HOST............................................................610 REMOTE_INDENT........................................................ 610 REMOTE_USER............................................................610 REQUEST-METHOD....................................................... 610 SCRIPT_NAME............................................................610 SERVER_NAME............................................................611 SERVER-PORT........................................................ 611 SERVER-PROTOCOL........................................................611 SERVER-SOFTWARE.................................................... 611 Получение данных....................................................... 611 Синтаксический разбор строки запроса................................... 612 Пример приложения: поиск прототипа функции......................... 616 Вопросы безопасности.....................................................619 Резюме................................................................. 621 ГЛАВА 24. Арифметика произвольной точности .......................... 622 Распространение ошибок при выполнении арифметических операций............623 Ошибки сложения и вычитания............................................623 Ошибка умножения..................................................... 623 Ошибка деления....................................................... 624 Выводы по размерам ошибок.......................................... 624 Переполнение, потеря значимости и деление на нуль...................... 624 Порядок выполнения операций..............................................625 Размеры целых чисел......................................................625 Точность операций над целыми числами................................. 626 Преобразования типов long и int........................................627 Размеры чисел с плавающей точкой.........................................627 Представление чисел с плавающей точкой.................................627 Максимальная точность................................................ 628 Нормализация...........................................................629 Точность операций над числами с плавающей точкой.......................629 Определение точности чисел с плавающей точкой..........................629 Никогда не делайте предположений относительно среды выполнения кода......632 Отрицательные числа не всегда представляются в виде дополнения до двух.633 Представления "от старшего к младшему" и "от младшего к старшему"......633 Примеры высокой и низкой точности...................................... 633 Различные методы представления сверхвысокой точности.....................634 Строки целых чисел................................................... 634 Пример кода для действий над строкой символов............................635 Сложение........................................................... 635 Умножение............................................................ 635 Дробные числа.......................................................... 638 Положение десятичной точки при умножении...............................638 Положение десятичной точки при делении.................................638 Общие арифметические функции...........................................639 Использование стандартных типов..........................................641 Выбор представления сверхточных чисел....................................647 Вычисление числа е с точностью до десяти тысяч знаков после точки........647 Резюме................................................................. 651
19 ГЛАВА 25. Обработка естественных языков..................................652 Синтаксис и семантика естественных языков.................... ..........652 Синтаксис естественных языков..........................................653 Семантика естественных языков.......................................... 658 Сложности ввода естественного языка......................................658 Команды обработки......................................................658 Игры...................................................................659 Машинный перевод.......................................................659 Эквивалентность...................................................... 660 Искусственный интеллект.............................................. 661 Установление авторства............................................... 661 Электронные игрушки.................................................. 662 Распознавание речи................................................. 663 Морфология........................................................... 664 Распознавание текста..........................,...................... 667 Синтаксический анализ естественно-языкового ввода......................667 Сложности вывода естественных языков................................... 668 Вывод звука.......................................................... 669 Вывод текста...........................................................669 Резюме...................................................................669 ГЛАВА 26. Шифрование.....................................................670 Оценка рисков нарушения безопасности................................. 670 Выявите угрозу....................................................... 670 Оцените ресурсы взломщика..............................................670 Определите, какова может быть цель взлома..............................671 Определитесь с оружием и тактикой взломщиков......................... 671 Узнайте своих пользователей.......................................... 671 Сосредоточьтесь на самом слабом звене..................................671 Почему не следует создавать новых алгоритмов шифрования..................672 Что плохого в новых шифрах.............................................672 Держите внешние границы на виду.................................... 672 Сложность — это не безопасность.................................... 673 Выбор алгоритма шифрования............................................. 673 Шифрование с одним ключом..............................................674 Шифрование с двумя ключами.............................................676 Одностороннее хеширование............................................ 676 Реализация шифрования....................................................678 Режимы работы.................................................... 679 Порядок байтов....................................................... 681 Обеспечение аутентичности открытого ключа............................ 684 Слишком высокая скорость шифрования.................................. 684 Слишком высокий уровень безопасности...................................685 Просто добавьте "соли”.................................................685 Постоянство памяти.....................................................686 В поисках помех.................................................... 686 Чем меньше, тем лучше..................................................687 Не оставляйте подсказок................................................687 Маскировка информации................................................ 688 Последние штрихи.......................................................688 Резюме................................................................. 689
Искусство программирования на С 20 ГЛАВА 27. Встроенные системы........................................ 690 Программирование встроенных систем на языке С.........................691 Подготовка к работе.................................................691 Запуск программы встроенной системы............................... 692 Базовые средства ввода/вывода..................................... 693 Печать сообщений и отладка программ.................................695 С-программирование встроенных систем и стандарт ANSI С................695 RTOS — операционные системы реального времени.........................696 Система RTEMS как типичный пример RTOS................................697 Резюме.............................................................. 698 ГЛАВА 28. Параллельная обработка.................................... 699 Основные концепции.................................................. 699 Компьютеры и параллельная обработка................................. 700 Приоритетная многозадачность........................................700 Кооперативная многозадачность................................... 701 Межпроцессная коммуникация........................................ 701 Потоки, многопоточность и синхронизация.............................701 Параллельная обработка в С.......................................... 702 Многозадачность в С............................................. 703 Многопоточность в С............................................... 703 Межпроцессная коммуникация в С.................................. 705 Синхронизация доступа к данным в С..................................707 Резюме................................................................709 ГЛАВА 29. Взгляд в будущее: С99.......................................710 Новое в стандарте С99............................................. 710 Новые типы.................................................... 710 Базовые типы................................................... 711 Производные типы............................................... 712 Новые свойства стандартной библиотеки............................. 712 Новые (и расширенные) ограничения...................................713 Новый синтаксис.....................................................713 Набор символов...................................................713 Синтаксис препроцессора...................................... 714 Объявления.......................................................714 Инициализация.................................................. 715 Другие новые свойства...............................................716 Потерянные возможности............................................ 716 Изменения по сравнению с С89..........................................716 Резюме................................................................717 Часть IV. Приложения..................................................719 ПРИЛОЖЕНИЕ А. Общедоступная лицензия GNU..............................720 ПРИЛОЖЕНИЕ В. Избранная библиография..................................724 ПРЕДМЕТНЫЙ УКАЗАТЕЛЬ..................................................726
Об авторах Ричард Хэзфилд (Richard Heathfield) — владелец и раз- работчик программного обеспечения компании Eton Computer Systems Ltd. Когда писал коды для страховых компаний, лечебных учреждений, банков и аэрофлота, он всегда задавался вопросом, сможет ли он когда-либо найти клиента, используя лишь систему C++ Builder. В настоящее время он живет недалеко от Поттерспури в городке Нортамптоншир. (Великобритания) вместе с женой, тремя прекрасными детьми, сетью из пяти ком- пьютеров и множеством С-компиляторов, с которыми он знает, что делать. Он прекратил избегать main() не- задолго перед тем как стать регулярным контрибьюте- ром в группе новостей comp.iang.c; это факт, которому он будет вечно признателен. Ричард тратит много време- ни на поисковом Web-сайте Yahoo! Chat, занимаясь про- блемой CDreamer, и всегда рад использовать такую воз- можность, чтобы сказать “Привет!” всем, кто посещает на этом сайте раздел Programming room по программирова- нию. В настоящее время Ричард интересуется программи- рованием на TCP/IP, автоматизированным генерирова- нием кода на С, стандартом С99 и кофе. Когда он не программирует, не консультирует или не пишет о про- граммировании (или не пьет кофе), он включает свои усилители и очень громко играет на электрогитаре. Ри- чарда можно найти на Web-сайте по адресу http:// users.powernet.co.uk/eton. Лоуренс Кирби (Lawrence Kirby), дипломированный специалист Кэмбриджского университета (Англия), был программистом на С в течение более 10 лет. Он является соучредителем компании Netactive Systems Ltd — ком- пании, специализирующейся на коммуникациях и на информации финансового рынка. Ранее он в течение не- скольких лет работал в компании British Telecom в от- деле сетевой службы и информационных систем. Ему нравится быть постоянным контрибьютером групп но- востей Usenet, таких как comp.lang.c; он находит исклю- чительно полезным как получать опыт в оказании по- мощи другим в решении проблем языка С, так и делиться своим собственным опытом с другими С-про- граммистами (от новичков до членов Комитета по стан- дартам С). Дэн Корбит (Dann Coibit) имеет степень по числен- ному анализу Вашингтонского университета и был ком- пьютерным программистом начиная с 1976 г. Частич- но список его связанных с компьютерами работ можно найти в internet по адресу ftp://38.168.214.175/pub/ CA.P.%20Biographies/DannCorbit.htm#_Toc441048186. Дэн программирует на С с середины 1980-х гг. и обучался этому языку программирования в Олимпий- ском колледже в Бремертоне (штат Вашингтон). Он писал журнальные статьи для Dr. Dobb ’s Journal и был соавтором научных трудов по китам Balaena mysticetus для организации North Slope Borough, ко- торая представила его к степени IWC. Он является основателем и менеджером международного проекта по шахматному анализу (Chess Analysis Project), в котором используются компьютерные программы для анализа позиций на шахматной доске и каталогизи- рования результатов в базе данных. Web-сайт часто задаваемых вопросов (FAQ) по проекту Chess Analysis Project можно найти по адресу ftp://38.168.214.175/ pub/Chess%20Analysis%20Project%20FAQ.htm. Как постоянного контрибьютера группы новостей news:comp.lang.c Дэна всегда можно найти здесь и по- лучить от него дружественный ответ в его немного экс- центричном стиле. Длительное время интересуясь ма- тематикой и другими науками, он занял первое место на научной ярмарке Mid Columbia Science Fair и был финалистом на междунарожной научной ярмарке International Science Fair со своим проектом An Analysis of a Solid Solution. Чед Диксон (Chad Dixon), в течение последних семи лет работающий сетевым инженером, имеет дело с мно- жеством широкомасштабных технологий в области ком- пьютерной обработки. Попутно он начал реализовать решения для своих клиентов, обращающихся на Web- сайт Client/Server Application development (разработка приложений клиент/сервер), и решения по базам дан- ных для систем сетевого мониторинга предприятий. Работа с такими предприятиями и организациями как NASA, Lyondell, Olin, Arco Chemical и British Petroleum дала ему значительный опыт в разработке межплатфор- менных сетевых приложений. Чед живет в Хьюстоне (штат Техас) со своей любимой женой Вероникой и Снежком — их любимым кроликом. С Чедом можно связаться по адресу http://www.loopy.org. Вильям Фишбурн (William Fichbume) получил уче- ную степень бакалавра в области компьютерных наук в 1989 г. в Мэрилендском университете. В настоящее вре- мя он старший консультант корпорации Alphatech Corporation. Вильям обучался также в колледже Prince George’s Community College в Мэриленде и в настоящее время является членом ассоциации ACCU пользовате- лей С и C++. Вильям занимается программированием на С еще со времени своего обучения и работал в таких разнообраз- ных областях как спутниковая коммуникация, форми- рование изображений и системы финансовых расчетов. Он работал на самых разнообразных UNIX-платформах и был одним из первых участников переноса операци- онной системы Microsoft Widows NT на многопроцес- сорные платформы. Вильям писал статьи для популяр- ных компьютерных журналов по программированию таких как Dr. Dobb’s Journal. Кроме того, он включился
Искусство программирования на С 22 в добровольную деятельность по такому проекту как Project Gutenberg (проект Гутенберг по сохранению книг, которые более не защищены авторским правом, и бесплатному распространению их в электронном виде). Вы можете встретить Вильяма в Internet по адресу wfishbume@atcnet.com и посетить Web-страничку про- екта Гутенберг по адресу http://www.gutenberg.net. Скот Флурер (Scott Fluhrer) программирует на С с 1979 г. В настоящее время работает инженером по раз- работкам компании Cisco Systems. Другие его интере- сы включают научный вымысел (фикцию) и криптог- рафию. Сэм Хоббс (Sam Hobbs) занялся проблемами компь- ютерной обработки данных еще в 1966 г., и начинал с 25-битовых последовательных компьютеров со 196 сло- вами основной памяти барабанного типа, на которых языком программирования был только машинный язык. С тех пор он работал на Фортране, Бейсике, Фокале, С, C++ и с различными сценарными языками (в основном AWK и Perl), а также с некоторыми языками баз дан- ных. С языком С Сэм работает начиная с 1985 г. Свой первоначальный опыт он получил в электроэнергетичес- кой индустрии (в основном в области коммерческой атомной энергетики), исполняя технические, контроли- рующие и управленческие роли. В настоящее время работает в качестве инженерного и управленческого кон- сультанта в индустрии атомной энергетики. Ян Келли (Ian KeLly) является консультантом по компьютерным системам, который впервые стал про- граммировать профессионально в 1963 г. Он прошел путь от оказания технической помощи в программиро- вании главной электрогенерирующей компании Вели- кобритании, разработки и управления написанием опе- рационной системы (ADAM), конструирования число- вых компиляторов — до написания материалов по переносимости программного обеспечения для Европей- ской Комиссии. Ян имеет специальную степень бака- лавра по математике, и это служит оправданием тому, что последние 35 лет он пытался сделать что-нибудь в области математики — но безуспешно. Вместо этого он досконально изучил (путем практического применения) более чем двадцать компьютерных языков в самых раз- нообразных средах. В настоящее время он консульти- рует по вопросам о том, как могут быть улучшены и расширены системы, в которых используются смешан- ные языки. Будучи убежденным, что французский язык так же прост как и Кобол, в конце 1970-х гг. он заинтересовал- ся проблемами машинного перевода (МТ), написал две книги и несколько статей на эту тему и был (в течение более 15 лет) главным специалистом группы по МТ. Ян живет в Суррее (Англия) недалеко от пивной. Он играет на органе в церкви с тысячелетней историей и аранжирует музыку для женского хора, которым руко- водит Счастриво женат уже более чем 30 лет, имеет двоих детей, гвинейского поросенка и шиншиллу. Ян обещает, что он вернется к пробежкам вокруг газона — на следующей неделе. Джек Клейн (Jack Klein) занялся программировани- ем настольных компьютеров и, прежде всего, проекти- рованием и программированием встроенных систем в 1980 г. Он участвовал в разработке продуктов, исполь- зующих множество различных 8-, 16- и 32-битовых про- цессоров, и программировал их на естественном ассем- блерном языке, а начиная с 1983 г. — на языке програм- мирования С. Специализируясь на процессном контроле, контро- ле движения и коммуникациях, Джек разрабатывал и программировал встроенные системы для использования в промышленном, медицинском, бытовом и офисном оборудовании. Джек в настоящее время является главным инжене- ром группы ядерной медицины компании Siemens Medical Systems, где он работает над проектными схе- мами встроенных систем, над вопросами управления движением и безопасности пациентов, а также разраба- тывает программное обеспечение сборки образов для медицинских устройств формирования изображений. Майкл Ли (Michael Lee) занимается программиро- ванием с 1979 г. и имеет многолетний опыт работы с языками про1раммирования PL/1 и С. Первоначально его деятельность была сосредоточена на разработке потреби- тельских систем баз данных. Впоследствии его усилия были направлены на привязку SQL-интерфейса к базе данных, первоначально реализованной на Фортране. Возможно, его наиболее значительным проектом был оптимизатор запроса — небольшой фрагмент кода, который анализирует поступающий к базе данных зап- рос и трансформирует его в эквивалентный, но более эффективных запрос, который улучшает использование связей и индексов базы данных. Другие его проекты включают исчерпывающую Web-страницу по оптими- зации различных приемов и методов, а также программ- ное обеспечение для генерирования списков лучшей десятки групп новостей Usenet. Когда Майк не программирует, он любит покатать- ся на горном велосипеде по склонам недалеко от его калифорнийского дома, определенно предпочитая спус- каться по склону, а не подниматься в гору. Бэн Пфаф (Ben Pfaff) — студент-электротехник Ми- чиганского государственного университета, где он со- стоит в команде фехтовальщиков MSU. После обучения он надеется получить работу по проектированию циф- ровых интегрированных схем. Бэн работает с компью- терами уже 14 лет, 10 из которых — с языком програм- мирования С. Его интересы помимо компьютеров
Искусство программирования на С 23 включают французский и японский языки, научный вы- мысел и путешествия. В Internet вы часто можете найти его ответы на вопросы в форуме Usenet comp.langx. Бен так- же сопровождает систему Debian GNU/Linux (www.drbbin.oig) и автор программ для Фонда Бесплатно- го Программного Обеспечения (www.fsf.org)- С Бэном можно связаться по электронной почте pbflben@msu.edu. Питер Сибеч (Peter Seebach) является заядлым лю- бителем языка С. Он научился читать программы на С с принтерных распечаток и, в конечном счете, научил- ся также писать на С. Он состоит в комитете по стан- дартам ANSI/ISO С, поскольку, как он утверждает, "ANSI/ISO С звучит как музыка”. Питер участвует в группе comp.lang.c.moderated. Он обычно программи- рует, поскольку "это — лучший способ не поддаться со- блазну видеоигр”. Стив Саммит (Stiv Summit) — ветеран среди про- граммистов на С, автор и преподаватель. Вот уже 20 лет он программирует почти исключительно на С. Он со- провождает список наиболее часто задаваемых вопросов в труппе новостей comp.lang.c. Его программистские ин- тересы варьируют, но они всегда тесно связаны с тема- ми чистоты, применимости, переносимости и подразу- мевающейся правильностью программ на С. Мэтью Ватсон (Mathew Watson) написал свою пер- вую программу на машинном коде (поскольку высоко- уровневые языки выполнялись на первом 8-битовом процессоре очень медленно) в возрасте 10 лет, и с тех пор был захвачен возможностями компьютеров. Он изу- чал компьютерные науки в Лондонском университете и работал для различных компьютерных фирм; он воочию увидел, насколько необходимо начальное проектирова- ние программного обеспечения для определения воз- можных ошибок фактического выполнения программ. Сейчас Мэтью ожидает рождения своего первенца, так что он/она может показать ему, как запрограммировать все, что будет эквивалентно видеомагнитофону в 21 веке — так же, как сам Мэтью делал это для своего папы. Стефан Уилмс (Stephan Wilms) — профессиональный разработчик программного обеспечения с опытом в об- ласти промышленной обработки изображений и меди- цинских систем для кардиологических исследований и лечения. На протяжении своей карьеры он работал с вы- сокопараллельными встроенными системами и разраба- тывал сложные приложения для конечных пользовате- лей Widows. Ян Вудс (Ian Woods) занимался профессиональным программированием семь лет, и в настоящее время ра- ботает над написанием встроенного программного обес- печения реального времени для одной исследовательс- кой компании. Он специализируется на С и различных ассемблерных языках. Многие его проекты основывают- ся на глубоких знаниях дискретной математики и мето- дов обработки сигналов. Сейчас он изучает разговорный язык, называемый Lojban — инженерный логический язык с формальной грамматикой, которая существенно облег- чает обработку и генерирование “живой” речи. Боль- шинство его интересов направлены на исследования его неординарных идей, которые охватывают многие обла- сти в вопросах разработки программного обеспечения. Майк Райт (Mike Wright) является главным техно- логом Innovative Systems Architect — корпорации, кото- рая заработала внушительную репутацию поддержкой технических нужд тестирования и подготовки персо- нала полигонов Министерства Обороны США. Дея- тельность Майка направлена на проектирование и раз- работку усовершенствованного анализа задач и наборов инструментов визуализации для полигона Nevada “Black” Range. Майк полностью удовлетворен трудно- стями своей работы, постоянно проводя исследования новых усовершенствований в инженерии программно- го обеспечения. Вы можете встретиться с ним по адре- су mike_wisdom@yahoo.com.
Искусство программирования на С 24 Посвящение Моей Никки (Nicky) — самой очаровательной женщине в мире. С любовью — Ричард Хэзфилд Благодарности Я признателен многим людям. И соавторам — в первую очередь: без них не было бы этой книги. Но особую благодарность хочу выразить Дэну Корбиту и Бэну Пфаффу, которые квалифицированно разобрали мои исходные коды и уничтожили в них немало демонов, а также Чеду Диксону, оказавшему мне большую помощь в часы сомнений, и Яну Келли — несомненно, англий- скому Дональду Кнуту (Donald Knuth). Огромная признательность Кэрол Аккерман (Carol Ackerman) из издательства Sams Publishing, которая мно- го сделала для того, чтобы эта книга вообще появилась, Гасу Микласу (Gus Miklos), уверенной рукой держав- шему руль по нужному курсу, а также всем другим сотрудникам издательства Sams, которые так тяжело ра- ботали, чтобы довести эту книгу от идеи до ее реализации. Множество других людей также бескорыстно отда- ли свое время, энергию, идеи и наработки. Благодарю Олю Энжделсмарк (Ola Angelsmark), Ника Крамера (Nick Cramer), Марка Дэна (Mark Dean), Джонатана Джорджа (Jonathan George), Мэта Гэсснера (Matt Gessner), Стэфена Хэзфилда (Stephen Heathfield), Джо- на Хиксона (John Hixson), Лейбиш Мэрмелстейн (Leibish Mermelstein), Эдмунда Стефен-Смита (Edmund Stefen-Smith) и Брайана Вильямса (Bryan Williams). Наконец, большое спасибо наилучшей в мире бра- тии хакеров, жителей тех глубоких, темных и непрохо- димых джунглей, которые называются comp.iang.c, слу- чайно обеспечив мне экспертную техническую помощь своими ответами на вопросы, задаваемые другими людь- ми, и таким образом сэкономив мои усилия, которые я затратил бы сам, отвечая на такие вопросы. — Ричард Хэзфилд
Пересмотренный язык С ЧАСТЬ В ЭТОЙ ЧАСТИ Энциклопедия С-программиста Войны стандартов программирования: причины и пути перемирия Оптимизация Работа с датами Игры с битами и байтами Хранение и извлечение данных Исправление кода программ Управление памятью Моделирование и контроллеры Рекурсия
Энциклопедия С-программиста В ЭТОЙ ГЛАВЕ Для кого предназначена эта книга Какие знания вы должны иметь Как наиболее эффективно использовать книгу Для чего нужна еще одна книга по С Почему используется стандарт ANSI С Какие платформы охватывает эта книга Об исходном коде Качество программного кода Как организована эта книга Проблемы авторских прав Содержимое компакт-диска Общество С-программистов Ричард Хэзфилд Добро пожаловать в страну С! Издательство Sams при- ступило к выпуску серии компьютерных книг серии "Энциклопедия”. На сегодняшний день имеется очень широкий выбор таких книг-энциклопедий, предостав- ляющих исчерпывающую информацию по тому или иному языку либо компилятору. Что касается данной книги, то она является результатом кропотливого тру- да авторов по изучению и описанию особенностей при- менения языка С. С является простым, но чрезвычайно мощным язы- ком; он использовался для реализации операционных систем, видеоигр, программирования микроволновых печей, текстовых процессоров и фактически чуть ли не каждого современного электронного устройства, какое только можно себе представить. Если бы язык С имел какие-либо серьезные ограничения, он не был бы столь популярен, а эта книга имела бы, вероятно, намного меньший объем. По традиции эта книга называется "С. Энциклопедия пользователя”, но я бы предпочел на- звать ее "Энциклопедия С-программиста". Представьте себе, что вы обратились к специалисту с таким вопросом: "я пытаюсь выполнить некоторые вычисления, в которых очень важна точность. Мне тре- буется 500 значащих цифр, но я могу получить лишь 15 или около того. Как достигнуть большей точности?” Еще недавно на этот вопрос вы получили бы такой ответ: "Это невозможно сделать на С. На современных системах количество значащих цифр переменных двой- ной точности double определено константой DBL_DIG в библиотеке <float.h> и составляет обычно 15. Вы уве- рены, что вам требуется 500 цифр?” Но с выходом в свет этой книги у вас появился шанс разрешить эту задачу. Вы узнаете, как это можно сделать на С. В частности, вы убедитесь что осуществлять сорти- ровку в С можно быстрее, чем с помощью программы Quicksort. Можно более эффективно обрабатывать раз- говорные языки, а также автоматически определять утечку памяти, перемножать два массива и многое-мно- гое другое. К сожалению, не все задачи поддаются решению при использовании чистого языка С, основанного на стандарте ANSI С (American National Standard for Information for С — Американский национальный ин- формационный стандарт программирования на С. — Прим, науч, ред.) Идея стандарта ANSI С состоит в том, чтобы сделать язык С переносимым на любые платфор- мы. В результате желаемые свойства, которые, как пред- полагается, должны быть стандартными в любом язы- ке, часто отсутствуют просто потому, что не все платформы поддерживают эти свойства. Например, не- возможно очистить экран из программы на С. Почему? Да просто потому, что не все системы вообще имеют экран! Моя собственная Linux-система большую часть времени отключается от экрана и клавиатуры, но все же управляет выполнением полезной работы. Как прави- ло, программы, ориентированные на конкретные аппа- ратные средства (такие, как лазерный принтер или во-
27 обше принтер любого типа) либо требует частной па- радигмы операционной системы (например, концепции структуры директории), вряд ли может быть реализо- вано на ANSI С. В наше время следует обращаться к до- полнительным функциям, обеспечиваемым конкретной реализацией. Несмотря на это ограничение, в среде языка С мож- но выполнять множество таких операций, о которых многие и не подозревают. Для этого придется немного подумать и выполнить определенную работу. Мы попы- тались, насколько смогли, сами поразмышлять и выпол- нить черновую работу так, чтобы вы могли наслаждать- ся ее результатами. Для кого предназначена эта книга Книга "С. Энциклопедия пользователя" предназначена для тех программистов, которые уже приобрели хотя бы самый минимальный опыт программирования на язы- ке С, т.е. либо ежедневно используют его в своей рабо- те, либо подробно изучают его в течение, по крайней мере, года. Для новичков книга будет достаточно трудна, поэтому рекомендуется сначала обратиться к одной из многих превосходных книг по языку С, написанных для начинающих. Если требуется рекомендация, то предлагаю книгу К.Н.Кинга (K.N.King) С Programming: A Modern Approach, изданную компанией W.W.Norton&Company в 1996 году, ISDN 0-393-96945-2. Если вы хотите, чтобы ваши С-программы были как можно более быстродействующими, читайте эту книгу. Если вы собираетесь сделать свои С-программы бо- лее переносимыми, читайте эту книгу. Если вас интересует, как современные быстрые и эффективные алгоритмы можно реализовать на С, опять-таки, читайте эту книгу. Если же вас не беспокоит ни один упомянутый выше вопрос, все равно читайте эту книгу, чтобы вы- яснить, почему эти вопросы все-таки должны вас вол- новать. Если вы занимаетесь программированием на C++ и по ошибке выбрали эту книгу, а теперь обеспокоены вопросом относительно возмещения денег за нее, не переживайте. Все идеи и большая часть программного кода в этой книге вполне подойдут и для C++, если сделать несколько незначительных изменений. Наконец, если вы, милая леди, выбрали эту книгу в качестве подарка ко дню рождения своего внука и ин- тересуетесь, понравится ли она ему, ответом будет бес- спорное ДА. Немедленно берите ее и поспешите опла- тить в кассе. И пока продавцы будут заворачивать ваш подарок, почему бы вам не купить внуку вводную кни- гу по С, а также С-компилятор да и сам компьютер? Энциклопедия С-программиста Глава 1 Какие знания вы должны иметь При написании этой книги предполагалось, что чита- тель имеет представление о ядре языка С. Если вам зна- комы такие понятия, как типы, операторы, выражения, управляющие потоки, синтаксис функций, указатели, массивы, структуры, объединения, входной и выходной потоки, обработка строк, область видимости и синтак- сис базового препроцессора, то для вас не покажется трудным восприятие материала этой книги. Среди указанных понятий имеется одно, которое можно отнести к разряду особо сложных, речь идет об указателях (pointers). Учитывая это, в книге дан крат- кий курс по элементарной теории указателей. Указатель является переменной. Это наиболее важ- ная идея, которую следует взять во внимание. Она явля- ется отправной точкой для освоения всего материала. Необходимо срезу же подчеркнуть, что это не про- сто переменная, а специальный тип переменной, зна- чения которой содержат адреса. Переменная-указатель может содержать адрес другой переменной. Если у нас нет переменной, адрес которой необходимо сохранить в указателе прямо сейчас, можно поместить в указатель адрес специальной ячейки NULL. Мы назначаем указатель для адреса объекта с ис- пользованием оператора взятия адреса, имеющего вид амперсанда (&). Получить отмеченный таким образом объект можно с использованием оператора косвенной адресации, обозначаемого символом звездочки (*). ПРИМЕЧАНИЕ В языке С объект — это просто поименованная область памяти, и не следует его путать с принятым в C++ оп- ределением объекта как экземпляра класса. Наиболее важным фактом, который связан с указа- телями и который нужно запомнить, является то, что они должны указывать на что-либо конкретное, преж- де чем быть переопределенными. Таким образом, мы не можем написать: char *р strcpy (р, ’’Hallo, world”) ; Прежде всего нужно выделить пространство (па- мять). Можно выделить памяти больше, чем требуется, или ровно столько, сколько необходимо. Оба этих спо- соба показаны в листинге 1.1 (этот код предназначен исключительно для иллюстрации, ведь мы не будем в действительности писать программы типа "hello, world” (привет, мир) с целью заработать).
28 Пересмотренный язык С Часть I Листинг 1.1. Обеспечение места, на которое могут указывать указатели. linclude <stdio.h> linclude <string.h> linclude <stdlib.h> int main(void) { char *p; lifdef SCROOGE /* Выделяется ровно столько пространства, * сколько требуется, с учетом концевого * признака (указателя) '\0'. */ р = malloc(strlen([dbl]Bello world(dblj) + i); lelse /* Выделение памяти с избытком */ р = malloc(lOOO); lendif /* Примечание: функция malloc возвращает * NULL, если здесь выделено недостаточно * памяти. */ if(p 1= NULL) { strcpy(p, [dbl]Hello world(dbl)); printf([dbl]%s\n[dbl], p); free(p); /* Мы всегда освобождаем память, когда * заканчиваем работать с ней. */ return 0; 1 Здесь использован указатель специального вида, называемый пустым (Void pointer, или pointer to void — указатель на пустое место. — Прим. пер.)9 который ука- зывает на определенный тип, неизвестный компилято- ру. Предположим, дан некоторый указатель Р на тип Т. Можно использовать пустой указатель V для указания на тот же объект, на который указывает Р, но никакой серьезной работы выполнить с этим указателем невоз- можно. Он является лишь маркером места в памяти. (И, как можно будет убедиться, он оказывается очень по- лезным.) Чтобы сделать что-либо полезное с памятью, на которую мы указываем, нужно использовать указа- тель правильного типа. Таким образом, допустима сле- дующая запись: т *р; void *v; Т *q; р = malloc(sizeof *p); if(p 1= NULL) { v = p; /* ... */ q = v; /* ... сделать что-либо с памятью, используя q ... */ free(q); ) Заметьте, что здесь полностью отсутствуют приве- дения (типов). Приведения крайне не рекомендуется использовать в С. Здесь в них совершенно нет необхо- димости. Как наиболее эффективно использовать эту книгу Чтобы извлечь как можно больше пользы из этой кни- ги, настоятельно рекомендую очень внимательно про- читать ее. Почему? Потому что на моих книжных пол- ках лежит не менее сотни книг по компьютерной обработке данных общим объемом, наверное, 50 тыс. страниц. Я сомневаюсь, что фактически прочитал боль- ше чем около 10 тыс. этих страниц. Так что я не полу- чил максимальной пользы из своей книжной коллекции. Пусть этого не случится с вами! Эта книга охватывает множество методик, с которы- ми вы раньше не встречались или которые не догадались применить к своим конкретным нуждам программирова- ния. Многие недооценивают силу "классических" струк- тур и алгоритмов, полагая их несколько старомодными и подсознательно надеясь, что современные компьюте- ры настолько мощны, что фактически даже не возник- нет вопроса, какой алгоритм использовать, поскольку даже самый медлительный метод не повлияет на ско- рость работы. Однако это не так. Да, в настоящее вре- мя компьютеры отличаются высоким быстродействием. Но разве не приходилось вам когда-либо сидеть перед компьютером и нетерпеливо ждать завершения той или иной задачи? Мне лично приходилось. Следовательно, компьютеры являются еще недостаточно мощными, чтобы молниеносно исправлять неадекватность наших программ. Так что постараемся писать свои программы разумно. И чем более скоростными становятся компь- ютеры, тем большего мы ожидаем от них, но они ни- когда не будут настолько быстродействующими, чтобы превращать плохие алгоритмы в хорошие. Так что пусть у вас будет стимул для внимательно- го чтения этой книги. Мы попытались разбить ее на два уровня: для заинтересованных и для ленивых читателей. Надеемся, что вы достаточно заинтересованы в приоб- ретении средней или даже высокой квалификации С- программиста, чтобы тщательного изучать текст и ис- ходные коды, стараться в полной мере понять их и учиться применять полученные знания для решения повседневных задач. Не исключено, конечно, что вы не имеете времени для удовлетворения своей заинтересо-
Энциклопедия С-программиста Глава 1 29 ванности. Начальник ждет от вас готовой программы завтра, и ваша программа нуждается в подпрограмме действительно быстрой сортировки, а у вас нет време- ни, чтобы достигнуть фактического понимания напол- нения этой подпрограммы. В такой ситуации чаще все- го ничего не остается как только включить ее в свою программу, а там — хоть трава не расти! Мы это пре- красно понимаем, поэтому попытались упорядочить текст так, чтобы читатель мог получить разумное пред- ставление о том, о чем идет речь, без необходимости изучать каждое отдельное слово на каждой отдельной странице. Надеемся, что вы будете возвращаться к тексту позже и перечитывать тот или иной материал более осоз- нанно. Так что, если у вас действительно нет времени для скрупулезного изучения материала, читайте только то, что необходимо, и игнорируйте все остальное. Можете не беспокоиться — вам не придется читать эту книгу от корки до корки. Каждая глава является более или менее самостоятельной, так что книга идеаль- но подходит для чтения на ночь. Смело пропускайте все, что вам уже хорошо знакомо, ныряйте в какой угод- но водоем знания, к которому влечет вас воображение, погружайтесь в него полностью, чтобы получить и пользу, и удовольствие. Для чего нужна еще одна книга по С Язык С ежедневно используют десятки тысяч програм- мистов, поскольку это быстрый, устоявшийся, достаточ- но простой и переносимый язык. Хотя C++ все более завоевывает рынок, пытаясь оттеснить язык С, но пос- ледний по-прежнему не сдает своих позиций и продол- жает занимать заслуженное место в мире программиро- вания. Он стал языком лингва франка (lingua franca) сообщества разработчиков и имеет высокий авторитет среди современных программистов. Язык С не является совершенным; его самые горя- чие сторонники обычно первыми жалуются на прису- щие ему дефекты. Однако он успешно выдержал испы- тание временем. Эта книга позволит вам стать более квалифицирован- ным С-программистом, если вы будете следовать сове- там и приемам экспертов, которые уже по нескольку раз прошли сквозь огонь, воду и медные трубы. Мы пока- жем вам, как работать. Вы получите исходный код, ко- торый сможете встроить прямо в свои приложения. Почему используется стандарт ANSI С Во многих книгах по языку С делаются определенные предположения относительно используемой вами плат- формы. Некоторые книги охватывают конкретные ком- пиляторы и операционные системы; очевидным приме- ром может служить книга, посвященная программиро- ванию Windows или языку Borland C++. (Еще хуже — некоторые книги охватывают лишь определенные вер- сии определенных компиляторов!) Иногда это препод- носится более тонко, например, предполагается, что читатель выполняет сортировку последовательности символов стандарта ASCII. Это, в частности, нарушает планы тех, кто вынужден (не по своей собственной воле) иногда использовать расширенный двоично-деся- тичный код обмена EBCDIC. В этой книге стандарт ANSI С рассматривается по- чти полностью, поскольку, следуя стандарту С, можно повысить уровень переносимости программных кодов. Эта книга не предназначена исключительно для С-про- граммистов в Windows, в DOS или в UNIX, она пред- назначена для всех С-программистов. Мы будем рас- сматривать двоичные деревья, которые можно использовать на любой платформе. Кроме того, будет подробно описан процесс обработки естественного язы- ка, который можно выполнять в программах LE/370, а также в UNIX- или Windows-программах либо во всех трех одновременно, а при необходимости и на некото- рой совершенно отличной от них платформе. Будут рассмотрены также методики сортиррвки, которые мож- но использовать независимо от своего местоположения. В любом случае, если вы имеете компилятор, эта книга окажется полезной для вас. Однако не всегда представляется возможным напи- сать всю программу только на ANSI С. Всегда находят- ся раздраженные заказчики, которые настаивают на том, чтобы программа выполняла увертливые и сверхъесте- ственные действия, такие как — внимание! — очистка экрана. Стандарт ANSI С вообще не требует наличия экрана, так что очистить экран в ANSI С довольно про- блематично. (Фактически 100%-ной гарантии перено- симости решения этой проблемы не существует.) Ана- логично нет полностью переносимого способа получить последовательность нажатия клавиш от пользователя без отображения на экране вводимых с клавиатуры симво- лов (как в случае, например, фиксации пароля). Так что могут возникнуть моменты, когда будет необходимо отойти от стандарта и использовать ориентированные на конкретную реализацию расширения языка. Пуристы С будут упрямиться этому, но для некоторых приложе- ний использования расширений С не избежать. В кон- це концов, хотя, несомненно, можно написать тексто- вый редактор полностью на ANSI С, сомневаюсь, что удастся его хорошо продать. Как правило, такой редак- тор оказывается переносимым, когда это необязательно, и непереносимым, когда это необходимо сделать. Эта книга написана, насколько возможно, с использовани- ем кода ANSI С. Когда нужно будет отступить от стан- дарта, мы обязательно обратим ваше внимание на этот факт.
Пересмотренный язык С Часть I 30 Если наша настойчивость на максимальном исполь- зовании ядра стандарта языка кажется вам скорее огра- ничением, чем преимуществом, это действительно так. Программный код, представленный в книге, вместо того чтобы иметь возможность свободно эксплуатировать все различные мощные опции одной частной версии одно- го частного компилятора для одной частной операци- онной системы, является ограниченным и подходит только ко всем ANSI С-компиляторам на всех операци- онных системах в мире. Наверное, мы легко можем смириться с таким ограничением. По закону Мэрфи (или одному из его следствий) вы с удивлением обнаружите, что, как только напишете некоторый непереносимый код, входящий в огромное приложение, так уже через неделю это приложение нужно будет переносить на некоторую совершенно чуж- дую платформу. Такая задача чрезвычайно упрощается, если изолировать непереносимый код для более легкой его замены, когда придет время перемещать платформы. Так что далее мы будем рассматривать различные спо- собы, позволяющие сделать это, и в качестве примера выполним программирование сокета (гнезда). Какие платформы охватывает эта книга Все! В этой книге по большей части описывается про- граммирование на языке ANSI С (строго говоря, на ANS1/1SO С). Следовательно, она охватывает все плат- формы, а не какую-либо одну. Здесь представлена лишь совсем маленькая платформно-зависимая программка. Говоря более конкретно, в книге не поднимаются воп- росы Windows- или UNIX-программирования, хотя в ней приведены полезные переносимые сведения и под- ключаемые исходные коды, которые можно использо- вать на этих (и многих других) платформах. Кроме того, коротко описываются ядро Linux, многопоточ- ность, многопроцессность и встроенные системы. Об исходном коде Почти все исходные коды в этой книге будут работать на любом ANSI С-компиляторе для любой платформы, обеспечивая достаточное количество ресурсов (таких, как память), доступных во время выполнения. Несколь- ко исключений из этого правила, в частности проблема переносимости, рассмотрены выше. Ранее уже было отмечено, чем отличаются заинте- ресованные читатели от ленивых. Исходные коды раз- работаны так, что они будут полезны и для тех, и для других. Если вы заинтересованный читатель, то найде- те множество увлекательных вещей, благодаря исполь- зованию которых сможете повысить уровень своей ра- боты. Если же у вас недостает времени или энергии внимательно читать и прорабатывать эту книгу, просто изучите код драйвера, посмотрите, как используются функции, затем удалите драйвер — и вы получите биб- лиотеку, которую сразу же можете использовать. (Факти- чески вам даже не нужно будет удалять цк1Я1*ер, посколь- ку он будет находиться в отдельном файле в программной библиотеке. Но, если можете, хотя бы изучите драйвер, чтобы узнать, как работает библиотека.) Качество программного кода Нет ничего необычного в книге, подобной этой, увидеть такой демонстрационный программный код: void cat3 (char *s, char *tf char *u) { strcat(s,t); strcat(sru); I Это прекрасная программка, которая, несомненно, будет работать длительное время. Хотя можно спросить: что случится, если один или более указателей примут значение NULL? Нетрудно проверить, что произойдет, но автор не побеспокоился об этом, полагая, возмож- но, что педантичный (т.е. осторожный) читатель может самостоятельно вставить код такой проверки. Авторы этой книги надеются, что предосторожность — достоинство, а не грех. Нашей целью было писать ус- тойчивый, надежный, действенный код без компро- миссов относительно его выполнимости. Насколько мы преуспели в этом направлении, покажет время. Если у вас есть что сказать о качестве программного кода в этой книге или вы обнаружите в нем ошибки, пожалуйста, дайте нам знать. В случае расхождения между исходным кодом в этой книге и кодом на Web-сайте издательства "ДиаСофт", предпочитайте код на Web-сайте. Мы попытались очень тщательно проверить и убедиться в том, что код в этой книге корректен, но люди есть люди, так что возможно и появление ошибок. Код на Web-сайте издательства "ДиаСофт", по крайней мере, был скомпилирован и протестирован. Вы когда-нибудь пробовали достать ком- пилятор для чтения с бумажного листа? Это непросто! Как организована эта книга Надеемся, что эта книга позволит вам повысить свою квалификацию как С-программиста со среднего уровня до профессионала. Для этого мы постараемся воору- жить вас знаниями, которых вы прежде не имели — тех вещей, которые отличают эксперта от профессионала. Эта книга состоит из трех частей — "Пересмотрен- ный язык С", "Организация данных" и "Дополнитель- ные темы".
Энциклопедия С-программиста Глава 1 31 В части 1 ’’Пересмотренный язык С” исследуются некоторые аспекты программирования на С, которые должны быть очевидными, но, как это часто бывает, таковыми не являются. Начинается рассмотрение с не- которых трудных проблем стиля кодирования и техни- ческих ловушек, которые часто могут стать причиной споров в команде разработчиков, особенно с точки зре- ния того, что является важным и просто интересным в конкретных стилях программирования. Компьютеры используются для быстрого выполне- ния наших команд, но в последние годы они, кажется, становятся все более медлительными. Это можно час- тично объяснить тем, что мы ожидаем от программ слишком многого, а частично тем, что в настоящее вре- мя пишут не слишком надежные программы. Так что придется искать некоторые оптимизирующие методи- ки, которые бы успешно работали для сокращения времени выполнения программы. Как сказал однажды Питер ван дер Линден (Peter van der Linden), "любой, кто думает, что программирование данных и получение сразу верного результата — простое дело, вероятно, не слишком много сделал в этой обла- сти". Поэтому в часть I книги включена глава, посвящен- ная надежности обработки данных. Несколько страниц также отводится для исследования битов и байтов. В языке С имеется богатый набор операторов bittwidding, позволяющих легко получать требуемые комбинации битов. Автор этой главы — Лоренс Кирби (Lawrence Kirby) — проведет читателя через "минное поле" воз- можных неожиданных сюрпризов. Остальные главы этой части книги посвящены об- работке файлов, отладке программ, управлению памя- тью, моделированию и рекурсии. Часть II "Организация данных" охватывает многие классические структуры данных. Массивы, списки, сте- ки, обычные очереди и очереди с двусторонним досту- пом, деревья (специальный род древовидной структу- ры), разреженные массивы и графы. — все это описано и реализовано в форме повторно используемой библио- теки. Отдельная глава посвящена сортировке данных. Содержание части III "Дополнительные темы” пол- ностью соответствует ее названию. Здесь рассказывает- ся о том, как выполнять арифметические действия с матрицами (это очень полезно, если вы хотите выпол- нить некоторую полиномиальную интерполяцию или решить систему уравнений). Мы откроем для вас мир обработки цифровых сигналов (быстрое преобразование Фурье и т.п.). Далее речь пойдет о синтаксическом анализе выра- жений. Мне всегда хотелось создать свой собственный (нетривиальный) компьютерный язык. Под руковод- ством эксперта Яна Келли (Ian Kelly) я планирую сде- лать это в рамках своего следующего проекта. Многие программы, представленные в книге, явля- ются короткими одноразовыми программками, которые никто из пользователей (кроме нас с вами, конечно) никогда даже не увидит. Эти специальные инструмен- тальные средства очень полезны; мы нашли некоторые способы, позволяющие сделать их еще более полезны- ми. Затем можно сесть и помечтать о несметном богат- стве, которое нам дал Майк Райт (Mike Wright) путем нескольких потенциально прибыльных проникновений на автоматическую фондовую биржу с использованием Универсальных Алгоритмов (Generic Algorithms). В наш век мы не можем игнорировать Internet. Вы, несомненно, слышали о CGI (Common Gateway Interface — Интерфейс общей шины. — Прим. науч. ред.). Надеем- ся, сюрпризом для вас станет возможность написать CGI-программу полностью на ANSI С. Чед Диксон (Chad Dixon) покажет, как это сделать. Однако иногда придется погружаться в мрачный мир платформно-зависимого программирования. Используя в качестве примера сетевое программирование, мы по- кажем, как можно минимизировать проблемы при пе- реносе программного кода. Я — законченный фанат числа л — отношения дли- ны окружности к ее диаметру. Где-то в темных глуби- нах моей памяти еще со времен учебы хранится воспо- минание о книге, в которой говорится о значении этого числа с точностью до одного миллиона значащих цифр после десятичной точки. Если вас когда-либо заинтере- сует, как выполняются такого рода вычисления, прочи- тайте главу 24, написанную Яном Келли. Если вам приходилось использовать современный интегрированный офисный пакет, значит, вы знакомы с технологией типа "спроси эксперта..." или "как это сделать", которая, кажется, "понимает" то, что пользо- ватель набирает на клавиатуре. В главе 25 вы сможете воочию увидеть, как с помощью электроники интерпре- тируется человеческий язык. Безопасность данных в современном мире является не менее важной, чем электронная торговля и коммер- ция. Мы опробуем несколько методик шифрования и расшифровки данных. Если вам невдомек, как пишутся С-программы для стиральных машин или микроволновых печей (у кото- рых отсутствует даже запоминающее устройство на гиб- ком диске для загрузки программы в эти машины), по- листайте написанную Стефаном Уилмсом (Stephan Wilms) главу 27. Самая грандиозная новость о С-программировании за последние десять лет появилась в октябре 1999 года, когда комитет ANSI/ISO окончательно ратифицировал С99 — новый языковый стандарт. Чтобы завершить кни- гу, Лоренс Кирби делает обзор внесенных стандартом из- менений, многие из которых принципиально важны.
Пересмотренный язык С Часть I 32 Это список только наиболее важных тем. Но не бес- покойтесь, вам не нужно читать их все одновременно. Просто уделите немного времени каждой главе. Это ведь не соревнование, кто быстрее доберется до конца книги! Проблемы авторских прав Почти все исходные программные коды на сопровож- дающем книгу компакт-диске охвачены лицензией GNU Public License (коротко GPL). Если же вы не знакомы с GPL, это значит, попросту говоря, что вы можете ис- пользовать эти коды до тех пор, пока вас не попытают- ся остановить другие люди, тоже их использующие. Для более детального ознакомления просмотрите текст са- мой лицензии, помещенной в приложении А. Главная задача исходного кода, представленного в книге, состоит в том, чтобы упростить вашу работу. Мы не хотим ограничивать читателя в использовании про- граммного кода. Все, о чем мы просим, — не утверж- дайте, будто написали его сами, и не останавливайте людей, которые тоже используют эти программы. Но не стесняйтесь включать в свои собственные программы наш код. Все рисунки, диаграммы и эмблемы в книге сохра- няют авторские права, как и сам текст книги. Если вы хотите что-то скопировать из нее, то сначала получите письменное разрешение издательства Sams Publishing. Общество С-программистов Эта книга была написана членами общества С-програм- мистов. Если вы невольно задались вопросом: "А как туда записаться?”, не волнуйтесь — вам совершенно незачем записываться в это общество. Вы уже являетесь его членом. Звание члена "Общества С-программистов" относится ко всем С-программистам во всем мире, включая конечно же и вас. Лоренс Кирби и я, Ричард Хэзфилд, являемся прин- ципиальными авторами этой книги, но мы никогда не встречались лично. Мы лишь однажды разговаривали по телефону, чтобы обсудить книгу, и это все. Все наши другие контакты осуществлялись через электронную почту и сеть Usenet. Я знаю только двоих наших соав- торов — Мэта Уатсона и Яна Келли, с которыми я лич- но встречался. Это едва ли удивительно, поскольку боль- шинство авторов являются гражданами Америки, а я никогда не был в Соединенных Штатах. Тем не менее, мне кажется, что я знаю этих авторов уже много лет. Они по большей части являются регулярными пользователями группы новостей сети Usenet, которую можно найти по адресу news:comp.Iang.c и которая представляет собой международный форум для С-программистов. Подпис- ка на comp.lang.c совершенно бесплатна (если не счи- тать обычные расходы на провайдера службы Internet и оп- лату телефонных счетов). Она позволит вам познакомиться с обширным миром талантливых С-программистов, а также с интеллектуальными оппонентами, но их, к сча- стью, не много. Указанный адрес является местом, где люди не стес- няются в выборе выражений и не брезгуют грубоватым обращением. Если вы обладаете мягким характером, можете предпочесть более тихую и более вежливую группу новостей news:comp.lang.c.moderated, где все статьи перед публикацией проверяются модератором (арбитром). В случае, если ранее вы не использовали Usenet, скажу, что эта сеть представляет собой огромную кол- лекцию групп новостей (за последнее время я проверил более 50 тыс. из них), каждая из которых выделена для исследования конкретного предмета. Группа новостей несколько напоминает электронную доску бюллетеней. Статьи публикуются для всеобщего обозрения, и любой человек может ответить на любую статью. Одни груп- пы новостей придерживаются постоянной тематики, другие — нет. Группа comp.lang.c остается верна своей тематике на протяжении уже многих лет. В результате она заслужила солидную репутацию как источник и репозитарий (хранилище) экспертизы ANSI С. Группа получает ежедневно множество статей по различным аспектам С-программирования. Часто эти статьи пред- ставляются в форме вопросов; на эти вопросы почти неизменно даются исчерпывающие ответы. Почему люди утруждают себя бесплатными ответами на вопросы в группе новостей, подобной comp.lang.c? Не потому ли. что им нравится демонстрировать свои знания? Или потому, что когда-то они сами были уче- никами. а сейчас хотят поделиться своими приобретен- ными знаниями? Скорее всего, ими руководит следую- щее соображение: "Однажды я лично задал вопрос относительно языка С. Отвечая на вопросы других лю- дей. я помогаю поддерживать деятельность междуна- родного сообщества, так что мне это тоже необходимо". Неважно, какие причины заставляют людей оказы- вать помощь, главное, что такая помощь, несомненно, существует. Если вам требуется поддержка в програм- мировании на ANSI С, обращайтесь именно по этому адресу. Большинство авторов этой книги, как уже было сказано, являются постоянными контрибьютерами (вкладчиками) группы comp.lang.c и всегда будут рады ответить на ваши вопросы, касающиеся языка С. Перед тем как направлять запрос в группу первый раз, вспомните, пожалуйста, что вы имеете дело с об- ществом (т.е. с отделением более широкого сообщества программистов, частью которого вы уже являетесь), а раз так, нужно следовать определенным соглашениям. Если вы не будете соблюдать эти соглашения, вряд ли
Энциклопедия С-программиста Глава 1 33 на свой вопрос вы получите такой ответ, какой ожида- ете. Указанные соглашения в точности делятся на два списка: "что нужно делать" и "чего делать нельзя". Пер- вый список включает следующие рекомендации: 1. Тщательно вычитывайте каждое отдельное сообще- ние, размещаемое в группе новостей, всеми мысли- мыми способами (по крайней мере, пока не убеди- тесь, что оно не стоит прочтения). Делайте это в течение недели, а еще лучше — в течение двух не- дель. Это даст вам хорошее понимание того, что представляет собой данная группа. 2. Перед тем как задать вопрос, убедитесь, что он не входит в список часто задаваемых вопросов FAQ (Frequently Asked Questions — Часто задаваемые воп- росы. — Прим. пер.). Людям надоедает снова и сно- ва отвечать на один и тот же вопрос, поэтому об- щие вопросы и ответы на них были собраны в одном Web-узле, чтобы на них было проще ссылаться. Однажды я задал вопрос в comp.iang.c о прототипе функции, которая получается как ее аргумент-ука- затель на функцию того же рода (т.е. функцию, которая получается как ее аргумент-указатель на функцию того же рода (т.е. функцию, которая по- лучается как ее аргумент-указатель...)); короче, идею вы поняли. Мне казалось, что я все сделал правильно, но получил такой ответ: "Перед тем как сделать запрос, читайте FAQ!". И я действительно убедился, что ответ на этот вопрос был дан уже давным-давно. Ниже указан адрес URL для списка FAQ группы comp.iang.c, который поддерживает Стив Саммит (Steve Summit): http: //www. eskimo. com/-scs/C-faq/top. html 3. Если вы просите о помощи в отладке (что встреча- ется чаще всего), постарайтесь, пожалуйста, на- сколько это возможно, облегчить задачу людей, помощи которых вы ищете. Для этого вручную уда- лите из программы все, что не имеет отношения к проблеме, и оставьте лишь минимально необходи- мый объем программы, которая все же демонстри- рует проблему и которую можно скомпилировать без ошибок. Очень часто такой процесс позволяет обнаружить источник проблемы самостоятельно и отказаться от размещения запроса в сети. Если же вам не повезет, вы, по крайней мере, уменьшите нагрузку на comp.lang.c. 4. Если ваша программа не компилируется и вы не знаете почему, убедитесь, пожалуйста, в том, что отправляемый вами исходный код содержит все не- обходимые определения данных (т.е. определения структуры), чтобы можно было легче помочь вам. Как и в предыдущем случае, отправьте минималь- ный программный код, который демонстрирует проблему компиляции. В свое послание включите точный текст всех диагностических сообщений, выдаваемых вашим компилятором (если их очень много, выберите первые десяток-полтора), включая предупреждения. Чтобы сделать это, выполняйте операции копирования и вставки: люди предпочи- тают отлаживать программный код, а не исправлять ошибки набора текста. 5. Если полученный таким образом исходный код со стороны выглядит правдоподобно, заключите его в тело статьи сразу после вашего вопроса. Итак, мы перечислили, что нужно делать. А теперь рассмотрим, что делать не нужно. Если вы хотите сде- лать свой опыт общения с comp.iang.c удачным, при- держивайтесь следующих правил: 1. Когда посылаете исходный код, не надейтесь на то, что у вашего клиента, читающего новости, есть воз- можность открывать ваши присоединения. Для очень многих людей открытие присоединения мо- жет стать проблематичным. Помещайте в свое по- слание только текст. Не используйте клиента, кото- рый настаивает на отправлении статьи в формате HTML, поскольку некоторые другие клиенты груп- пы новостей отфильтровывают HTML, получая, таким образом, совершенно пустой документ. 2. Не ждите, что люди будут выполнять работу за вас. Завсегдатаи comp.iang.c будут счастливы вам по- мочь, но это все же не сервисная служба выполне- ния домашних работ. Если вы просто пошлете спе- цификацию, разговор с вами будет очень короткий. Они помогут вам лучше понять домашнюю работу, но выполнять ее вместо вас не будут. 3. Избегайте многословия. Если вы просите о помо- щи, будьте кратки. Скажите точно, что ваша про- грамма должна выполнять и что она фактически делает. И конечно, включите исходный код самой этой ущербной программы, нужным образом уре- занной по размерам с учетом требуемой ее адекват- ности. 4. Не задавайте вопросов, связанных с платформно- зависимыми аспектами. Согласно моему тщательно- му исследованию (которое включало изобретение подходящего высокого процентного отношения, чтобы мне не нужно было беспокоиться об осуще- ствлении статистически значимой, но утомительной выборки), 99.0137% таких посланий остаются без^ ответов. Вместо этого автор статьи направляется для пересылки вопроса в группу новостей, связанную с определенной платформой или компилятором. Имеются группы новостей для Borland С, Microsoft 3 Зак. 265
Пересмотренный язык С Часть I 34 С и ряд других. Существуют также группы новостей отдельно для графики, для алгоритмов, для Windiws и для UNIX. Пожалуйста, задавайте только вопро- сы, непосредственно связанные с языком С. 5. Не будьте нетерпеливы. Вы можете получить ответ сразу, но может пройти два-три дня, пока кто-ни- будь заметит ваш вопрос и даст на него ответ. Если вы пошлете тот же вопрос снова, то, скорее всего, получите в ответ что-то вроде ’’будьте терпеливее”, а не разъяснение сути своего вопроса. 6. Не полагайтесь сразу же на первый ответ, который получите. Подождите день-другой, чтобы другие люди могли представить свои предложения. И вско- ре вы почувствуете, кто из них говорит по делу, а кто нет. 7. В любом случае не пытайтесь поправлять кого- либо, пока на все 100% не будете уверены, что они ошибаются, а вы правы. Это рассматривается как чудовищно антиобщественное поведение. Если вы думаете, что кто-либо сделал ошибку, но не впол- не уверены в этом, попросите его разъяснить свою точку зрения. Однажды я некорректно поправил очень авторитетного специалиста Мартина Амбула (Martin Ambuhl) по одному техническому вопросу (не помню точно по какому), и не думаю, что он когда-либо простит меня. Всегда убедитесь в пра- вильности своих замечаний и лишь затем публикуй- те поправки. Надеюсь, что не отпугнул вас от групп новостей навсегда! В действительности все эти наставления име- ют лишь общий характер. Иногда очень легко забыть, что пользователи Usenet являются фактически реальны- ми людьми, такими же, как мы с вами. Эти наставле- ния помогут вам помнить, что они, как и мы с вами, ценят сообразительность, особенно когда вы просите их о помощи ради своей пользы. Резюме В этой главе вы получили краткие сведения о книге, которую позволили себе приобрести. Мы объяснили, почему язык С не нуждается в еще одном подробном описании и как С-программисты были несправедливо скованы упрощенным представлением о том, что возмож- но и что невозможно в этом языке. Смеем утверждать, что книга является источником переносимых методик про- граммирования даже в высоко-платформно-зависимом мире обработки данных. Мы остановились на дополнительных темах в этой книге и коротко рассмотрели, что можно и чего нельзя делать с исходным кодом на сопровождающем книгу компакт-диске. Заканчивается глава некоторыми советами по пере- писке с группой новостей comp.lang.c, это действитель- но ценный ресурс полезных советов. Если вы предпо- читаете получить помощь из книги, просмотрите библиографию в приложении В, содержащем список книг, которые могут быть вам полезны. А теперь самое время заняться серьезными вещами. Итак, приступим!
Войны стандартов программирования: причины и пути перемирия 2 В ЭТОЙ ГЛАВЕ Стили расстановки фигурных скобок Использование пробелов Структурное программирование Инициализация Статические и глобальные объекты Имена идентификаторов Ж Написание полезных комментариев ж Общие ошибки и недоразумения ж Объявления, определения и прототипы ж Важность переносимости программ ж Макросы ж С против C++ Ричард Хэзфилд Человечество ведет священные войны с древнейших времен. Отголоски этих войн проникают во все сферы нашей жизни, а в комфортабельном мире С-программи- рования, похоже, они принимают особенно специфи- ческие формы. Программисты борются за соглашения по размещению, структуре, именованию и по всем ви- дам связанных с этим проблем. Теперь нормальные люди не борются за что-либо, если оно не представля- ется им важным. Иногда они правы — это действитель- но важно. Иногда это не так важно для дальнейшего выживания человеческой расы, как им хотелось бы ду- ’ мать. В этой книге мы должны выбрать, какие пробле- мы стиля являются для нас действительно важными, а какие — нет. Поскольку в нескольких последующих главах мы будем рассматривать множество программных С-кодов, следует также установить заранее, какие обыч- но используемые стандарты нам нужны, а какие будем игнорировать и почему. Помните, что компилятору совершенно безразлично, какой стиль размещения кода вы приняли. Проблема здесь состоит лишь в читабельности кода для програм- мистов сопровождения ваших программ. А почему вы должны заботиться о том, смогут ли программисты со- провождения прочитать ваш код? Во-первых, это воп- рос профессионализма. Если выполнение работы дру- гими людьми усложняется из-за чьей-то небрежности, это обычно расценивается как признак отсутствия про- фессионализма. Во-вторых, отладка читабельного кода значительно облегчается. И в-третьих, человеком, на- значенным сопровождать ту или иную программу в те- чение трех или, быть может, пяти лет, вполне можете оказаться лично вы. Целью этой книги является осво- бождение вас — С-программиста от авторитарности и бессмысленности некоторых положений стандартов. С другой стороны, мы также хотим предостеречь вас от ловушек, в которые часто попадают С-программисты. ПОВТОРНЫЙ ПРОСМОТР КОДА Если вы думаете, что пересматривать свой собственный код спустя многие годы вряд ли придется, могу вас уве- рить, что это иногда случается. Я потратил более года, выполняя тестирование системы и отладку на сложном модуле расчета страховки. Когда моя задача была выпол- нена, я занялся другими вещами. Четыре года спустя я заключил контракт с совершенно другим клиентом и стол- кнулся ’’лицом к лицу" с тем же самым модулем! В не- урочный час этот клиент купил копию исходного кода у пре- дыдущего моего клиента. Я был весьма доволен тем, что буду читать свой собственный код: он был подробно прокоммен- тирован и вполне мог претендовать на принадлежность программисту, которым я очень восхищаюсь, но который имел на полдесятилетия меньший опыт, чем я. Так что давайте поближе рассмотрим эту область стилей и стандартов программирования и попытаемся
Пересмотренный язык С Часть I 36 выяснить, какие аспекты стиля программирования яв- ляются важными, а какие нам просто безразличны, и тогда, возможно, мы сможем стать посредниками в мир- ном урегулировании упомянутых выше войн (либо, по крайней мере, в прекращении огня). Мы начнем с темы, близкой сердцу каждого программиста: со стилей рас- становки фигурных скобок. Стили расстановки фигурных скобок Сегодня имеется четыре наиболее распространенных стиля расстановки фигурных скобок (bracing style), ис- пользуемых в С-сообществе. Не сомневаюсь, что вы будете иметь свое собственное мнение о том, какой из этих стилей правильный. Но давайте рассмотрим их по очереди. Стиль 1TBS 1TBS является аббревиатурой стиля One True Bracing Style. Это единственно истинный стиль расстановки скобок. Он получил такое прекрасное название потому, что был использован Кернигеном (Kernighan) и Ричи (Ritchie) в их классической книге The С Programming Language — Язык программирования С. С-программис- ты смотрят на Кернигена и Ричи (везде далее в этой книге K&R) как на "полубогов*’, так что название One True Bracing Style имеет вполне подходящий религиоз- ный оттенок. Некоторые люди предпочитают называть его К&В-стиль или рациональный (kernel) стиль. Сто- ронники этого стиля обычно используют отступы в во- семь пробелов, но это не догма. Ниже приведен отрывок кода, иллюстрирующий стиль 1TBS: for (j*0; j<MAX_LEN; 3++) { foo(); } Большинство программистов обычно полагают, что преимущество такого стиля состоит в экономии верти- кального пространства. Оборотной стороной такого преимущества является тот факт, что может оказаться трудно найти символ {, спрятанный в конце строки. Стиль Алмена Эрик Алмен (Eric Allman) написал утилиты BSD в этом стиле, и некоторые любители называют его "стиль BSD". Отступы в стиле Алмена обычно (но не всегда) состав- ляют четыре пробела: for (3=0; j<MAX_LEN; j++) { foo(); } Этот стиль (и последующие) занимает больше вер- тикального пространства, чем стиль 1TBS. Аргументом в поддержку такого стиля является тот факт, что область видимости блочного оператора ясна и визуально ассо- циируется с управляющим оператором. Стиль Whitesmith Одно время существовал С-компилятор, который назы- вался Whitesmith С. В его документации есть пример форматирования программного кода, подобного этому: for (3=0; 3<MAXJLEN; 3++) { foo(); Этот стиль имеет преимущество в том, что скобки более тесно ассоциируются с кодом, который они вклю- чают и разграничивают, однако при визуальном про- смотре текста отыскать скобки оказывается чуть более сложно. Здесь приняты отступы в четыре пробела. Стиль GNU Программисты GNU фонда Free Software Foundation используют (в частности, в коде GNU EMAKCS) гиб- рид стилей Алмена и Whitesmith. (Между прочим, GNU установлен для "GNU, отличных от UNIX", и аббреви- атура EMAKCS получена из названия "Editing MACroS". Это текстовый редактор UNIX, но называть его текстовым редактором только UNIX все равно что называть компьютер дополнительной машиной.) Ниже приведен пример стиля GNU: for (j=0; j<MAX_LEN; 3++) { foo(); I Трудно сказать, дает ли такая комбинация стилей Ал- мена и Whitesmith преимущества или имеет недостатки. Напрашивается вопрос: "Какой способ расстановки фигурных скобок правильный?", но, конечно, они все корректны. Самое главное, не следует смешивать разные стили в одной программе. Выберите для себя стиль и все время его придерживайтесь. Он может даже не подхо- дить вам. Если вы работаете в формальной среде разра- ботки, то почти наверняка имеете дело с некоторым документом стандарта кодирования, который носит обычно авторитарный характер и дает минимальную возможность придерживаться здравого смысла. Однако намного легче читать код, в котором использован еди- ный стиль, чем код, использующий несколько различ- ных стилей одновременно. Рассмотрим следующий ужасный код, который содержит почти предельно воз- можную смесь разных стилей:
Войны стандартов программирования: причины и пути перемирия Глава 2 37 for(j = О; j < MAX_bEN; j++) { for (к = 0; к < MAXWIDTH; к++) { for(m = 0; m < MAX HEIGHT; m++) { for(n M 0; n < MAX-TIME; n++) { foo(j, k, m, n); } } } } Компилятор будет вполне доволен таким кодом, но ни вы, и никто из членов вашей команды не захотели бы иметь с ним дело. Так что если ваши руководители предложили некий стиль, выберите его и постарайтесь убедить других, менее проницательных членов вашей команды делать то же самое (если бы они были прони- цательными, то читали бы эту книгу и вам не нужно было бы убеждать их). Если же ваши руководители не сослались на некий устоявшийся стиль расстановки скобок, они должны сделать это. В действительности не имеет значения, какой стиль они предложат. Каким бы он ни был, используйте этот стиль для данного проек- та и ваши товарищи-программисты (и группа сопровож- дения!) навсегда вас полюбят. Если вы не работаете в команде разработчиков про- екта и, следовательно, у вас нет руководителя, это пре- красно. Лишь выберите стиль расстановки фигурных скобок, с которым вы чувствуете себя наиболее комфор- тно, и последовательно его придерживайтесь. Пока мы занимаемся темой расстановки скобок, рас- смотрим конструкцию for, do, while, if или else с един- ственным следующим за ней оператором: if (condition) foo() ; Здесь, конечно, все правильно. А теперь посмотрим на следующую модификацию этого кода: if (condition) foo(); bar () ; Как видим, запись структурирована. Но для чего программист сделал отступ — зависит ли вызов новой функции от состояния (condition) в операторе if или нет? Это трудно сказать. По этой причине в докумен- тах по стандартам рекомендуется всегда использовать блочный синтаксис операторов: if (condition) { foo(); } Такая методика, безусловно, помогает избегать оши- бок. Тем не менее, многие С-программисты полагают, что она слишком громоздка. В своем собственном коде я лично стремлюсь использовать полный синтаксис блочных операторов даже в случае наличия единствен- ного оператора, поскольку это придает коду большей ясности; во фрагментах кода в этой книге мы часто опускаем необязательные скобки, но делаем это исклю- чительно в интересах экономии вертикального простран- ства. Использование пробелов Стили расстановки скобок — не единственное поле, на котором происходят баталии войны стилей программи- рования. Способы использования пробелов также ока- зывают значительное влияние на читабельность вашего кода. Если вы думаете, что где-то должен быть, конеч- но, один стиль расстановки пробелов, которого придер- живается каждый программирующий для максимизации читабельности своего кода, я боюсь, вы ошибаетесь. Здесь имеется столько стилей расстановки пробелов, сколько существует С-программистов. Отступы Что представляют собой правильные установки табуля- ции и отступов? Чаще всего выбирают два, три, четы- ре, восемь пробелов или ни одного. Я надеюсь, вы со- гласитесь, что мы можем отклонить нулевой вариант как делающий код полностью нечитабельным, но все дру- гие варианты являются вполне читабельными. Боль- шинство программистов начинают с использования за- данной по умолчанию установки табуляции, которая часто имеет восемь колонок в ширину (но не обязатель- но; это зависит от вашего текстового процессора). По- степенно, особенно в случае большого числа уровней отступов, код начинает выглядеть немного растянутым: int foo(int arr [А] [В] [С]) { int а, Ь, с, total = 0; for (а « 0; а < А; а++) { for(b = 0; b < В; Ь++) { for (с “ 0; с < С; с++) { total += arr[a](b][c); } } } return total; ) Поэтому многие уменьшают количество пробелов до четырех:
38 Пересмотренный язык С Часть I int foo(int bar [А] [В] [С]) { int а, Ь, с, total = 0; for (а = 0; а < А; а++) { for(b ® 0; Ь < В; Ь++) { for (с = 0; с < С; C++) { total += bar[а][b][c]; } > > return total; Лично я пользовался таким способом на протяжении многих лет. Многим позже я обнаружил, что для моих целей наиболее удобными оказываются отступы в два пробела. Это была установка, от которой я не отступаю уже длительное время. Я приобрел привычку использо- вать два пробела, когда начал регулярно публиковать статьи в Usenet. Я использовал клавишу пробела вмес- то табулятора не только потому, что установка табуля- тора у моих читателей дает слишком широкий отступ, но мне также не нравилась сама идея вставки табуля- ции в публикации Usenet. Коль скоро такое решение было принято, я быстро обнаружил, что уменьшение количества символов пробела в 12 раз для трехуровне- вого отступа позволяет мне сэкономить немало време- ни, так что я начал использовать отступы в два пробе- ла. К своему удивлению, я нашел, что мне нравится это больше, чем четыре пробела, так что теперь я исполь- зую два пробела все время. Отступ в три пробела для многих С-программистов представляется неестественным. Тем не менее, он ис- пользуется на некоторых сайтах, и при этом текст выг- лядит неплохо. Какая же установка табуляции и отступа является правильной! Вы опередили меня, я знаю — действитель- но, нет одного правильного уровня. Если вашим проек- том стандарта кодирования предусмотрена установка, которую вы должны использовать, выберите уровень, который, по вашему мнению, выглядит наиболее чита- бельно, используйте его последовательно в своей про- грамме и будьте готовы пересмотреть свое мнение че- рез некоторое время. Табуляторы и мэйнфреймовские компиляторы Некоторые мэйнфреймовские С-компиляторы не отве- чают должным образом на вставку символов табуляции в исходные файлы. Если вы чувствуете, что попадаете в собственную ловушку, может оказаться необходимым потратить время (или деньги) на программный инстру- мент для замены табуляции пробелами. Фактически вы можете уже иметь такой инструмент. Например, редак- тор Microsoft Visual C++ имеет эту возможность. Пробелы вокруг символов Для улучшения читабельности программного кода сле- дует творчески подходить к использованию пробелов. Снова-таки,нет единственно правильного способа рас- становки пробелов, но неправильных способов — мно- жество. В приведенном ниже примере пробелы исполь- зованы скверно: #include <stdlib.h> iinclude <stdio.h> Idefine P printf idefine I atoi int main(int archar*v []){int r=5r i;if(a>l ) r=I(v[l]); if(r<=0) r=5;if(r%2==0)++r;for (i=0; i<r*r; P(i/r==( 3*r)/2-(i%r+l)||i/r== r/2 - i%r||i/r==r/2+i %r||i/r==i%r-r/2?"*": " ")ri++, i % r==0?P( "\n") : 0);return 0;} (Если вы удивлены, что такое вообще может быть, спешу сообщить, что я не выудил это из материалов Международных соревнований по головоломному С- кодированию. Я написал этот текст лично, так что могу посылать его в IRC (Internet Relay Chat — Обмен дру- жескими посланиями через Internet) всякий раз, когда кто-то задает довольно общий для домашней работы вопрос, к которому этот код подходит. Выяснение воп- роса о том, какие функции выполняет эта программа, я оставляю для вас в качестве упражнения. Сделать это сразу практически невозможно.) Вероятно, наиболее эффективный способ повышения читабельности кода состоит в том, чтобы ставить пробе- лы вокруг бинарных операторов. Рассмотрим строку a=b+c*d/e%f; и строку a = b + c*d/e%f; Какую из них, по вашему, легче читать? Именно так делаю я. Некоторым людям нравится также устанавливать пробелы вокруг круглых скобок: for ( foo = О ; foo < bar ; foo++ ) и даже вокруг прямоугольных скобок: а [ baz ] = О ; Лично мне кажется, что это уж чересчур усердное использование пробелов, но это стиль, с которым вы время от времени будете встречаться.
Войны стандартов программирования: причины и пути перемирия Глава 2 39 Косметические исправления кода Многие люди очень часто в процессе сопровождения кода, который не они лично написали, поддаются ис- кушению корректировать стили расстановки скобок и пробелов, чтобы угодить руководителям проекта, или даже делают это по своему вкусу. Я лично никогда это- го не делаю (по крайней мере, не тогда, когда за мной кто-то смотрит). Пока руководитель проекта вполне определенно не попросил вас сделать это, не проявляйте инициативу. Часто бывает полезно иметь возможность сравнить две версии исходного файла: текущую и пре- дыдущую, свободную от ошибок версию, чтобы увидеть, чем они отличаются и. следовательно, узнать, где мо- жет крыться ошибка. Если кто-то "поигрался" с пробе- лами, вы обнаружите множество ложных улучшений, которые замедляют вашу работу и оказываются причи- ной снижения вашей производительности (не говоря уже о явной раздражительности, которую вызывают такие манипуляции). Если вы не ведете историю реви- зии своего кода (хотя это и не принципиально, конеч- но), вы должны рассмотреть вопрос о внедрении хро- нологии изменений или системы управления версиями. Возможно, сейчас стоит упомянуть, что имеется утилита, называемая indent, которая может форматиро- вать код непротиворечивым образом. Если команда со- гласовала стиль и всегда пропускает код через утилиту indent перед проверкой его в системе управления вер- сиями, многие проблемы, связанные с расстановкой пробелов, отпадут. Утилита indent включена в дистри- бутивы Linux. Ее исходный код вы можете найти по адресу ftp://ftp.gnu.org/gnu/indent (но проверьте снача- ла адрес http://www.gnu.org, чтобы выяснить, имеется ли местный аналогичный сайт, который можно было бы использовать вместо вышеуказанного. Таким обра- зом ускоряется загрузка нужной программы). Структурное программирование С тех пор как Дейкстра (Djikstra) опубликовал свой знаменитый труд "Вредность оператора GOTO", люди бились над методами структурного программирования Полное описание структурного программирования вы- ходит за рамки этой книги, но здесь имеются опреде- ленные темы, которые регулярно неожиданно возника- ют в документах стандартов кодирования, так что давайте их коротко рассмотрим. Оператор goto Не только Дейкстра жалуется на оператор goto. На с. 65 книги K&R2 (Принятое автором сокращение для обо- значения книг Кернигена и Ричи, см. с. 36. — Прим, науч, ред.) мы найдем глубокие переживания Брайана Кернигена относительно того, что "[Язык] С обеспечи- вает бесконечно оскорбительный оператор goto". Если вы удивлены, откуда я знаю, что это был именно он, а не Дэннис Ричи, сообщаю: их общее соглашение состо- яло в том, что большую половину первой части книги — учебный раздел — пишет Керниген, а Ричи делает боль- шую часть или даже весь раздел ссылок.) Использование оператора goto может серьезно зат- руднить реализацию управляющей логики вашего кода. Исходный код, содержащий множество операторов goto, часто графически описывается как "код-спагетти", по- этому в документах по стандартам кодирования боль- шинства проектов осуждается их применение. Обычно довольно просто после некоторых размыш- лений найти альтернативу применению оператора goto. Этот процесс будет часто включать использование до- полнительной переменной состояния, поэтому про- граммисты встроенных систем (См. главу 27 "Встроен- ные системы". — Прим. науч, ред.) используют оператор goto чаще, чем другие, — во встроенных системах все- гда имеет место дефицит памяти. Основной причиной применения оператора goto является то, что он позволяет быстрее осуществить пе- реход из глубоко вложенного кода в случае возникно- вения ошибки. Это оправдание представляется вполне обоснованным, так что не следует быть догматиком и наотрез отказываться от использования goto. Тем не менее, по возможности самым пристальным взглядом сначала оцените возможность других решений. Если уж вы решили, что без goto не обойтись, имейте в виду, что обычно проще следовать управляющей логике вниз по коду, так что попытайтесь убедиться, что вы переходите вперед по коду, а не назад. Оператор break Оператор break очень полезен при использовании внут- ри оператора switch. Некоторым также нравится ис- пользовать его внутри циклов для обеспечения преждев- ременного выхода из цикла. Это не является частичным структурированием использования оператора, поэтому я никогда не применяю оператор break таким способом. Я не хотел бы ошибиться, сказав, что преждевременный выход из циклов является нежелательным. Однако я знаю многих высококвалифицированных и вдумчивых программистов, которые придерживаются мнения, что код должен отражать намерения своего создателя, так что, если вашей целью является преждевременное пре- рывание цикла, ваш код должен ясно показывать это намерение. Это мощный аргумент, который, случается, не вписывается в способ, которым я предпочитаю структурировать свои программы. Если вы все же наме- рены использовать в цикле оператор break, потрудитесь вставить в исходный код короткий пояснительный ком- ментарий, особенно если код достаточно плотный.
40 Пересмотренный язык С Часть I Оператор continue Подобно break, оператор continue можно использовать для модификации логики циклов таким образом, что пурист структурного программирования разразился бы безудержным кашлем (хотя этот оператор еще не так плох, как оператор ALTER в Коболе, который изменя- ет адрес перехода по оператору goto!). В отличие от break, оператор continue не удостоился быть связанным с оператором switch. Хорошо ли это? Оператор continue я использую только в пустых циклах в качестве индикации сопровождающему про- граммисту, что цикл является преднамеренно пустым, наподобие этого: while (*s++ = *t++) { continue; } Я бы хотел порекомендовать, чтобы вы ограничили применение оператора continue только этим случаем, но не могу этого сделать. Сторонники оператора continue выдвигают случаи, наподобие следующего: while (fgets (buffer, sizeof buffer, fp) !=. NULL) { if (strchr ("; I buffer [0] 1= NULL) { /* Это строка комментария - начните с * символа точки с запятой (;), диеза (#) * или слэма (/), чтобы пропустить его и * перейти на следумцу» строку */ continue; } /* Здесь помечается больной блок кода */ Здесь оператор continue упрощает код путем немед- ленного отклонения бесполезного состояния и попада- ния прямо на следующую итерацию цикла, избегая при этом необходимости добавлять else и дополнительный уровень отступа для большого блока, следующего за блоком if. Так что, опять-таки, имеются совершенно правомерные причины для принятия решения о том, применять оператор continue или нет. Давайте немного поразмыслим здраво. Перед тем как использовать оператор continue, остановитесь и поду- майте: ’’Почему я использую этот оператор: потому ли, что это быстрее и легче, чем адаптировать более струк- турный подход, или я искренне хочу обеспечить чита- бельность программы?” Если имеет место последний случай, есть все основания использовать continue. Цикл while(1) Это общая конструкция. Почти неизменно тело этого цикла содержит операторы break или return или даже вызов функции exit(). Заголовок такого цикла говорит: ’’Этот цикл выполняется вечно”, а тело цикла противо- речит этому. Ясно, что тело цикла выполняется все вре- мя до победного конца, но в действительности этого быть не может. Если нашей целью является сделать программный код легко читаемым, мы можем сделать это путем документирования цели цикла while в его за- головке и выбора подходящего конечного состояния. Можно, например, написать: int more = GetFirstItem(&foo) ; while(more) { Process(&foo); more = GetNextltem(Sfoo); } вместо if(GetFirstltem(&foo)) while(l) { Process (lifoo); if(!GetNextItem(&foo)) break; I где, хотя и делается попытка сократить объем кода, фактически оказывается на одну строку текста больше, чем в проверенном временем примере с использовани- ем переменной состояния. Иногда нам действительно необходим бесконечный цикл, который специально предназначен для выполне- ния в течение неопределенного времени (или, по край- ней мере, пока кто-нибудь не отключит микроволновую печь). С этой точки зрения, конечно, применение цик- ла while(l) вполне обосновано. Однако имеет место слу- чай, когда применяется цикл for (; ;) { /* ... */ ) поскольку здесь почти наверняка не будет генерировать- ся сообщение, выдаваемое некоторыми компиляторами при использовании while (1), — что-то наподобие стро- ки "Выражение состояния является константой”. Я сказал ’’почти наверняка”, потому что компилятор ANSI С может выдавать столько диагностических сооб- щений, сколько ему нравится, включая и такое: "Предупреждение: строка 37: ват мандарин требует заново покрасить его в желтый цвет и две чашки кофе”.
Войны стандартов программирования: причины и пути перемирия Глава 2 41 или еще более взволнованное: "Предупреждение: строка 1: вы эксплуатируете этот компилятор уже несколько НЕДЕЛЬ, почему бы вам не купить более совершенный? Вы ведь знаете, что хотите этого". в течение всего времени, пока он правильно компили- рует правильный код и правильно диагностирует син- таксические ошибки и нарушения ограничений. Оператор return Один из принципов структурного программирования состоит в том, что программа должна иметь одну точку входа и одну точку выхода. В языке С легко сконстру- ировать программу с многими точками выхода. К сожа- лению, так же просто сконструировать программу на- подобие этой (даже не пытайтесь выяснить, что этот код означает! Это только иллюстративный пример): char *fоо(char *s, char *t) { switch(*s) < case 0: return s; case 1: return t; case 2: return t - s; case 4: return s - t; case 8: return s + sizeof(long); case 16: return t + sizeof(double); default: break; } } He нужно быть гением, чтобы определить, что в общем случае не будет возвращено никакого значения. В более крупной функции, однако, со значительно большим числом разветвлений управляющей логики даже гений не обнаружил бы ошибки. К счастью, боль- шинство компиляторов предупредят вас об этом, но здесь нет никаких нарушений ограничений или син- таксических ошибок, поэтому Стандарт и не требует диагностического сообщения. Если же вызывающая функция использует значение, возвращаемое вызывае- мой функцией, из которой фактически не был заплани- рован возврат какого бы то ни было значения, поведе- ние программы будет неопределенным. Кроме того, функции со многими операторами return более сложны для чтения, чем те, которые имеют лишь один опера- тор return в конце программы. Конечно, реальная жизнь непроста, и бывают вес- кие причины для использования в некоторых функци- ях множества операторов return. Например, программу поиска в двоичном дереве можно написать значитель- но более просто, если мы используем несколько возвра- щаемых значений: TREE *SearchTree(TREE *node, char *s) { int diff; if(node == NULL) . return NULL; if(s == NULL) return NULL; diff = strcmp(s, node->data); if (dif f == 0) return node; else if (diff < 0) return SearchTrее(node->left, s); return SearchTrее(node->right, s); } Здесь очень легко проследить логику управления обработкой данных. Комментарии могут еще немного прояснить логику, но сам код оказывается более или менее самоопределяемым (по крайней мере, с точки зрения управляющей логики. Если вам не посчастливит- ся самостоятельно разобраться со смыслом вызывающей функции в этом примере, не паникуйте — рекурсию мы будем рассматривать далее в этой книге). Фактически этот код значительно короче, чем эквивалентный код только с одним оператором return. Моя собственная библиотека обработки двоичных деревьев фактически имеет лишь по одному оператору return в каждой функции, возможно потому, что я — законченный пурист. Так что есть веские аргументы для использования обоих подходов, поэтому в каждом кон- кретном случае придерживайтесь здравого смысла. СУМАСШЕДШИЕ УКАЗАТЕЛИ ФУНКЦИЙ Несколькими проектами ранее я работал на сайте, где стандарты кодирования утверждали: "Не используйте опе- ратор goto”. Проект предусматривал создание системы кодировки для зарубежной компании и содержал не- сколько переписанных со старого языка QBASIC программ на С, но программисты не имели доступа к самим QBASIC- программам. Актуарии (Статистики страхового общества (actuaries). — Прим, пер.) не сидели сложа руки и пе- ревели программы в спецификации. Актуарии сказали, что они не хотят делать никаких предположений относи- тельно доступных языку С возможностей, поэтому они ’’распороли” все циклы for QBASIC-программ. Таким об- разом, вместо того чтобы сказать: "Цикл по клиентам, вычисляющий дату их выхода на пенсию по датам их рож- дения", они говорили примерно следующее: 1.4.9.3 Установить идентификатор клиента ClientID в 1 1.4.9.4 Вычислить дату выхода на пенсию клиента с индексом ClientID, используя дату его рожде- ния и системную дату
Пересмотренный язык С 42i Часть I 1.4.9.5 Сохранить дату выхода на пенсию в поле RetAge клиента Client с индексом ClientID 1.4.9.6 Добавить 1 к индексу ClientID 1.4.9.7 Если индекс ClientID превышает количество кли- ентов для данной котировки, продолжить с шага 1.4.9.9 1.4.9.8 Продолжить с шага 1.4.9.4 1.4.9.9 ... Кроме того, поскольку актуарии были экспертами в об- ласти страховой статистики (а мы, программисты, — нет), мы находились под прессом инструкций, предписывающих в точности следовать спецификациям. Теперь большинство из нас понимают, что актуарии были только помехой, так что, естественно, мы игнорировали ограничивающие предписания и прокрутили код так, как это делается в соответствующем цикле (и действительно, один из них признался мне позже, что мы сделали в точности то, чего они и хотели). Один довольно педантичный програм- мист (обычно таким бывает один из лучших, но не все- гда) предложил другую идею. Он взял относительно про- стые спецификации и решил объединить оба принципа ("В точности следуйте спецификации" и "Не используйте goto") в символы. Везде, где он встречал оператор goto в спецификации, он записывал код функции обработки с помощью цикла. Такая функция возвращала бы указатель на следующую выполняемую функцию! Драйвер всего этого кода выглядит достаточно жестоко. Отладка кода и его модификация для внесения требуе- мых изменений были учебным экспериментом. (Факти- ческий автор кода должен был его отлаживать, поэтому он, конечно, покинул компанию.) В конечном счете я решил перенести сеть указателей функций и результиру- ющий документ в диаграмму, которая вылилась в третью самую страшную диаграмму в моей карьере. Однажды я схематически изобразил код, и он уже не выглядел так страшно, поскольку, по крайней мере, те- перь мы ясно увидели, куда код должен переходить даль- ше. Но когда мне показали оригинальную QBASIC-npo- грамму, я начал понимать, насколько простым был этот код и насколько ясно и легко он реализован с использо- ванием циклов. Безумные указатели функций — очень интересные штучки для забав, но им не место в разра- батываемых программах (хотя у меня нет проблем с нормальными указателями функций). Любые обобщения неверны (включая и это), но все- таки я намерен высказать общую точку зрения: чем более структурированно вы будете подходить к программирова- нию, тем меньшее количество ошибок будете получать при работе вашей программы. Более структурированный вами код читать очень легко. Код, который легко читать, явля- ется более простым для понимания, а значит, будет мо- дифицирован скорее, чем трудный код, и в результате будет иметь значительно меньше ошибок. Инициализация В языке С имеются скрытые ниши. Даже такая очевид- ная и простая вещь, как правильное выполнение ини- циализации, может оказаться достаточно сложным де- лом. Вопрос не только в стиле выполнения; переноси- мая инициализация действительно не так проста, как это кажется. Множественные определения в одной строке Как вы знаете, С позволяет определять множество объектов одного и того же типа и указатели на объекты этого типа в одной логической строке. Вы, вероятно, уже приготовились практически использовать такую возможность, и я не хочу сделать ошибки, отговаривая вас от этого. Советую, однако, принять к сведению те преимущества, которые дает определение каждой пере- менной в отдельной строке. Рассмотрим такой код: int foo(char *s, char *t, size_t len) { float PiTo2DecimalPlaces = 3.14, q, CircleRadius - 1.0; /* input file output file */ FILE * fpln, fpOut; /* ... */ > Во-первых, если мы хотим модифицировать тип q в тип, скажем, double, мы приходим к необходимости поместить его новую инициализацию где-нибудь в от- дельной строке. Так почему же не поместить ее в свою собственную строку с самого начала?! Во-вторых, труд- но что-либо найти среди этих соседствующих друг с другом длинных имен. И в-третьих, ошибка в типе IpOut (это должен быть тип FILE *, но фактически это только FILE) случается слишком уж часто. Инициализация в определении Язык С позволяет вам выбирать, инициализировать ли объект при его определении. Инициализация перемен- ной во время определения дает вам два преимущества. Первое заключается в том, что вам не нужно будет за- давать переменную начального значения. А второе по- зволяет вам быть уверенным, что функция начинает выполнение в известном состоянии. Например, установ- ка указателя в NULL в определении может не гаранти- ровать отсутствие ошибок указателя в вашей функции, но, несомненно, облегчит обнаружение и фиксацию таких ошибок. С другой стороны, если вы не задаете значения пе- ременной в определении, компилятор, перед тем как этой переменной будет присвоено значение в самом коде, значительно лучше разместит ее для последующе- го использования. Кроме того, некоторые компилято- ры будут выдавать предупреждение, если вы зададите
Войны стандартов программирования: причины и пути перемирия Глава 2 43 значение переменной, которая затем последовательно пе- реписывается путем другого назначения, как в записи int а = О; for (а — О; а < 10; а++) Опытные программисты предпочитают получать чистую компиляцию всякий раз, когда это возможно. Это связано с тем, что даже тривиальные предупрежде- ния (наподобие того, какое будет сгенерировано в слу- чае предыдущего кода), если позволить им размножать- ся, скоро затемнят все другие, более важные сообщения, делая их обнаружение и исправление соответствующих ошибок более затруднительным. Так что, повторю снова, догматиком быть неразум- но. Возможно, лучше применять смешанную стратегию. Конечно, хорошо сразу инициализировать указатель в NULL; не следует недооценивать также удобство ини- циализации массива или структуры с использованием {0}. Большинство программистов обнуляют массив или структуру примерно следующим образом: struct FOO foo; namset (&foo, sizeof foo); К сожалению, это не гарантирует получения жела- емого эффекта, если определение структуры FOO содер- жит числа с плавающей точкой (типов float, double или long double) или указатели, в которых представление нуля не гарантирует фактического обнуления всех би- тов. Например, в стандарте С нет ничего, что бы поме- шало реализации представлять указатель NULL с ис- пользованием набора битов 0x80000000 (неявно) в предположении, что соответствующие преобразования выполняются для кода, такого как: char *foo = О; Наш "нечетно мыслящий" компилятор может обна- ружить такое назначение и инициализацию и загрузить указатель с соответствующим битовым набором — 0x80000000 — позади сцены. К сожалению, мы не можем прервать (перехватить) memset таким же способом; стан- дарт ANSI С не позволяет это. Во многом такие же рас- суждения применимы и к числам с плавающей точкой. Большинство компиляторов не нуждаются в потвор- стве обману такого рода, но у вас нет гарантий, что ваш компилятор (или возможный следующий компилятор, на котором вы будете создавать свой код) действитель- но будет обнулять все биты в числах типа 0.0 и указа- телях NULL. К счастью, имеется простой способ, по- зволяющий инициализировать все элементы структуры в соответствующие полагаемые для них величины: struct FOO { double d; int пив; double e; float f; char *p; }; struct FOO foo = {0}; При инициализации структуры struct таким спосо- бом (применимым также к массивам) инициализация оказывается незавершенной. Вот что говорит стандарт о частичной инициализации: "Инициализация должна происходить в порядке списка инициализаторов, каждый инициализатор для конкретного подобъекта предусматривает отме- ну любого ранее перечисленного инициализатора для этого же подобъекта; все подобъекты, которые не инициализированы явно, должны быть инициа- лизированы неявно так же, как и объекты, которые имеют постоянный размер памяти". (Стандарт С9Х, раздел 6.7.8, январь, 1999 г.). Переведем это на доступный для понимания язык: в определении struct FOO foo = (О); элемент foo.d принимает значение 0 в скобках (и пре- образуется в 0.0), элемент foo.num инициализируется в 0, элемент foo.e — в 0.0, элемент foo.f — в 0.0F, а эле- мент foo.p — в NULL — независимо от внешнего представления чисел с плавающей точкой и указателей. Таким образом, мы имеем полностью переносимую методику инициализации структур и массивов. Статические и глобальные объекты Имеется два рода идентификаторов области видимости файла: идентификаторы области видимости файла с внутренней связью (статические объекты и функции) и идентификаторы области видимости файла с внешней связью (внешние объекты и функции). Последние обыч- но известны как глобальные объекты (хотя упоминание о глобальных переменных, которое я смог найти в стан- дарте ANSI, связано только с флагами и режимами сре- ды). Документы стандартов написания кодов программ часто рекомендуют или даже настаивают, чтобы гло- бальные переменные не использовались, а программи- сты так же часто игнорируют эти рекомендации. Кто прав? ГРИИЕЧАНИЕ В языке С, в котором объект представляет собой поиме- нованную область памяти, переменные представляют собой наиболее очевидный пример объектов. Их не сле- дует путать с экземплярами класса в C++, которые так- же называются объектами.
Пересмотренный язык С Часть I В данном случае я буду приветствовать документ стандарта и приведу только один пример, объясняю- щий, почему я это делаю. Прискорбно, что обычным делом для разработчиков приложений, находящихся в тисках установленного срока контракта, стало бросать структурность и модульность на ветер в попытках по- лучить программный код с первого раза В результате часто получаются очень длинные функции. Рассмотрим импликации особенно длинной функции, использующей набор глобальных переменных. Код был написан в последний день выделенного для этого време- ни и сопровождается в последний день следующего край- него срока. Понадобилось с помощью этого кода выпол- нять новые расчеты. Протраммисту сопровождения нужна новая целая переменная, поэтому он добавляет ее поверх функции: double PresentValueOfFutureProfits = 0.0. Он использует эту переменную в разных местах в соответ- ствии с программой новых требований спецификации. Если такой программист ранее не просмотрел этот код, он обязательно потерпит неудачу, поскольку его локаль- ное определение переменной PresentValueOfFutureProfits было неосторожно замаскировано переменной с облас- тью видимости файла и внешней (глобальной) связью, видимой в том же самом исходном файле. Эта перемен- ная случайно тоже имеет тип double. Программист не может обнаружить этого, и кол модуля отправляется на тестирование, которое он с блеском проходит. Насту- пает общее тестирование, и программа проходит его с "незапятнанной” репутацией. К счастью, ошибка была в конечном счете выловле- на — в группе User Acceptance Testing. (Приемочное пользовательское тестирование. — Прим, пер.) Потребо- валась большая часть дня, чтобы проследить фактичес- кую причину ошибки. Когда она была найдена, ее ис- правление оказалось тривиальным (изменение имени локальной переменной, хотя даже затем была проведе- на проверка с учетом истории версии функции, чтобы гарантировать, что были изменены только корректные ссылки на имя). Описанную историю я не выдумал — этим программистом сопровождения был я. Не хочу обременять вас описанием деталей моей реакции, ког- да я обнаружил, что меня подвела глобальная перемен- ная (явно запрещенная стандартами кодирования про- грамм); предлагаю читателям самим прочувствовать это. Если обстоятельства все-таки заставляют вас исполь- зовать переменные с областью видимости файла (будь они с внешними или с внутренними связями), рассмот- рите возможную необходимость ограничения доступа к ним путем написания функций доступа. Но перед тем как вы это сделаете, нам нужно еще кое-что рассмот- реть. Проблемы с повторным использованием Давайте возьмем простую стековую библиотеку. Наш план очень прост — мы хотим помещать (проталкивать, push) кое-какие элементы в стек и извлекать (выталки- вать, pop) из него эти элементы. Поскольку мы имеем две функции для доступа к стеку, нам нужно разделить информацию между ними. Введем переменную облас- ти видимости файла. Однако, поскольку нам не нужно разделять информацию с другими функциями, мы по- местим функции push() и рор() в их собственный ис- ходный файл, который не содержит других функций, кроме этих двух, и сделаем наш стек статическим — static. Это хороший пример скрытия информации. tdefine MAX_STACK 1024 int stack[MAXSTACK]; int stackptr; int push (int i) { int failed = 1; if(stackptr < MAXSTACK - 1) { stack[stackptr++] = i; failed ~ 0; } return failed; } int pop(int *i) { int failed a 1; if(stackptr > 0) { *i = stack[-stackptr]; failed = 0; } return failed; Таким образом, получили одну стековую библиоте- ку. К сожалению, пока у нас имеется несколько про- блем, из которых сейчас интерес для нас представляет только одна, программы могут использовать эту библио- теку для создания и управления только одним стеком. Имеется только один стековый указатель и только один стек, так что каждый раз мы можем использовать толь- ко один стек. Конечно, человеческая изобретательность безгранична, и было бы не так трудно написать стеко- вую библиотеку, которая будет полностью скрывать дан- ные и все же обрабатывать множество стеков. Но сделать это было бы труднее, в то время как существует более простой метод, предусматривающий использование тех- нологии передачи структуры в качестве аргумента. Следующая проблема со статическими переменны- ми static состоит в том, что они могут подвести вас в многопоточных средах. Язык С не поддерживает поточ-
Войны стандартов программирования: причины и пути перемирия Глава 2 45 ность, но его реализации часто обеспечивают такую поддержку. В типичной потоковой среде переменные рискуют оказаться доступными и измененными одно- временно более чем одним потоком. Ничего из того, о чем мы сейчас говорили, не пред- назначено, чтобы препятствовать вам в использовании статических переменных, но важно помнить, что со статикой связаны и другие проблемы, отличные от про- стых правил установления области видимости. Имена идентификаторов Соглашения именования часто являются источником горячих дебатов. Я не предлагаю выделить какое-либо конкретное соглашение, поскольку это будет еще одним залпом в рассматриваемой нами войне стилей програм- мирования. Следующий ниже обзор основан на моем личном опыте, но другой опытный программист впол- не может иметь свою отличную от моей точку зрения. Длина Длина имени идентификатора, конечно, оказывается пропорциональной его описательной способности. Если вы планируете использовать переменную интенсивно, будет разумно дать ей осмысленное имя, что обычно предполагает длинное имя. Если же, с другой стороны, вам нужен лишь счетчик цикла, то в длинном имени, вероятно, нет необходимости. Многие в качестве счетчи- ков цикла используют i, j и к. Если они участвуют в про- стых циклах, это хорошо. Однако, если вы заполняете массив массива массивов (!) некоторой структуры, вы вполне можете захотеть выбрать более удобные имена для своих счетчиков, чтобы они напоминали вам о том, что фактически обозначает каждый индекс массива. Имеются ограничения на значимую длину идентифи- каторов. Вы можете делать идентификаторы сколь угод- но длинными, но гарантированно значимыми они бу- дут иметь только 31 символ. Фактически и 31 символ — это много. В именах идентификаторов с внешними свя- зями, однако, гарантированно уникальными могут быть только до шести первых символов. И это плохо — нет гарантии, что редактор связей в любом случае сможет различить внешние идентификаторы. Таким образом, редактор вполне может рассматривать следующие опре- деления: int NotableValue; int notablevalue; int NoTableFound; int NotAbleToExecute; int NotABlankSpace ; как ссылку на один и тот же идентификатор. Это серь- езное ограничение, которое было несколько смягчено в более поздней версии стандарта ANSI/ISO С. Однако, пока не наступит такое время, когда компилятор С99 станет общепринятым, есть смысл просто избегать вне- шних связей где только возможно. Заметим, что это ограничение относится к идентификаторам вообще, а не только к переменным. Таким образом, в именах функ- ций, не квалифицированных ключевым словом static, гарантированно значимыми могут быть лишь до шести символов. Ясность Выбирая имя для идентификатора, помните о трех мо- ментах. Вам самому придется набирать это имя на кла- виатуре. Эта "темная личность" — программист сопро- вождения — должен будет читать его. И конечно, вам обоим нужно знать, что это имя означает. Следователь- но, есть смысл выбирать такие имена, которые легко читать, набирать на клавиатуре и которые однозначно определяют свой объект. Язык С традиционно является языком нижнего ре- гистра, но ничто не помешает вам использовать смешан- ный регистр. Ниже приведены несколько общеприня- тых стилей именования идентификаторов: all__lower_case_with_underscores_to_separate_words lowerCaseFirstLetterThenMixedCase AllMixedCase UPPERCASENOSEPARATOR UPPER_CASE_WITH_SEPARATOR Хотя С делает различие между регистрами, исполь- зование имен идентификаторов, отличающихся только регистром, проблематично. Кроме того, что некоторые редакторы связей не способны отличать регистры, име- ется и проблема читабельности: int foo (int Foo) { int foo, FoO, foO, FoO = FOO /* ... */ 1 Такое можно позволить себе в ЮССС (Input/output control center/command — Узел/команда управления вво- дом/выводом. — Прим. науч, ред.), но никак не в разра- батываемом программном коде! Йнгда нбхдмо скращть иднтфктры до аббрвтр, чсто путм выбрсывния гленх бкв. (Как должен был догадать- ся читатель, это не опечатки — автор демонстрирует технику сокращения записи слов без существенной по- тери смысла. — Прим. науч. ред.). Например, самое луч- шее имя для переменной может оказаться довольно длинным: double Presen tValueOfFutureProf itsCalculatedGros s ; double PresentValueOfFutureProfitsCalculatedNet;
46 Пересмотренный язык С Часть I Кроме того, что эти имена слишком длинны и не- удобны для набора на клавиатуре, они еще нарушают правило, в соответствии с которым гарантируется раз- личение только первых 31 символа. Аббревиатуры double PrsntVlfFtrPrftsClcltdGrss ; double PrsntVlfFtrPrftsClcltdNt; конечно, представляют два различных идентификатора — с точки зрения компилятора, но разве улучшилась их обозримость? Я так не думаю. Такие имена переменных выглядят просто ужасно! Эти идентификаторы уже смотрятся лучше, пусть даже они несколько длиннее, чем просто аббревиатуры: double Presen tValFutureProfitsGross ; double PresentValFutureProfitsNet; хотя они все же слегка длинноваты, чтобы быть удоб- ными в использовании. В таком случае есть смысл про- сто укоротить их double GrossPVFP; double NetPVFP; поскольку PVFP — вполне распознаваемая аббревиату- ра. Как бы там ни было, используйте аббревиатуры, распознаваемые внутри вашего приложения соответству- ющим образом. Главным принципом должно быть то, что различные переменные в программе должны быть легко различимыми для читателя. Зарезервированные имена Что неправильно в этом заголовочном файле? fifndef RAGBAG Н idefine _RAGBAG_H_ extern double strength; extern char memorandum[1024]; extern int isotope; extern float tolerance; #endif Затрудняетесь сказать? Вы будете очень удивлены, когда я скажу вам, что все идентификаторы в этом файле заголовка, включая символьные константы, нарушают стандарт ANSI! Поскольку язык С имеет очень немного ключевых слов и соответственно маленькую стандартную библио- теку, многие программисты обычно полагают, что они могут называть свои идентификаторы почти как угод- но. Это совершенно не так. Во-первых, любой идентификатор, который начина- ется с символа подчеркивания, за которым следует либо буква в верхнем регистре, либо другой символ подчерки- вания, являются зарезервированными для использования в реализации компилятора. Это обеспечивает реализа- цию способом определения функций и переменных без пересечения с вами и вторжения в вашу область имен. А ведь было бы хорошо совсем оставить за реализацией фор- мирование области имен! Безопаснее всего не начинать имен идентификаторов с символа подчеркивания. Хотя_- кснечносимволыподчеркиванияэтохорошаявещь. Хорошо, что реализации обычно включают защиту вокруг своих заголовков. Но это хорошая вещь и для пользователей-программистов. Прискорбно, но привыч- ка копировать соглашениеJ4AME__H_ реализатора ста- новится широко распространенной. (Реализаторы вер- сии компилятора разрешают использовать лидирующие символы подчеркивания. Смысл ограничения состоит в том, чтобы предотвратить конфликт ваших идентифи- каторов с идентификаторами реализации.) Как бы там ни было, используйте последующие символы подчерки- вания, но если вы хотите, чтобы ваша программа была переносимой, не используйте лидирующих символов подчеркивания. Ниже приведен хороший способ обеспечения вклю- чения защиты: fifndet NAME_H_ fdefine NAME_H_ /* Здесь следует содержательная часть заголовка * / /* ... */ fendif А что насчет объявлений в этом заголовочном фай- ле? Почему они недопустимы? Все здесь связано в расширяемостью. Комитет ANSI признает, что языки либо развиваются, либо умирают. Недавнее обновление стандарта С, например, предста- вило различные мощные новые методики (которые мы рассмотрим далее в этой книге). Определенно, комитет хотел выделить место для новых функций в стандарт- ной библиотеке и в то же время минимизировать воз- можность разрушения существующих программ. Чтобы покончить с этим, они зарезервировали определенные комбинации букв, которые частично подобны тем, ка- кие формируют начала новых имен функций. Напри- мер, все, что начинается с "str" (Сокращение от string — строка. — Прим, пер.), зарезервировано, поскольку, хотя имеется уже множество стандартных библиотечных функций, начинающихся с "str", существует много по- лезных процедур обработки строк, которым программи- сты захотели бы дать имена, начинающиеся с "str". Что- бы выйти из этого положения, комбинация букв "str" была зарезервирована. То же самое относится и к "is" и "to" (чтобы можно было расширять <ctype.h>), "mem" (для функций манипуляции дополнительной памятью), "SIG" (для новых сигналов) и несколько других пре- фиксов. Ниже приведен сокращенный список всех иден- тификаторов, которые вы не можете использовать.
Войны стандартов программирования: причины и пути перемирия Глава 2 47 В целях экономии места я опустил наиболее стандарт- ные библиотечные функции и другие идентификаторы в глубоком убеждении, что если кто-то достаточно умен, чтобы купить эту книгу, он будет так же умен, чтобы не использовать эти символы. Но в список я включил неко- торые наиболее странные и наименее известные из заре- зервированных идентификаторов. ПРИМЕЧАНИЕ Чтобы еще больше сократить список, я использовал ре- гулярный синтаксис выражений. Здесь [A-Z] означает любой символ алфавита в верхнем регистре; [0-9] озна- чает любую цифру; * (звездочка) означает все что угод- но, и т.д. Итак, без дальнейших рассуждений просто не ис- пользуйте эти идентификаторы. Е[0-9]* LC[A-Z]* Offsetof str[a-zj* E[A-Z]* mem[a*-z] raise to[a-z]* is[a-z]* NDEBUG SIG[A-ZJ* wcs[a-z]* А если вы все же будете это делать? Что, если у вас есть переменная с внешней связью, называемая total? Какой от этого может быть вред? Строго говоря, вред состоит в том, что ваша програм- ма не будет застрахована от сбоев в работе. Все может выглядеть так, будто программа работает, она может даже успешно пройти фазу тестирования, но стандарт С не гарантирует* что программа будет работать пра- вильно в любой своей части в любое время. Префиксы: трансильванская ересь Несколько лет назад Чарльз Симоний (Charles Simonyi), который позже стал известным в Microsoft программи- стом, изобрел основанное на префиксах соглашение именования, которое в его честь стало называться “Вен- герская нотация". Его идея состояла в том, чтобы дать каждому идентификатору префикс в соответствии с тем, что собой представляет этот идентификатор. Поз- же Microsoft адаптировала эту идею, давая каждому идентификатору префикс, указывающий на его тип дан- ных. Таким образом, типы int получили бы префикс п, long int — префикс nl, массивы типов char начинались бы с са, а строки (массивы char, оканчивающиеся сим- волом нуль) начинались бы с sz. Эти имена могли бы получаться совершенно причудливыми. Например, IpszFoo означало бы "Foo, которое является длинным (в смысле 32-битовым или "длиннее", под архитектурой сегментированной памяти Intel/DOS) указателем на строку, оканчивающуюся нулем (символом нуль)". Это соглашение дало вам преимущество идентифи- цировать тип переменной путем простого взгляда на ее имя без необходимости отыскивать ее определение. К сожалению, это ведет не только к затруднительному произношению имен, но и делает задачу изменения типа переменной значительно более сложной. В Windows 3.1 тип int имеет размер 16 битов. Если вы начнете с типа int, но обнаружите (после придания это- го int тридцати или сорока функциям в качестве фор- мального параметра), что тип int, в конце концов, не- достаточно велик для ваших целей, то должны будете изменить не только тип переменной, но и ее имя — во всех тридцати или сорока функциях! Венгерская нотация впала в немилость у всех, за исключением нескольких особо упорных Windows-про- граммистов, поскольку для них это было непрактично. (Это реплика для 30 или 40 сердитых сетевиков во всем мире, которые уверяют меня, что венгерская нотация по-прежнему используется и многими любима.) Нет сомнений, что в сети имеется несколько сайтов, где она продолжает существовать, но большинство людей отка- зались от нее. Писать префиксы, вообще, — плохая идея, поскольку они слишком плотно связывают пере- менную с ее типом. По этой причине опытные програм- мисты для функции malloc используют такую конструк- цию (здесь Т — некоторый тип) т *р р = malloc (sizeof *р) вместо т *р р = malloc (sizeof (Т)) В последнем случае изменение типа р приводит к несогласованности, которую необходимо как-то зафик- сировать, — проблема, которой просто не возникает в первом случае. Один из реликтов венгерской нотации, который до- жил до наших дней, реализовался в практику предше- ствования указателю буквы р, указателю на указатель — букв рр и т.д., так что указатель на указатель на пере- менную типа FOO может быть определен как FOO **ppFoo; Предоставляю вам возможность самим составить свое собственное мнение о том, хорошая это идея или плохая. Именование переменных Имя переменной должно отражать ее назначение. По устоявшемуся соглашению большинство сайтов прини- мают, что для счетчиков циклов, используемых в ма- леньких плотных циклах, совершенно правомерно ис- пользовать переменные i, j, k, m, n. Букву 1 лучше всего исключить из этого списка, поскольку ее очень легко спутать с цифрой 1, букву о по аналогичной причине лучше всего опустить. Поскольку буква р используется
48 Пересмотренный язык С Часть I часто для описания указателя, ее лучше не использовать в качестве счетчика цикла. В графических программах нередко можно увидеть циклы, управляемые перемен- ными х и у. Для более постоянных переменных следует исполь- зовать более осмысленные имена, и очень важно хоро- шо подобрать эти имена (фактически я действительно верю, что выбор хороших имен для идентификаторов является одним из наиболее важных и сложных аспек- тов программирования вообще). Для переменных, представляющих свойства (такие, как цвет, высота, соотношение и т.п.), лучше всего ис- пользовать соответствующее существительное. Иногда вам потребуется квалификатор, такой как BackColor или ForeColor. Переменные, представляющие булевы состояния, должны носить имена, отражающие их текущее состо- яние. Имеется большое искушение использовать для этой цели слово "Is". Это можно делать, если вы ис- пользуете локальные переменные. Таким образом, IsInComment может быть хорошим именем переменной в исходной программе синтаксического анализа, кото- рая получала бы, например, значение 1, если програм- ма в настоящий момент наткнулась на символ в ком- ментарии, и 0 — в противном случае. Именование констант Большинство С-программистов предпочитают давать символическим константам имена в верхнем регистре. Я делаю так же. Лично мне кажется, что имена напо- добие, скажем, DIRECTVIDEOD1SK более сложны для чтения, чем DIRECT_V1DEO_D1SK, — когда исполь- зуется только один регистр (верхний или нижний), раз- деляющий символ подчеркивания помогает внести яс- ность. Константы, типы и макросы — все эти объекты тра- диционно именуются В ВЕРХНЕМ РЕГИСТРЕ. Теоре- тически это должно было бы означать, что их можно спутать друг с другом, но практически (по крайней мере, в моей личной практике) по контексту всегда ясно, какое имя что означает. Поскольку я никогда не использую директивы #define для совмещения имен типов, я знаю, что лексема (маркер) верхнего регистра в определении, в объявлении или в приведении либо используемая в качестве операнда для sizeof является псевдонимом типа. Довольно легко догадаться, что если в верхнем регистре поименован какой-либо другой объект, значит, это макрос или символьная константа. Именование типов Лучшие имена типов получаются с использованием су- ществительных. Попытайтесь подобрать существитель- ное, которое корректно описывает тип на нужном уров- не общности. Например, MAN (человек) — более под- ходящее имя типа, чем SIMON (Симон), а для боль- шинства приложений еще лучше использовать просто PERSON (лицо). Иногда для упрощения объявления вам захочется использовать typedef. В таких случаях именование все еще важно, но уже менее важно, чем то, что имя долж- но быть легкочитаемым существительным. Имя долж- но меньше всего отражать смысл типа. Давайте, например, определим указатель на массив из 25 указателей на функцию, принимающую тип double, и указатель на функцию, принимающую char * и возвращающую тип int, а также возвращающую int (!). Массив будет использоваться, скажем, для расчетов зак- ладных. Прежде всего давайте определим псевдоним типа для указателя на функцию, принимающую char * и возвращающую int: typedef int (*I_PF_STR) (char *) ; Здесь использованы такие элементы: 1_ — для пред- ставления возвращаемого типа функции, на которую указывает экземпляр типа, PF — для указателя на фун- кцию и „STR — для представления параметра списка. Это отголоски венгерской нотации, но ведь мы имену- ем тип, а не переменную, а имя типа должно отражать его природу (по этой причине целые числа именуются int (integer), а не каким-нибудь zog). Теперь наша проблема становится проще — мы хо- тим указать на массив 25 указателей на функции, при- нимающие double и I_PF STR и возвращающие int. Так что давайте определим псевдоним типа для этого ново- го вида указателя на функцию: typedef int (*I_PF_DBL_I_PF_STR) (double, I_PF_STR) ; Теперь — массив из 25 таких же указателей: typedef I_PF_DBL_I_PF_STR MORTGAGE_ARRAY [25] ; А теперь определим указатель на этот массив: MORTGAGE_ARRAY *р; Именование макросов Мы уже рассматривали именование символических кон- стант; здесь я скажу лишь о "функциеподобных" мак- росах. Если вам придется их использовать, применяй- те, пожалуйста, для их имен верхний регистр. Имеется опасность, свойственная использованию похожих на функции макросов; в некоторых расширениях языка С эта опасность может быть немного уменьшена путем предупреждения программиста о том, что он использу- ет фактически макрос, а не функцию. По этой же при- чине не используйте верхний регистр для имен функ- ций (лучше применять смешанный регистр). Регистр
Войны стандартов программирования: причины и пути перемирия Глава 2 49 является визуальным ключом и притом высокоэффек- тивным. Именование функций Правильное именование функций действительно очень важно. Хорошие имена функций могут значительно улучшить читабельность программы, а недостаточно тща- тельно подобранные имена для функций могут сильно затемнить ясную с других точек зрения программу. Функция предназначена для выполнения ^каких-то действий, так что кажется вполне естественным в ее имя ввести глагол. Кроме того, функции редко существуют в вакууме, сами по себе. Функция не просто выполня- ет какие-то действия, но и делает это с определенной целью. Если это так, было бы хорошо включить в на- звание функции соответствующее существительное. Таким образом, например, strcpy () — хорошее имя для функции копирования строки. Я лично предпочитаю такую форму имени функции — Verb [Noun] () (Глагол [Существительное] (). — Прим, пер.) — с использованием смешанного регистра (букв): сначала идет глагол, а затем необязательное соответству- ющее существительное (а также, возможно, прилага- тельное). Вот несколько примеров: Wait () ; PollTimer (); PrintReport (); CalcGrossInterest () ; Если в вашей библиотеке реализации версии языка С использован именно такой стиль именования функ- ций, можете применить другой стиль либо добавить к именам своих функций уникальный префикс, не пред- ставленный в реализации API, чтобы избежать возмож- ных конфликтов. Написание полезных комментариев Прискорбно, но программисты часто слишком легко- мысленно относятся к комментариям в текстах своих программ. Действительно, комментарии не различают- ся компилятором, поскольку препроцессор исключает их. Однако комментарии могут оказаться неоценимым подспорьем для программистов сопровождения, если они написаны тщательно. КОММЕНТАРИИ ПОЛЕЗНЫ, КОГДА ИХ ЧИТАЮТ! Год или два назад я делал для банка одну превентивную работу, связанную с проблемой 2000 года. Однажды парень из отдела разработки программного обеспечения спросил меня, почему я изменил некую функцию, кото- рая вычисляла, не является ли заданный год високосным. Эта функция до моих изменений имела такой вид: int IsLeapYear (int Year) { return Year % 4 -= 0 && (Year ft 100 != 0 | | Year % 400 -= 0); I Я изменил ее следующим образом: int IsLeapYear (int Year) { if (Year <= 100) return Year % 4 == 0; return Year % 4 == 0 && (Year % 100 1= 0 | | Year % 400 == 0); I Так почему же я изменил ее? Я бы в жизни не вспом- нил. Я даже вызвал коллегу, который работал на том же самом модуле; вместе мы начали просматривать код и провозились целый час (честно!), и, когда мы были толь- ко на шаг от решения, мне стало казаться, что я внес совершенно бессмысленное изменение. Затем наконец я заметил, что в самом начале кода я написал какой-то комментарий. Из него следовало, что функция IsLeapYear (1 вызывалась обычно с двухцифро- вой датой, но иногда передавался элемент tm_year из структуры struct tm. Как я знал, эта структура хранила годы начиная с 1900. Таким образом, полная проверка года на високос ность должна была бы в результате дать сбой для 2000 года, если бы он хранился в элементе tm_year структуры struct tm. Именно это и послужило причиной изменения рассматриваемой функции. Если бы я не вставил комментарий, когда изменял код, мне впол- не могло показаться, что мое изменение было бессмыс- ленным, и я мог отменить изменение, а это снова рань- ше или позже привело бы к ошибке. Так комментарий сэкономил мне целый день, а я получил хороший урок — комментарии нужно не только писать, но и читать! Когда я перечитывал эту врезку, мне припомнились два других момента относительно функций. Первый — что первоначальная функция была ясно написана для ожидания четырехцифровых дат, хотя фактически она передавала только двух- или трехцифровые даты. Вто- рой — имя функции нарушает стандарт, поскольку вне- шний идентификатор начинается с символов "1s” (По- мните? Внешние идентификаторы не гарантируют чувствительности к регистру, так как некоторые редак- торы связей к регистру нечувствительны.) Стили размещения комментариев /««А************************************** * * * Это блочный комментарий. * * Настоящая головная боль * * при сопровождении. * * * *****************************************/ Связанная с таким стилем проблема состоит в том, что вы не можете просто обновить комментарий без расхода времени на выравнивание колонок звездочек (*). 4 3« 265
50 Пересмотренный язык С Часть I (Можно, однако, писать код, выполняя форматирова- ние, и, во всяком случае, некоторые редакторы уже могут делать это за вас.) Иногда проблему сопровождения блочных коммен- тариев можно обойти, например, так: у*************************************** * * Это блочный комментарий с * удаленной с правой стороны * колонкой звездочек. * ***************************************/ Так намного лучше, поскольку у вас уже есть почти все звездочки и преимущества привлекающего внимание блока. Некоторые используют полностью минимизиро- ванный блок: Это блочный комментарий с удаленными справа, слева, сверху и снизу рядами звездочек. */ Нетрудно представить себе, что имеется почти столько стилей многострочных комментариев, сколько самих программистов: /* * Это мой собственный любимый метод * написания многострочных комментариев. Здесь * имеется хороио различимая колонка звездочек, * но нет необходимости отступать первые три * колонки текуцего отступа; таким образом, по * моему мнению, это быстрый, легкий и * эффективный метод. Вы можете изменять * расстояние от края по своему усмотрению. */ /* "Крылатые” комментарии выглядят наподобие этого - совсем неплохо..• */ int foo; /* ...дополнительный комментарий справа от кода. */ int bar; // Это другой вид однострочного комментария Обратите внимание, что этот последний стиль — любимый стиль C++- и BCPL-программистов — не был узаконен стандартом С89; он стал частью языка С лишь в октябре 1999 года, когда был ратифицирован С9Х, а затем — стандартом С99. Так что, если ваш компиля- тор не поддерживает С99, вы фактически не сможете ис- пользовать комментарии типа //. Остерегайтесь, кроме того, использовать коммента- рии //, когда ваш комментарий заканчивается обратным слэшем: foo (path) ; // работает на С: \MYAPP\ bar (path); baz (path); Соединяющая линия \, встречающаяся в конце ком- ментария, удаляется, поэтому предыдущий код преоб- разуется в следующий: foe (path); // работает на C:\MYAPPbar (path); baz (path) ; Далее комментарий удаляется препроцессором, и, таким образом, функция baz () вообще никогда не бу- дет вызвана. Это — порочная ошибка и, по моему глу- бокому убеждению, достаточная причина для того, что- бы вообще не использовать комментарии типа //. Но, конечно, вы должны составить свое собственное мнение на этот счет. Когда комментарии излишни Как вы думаете, откуда взят приведенный здесь ком- ментарий? i++; /* добавить 1 к i */ Если вы ответили: "из маленького блока", то полу- чаете шесть очков. Если вы ответили: "из учебника по С", то получаете десять очков и бесплатный виноград (пока запасы не иссякли; у меня есть только одна кисть, и мой ребенок любит виноград). Нет необходимости добавлять комментарий в каж- дую строку кода. Если смысл выполняемых кодом дей- ствий ясен программисту сопровождения, не нужно его комментировать. Например, int i = 0; while (IDNumber 1= client [i].IDNumber && i < MAXCLIENTS) { i++; ) Совершенно ясно, что происходит в этом фрагмен- те программы, так что он, по сути, не требует коммен- тариев. Описание выполняемых кодом действий Хорошие комментарии отражают то, для чего код пред- назначен (т.е. намерения программиста), а не сам ме- ханизм выполнения. Более того, механизм может вре- мя от времени меняться по мере того, как обнаруживаются усовершенствованные и новые спосо- бы выполнения той или иной операции. Однако, если меняется цель программы (т.е. намерения программис- та), значит, новая программа действительно нуждается в новом исходном коде (и в новых комментариях). Рас- смотрим пример: /* сохранить текущий заголовок списка */ temp = *list; /* перенести указатель списка на следуюцую позицию в списке */ ♦list = (*list) -> next; /* копировать даншм» из текучей позиции списка */ г»«веру (dataptx, temp -> data, sizeof *dataptx);
Войны стандартов программирования: причины и пути перемирия Глава 2 51 Эти комментарии документируют механизм, но не намерения программиста. Значительно лучше коммен- тировать сразу несколько строк кода: /* обработать этого клиента и сделать текущим следующего клиента */ temp ® ‘list; ‘list = (‘list) -> next; nemcpy (dataptx, tenp -> data, sizeof ‘dataptx) ; Такой комментарий намного лучше — когда мы ре- шим удалить список и использовать вместо него простой массив, нам не придется переписывать комментарий. Комментирование сложного кода Однако иногда требуется задокументировать механизм реализации той или иной операции. Лучше всего писать самый простой код, но иногда невозможно совместить простоту со скоростью кодирования. Когда вы пишете сложный код, думайте о программисте сопровождения и объясняйте, что и как работает. ’’Если это было трудно написать, значит будет труд- но и прочесть”. Такой вывод ошибочен. Если даже при написании кода возникали трудности, важно позабо- титься, чтобы этот код читался как можно проще, по- скольку в противном случае стоимость сопровождения будет непомерно высока, а шансы выхода программы из строя в процессе сопровождения существенно возрастут. Комментирование закрывающих скобок Когда подходит крайний срок сдачи работы, а ваши функции слишком громоздки, бывает трудно опреде- лить, какая закрывающая скобка } какой открывающей скобке { соответствует. Некоторые редакторы обеспечи- вают макросы, вызываемые нажатием клавиш, для по- мощи в поисках соответствующих скобок (если вам посчастливится в нужное время находиться рядом с таким редактором). К сожалению, я не знаю ни одной компании-поставщика, которая бы обеспечивала аппа- ратурой для поиска соответствующих пар скобок на нескольких листах бумаги. Следовательно, в больших программах необходимо позаботиться о комментирова- нии ваших закрывающих скобок: if (IncomingMissile = TRUK) { /* ... 20 строк кода ... */ while (PhaseOfMoon == FULLMOON) { /* ... еще 40 строк кода ... */ } /* конец блока while (PhaseOfMoon FULLMOON) */ } /* конец блока if (XncomingMissile) */ Стандарты же кодирования, которые настаивают на том, чтобы вы делали это в конце каждого отдельного блока операторов, становятся просто бессмысленно ав- торитарными: if (Р = { sign = NEG; ++р; } /* конец блока if (р = ’ -') */ — делать это явно глупо. Не "закомментируйте" код Проблема снабжения кода символами комментариев (закомментирование) состоит в том, что трудно заком- ментировать его вдоль и поперек. Поскольку С не.до- пускает наличия вложенных комментариев, использо- вание символов комментариев для закрытия кода, содержащего комментарии, может стать поистине испы- танием терпения. Повторное включение кода в програм- му также является болезненным, поскольку вам нужно переиначить все комментарии, которые вы собрали, когда начали весь процесс. (Если вы хотите схитрить или используете компилятор С99, можете воспользовать- ся комментариями типа //; в этом случае рассматрива- емый процесс немного упрощается.) Существует намного более простой и более удобный способ, который намного превосходит во всех отноше- ниях снабжение кода символами комментария: препро- цессор. Он как-будто специально приспособлен для этой задачи. Ведь когда вы закрываете код символами коммен- тария, то вы фактически временно удаляете его из фазы компиляции (либо потому, что вам не нужна его функ- циональность после всех изменений, но вы не желаете уничтожать его на случай обратных изменений специ- фикации, либо поскольку вы вылавливаете ошибки). Препроцессор сам может делать первоклассную ра- боту по удалению кода из фазы компиляции. Ниже приведен один пример: finclude <stdio.h> int main (void) { I if 0 /* имеются проблемы с функцией print f */ /* выборка имени из среды */ printf ("Hello %s and ", getenv ("NAME")); lendif printf (hello everyone else.Xn"); return 0; Такая методика более предпочтительна, поскольку, во-первых, она действительно очень проста для повтор- ного разрешения кода (путем изменения #if 0 на #if 1) и, во-вторых, вы легко можете избавить препроцессор от лишней работы путем простого уничтожения строк #if 0 и #endif, когда они вам уже не нужны.
52 Пересмотренный язык С Часть! Общие ошибки и недоразумения Следующие несколько разделов охватывают некоторые разнообразные обычные в С-программировании ошиб- ки и недоразумения. Ересь void main () Функция main () возвращает тип int. Если вы уже это знаете, можете при желании сразу перейти к следующе- му разделу. Если вы присвоите функции main () тип возврата, отличный от int, то в компиляторах, предшествующих компиляторам С99, вы получите неопределенное пове- дение своей программы. В компиляторах С99 вы полу- чите неспецифицированное поведение, если так гово- рит реализация версии, или неопределенное поведение — если она этого не делает. Доверяете ли вы своей про- грамме в этом отношении? Многие просто не верят мне, когда я говорю им это (точно так же, как не верил я, когда впервые узнал об этом). Частично это связано с тем, что несколько ши- роко известных учебников по С и, по крайней мере, по одной авторитетной программе-компилятору использу- ют void main () с тревожной регулярностью. Ниже при- ведена формулировка стандарта С99 (который фактичес- ки чуть более снисходителен, чем стандарт С89, который вам, возможно, более знаком): "5.1.2.2.1. Запуск программы [# 1] Функция, вызыва- ющая запуск программы, называется main (главная). Реализация не объявляет прототип для этой функ- ции. Он должен быть определен путем возврата це- лого типа int без параметров: int main (void) { /* — */ ) либо с двумя параметрами (здесь они называются aigc и argv, хотя можно использовать любые имена в том порядке, как они размещены в функции, в которой объявлены): int main (int argc, char *argv [ ] ) {/*...*/} либо некоторым другим определенным реализацией способом". Немного дальше читаем об окончании программы: "5.1.2.2.3 Завершение программы #[1] Если возвра- щаемый главной функцией тип является типом, со- вместимым с int, то возврат в главную функцию эк- вивалентен вызову функции выхода со значением, возвращенным главной функцией в качестве ее ар- , гумента; при достижении скобки}, которая заверша- ет главную функцию, возвращается значение 0. Если возвращаемый тип несовместим с типом int, состо- яние завершения, возвращаемое в хост-среду, явля- ется неспецифицированным". В данном контексте "неспецифицированнЫй" озна- чает, что стандарт не требует какого-либо специфичес- кого поведения от компилятора, который волен возвра- тить в хост-среду (обычно это операционная система) любое состояние, какое ему нравится, и это применя- ется, только если документами реализации установле- но, что она поддерживает возвращаемые из main () типы, отличные от int. Если у вас пуская (void) главная функция main () и вы пишете код для ядерного реакто- ра или военного самолета, вы, возможно, почувствуете легкую нерешительность, и я не виню вас. Кроме того, определение main () для возврата типа void (пустой) не является синтаксической ошибкой или нарушением ограничения, так что компилятор не обязательно дол- жен выдавать какое-либо диагностическое сообщение. Давайте посмотрим на это несколько под иным уг- лом. Рассмотрим четвертый аргумент функции сорти- ровки qsort. Он специфицирован как указатель на не- кую функцию сравнения, принимающую в качестве аргументов две константы типа const void * и возвраща- ющую тип int. Функция qsort вызывает эту функцию сравнения и использует возвращенное ею значение для установления взаимоотношений между двумя объекта- ми в массиве, который подлежит сортировке. Так вот, что случится, если вы напишете функцию сравнения, подобную этой? void Coinpints (const void *pl, const void *p2) { const int *nl = pl; const int *n2 = p2; int diff; if(*nl > *n2) diff = 1; else if(*nl == *n2) diff » 0; else diff = -1; 1 Слов нет, верно? У функции qsort hqt способа по- лучить информацию, в которой она нуждается. Вы не можете специфицировать прототип функций сравнения — вам потребуется отбросить правила, обращения к функ- ции qsort, если вы хотите заставить эту функцию делать то, чего от нее ожидаете. Хорошо, вернемся к функции main (). Здесь точно такая же ситуация. Вы не ответственны за определение интерфейса main (). Для этого есть вызывающая функ- ция. Кто вызывает main ()? Это не вы (хотя фактичес- ки вы можете, если захотите, вызвать main (), точно так же как вы можете при желании вызвать свою функцию сравнения из qsort). Но первичным "заказчиком" функ- ции main О является код запуска. Как же код запуска
Войны стандартов программирования: причины и пути перемирия Глава 2 53 определит, успешно ли завершилась программа, если вы не сообщите ему об этом? Этот вызов "сидит" где-то глубоко во внутренностях системы (здесь я немного упростил его): int returnstatus; returnstatus = main (argc, argv); Если вы "опустошите" функцию main (), появится несколько интересных возможностей: • Программа может работать в точности так, как вы ожидаете. • returnstatus может попасть в ловушку и вызвать ава- рийный отказ программы (либо всего компьютера). • Код запуска может отправить поддельный код воз- врата операционной системе, которая затем решит перемотать назад транзакции базы данных, посколь- ку программа не возвратила ожидаемого значения. • А это хуже всего — код запуска может снаружи до- стигнуть вашего носа и начать извлекать из него де- монов. (Демон (Demon) — процедура, запускаемая автоматически при выполнении некоторых условий и характеризуемая непредсказуемостью поведения. — Прим. науч. ред.). Функция main () возвращает тип int. Существует только три переносимых на другие платформы значе- ния, которые вы можете вернуть из main (): о EXITSUCCESS EXIT_FAILURE Два последние определены в <stdlib.h>, и их фак- тические значения варьируются в зависимости от кон- кретной системы. (Другими словами, не следует отыс- кивать эти значения в библиотеке <stdlib.h> и переносить их в свою программу.) Если вы вернули 0, код запуска сообщит операци- онной системе или другой хост-среде, что ваша про- грамма выполнилась успешно, переведя 0 при необхо- димости в некоторое другое значение. Количество аргументов функции main () Фактически функцию можно определить путем опреде- ления реализации. Следовательно, такое определение main, как int main (int argc) ИЛИ int main (int argc, char **argv, char **env) не обязательно запрещено, но вы должны знать, что компилятор обязан поддерживать любое из этих двух определений. Они разрешены только для компиляторов, которые документируют эти альтернативные формы и которые конечно же не включают всех компиляторов. Ниже приведены переносимые определения функ- ции main (): • int main (void) • int main (int argc, char **argv) • int main (int argc, char **argv [ ] ) • Любое определение, в точности эквивалентное лю- бому из трех предыдущих Таким образом, вы можете использовать различные имена переменных в качестве argc и argv, а также мо- жете использовать FOO, если определению предшеству- ет typedef int FOO, и т.д. Но чтобы возвращаемое зна- чение было переносимым, из main вы должны возвращать тип int, а также задавать либо ни одного аргумента, либо два специфицированных стандартом аргумента. Если есть необходимость получить доступ к среде переносимым способом, можете, конечно, ис- пользовать функцию getenv (). Целочисленная математика против математики плавающей точки "Каждый знает, что целочисленная математика быстрее, чем математика плавающей точки, и это объясняет, почему профессионалы выполняют вычисления в целых числах где только возможно". Этот факт достаточно проверен, чтобы быть истинным. В последние несколь- ко лет, однако, математические сопроцессоры в новых настольных компьютерах стали почти обязательными. В результате в наше время случается, что операции с плавающей точкой выполняются быстрее, чем их цело- численные эквиваленты. Старое правило здравого смыс- ла более неприменимо безусловно. Это только один аспект более обшей точки зрения на эффективность. Стандарт С не определяет, как быс- тро выполняются функции языка С, ни в абсолютных величинах, ни относительно друг друга. О любых двух данных алгоритмах одинаковой степени сложности (бо- лее детально алгоритмы описываются в главе 3) вы про- сто не в состоянии сказать, какая техника программи- рования будет более эффективной. Например, нет гарантии, что программа int ch; ch = fgetc (fp); if (ch 1= EOF) { будет выполняться быстрее (или медленнее), чем про- грамма
Пересмотренный язык С Часть I unsigned char с; if (freed (fcc, 1, 1, fp) > 0) { /* ... */ Единственный способ, позволяющий определить это, состоит в том, чтобы запустить ее и смотреть. Даже когда вы сделаете это, ваши результаты не будут пере- носимыми на другой компьютер, операционную систе- му или реализацию (или даже на другую версию одно- го и того же компилятора). Имеет смысл инвестировать капитал в хороший кодовый профайлер, если для ваше- го приложения эффективность является ключевым ас- пектом. (Если эффективность не важна для вашего при- ложения, почему тогда вы в первую очередь озабочены этим?) Для большинства приложений вопросы эффек- тивности возникают только тогда, когда программа вы- полняется подобно тому, как бежит одноногая собака. В этих обстоятельствах вы, вероятнее всего, захотите получить значительный выигрыш в эффективности пу- тем пересмотра своего выбора алгоритма, а не путем экономии в программе одного цикла здесь и одного цикла — там. Если ваш документ стандарта кодирования утверж- дает, что целочисленная математика быстрее, чем ма- тематика плавающей точки, вы должны отвечать: ’’До- кажите это!” Обработка сигналов Всякий раз, когда я вижу предположительно переноси- мую программу, которая выполняет сигнальную обра- ботку, я становлюсь подозрительным. Большинство программ, обрабатывающих сигналы, делают это некор- ректно. В вашей функции обработки сигнала вам фак- тически не разрешено (за исключением наличия опре- деленных довольно ограниченных обстоятельств) ссылаться на любой статический объект, если он был объявлен как имеющий тип volatile sigatomict. (Пере- менная atomic (атомарный), упрощенно говоря, это то, что может ссылаться на единственную машинную опе- рацию.) Большинство С-программистов, которых я знаю, никогда не слышали о типе sig atomic t, и пото- му многие из них все еще продолжают добавлять в свои программы средства обработки сигналов. Более того, вам запрещено вызывать любую стандар- тную библиотечную функцию (или другую функцию, которая может вызвать стандартную библиотечную функцию) изнутри обработчика сигнала (исключение делается для функций abort () и, при определенных обстоятельствах, для функций signal ()). Это, конечно, не останавливает людей, и они продолжают вызывать библиотечные функции, но результат такого вызова непереносим и должен быть пустой операцией в пере- носимой программе. На практике многие программы используют сигна- лы, и не только для перехватывания прерываний при операциях с плавающей точкой. Например, сигналы часто используются в сетевых UNIX-приложениях. Мое мнение таково, что такое использование основано на реализации, разрешающей и поддерживающей его, так что вы должны быть осторожны. (Строго говоря, вы надеетесь на неопределенное поведение, но если ваш поставщик задокументировал поведение и вы можете доверять ему, у вас все должно быть в порядке, по край- ней мере пока не будет реализована следующая версия вашего компилятора.) Передача по значению Мы все знаем, что не можем делать перестановку (или свапирование) двух типов int с помощью функции, по- добной этой: void swap (int a, int b) { int t; t = af a = bf b = t; ) поскольку С является языком передачи-по-значению. К сожалению, некоторые учебники по С предполагают, что вы вместо этого передаете по ссылке: void swap (int *а, int *b) { int t; t = *ar *a = *br *b = t; } Уверен, что это работает. Но это не передача по ссылке. В языке С вообще нет такой вещи. Фактически же происходит вот что: адреса а и b передаются, но по значению! Более ясно это видно на примере этой про- стой демонстрационной программы: #include <stdio. h> void increment(char *p) < ++p; } int main(void) { char *s s "Hello world"; increment s); printf("%s\n", s); return 0; } Если бы С передавал указатели по ссылке, эта про- грамма должна была бы напечатать следующее: ello world Но она не делает этого. Она печатает: Hello world
Воины стандартов программирования: причины и пути перемирия Глава 2 Это говорит о том, что модификация р функцией increment () не оказала влияния на первоначальный ука- затель s. Именно такого поведения мы и ожидали при передаче по значению. По этой же причине следующий код работать не будет (и фактически проявит неопре- деленное поведение): #include <stdio.h> int openfile (char *sf FILE *fp) { fp = fopen(s, "г"); return fp ? 1 : 0; } int main(void) { FILE *fp; char bufferf1024]; if(openfile("readme*, fp)) { while(fgets(buffer, sizeof buffer, fp)) printf("%s", buffer); fclose(fp); printf("\n"); ) return 0; } Если затем вы захотите внутри функции изменить объект, на который указывает указатель, то как вам это сделать? Методика будет такой же, как и в случае с перемен- ной: вы передаете указатель на объект, который хотите изменить. В предыдущем примере мы хотели сделать указатель указывающим на возвращаемое значение фун- кции fopen. Ниже представлена модифицированная программа, которая будет работать корректно: #include <stdio.h> fdefine MAXBUFF 1024 int openfilefchar *s, FILE **fp) { *fp = fopenfs, "r"); return *fp ? 1 : 0; } int main(void) { FILE *fp; char buffer[MAX_BUFF); if(openfile("readme”, ifp)) { while(fgets(buffer, sizeof buffer, fp)) printf("%s", buffer); fclose(fp); printf("\n"); } return 0; Аналогично, если вам необходимо изменить пере- менную ppt на тип **Т (где Т является некоторым ти- пом), вам нужно передать &ppt в свою функцию, кото- рая была бы прототипирована, например, так: int foo (Т *** рррТ); (В действительности это может принять даже более глупый вид, чем то, что написано здесь. Сначала я хо- тел было использовать здесь пять звездочек, как в од- ном реальном коде. К счастью, такое требуется редко.) Проблемы с включающим ИЛИ Некоторые компьютеры хранят числа таким образом, что первым следует наиболее значимый байт. Говорят, что такие компьютеры имеют порядок хранения "от старшего к младшему" (big-endian). В других компьюте- рах первым следует наименее значимый байт (little- endian), поскольку такое расположение оказывается более эффективным для их конкретной архитектуры. Поэтому иногда бывает необходимым определять ”по- рядок следования” ("endianness”) битов в конкретном компьютере (По этим вопросам см. также главу 5. — Прим, науч, ред.) Ниже приведена обычно используемая методика для определения порядка следования битов в компьютере: #include <stdio.h> union U { long bignum; short littlenum[2); ); int main (void) { union U u = {1L}; if(u.littlenumf0] == 1) { printf("Little-endian\n"); 1 else { printf("Big-endian.\n"); return 0; } К сожалению, работоспособность такой методики не гарантирована (точнее, ее работоспособность в старом стандарте С определяется реализацией компилятора, а в С99 она не определена). Можно лишь выяснить, что и куда вы переносите в операции объединения union. (Дословно — объединение, здесь — операция "включа- ющее ИЛИ". — Прим. науч, ред.) Если вы переносите куда-либо тип long int, то извлечь можете только тип long int.
56 Пересмотренный язык С Часть I Утверждение о том, что поведение этого кода опре- делено реализацией или не определено вообще, может показаться слишком придирчивым, но ведь полная оп- ределенность — вполне естественное требование для пе- реносимости. Почему каждый программист (кроме тех, кто занимается одноразовыми упражнениями) беспоко- ится о том, чтобы написать процедуру для определения во время выполнения того, имеет ли компьютер конк- ретное представление, даже если он собирается запус- кать ее на одной реализации одного компьютера? Имеется исключение из этого правила объединения, связанное с обработкой структур с общими начальны- ми последовательностями. Это проще увидеть, чем об этом рассказать. Следующие определения я расположил горизонтально не только в целях экономии места, но и чтобы выделить их общую начальную последователь- ность: typedef struct FOO { int i; double j; long k; } FOO; typedef struct BAR { int a; double b; char c; } BAR; typedef struct BAZ { int d; double e; FOO f; } BAZ; typedef union QUUX { FOO foo; BAR bar; BAZ baz; } QUUX; FOO first = { 3, 3.14, 42 }; QUUX q; q.foo s= first; printf("%d if\n*, q.bar.a, q.baz.e); Поскольку FOO, BAR и BAZ разделяют общую на- чальную последовательность int, double, ничто не меша- ет проверить любой элемент этой последовательности с использованием q.foo, q.bar или q.baz. Но если вы сохраняете значение в элементе не в общей начальной последовательности (например, q.fbo.first.k = 9;), то считывание этой информации посредством q.foo, q.bar или q.baz будет непереносимым. Оператор sizeof sizeof — это оператор, а не функция. Для некоторых людей это может оказаться сюрпризом. Оператор sizeof выдает размер объектно-типированного выражения (или типа) во время компиляции. Это унарный оператор, так что он принимает только один объект либо только одно объектно-типированное выражение или тип. Таким об- разом, этот фрагмент кода FOO foo; size__t sizeof foo; вполне допустим. Если вам необходимо вместо этого задать тип, то нужно заключать его в скобки; при этом sizeof не становится функцией: size_y othersize ж sizeof (FOO); Ниже показан прием, который фактически будет работать: void foo(int *array) { printf("array is stored in tu bytes.\n", (unsigned) sizeof array); printf("It has %u elements.\n", (unsigned)(sizeof array / sizeof(int))); 1 К сожалению, sizeof array не позволит нам опреде- лить размер массива, а только размер указателя на тип int. Более того, предыдущий фрагмент кода будет все- гда выдавать одно и то же число, соответствующее ко- личеству элементов в массиве. Это значение определе- но реализацией, компилятора, но на многих системах этот код печатал бы 1, поскольку указатель часто полу- чает такой же объем памяти, как и тип int. Ключевое слово return Ключевое слово return — это не функция, так что оно не требует скобок. Вы можете их использовать, если хотите, но это не обязательно. Объявления, определения и прототипы С-программисты часто затрудняются сказать, в чем со- стоит различие между объявлениями, определениями и прототипами. Что же они действительно собой пред- ставляют? Объявления Объявление (или декларация, declaration) дает компи- лятору информацию о типе идентификатора. (Строго говоря, объявление специфицирует реализацию и атри- буты установки идентификаторов.) Определение — это объявление, но объявление не обязательно должно быть определением. Например, переменная области видимо- сти файла с внешней связью может быть определена в программе лишь однажды, но объявить ее вы можете столько раз, сколько вам угодно. Таким образом, в ис- ходном файле А мы могли бы иметь переменную int GlobalX = О; определенную в области видимости файла. В исходных файлах В, С и D мы могли бы законно объявить extern int GldbalX;
Войны стандартов программирования: причины и пути перемирия Глава 2 Кроме того, мы вполне можем разместить это объяв- ление прямо в А независимо от законности уже сделан- ного определения. Определения Определение (definition) объекта (такого, как перемен- ная, например) означает отведение для него памяти и (необязательно) присвоение ему некоторого значения. Объект внутри программы должен быть определен толь- ко однажды. Объект может быть определен также эксперименталь- но (tentatively). Такое определение является определе- нием в области видимости файла без инициализатора и без спецификатора памяти (либо со спецификатором static). Я был бы счастлив, если бы мог сообщить вам, что это полезная вещь, но я не вижу абсолютно ника- кого смысла в таком экспериментальном определении. Определение функции означает придание функции тела. Поэтому само определение функции включает ее тело. Таким образом, С-программу можно рассматривать как коллекцию определений. Прототипы В соответствии со стандартом, прототипом (prototype) функции является объявление функции, которая объяв- ляет типы своих параметров. Прототип — это всегда объявление (и, может быть, определение) функции. Прототипы используются ком- пилятором для подтверждения правильности списка аргументов в обращениях (вызовах) к прототипирован- ным функциям. Я убежден, вы уже знаете о прототи- пах как об объявлениях, поэтому приведу пример того, каким образом определение функции может также быть прототипом: *indude <stdio. h> linclude <string.h> int foo(char *s) { return (int)strlen(s); } int main(void) { char *hi = "Hello world"; printf("The string %s is id bytes long\n", hi, foo(hi)); return 0; } Здесь строка int foo (char *s) является не только первой строкой определения функции; она является также полным прототипом. Важность переносимости программ Одним из наиболее трудных для преодоления препят- ствий является переход из “детского сада** С в реальный мир переносимых приложений. Понятна тенденция предполагать, что “компилятор является языком*'. Сис- тему может хватить настоящий (кулаком) удар, когда вы вдруг обнаружите, что функция getch () не является частью языка С! Поразрядное дополнение до единицы и до двух В языке С не подразумевается, что вы работаете на ком- пьютере с дополнительным кодом числа (Twos complement — поразрядное дополнение до двух в дво- ичной системе счисления. — Прим. науч. ред.). Это объясняет, почему стандарт ANSI требует от реализа- торов делать параметр INT_MIN равным по крайней мере 32767 по абсолютной величине (она может быть и отрицательной, конечно), а не 32768. В случае обрат- ного кода числа (Ones complement — поразрядное до- полнение до единицы в двоичной системе. — Прим, науч, ред.) вы не сможете хранить величину -32768 в 16 битах (хотя вы получаете дополнительную возможность различать 0 и -0). Поскольку для хранения отрицатель- ных чисел в компьютерах с обратным и дополнитель- ным кодами используются различные наборы битов, результат смещения отрицательного числа оказывается в лучшем случае определенным реализацией компиля- тора (как и любая операция сдвига, включающая зна- ковый бит) и неопределенным — в худшем случае. Во- обще, когда сдвигаются биты, безопаснее всего придерживаться беззнаковых типов. Определение неопределенного поведения Посмотрим, что стандарт С говорит о неопределенном поведении: "3.18 [#1] Неопределенное поведение [является] по- ведением, возникающим в результате использования непереносимой или ошибочной программной конст- рукции, ошибочных данных или неопределенных (по значению) объектов, которые не охватываются требо- . ваниями этого Международного стандарта". Ниже приведена С-программа, не содержащая син- таксических ошибок или нарушений каких-либо огра- ничений. (Другими словами, компилятор ANSI С до- пускает компиляцию этой программы без выдачи каких-либо диагностических сообщений.) void main () { printf ("Hello world\n"); I
58 Пересмотренный язык С Часть I С точки зрения стандарта С эта программа может выполнять любые функции. Наиболее вероятным ре- зультатом будет выдача приветствия всему миру: Hello world на стандартное выводное устройство, но нет абсолют- ной гарантии, что фактически будет сделано именно это. Для этого есть две причины. Первая состоит в том, что функция некорректно специфицирует main () как возвращающую тип void; в языке С функция main О должна возвращать тип int. Второй причиной является тот факт, что функция printf была вызвана без полного прототипа для ее введения в область видимости. Итак, эта программа может хорошо работать на ва- шем компьютере. Но это не означает, что она будет работать на том компьютере, на котором вы ее скомпи- лируете. Это не означает также, что программа будет работать на другом компиляторе, даже если он предназ- начен для той же самой операционной системы. Фак- тически это даже не означает, что программа будет ра- ботать на предыдущей или на последующей версии вашего собственного компилятора. Завсегдатаи группы comp.iang.c укажут вам, что ничего не остановит реа- лизацию вытаскивать демонов из вашего носа (и дей- ствительно, термин "носовые демоны" в этой группе но- востей является псевдонимом неопределенного поведения). Конечно, демонографическая ринология — не единственно возможный результат. Если у вас хоро- шие аппаратные средства, эта программа способна вы- полнить любые возможные и невозможные действия. Подавление буферов ввода Ниже демонстрируется обычно применяемая методика освобождения от нежелательных символов во входном потоке: printf ("Введите возраст:"); scant("%d"r (аде); /* Получение возраста от пользователя */ printf ("Введите размер обуви:"); fflush (stdin); /* Избавились от перевода строки */ scant ("%d", fishoesize); /* Получение от пользователя размера обуви */ Почитаем, что стандарт говорит об этом фрагменте: ^include <stdio.h> int fflush (FILE *stream); Из раздела 7.19.5.2: "[#2] Если stream указывает на выходной поток или на модификацию потока, в кото- ром последняя операция не была операцией ввода, фун- кция fflush удаляет любые непечатаемые данные, для которых поток доставлен в хост-среду для записи в файл; в противном случае поведение не определено". Если вы хотите избавиться от случайных символов между обращениями к scant, можете сделать это следу- ющим образом: printf ("Введите возраст:"); fflush (stdout); scant("%d". (age); /* Получение возраста от пользователя */ printf ("Введите размер обуви:"); fflush (stdout); scant (" %d", Sshoesize); /* Обратите внимание на лидируиодм пробел */ (Как вы видите, я принял возможность продемон- стрировать, что вы можете переносимо собрать ввод на той же строке, что и предыдущий вывод, разумно ис- пользуя функцию fflush ().) Фактически лучше не использовать функцию scant () для интерактивного получения данных. Она предназна- чена для чтения форматированных данных из stdin, и человечество не нашло лучшего источника форматиро- ванных данных. Использовать scant () намного лучше, когда stdin является, например, переназначенным из файла с тщательно определенным форматом. Если вы привыкли использовать scant () и теперь предпочли бы альтернативное предложение, вниматель- но присмотритесь к функции fgets (), которая идеаль- но приспособлена для сбора данных от пользователя. Функции is () и to () Эти функции из <ctype.h> (isupper, tolower и т.п.) при- нимают в качестве аргумента тип int, но этот аргумент должен быть либо EOF, либо значением, представляе- мым как тип unsigned char. Любое другое значение вы- зывает неопределенное поведение. Если вы хотите пол- ностью обезопасить эти функции, предварительно приведите их аргументы к типу unsigned char. (Это не- посредственно будет причиной проблем, связанных с EOF, но вы всегда можете разработать свой алгоритм так, чтобы передача EOF в функцию просто не могла бы иметь места.) Приведение к типу unsigned char бу- дет гарантировать безопасность операции, а компиля- тор позаботится о том, чтобы значению вернуть тип int. Проблема возникает только тогда, когда ваша стро- ка имеет символы, представленные величинами вне диапазона от 0 до UCHARJVIAX. Например, ваша про- грамма может выполняться в системе, такой как DOS, где тип char обычно по умолчанию является signed (имеет знак), и пользователь может использовать ALT в конъюнкции с числовой вспомогательной клавиатурой для ввода символа не из набора символов ASCII. В та- ких ситуациях вам следует быть предусмотрительным Приведенный ниже фрагмент кода иллюстрирует, как можно убедиться в том, что ваш код устойчив:
Войны стандартов программирования: причины и пути перемирия Глава 2 59 void MakeStringUpperCase(char *в) { while(*s) { *в = toupper((unsigned char)*s); ++s; } Аннулирование строки Ниже показан обычный прием, используемый для об- мена значениями двух целых чисел без участия времен- ной промежуточной переменной: а А= Ь А= а А= Ь; Этот прием может выглядеть очень мило, но пользо- ваться им корректно невозможно. А вот еще один часто используемый способ: void strrev(chax *s) { int len = strlen(s); int ir j; for(i = 0г j = len - 1; i < j; i++f j—) s(i] s[j] Л= s[i] ж= s[j); } Если вы подумали, что этот код выглядит очень непривлекательно, вы правы. Но дело не только в этом, он еще и неправилен. Поскольку код модифицирует значение объекта дважды между точками последователь- ности, в результате можно получить неопределенное поведение. Судите сами: сбой во включении <string.h> для strlen, неправильный тип для len (он должен иметь тип size_t) и нарушение области имен (strrev) кажутся совершенно тривиальными. (Между прочим, если вы делаете len типом size_t, то должны добавить проверку на специальный случай, когда строка имеет длину нуль байтов.) Шутка ли сказать, ни одна из этих ошибок не явля- ется синтаксической ошибкой или нарушением ограни- чения, так что никакой диагностики не требуется! Если вам интересно, приведу код, написанный со- вершенно законно: fxnclude <string.h> void revstr(char *s) { size_t len = strlen(s); size t if j; if(len > 0) for(i « 0г j = len - 1; i < j; i++, j—) s[i] Л= B[j] *= s[i]f s[i] A= s[jl; } Программа будет работать всегда, поскольку запятые вводят точки последовательности в код. (Это все еще довольно ужасно, но по крайней мере не нарушает за- конов С. Лучшее и более ясное решение получается с использованием временной переменной.) "НОСОВЫЕ ДЕМОНЫ" Мною лично засвидетельствованы два наиболее драмати- ческих результата неопределенного поведения, и оба они были связаны с отведением недостаточной памяти в мас- сиве char для завершающего нулевого символа строки. Первый случай произошел в 1989 году, и его жертвой был мой брат Стив. Однажды он привлек мое внимание к экрану с отображенным на нем сообщением, в котором его совершенно неожиданно просили подтвердить, что он желает форматировать свой жесткий диск. Он был сча- стливчик — его спросили. Г од или около того спустя мой коллега (его звали Кевин) оказался не таким везучим. Первым знаком неопреде- ленного поведения его программы было то, что его ком- пьютер стал "зависать". Вторым знаком было то, что он не перезагружался! После этого ему пришлось потратить много времени, чтобы устранить неполадки, используя диагностические гибкие диски, поставляемые производи- телем. Что касается лично меня, то со мной часто случались разного рода неприятности. В некотором отношении я был неудачником. Я с удовольствием рассказал бы вам о том, как свергнул правительство путем разыменования указа- теля NULL или затопил город путем передачи (UCHAR_MAX + 1] в функцию isupper (|. Но мои соб- ственные сбои в действительности были значительно ме- нее драматичны. Извините, если я вас разочаровал. Размеры целых типов Строго говоря, длинный целый тип (long Int) имеет длину sizeof (long int) байтов. Вы знаете, что эта длина составляет не менее 32 битов, поскольку она должна представлять все целые числа между -2147483647 и +2147483647. Рассматриваемый тип может иметь раз- мер, превышающий 32 бита. Это объясняется тем, что на вашей конкретной реализации значение LONG_MAX выше чем 32 бита, либо тем, что были использованы дополнительные биты (это не обязательно, но компи- ляторы позволяют использовать их). Чтобы определить потребности в памяти, работайте с sizeof (long int). А чтобы установить, как большое число может быть со- хранено в типе long int, используйте LONG_MAX. На текущих системах длинный целый тип long int обычно имеет длину 32 бита. Те же рассуждения можно применить и к коротко- му целому типу short int, который имеет длину sizeof (short int) байтов и по крайней мере 16 битов, посколь- ку этот тип должен представлять все целые числа в ди- апазоне между -32767 и +32767. Этот тип может иметь длину более 16 битов, опять же по той причине, что для компилятора характерны более высокие пределы реали- зации, чем требуется (т.е. совершенно законно), либо из-за дополнительных битов. На текущих тстемах ко-