Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
kernigan_paik.doc
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
2.91 Mб
Скачать

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 или программам работы с электронными табли­цами.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]