Форматирование данных
Между тем, что мы хотим сказать компьютеру ("реши мою проблему"), и тем, что нам приходится ему говорить для достижения нужного результата, всегда существует некоторый разрыв. Очевидно, что чем этот разрыв меньше, тем лучше. Хорошая нотация поможет нам сказать лменно то, что мы хотели, и препятствует ошибкам. Иногда хороший ;пособ записи освещает проблему в новом ракурсе, помогая в ее решении и подталкивая к новым открытиям.
Малые языки (little languages) — это нотации для узких областей применения. Эти языки не только предоставляют удобный интерфейс, но| л помогают организовать программу, в которой они реализуются. Хоро-лим примером является управляющая последовательность printf:
printf("%d %6.2f %-10.10s\n", f, s);
Здесь каждый знак процента обозначает место вставки значения сле-гующего
аргумента printf ; за ним следуют необязательные флаги и раз- \ меры поля
и, наконец, буква, которая указывает тип параметра. Такая нотация компактна,
интуитивно понятна и легка в использовании; ее реализация достаточно проста
и прямолинейна. Альтернативные возможности в C++ (lost ream) и Java (
j ava . io) выглядят гораздо менее привле-сательно, поскольку они не предоставляют
специальной нотации, хотя могут расшириться типами, определяемыми пользователем, и обеспечив 5ают проверку типов.
Некоторые нестандартные реализации printf позволяют добавлять] ;вои приведения типов к встроенным. Это удобно, когда вы работаете] : другими типами данных, нуждающимися в преобразованиях при вы- р| зоде. Например, компилятор может использовать знак %1_ для обозначе- ; шя номера строки и имени файла; графическая система — использовать »Р для точки, a %R — для прямоугольника. Строка шифра из букв и номе- , юв — сведения о биржевых котировках, которая рассматривалась нами главе 4, относится к тому же типу: это компактный способ записи та-сих котировок.
Схожие примеры можно придумать и для С и C++. Представим себе, что нам нужно пересылать пакеты, содержащие различные комбинации типов данных, из одной системы в другую. Как мы видели в главе 8, самым чистым решением была бы передача данных в текстовом виде. Однако для стандартного сетевого протокола лучше иметь двоичный формат по причинам эффективности и размера. Как же нам написать код для обработки пакетов, чтобы он был переносим, эффективен и прост в эксплуатации?
Для того чтобы дальнейшее обсуждение было конкретным, представим себе, что нам надо пересылать пакеты из 8-битовых, 16-битовых и 32-битовых элементов данных из одной системы в другую. В стандарте ANSI С оговорено, что в char может храниться как минимум 8 битов, 16 битов может храниться в sho rt и 32 бита — в long, так что мы, не мудрствуя лукаво, будем использовать именно эти типы для представления наших данных. Типов пакетов может быть много: пакет первого типа содержит однобайтовый спецификатор типа, двухбайтовый счетчик, однобайтовое значение и четырехбайтовый элемент данных:
Пакет второго типа может состоять из одного короткого и двух длинных слов
данных:
Один из способов — написать отдельные функции упаковки и распаковки для
каждого типа пакета:
Для настоящего протокола потребовалось бы написать не один десяток сих
функций — на все возможные варианты. Можно было бы несколь-упростить процесс,
используя макросы или функции для обработки ювых типов данных (short,
long и т. п.), но и тогда подобный повто-ющийся код было бы трудно воспринимать,
трудно поддерживать, 5 итоге он стал бы потенциальным источником ошибок.
Именно повторяемость кода и является его основной чертой, и здесь-то м и может помочь грамотно подобранный способ записи. Позаимство-в идею у printf, мы можем определить свой маленький язык специфи-ции, в котором каждый пакет будет описываться краткой строкой, дающей информацию о размещении данных внутри него. Элементы пакета даруются последовательно: с обозначает 8-битовый символ, s — 16-би-iBoe короткое целое, а 1 — 32-битовое длинное целое.Таким образом, на-зимер, пакет первого типа (включая первый байт определения типа) моет быть представлен форматной строкой cscl. Теперь мы в состоянии :пользовать одну-единственную функцию pack для создания пакетов обых типов; описанный только что пакет будет создан вызовом
pack(buf, "cscl", 0x01, count, val, data);
В нашей строке формата содержатся только описания данных, поэтому ам нет нужды использовать какие-либо специальные символы — вроде в printf.
На практике о способе декодирования данных могла бы сообщать риемнику информация, хранящаяся в начале пакета, но мы предположим, что для определения формата данных используется первый байт пакета. Передатчик кодирует данные в этом формате и передает их; приемник считывает пакет, анализирует первый байт и использует его для (екодирования всего остального.
Ниже приведена реализация pack, которая заполняет буфер buf кодированными в соответствии с форматом значениями аргументов. Мы сделали значения беззнаковыми, в том числе байты буфера пакета, чтобы избежать проблемы переноса знакового бита. Чтобы укоротить описания, мы использовали некоторые привычные определения типов:
Точно так же, как sprinth, strcopy и им подобные, наша функция предполагает,
что буфер имеет достаточный размер, чтобы вместить результат; обеспечить
его должна вызывающая сторона. Мы не будем делать попыток определить несоответствия
между строкой формата и списком аргументов.
Функция 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:
Для распаковки мы делаем то же самое и вместо того, чтобы писать тдельный
код для обработки каждого типа пакетов, вызываем общую функцию unpack
с соответствующей форматной строкой. Это централизирует преобразования
типов:
Так же как, например, scanf, функция unpack должна возвращать вызвавшему
ее коду множество значений, поэтому ее аргументы являются указателями
на переменные, где и будут храниться требуемые результаты. Значением функции
является количество байтов в пакете, его можно использовать для контроля.
Все значения у нас беззнаковые. Мы придерживались размеров, которые ANSI С определяет для типов данных, и поэтому наш код можно переносить даже между машинами, имеющими разные размеры для типов short и long. Если только программа, использующая pack, не будет пытаться переслать как long (к примеру) значение, которое не может быть представлено 32 битами, то значение будет передано корректно; на самом деле мы передаем младшие 32 бита числа. Если же потребуется передавать более длинные значения, то нужно придумать другой формат.
Благодаря использованию unpack, функции для распаковки пакетов в зависимости от их типа стали выглядеть гораздо проще:
Перед тем как вызывать unpack_type2, мы должны сначала убедиться,, о имеется
пакет именно 2-го типа; распознаванием типа пакетов зани-сется цикл получателя,
примерно такой:
Подобный стиль описания функций довольно размашист. Можно более мпактно
определить таблицу указателей на распаковывающие функ-:и, причем номер
в таблице будет типом пакета:
Каждая функция в таблице разбирает пакет своего типа, проверяет ре-пьтат
и инициирует дальнейшую обработку этого пакета. Благодаря эй таблице работа
приемника получается более прямолинейной:
Итак, теперь код для обработки каждого пакета стал компактен; основная
часть всей обработки происходит в одной функции и потому поддерживать
такой код нетрудно. Код приемника теперь мало зависит от самого протокола;
код его также прост и однозначен.
Этот пример основан на некоем реальном коде настоящего коммерческого сетевого протокола. Как только автор осознал, что этот, в общем, не-хитрЦй подход работоспособен, в небытие ушли несколько тысяч строк повторяющегося кода, напичканного ошибками (скорее даже, описками), и вместо него появились несколько сот строк, поддерживать которые можно без всякого напряжения. Итак, хороший способ написания существенным образом улучшил как сам процесс работы, так и ее результат.
Упражнение 9-1
Измените pack и unpack так, чтобы можно было передавать и значения со знаком, причем даже между машинами, имеющими разные размеры short и long. Как вы измените форматную строку для обозначения элемента данных со знаком? Как можно протестировать код, чтобы убедиться, что он корректно передает, например, число -1 с компьютера с 32-битовым long на компьютер с 64-битовым long?
Упражнение 9-2
Добавьте в pack и unpack возможности обработки строк. (Есть вариант включать длину строки в форматную строку.) Добавьте возможность обработки повторяющихся значений с помощью счетчика. Как это соотносится с кодировкой строк?
Упражнение 9-3
Вспомните таблицу указателей на функции, которую мы применили олько что в программе на С. Такой же принцип лежит в основе механиз-ia виртуальных функций C++. Перепишите pack, unpack и receive на ;++, чтобы прочувствовать все удобство этого способа.
Упражнение 9-4
Напишите версию printf для командной строки: пусть эта функция ечатает свой второй и последующие аргументы в формате, заданном первом аргументе. Надо отметить, что во многих оболочках имеется строенный аналог такой функции.
Упражнение 9-5
Напишите функцию, реализующую спецификации.формата, .исполь-/емого в какой-нибудь программе работы с электронными таблицами ли в Java-классе Decimal Format, где числа отображаются в соответствии некоторым заданным шаблоном, указывающим количество обязатель-ых и возможных символов, положение десятичной точки и тысячных шятых и т. п. Для иллюстрации рассмотрим строку
##,##0.00
Эта строка задает число с двумя знаками после десятичной точки, э крайней мере одним знаком перед десятичной точкой, запятой в качестве разделителя тысяч и заполняющими пробелами до позиции 10 000. аким образом, число 12345.67 будет представлено как 12, 345. 67, а .4 — ж **,**0.40 (для наглядности вместо пробелов мы вставили звездоч-i). Для получения полной спецификации можете обратиться к описа-«о DecimalFormat или программам работы с электронными таблицами.