Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Kernigan_B__Payk_R_Praktika_programmirovania.pdf
Скачиваний:
79
Добавлен:
18.03.2016
Размер:
2.53 Mб
Скачать

Нотация

Форматирование данных

Регулярные выражения

Программируемые инструменты

Интерпретаторы, компиляторы и виртуальные машины

Программы, которые пишут программы

Использование макросов для генерации кода

Компиляция "на лету"

Дополнительная литература

Из всех творений человека самым удивительным является язык.

Джайлс Литтон Страчи. Слова и поэзия

Правильный выбор языка может решающим образом влиять на простоту написания программы. Поэтому в арсенале практикующего программиста находятся не только языки общего назначения вроде С и его родственников, но и программируемые оболочки, языки скриптов, а также большое количество языков, ориентированных на конкретные приложения.

Преимущества хорошей нотации — способа записи — появляются при переходе от традиционного программирования к узкоспециальным проблемным областям. Регулярные выражения позволяют использовать компактные (из-за этого подчас превращающиеся в тайнопись) описания классов строк. Язык HTML позволяет определять внешний вид интерактивных документов, нередко используя встроенные программы на других языках, вроде JavaScript. PostScript рассматривает целый документ — например эту книгу — как стилизованную программу. Электронные таблицы и текстовые процессоры часто содержат в себе языки программирования типа Visual Basic, они используются для вычисления выражений, доступа к информации, управления размещением данных в документе.

Если вы ловите себя на том, что приходится писать слишком много кода' для выполнения рутинных операций, или если у вас возникают проблемы с тем, чтобы в удобной форме описать весь процесс, знайте — скорее всего, вы выбрали неправильный язык. Отсутствие правильного языка можно считать хорошим поводом написать его самостоятельно. Придумать свой язык вовсе не означает создать преемника Java: просто нередко самые запутанные проблемы проясняются при выборе должной нотации. В связи с этим вспомните форматные строки семейства printf, которые дают нам компактный и выразительный способ для управления выводом.

В этой главе мы говорим о том, как способ записи может помочь в решении наших проблем, а также демонстрируем ряд приемов, которые вы мо-Ц «ете использовать для создания собственных специализированных языков. Мы даже заставим программу писать другую программу, такова экстремальное использование способа записи распространено гораздо пире и осуществляется гораздо проще, чем думают многие программисты.

Форматирование данных

Между тем, что мы хотим сказать компьютеру ("реши мою проблему"), и тем, что нам приходится ему говорить для достижения нужного результата, всегда существует некоторый разрыв. Очевидно, что чем этот разрыв меньше, тем лучше. Хорошая нотация поможет нам сказать лменно то, что мы хотели, и препятствует ошибкам. Иногда хороший ;пособ записи освещает проблему в новом ракурсе, помогая в ее решении и подталкивая к новым открытиям.

Малые языки (little languages) — это нотации для узких областей применения. Эти языки не только предоставляют удобный интерфейс, но| л помогают организовать программу, в которой они реализуются. Хоро-лим примером является управляющая последовательность printf:

printf("%d %6.2f %-10.10s\n", f, s);

Здесь каждый знак процента обозначает место вставки значения сле-гующего аргумента printf ; за ним следуют необязательные флаги и раз- \ меры поля и, наконец, буква, которая указывает тип параметра. Такая нотация компактна, интуитивно понятна и легка в использовании; ее реализация достаточно проста и прямолинейна. Альтернативные возможности в C++ (lost ream) и Java ( j ava . io) выглядят гораздо менее привле-сательно, поскольку они не предоставляют специальной нотации, хотя могут расшириться типами, определяемыми пользователем, и обеспечив 5ают проверку типов.

Некоторые нестандартные реализации printf позволяют добавлять] ;свои приведения типов к встроенным. Это удобно, когда вы работаете] : другими типами данных, нуждающимися в преобразованиях при вы- р| зоде. Например, компилятор может использовать знак %L_ для обозначения номера строки и имени файла; графическая система — использовать Р для точки, a %R — для прямоугольника. Строка шифра из букв и номеров — сведения о биржевых котировках, которая рассматривалась нами главе 4, относится к тому же типу: это компактный способ записи таких котировок.

Схожие примеры можно придумать и для С и C++. Представим себе, что нам нужно пересылать пакеты, содержащие различные комбинации типов данных, из одной системы в другую. Как мы видели в главе 8, самым чистым решением была бы передача данных в текстовом виде. Однако для стандартного сетевого протокола лучше иметь двоичный формат по причинам эффективности и размера. Как же нам написать код для обработки пакетов, чтобы он был переносим, эффективен и прост в эксплуатации?

