- •1. Стиль 10
- •3. Проектирование и реализация 63
- •4. Интерфейсы 85
- •5. Отладка 115
- •6. Тестирование 134
- •7. Производительность 157
- •8. Переносимость 180
- •9. Нотация 203
- •Введение
- •Брайан в. Керниган
- •1.1. Имена
- •1.2. Выражения
- •Упражнение 1 -6
- •1.3. Стилевое единство и идиомы
- •1.4. Макрофункции
- •1.5. Загадочные числа
- •1.6. Комментарии
- •1.7. Стоит ли так беспокоиться?
- •Дополнительная литература
- •2.1. Поиск
- •2.2. Сортировка
- •2.3. Библиотеки
- •2.4. Быстрая сортировка на языке Java
- •2.5. "О большое"
- •2.6. Динамически расширяемые массивы
- •2.7. Списки
- •Упражнение 2-8
- •2.8. Деревья
- •Упражнение 2-15
- •2.10. Заключение
- •Дополнительная литература
- •Проектирование и реализация
- •3.1. Алгоритм цепей Маркова
- •3.2. Варианты структуры данных
- •3.3. Создание структуры данных в языке с
- •3.4. Генерация вывода
- •3.5.Java
- •Into the air. When water goes into the air it
- •3.7. Awk и Perl
- •3.8. Производительность
- •3.9. Уроки
- •Дополнительная литература
- •4. Интерфейсы
- •4.1. Значения, разделенные запятой
- •4.2. Прототип библиотеки
- •4.3. Библиотека для распространения
- •Упражнение 4-4
- •4.5 Принципы интерфейса
- •4.6. Управление ресурсами
- •4.7. Abort, Retry, Fail?
- •4.8. Пользовательские интерфейсы
- •Дополнительная литература
- •5. Отладка
- •5.1. Отладчики
- •5.2. Хорошие подсказки, простые ошибки
- •5.3, Трудные ошибки, нет зацепок
- •5.4. Последняя надежда
- •5.5. Невоспроизводимые ошибки
- •5.6. Средства отладки
- •5.7. Чужие ошибки
- •5.8. Заключение
- •Дополнительная литература
- •6. Тестирование
- •6.1. Тестируйте при написании кода
- •6.2. Систематическое тестирование
- •6.3. Автоматизация тестирования
- •6.4. Тестовые оснастки
- •6.5. Стрессовое тестирование
- •6.6. Полезные советы
- •6.7. Кто осуществляет тестирование?
- •6.8. Тестирование программы markov
- •6.9. Заключение
- •Дополнительная литература
- •7.Производительность
- •7.1. Узкое место
- •7.2. Замеры времени и профилирование
- •7.3. Стратегии ускорения
- •7.4. Настройка кода
- •7.5. Эффективное использование памяти
- •7.6. Предварительная оценка
- •7.7. Заключение
- •Дополнительная литература
- •8. Переносимость
- •8.1. Язык
- •8.2. Заголовочные файлы и библиотеки
- •8.3. Организация программы
- •8.4. Изоляция
- •8.5. Обмен данными
- •8.6. Порядок байтов
- •8.7. Переносимость и внесение усовершенствований
- •8.8. Интернационализация
- •8.9. Заключение
- •Дополнительная литература
- •9.1. Форматирование данных
- •9.2. Регулярные выражения
- •Упражнение 9-12
- •9.3. Программируемые инструменты
- •9.4. Интерпретаторы, компиляторы и виртуальные машины
- •9.5. Программы, которые пишут программы
- •9.6. Использование макросов для генерации кода
- •9.7. Компиляция "налету"
- •Дополнительная литература
- •Интерфейсы
- •Отладка
- •Тестирование
- •Производительность
- •Переносимость
8.6. Порядок байтов
Несмотря на все описанные выше недостатки, иногда двоичные данные оказываются необходимы. Например, они несравненно более компактны и их гораздо быстрее декодировать, а эти факторы очень важны для компьютерных сетей. Но двоичные данные имеют серьезнейшие проблемы с переносимостью.
По крайней мере один вопрос решен: все современные машины имеют 8-битовые байты. Однако все объекты, большие байта, представляются на разных машинах по-разному, поэтому полагаться на какие-то определенные свойства было бы ошибкой. Короткие целые числа (обычно 16 битов, или 2 байта) могут иметь младший байт, расположенный как по меньшему адресу (little-endian, младшеконечное расположение), чем старший, так и по большему (big-endian, старшеконечное). Выбор варианта произволен, а некоторые машины вообще поддерживают обе модели.
Итак, несмотря на то, что и старшеконечные и младшеконечные машины рассматривают память как последовательность слов, расположенных в одном и том же порядке, байты внутри слова они интерпретируют различно. На приведенной диаграмме четыре байта, начинающиеся с позиции 0, представляют шестнадцатеричное целое 0x11223344 для стар-шеконечников и 0x44332211 — для младшеконечников.
Для того чтобы увидеть порядок байтов в действии, запустите следующую программу:
/* byteorder: отображает байты длинного целого */
int main(void)
{
unsigned long x;
unsigned char *p;
int i;
/* 11 22 33 44 => big-endian */
/. 44 33 22 11 => little-endian */
/* x = 0x1122334455667788UL; для 64-битового long */
x = 0x11223344UL;
p = (unsigned char *) &x;
for (i = 0; i < sizeof(long); i++)
printf("%x ", *p++);
printf(“\n”);
return 0:
}
На 32-битовом старшеконечнике на экран будет выведено
11 22 33 44
на младшеконечнике —
44 33 22 11
а на PDP-11 (16-битовая машина, все еще встречающаяся во встроенных системах) результатом будет
22 11 44 33
На машинах с 64-битовым типом long мы можем рассмотреть константу большей длины и увидеть те же результаты.
Наша программа выглядит довольно глупо, но если нам надо послать целое число через побайтовый интерфейс, например по сети, то надо решить, какой байт посылать первым, а какой вторым, и в этом выборе суть проблемы со старше- и младшеконечниками. Другими словами, наша программа неявно выполняет то, что выражение
fwrite(&x, sizeof(x), 1, stdout);
делает явным образом. Небезопасно писать (отправлять) int (или short, или long) на одном компьютере и читать это число как int на другом.
Например, если компьютер-передатчик пишет с помощью
unsigned short x;
fwrite(&x, sizeof(x), 1, stdout);
а компьютер-приемник производит чтение так:
unsigned short x;
fread(&x. sizeof(x), 1, stdin);
то, если эти компьютеры имеют разный порядок байтов, значение х будет воспроизведено неправильно. Например, если отправлено было число 0x1000, то прочитано оно будет как 0x0010.
Эта проблема часто решается посредством условной компиляции и перестановки байтов, то есть примерно так:
? short x;
? fread(&x, sizeof(x), 1, stdin);
? #ifdef BIG-ENDIAN
? /* осуществляем перестановку байтов */
? х = ((x&0xFF) << 8) | ((х>>8) & 0xFF);
? #endif
При пересылке большого количества двух- и четырехбайтовых целых такой подход получается слишком громоздким. На практике получается, что байты при пересылке не один раз подвергаются перестановке.
Если ситуация выглядит невесело для short, то для более длинных типов она еще хуже — для них существует больше способов перепутать байты. А если к этому добавить еще всяческие преобразования между членами структур, ограничения на выравнивание и абсолютно таинственный порядок байтов в старых машинах, то проблема покажется просто неразрешимой.
Используйте при обмене данными фиксированный порядок байтов.
Решение проблемы все же существует. Записывайте байты в каноническом порядке, используя переносимый код:
unsigned short x;
putchar(x >> 8); /* пишем старший байт */
putchar(x & 0xFF); /* пишем младший байт */
и считывайте их обратно побайтово, собирая первоначальные значения:
unsigned short x;
х = getchar() << 8; /* читаем старший байт */
х |= getchar() & 0xFF; /* читаем младший байт */
Этот подход можно перенести и на структуры: записывайте значения членов структур в определенной последовательности, побайтово, без выравнивания. Неважно, какой порядок байтов вы изберете, — подойдет любой, лишь бы передатчик и приемник согласовали порядок байтов в передаче и количество байтов в каждом объекте. В следующей главе мы покажем несколько инструментов для упаковки и распаковки данных.
Побайтовая обработка может показаться довольно дорогим удовольствием, но в сравнении с вводом-выводом, при котором необходима упаковка и распаковка данных, потери времени окажутся незначительными. Представьте себе систему X Window, в которой клиент пишет данные с тем порядком байтов, что используется у него на машине, а сервер должен распаковывать все, что клиент посылает. На клиентском конце достигнута экономия в несколько инструкций, но сервер становится больше и сложнее, поскольку ему приходится обрабатывать одновременно данные с разными порядками байтов: ведь клиенты вполне могут иметь различные типы машин. В общем, сложностей куда больше. Кроме того, не забывайте, что это графическая оболочка, где расходы на распаковку байтов будут с лихвой перекрыты выполнением той графической операции, которую они содержат.
Система X Window воспринимает данные от клиента с любым порядком байтов: считается, что это забота сервера. А система Plan 9, наоборот, сама задает порядок байтов для отправки сообщений файл-серверу (или графическому серверу), а данные запаковываются и распаковываются неким универсальным кодом вроде приведенного выше. На практике определить временные затраты во время исполнения оказывается практически невозможно; можно только сказать, что на фоне ввода-вывода упаковка данных оказывается малозаметной.
Java — язык более высокого уровня, чем С и C++, в нем порядок байтов скрыт совсем. Библиотеки представляют интерфейс Serializable, который определяет, как элементы данных пакуются для передачи.
Однако, если вы пишете на С или C++, всю работу придется выполнять самостоятельно. Главное, что можно сказать про побайтовую обработку: она решает имеющуюся проблему для всех машин с 8-битовыми байтами, причем решает без участия №ifdef. Мы еще вернемся к этой теме в следующей главе.
Итак, лучшим решением нередко оказывается преобразование информации в текстовый формат, который (не считая проблемы CRLF) является абсолютно переносимым: не существует никаких неопределенностей с его представлением. Однако и текстовый формат не является панацеей. Время и размер для некоторых программ могут быть критичны, кроме того, часть данных, особенно числа с плавающей точкой, могут потерять точность при округлениях в процессе передачи через printf и scanf. Если вам надо передавать числа с плавающей точкой, особо заботясь о точности, убедитесь, что у вас есть хорошая библиотека форматированного ввода-вывода; такие библиотеки существуют, просто ее может не оказаться конкретно в вашей системе. Особенно сложно представлять числа с плавающей точкой для переноса их в двоичном формате, но при должном внимании текст может эту проблему решить.
Существует один тонкий момент, связанный с использованием стандартных функций для обработки двоичных файлов, — эти файлы необходимо открывать в двоичном режиме:
FILE *fin;
fin = fopen(binary__file, "rb");
с = getc(fin);
Если 'b' опущено, то в большинстве систем Unix это ни на что не повлияет, но в системах Windows первый встретившийся во вводе байт Control-Z (восьмеричный 032, шестнадцатеричный 1А) прервет чтение (такое происходило у нас с программой strings из главы 5). В тоже время при использовании двоичного режима для чтения текстовых файлов вам придется вставлять символы \r во ввод и убирать их из вывода.
