- •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. Компиляция "налету"
- •Дополнительная литература
- •Интерфейсы
- •Отладка
- •Тестирование
- •Производительность
- •Переносимость
9.1. Форматирование данных
Между тем, что мы хотим сказать компьютеру ("реши мою проблему"), и тем, что нам приходится ему говорить для достижения нужного результата, всегда существует некоторый разрыв. Очевидно, что чем этот разрыв меньше, тем лучше. Хорошая нотация поможет нам сказать именно то, что мы хотели, и препятствует ошибкам. Иногда хороший способ записи освещает проблему в новом ракурсе, помогая в ее решении и подталкивая к новым открытиям.
Малые языки (little languages) — это нотации для узких областей применения. Эти языки не только предоставляют удобный интерфейс, но и помогают организовать программу, в которой они реализуются. Хорошим примером является управляющая последовательность printf:
printf("%d %6.2f %-10.10s\n", i, f, s);
Здесь каждый знак процента обозначает место вставки значения следующего аргумента р rintf; за ним следуют необязательные флаги и размеры поля и, наконец, буква, которая указывает тип параметра. Такая нотация компактна, интуитивно понятна и легка в использовании; ее реализация достаточно проста и прямолинейна. Альтернативные возможности в C++ (iostream) и Java (java. io) выглядят гораздо менее привлекательно, поскольку они не предоставляют специальной нотации, хотя могут расшириться типами, определяемыми пользователем, и обеспечивают проверку типов.
Некоторые нестандартные реализации printf позволяют добавлять свои приведения типов к встроенным. Это удобно, когда вы работаете с другими типами данных, нуждающимися в преобразованиях при выводе. Например, компилятор может использовать знак %L для обозначения номера строки и имени файла; графическая система — использовать %Р для точки, a %R — для прямоугольника. Строка шифра из букв и номеров — сведения о биржевых котировках, которая рассматривалась нами в главе 4, относится к тому же типу: это компактный способ записи таких котировок.
Схожие примеры можно придумать и для С и C++. Представим себе, что нам нужно пересылать пакеты, содержащие различные комбинации типов данных, из одной системы в другую. Как мы видели в главе 8, самым чистым решением была бы передача данных в текстовом виде. Однако для стандартного сетевого протокола лучше иметь двоичный формат по причинам эффективности и размера. Как же нам написать код для обработки пакетов, чтобы он был переносим, эффективен и прост в эксплуатации?
Для того чтобы дальнейшее обсуждение было конкретным, представим себе, что нам надо пересылать пакеты из 8-битовых, 16-битовых и 32-битовых элементов данных из одной системы в другую. В стандарте ANSI С оговорено, что в char может храниться как минимум 8 битов, 16 битов может храниться в short и 32 бита — в long, так что мы, не мудрствуя лукаво, будем использовать именно эти типы для представления наших данных. Типов пакетов может быть много: пакет первого типа содержит однобайтовый спецификатор типа, двухбайтовый счетчик, однобайтовое значение и четырехбайтовый элемент данных:
Пакет второго типа может состоять из одного короткого и двух длинных слов данных:
int pack_type1(unsigned char *buf, unsigned short count,
unsigned char val, unsigned long data)
{
unsigned char *bp;
bp = buf;
*bp++ = 0x01;
*bp++ = count >> 8;
*bp++ = count;
*bp++ = val;
*bp++ = data >> 24;
*bp++ = data >> 16;
*bp++ = data >> 8;
*bp++ = data;
return bp - buf;
}
Для настоящего протокола потребовалось бы написать не один десяток таких функций — на все возможные варианты. Можно было бы несколько упростить процесс, используя макросы или функции для обработки базовых типов данных (short, long и т. п.), но и тогда подобный повторяющийся код было бы трудно воспринимать, трудно поддерживать, и в итоге он стал бы потенциальным источником ошибок.
Именно повторяемость кода и является его основной чертой, и здесь-то нам и может помочь грамотно подобранный способ записи. Позаимствовав идею у printf, мы можем определить свой маленький язык спецификации, в котором каждый пакет будет описываться краткой строкой, дающей информацию о размещении данных внутри него. Элементы пакета кодируются последовательно: с обозначает 8-битовый символ, s — 16-битовое короткое целое, а 1 — 32-битовое длинное целое. Таким образом, например, пакет первого типа (включая первый байт определения типа) может быть представлен форматной строкой cscl. Теперь мы в состоянии использовать одну-единственную функцию pack для создания пакетов любых типов; описанный только что пакет будет создан вызовом
pack(buf, "cscl", 0x01, count, val, data);
В нашей строке формата содержатся только описания данных, поэтому нам нет нужды использовать какие-либо специальные символы — вроде % в printf.
На практике о способе декодирования данных могла бы сообщать приемнику информация, хранящаяся в начале пакета, но мы предположим, что для определения формата данных используется первый байт пакета. Передатчик кодирует данные в этом формате и передает их; приемник считывает пакет, анализирует первый байт и использует его для декодирования всего остального.
Ниже приведена реализация pack, которая заполняет буфер buf кодированными в соответствии с форматом значениями аргументов. Мы сделали значения беззнаковыми, в том числе байты буфера пакета, чтобы избежать проблемы переноса знакового бита. Чтобы укоротить описания, мы использовали некоторые привычные определения типов:
typedef unsigned char uchar;
typedef unsigned short ushort;
typedef unsigned long ulong;
Точно так же, как sprintf, strcpy и им подобные, наша функция pack предполагает, что буфер имеет достаточный размер, чтобы вместить результат; обеспечить его должна вызывающая сторона. Мы не будем делать попыток определить несоответствия между строкой формата и списком аргументов.
#include <stdarg. h>
/* pack: запаковывает двоичные элементы в буфер, */
/* возвращает длину */
int pack(uchar *buf, char *fmt, ...)
{
va_list args;
char *p;
uchar *bp;
ushort s;
ulong 1;
bp = buf;
va_start(args, fmt);
for (p = fmt; *p != '\0'; P++) {
switch (*p) {
case 'c': /* char */
*bp++ = va_arg(args, int);
break;
case 's': /* short */
s = va_arg(args, int);
*bp++ = s >> 8;
*bp++ = s;
break;
case 'l': /* long */
l = va_arg(args, ulong);
*bp++ = 1 >> 24;
*bp++ = l >> 16;
*bp++ = 1 >> 8;
*bp++ = 1;
break;
default: /* непредусмотренный тип */
va_end(args);
return -1;
}
}
va end(args);
return bp - buf;
}
Функция pack использует заголовочный файл stdarg. h более активно, чем функция eprintf в главе 4. Аргументы последовательно извлекаются с помощью макроса va_arg, первым операндом которого является переменная типа va_list, инициализированная вызовом va_sta rt; а в качестве второго операнда выступает тип аргумента (вот почему va_arg — это именно макрос, а не функция). По окончании обработки должен быть осуществлен вызов va_end. Несмотря на то что аргументы для ' с' и ' s' представлены значениями char и short соответственно, они должны извлекаться как int, поскольку, когда аргументы представлены многоточием, С переводит char и short в int.
Теперь функции pack_type будут состоять у нас всего из одной строки, в которой их аргументы будут просто заноситься в вызов pack:
/* pack_type1: пакует пакет типа 1 */
int pack_type1(uchar *buf, ushort count, uchar val, ulong data)
{
return pack(buf, "cscl", 0x01, count, val, data);
}
Для распаковки мы делаем то же самое и вместо того, чтобы писать отдельный код для обработки каждого типа пакетов, вызываем общую функцию unpack с соответствующей форматной строкой. Это централизует преобразования типов:
/* unpack: распаковывает элементы из buf, возвращает длину */
int unpack(uchar *buf, char *fmt, ...)
{
va_list args;
char *p;
uchar *bp, *pc;
ushort *ps;
ulong *pl;
bp = buf;
va_start(args, fmt);
for (p = fmt; *p !- '\0'; p++) {
switch (*p) {
case 'c': /* char */
pc = va_arg(args, uchar*);
*pc = *bp++;
break;
case 's': /* short */
ps = va_arg(args, ushort*);
*ps = *bp++ << 8;
*ps | = *bp++;
break;
case ‘l’: /* long */
pl = va_arg(args, ulong*);
*pl = *bp++ << 24;
*pl | = *bp++ << 16;
*pl | = *bp++ << 8;
*pl | = *bp++;
break;
default: /* непредусмотренный тип */ va_end(args);
return -1;
}
}
va_end(args);
return bp - buf;
}
Так же как, например, scanf, функция unpack должна возвращать вызвавшему ее коду множество значений, поэтому ее аргументы являются указателями на переменные, где и будут храниться требуемые результаты. Значением функции является количество байтов в пакете, его можно использовать для контроля.
Все значения у нас беззнаковые. Мы придерживались размеров, которые ANSI С определяет для типов данных, и поэтому наш код можно переносить даже между машинами, имеющими разные размеры для типов short и long. Если только программа, использующая pack, не будет пытаться переслать как long (к примеру) значение, которое не может быть представлено 32 битами, то значение будет передано корректно; на самом деле мы передаем младшие 32 бита числа. Если же потребуется передавать более длинные значения, то нужно придумать другой формат.
Благодаря использованию unpack, функции для распаковки пакетов в зависимости от их типа стали выглядеть гораздо проще:
/* unpack_type2: распаковывает и обрабатывает пакет типа 2 */
int unpack_type2(int n, uchar *buf)
{
uchar с;
ushort count;
ulong dw1, dw2;
if (unpack(buf, "csll", &c, &count, &dw1, &dw2) != n)
return -1;
assert(c == 0x02);
return process_type2(count, dw1, dw2);
}
еред тем как вызывать unpack_type2, мы должны сначала убедиться, что имеется пакет именно 2-го типа; распознаванием типа пакетов занимается цикл получателя, примерно такой:
while ((n = readpacket(network, buf, BUFSIZ)) > 0) {
switch (buf[0]) {
default:
eprintf("неправильный тип пакета 0x%x", buf[0]);
break;
case 1:
unpack_type1(n, buf);
break;
case 2:
unpack_type2(n, buf);
break;
….
}
}
Подобный стиль описания функций довольно размашист. Можно более компактно определить таблицу указателей на распаковывающие функции, причем номер в таблице будет типом пакета:
int (*unpackfn[])(int, uchar *) = {
unpack_type0,
unpack_type1,
unpack_type2,
};
Каждая функция в таблице разбирает пакет своего типа, проверяет результат и инициирует дальнейшую обработку этого пакета. Благодаря этой таблице работа приемника получается более прямолинейной:
/* receive: читает пакеты из сети, обрабатывает их */
void receive(int network)
{
uchar type, buf[BUFSIZ];
int n;
while ((n = readpacket(network, buf, BUFSIZ)) > 0) {
type = buf[0];
if (type >= NELEMS(unpackfn))
eprintf ( "неправильный тип пакета 0х%х", type);
if ((*unpackfn[type])(n, buf) < 0)
eprintf ("ошибка протокола, тип %х длина %d", type, n);
}
}
Итак, теперь код для обработки каждого пакета стал компактен; основная часть всей обработки происходит в одной функции и потому поддерживать такой код нетрудно. Код приемника теперь мало зависит от самого протокола; код его также прост и однозначен.
Этот пример основан на некоем реальном коде настоящего коммерческого сетевого протокола. Как только автор осознал, что этот, в общем, нехитрый подход работоспособен, в небытие ушли несколько тысяч строк повторяющегося кода, напичканного ошибками (скорее даже, описками), и вместо него появились несколько сот строк, поддерживать которые можно без всякого напряжения. Итак, хороший способ написания существенным образом улучшил как сам процесс работы, так и ее результат.
Упражнение 9-1
Измените pack и unpack так, чтобы можно было передавать и значения со знаком, причем даже между машинами, имеющими разные размеры short и long. Как вы измените форматную строку для обозначения элемента данных со знаком? Как можно протестировать код, чтобы убедиться, что он корректно передает, например, число -1 с компьютера с 32-битовым long на компьютер с 64-битовым long?
Упражнение 9-2
Добавьте в pack и unpack возможности обработки строк. (Есть вариант включать длину строки в форматную строку.) Добавьте возможность обработки повторяющихся значений с помощью счетчика. Как это соотносится с кодировкой строк?
Упражнение 9-3
Вспомните таблицу указателей на функции, которую мы применили только что в программе на С. Такой же принцип лежит в основе механизма виртуальных функций C+ + . Перепишите pack, unpack и receive на C++, чтобы прочувствовать все удобство этого способа.
Упражнение 9-4
Напишите версию printf для командной строки: пусть эта функция печатает свой второй и последующие аргументы в формате, заданном в первом аргументе. Надо отметить, что во многих оболочках имеется встроенный аналог такой функции.
Упражнение 9-5
Напишите функцию, реализующую спецификации формата, .используемого в какой-нибудь программе работы с электронными таблицами или в Java-классе Decimal Format, где числа отображаются в соответствии с некоторым заданным шаблоном, указывающим количество обязательных и возможных символов, положение десятичной точки и тысячных запятых и т. п. Для иллюстрации рассмотрим строку
##,##0.00
Эта строка задает число с двумя знаками после десятичной точки, по крайней мере одним знаком перед десятичной точкой, запятой в качестве разделителя тысяч и заполняющими пробелами до позиции 10 000. .Таким образом, число 12345.67 будет представлено как 12, 345. 67, а .4 — как **, **0. 40 (для наглядности вместо пробелов мы вставили звездочки). Для получения полной спецификации можете обратиться к описанию DecimalFormat или программам работы с электронными таблицами.