Для того чтобы дальнейшее обсуждение было конкретным, представим себе, что нам надо пересылать пакеты из 8-битовых, 16-битовых и 32-битовых элементов данных из одной системы в другую. В стандарте ANSI С оговорено, что в char может храниться как минимум 8 битов, 16 битов может храниться в sho rt и 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 - but; }

Для настоящего протокола потребовалось бы написать не один десяток сих функций

— на все возможные варианты. Можно было бы несколь-упростить процесс, используя макросы или функции для обработки ювых типов данных (short, long и т. п.), но и тогда подобный повторяющийся код было бы трудно воспринимать, трудно поддерживать, 5 итоге он стал бы потенциальным источником ошибок.

Именно повторяемость кода и является его основной чертой, и здесь-то и может помочь грамотно подобранный способ записи. Позаимствовав идею у printf, мы можем определить свой маленький язык спецификации, в котором каждый пакет будет описываться краткой строкой, дающей информацию о размещении данных внутри него. Элементы пакета даруются последовательно: с обозначает 8-битовый символ, s — 16-битовoe короткое целое, а 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;

Точно так же, как sprinth, strcopy и им подобные, наша функция предполагает, что буфер имеет достаточный размер, чтобы вместить результат; обеспечить его должна вызывающая сторона. Мы не будем делать попыток определить несоответствия между строкой формата и списком аргументов.

#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'i 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 '!': /* long */ 1 = va_arg(args, ulong); *bp++ = 1 »

24; *bp++ = 1 » 16; *bp++ = 1 » 8; *bp++ = 1; break;

default: /* непредусмотренный тип */ va_end(args); return -1; }

} va_end(args); return bp - buf;

}

Функция pack использует заголовочный файл stda rg . h более активно, чем функция eprintf в главе 4. Аргументы последовательно извлекаются с помощью макроса va_arg, первым операндом которого является переменная типа va_list, инициализированная вызовом va_start; а в качестве второго операнда выступает тип аргумента (вот почему 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 !='\О'; р++) { 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 '!' /* long */

pi = 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 tinpack_type2(int n, uchar *buf)

{

uchar с; ushort count; ulong dw1, dw2;

if (unpack(buf, "call"', &c, Scount, &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[OJ)

{default:

eprintf("неправильный тип пакета Ох%х", buf[0]); break; case 1:

unpack_type1(n, buf); break; case 2: unpack_type2(n, buf); break;

} }

Подобный стиль описания функций довольно размашист. Можно более мпактно определить таблицу указателей на распаковывающие функции, причем номер в таблице будет типом пакета:

int (*unpackfn[])(int, uchar *) = { unpack_typeO,

unpack_type1, unpack_type2, };

Каждая функция в таблице разбирает пакет своего типа, проверяет ре-пьтат и инициирует дальнейшую обработку этого пакета. Благодаря эй таблице работа приемника получается более прямолинейной:

/* receive: читает пакеты из сети, обрабатывает их */ void receive(int network)

{

uchar type, but[BUFSIZ]; int n;

while ((n = readpacket(network, buf, BUFSIZ)) > 0) { type = buf[0]; if (type >= NELEMS(unpackfn)) eprintf("неправильный тип пакета Ох%х", 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

Вспомните таблицу указателей на функции, которую мы применили олько что в программе на С. Такой же принцип лежит в основе механизмa виртуальных функций C++. Перепишите pack, unpack и receive на ;++, чтобы прочувствовать все удобство этого способа.

Упражнение 9-4

Напишите версию printf для командной строки: пусть эта функция ечатает свой второй и последующие аргументы в формате, заданном первом аргументе. Надо отметить, что во многих оболочках имеется строенный аналог такой функции.

Упражнение 9-5

Напишите функцию, реализующую спецификации.формата, .исполь-/емого в какойнибудь программе работы с электронными таблицами ли в Java-классе Decimal Format, где числа отображаются в соответствии некоторым заданным шаблоном, указывающим количество обязательных и возможных символов, положение десятичной точки и тысячных шятых и т. п. Для иллюстрации рассмотрим строку

##,##0.00

Эта строка задает число с двумя знаками после десятичной точки, э крайней мере одним знаком перед десятичной точкой, запятой в качестве разделителя тысяч и