Последний раз обновлено 27.06.01
Просмотр файла с памятью (проект VM)
Как уже говорилось ранее, мне не удалось создать примитивной системы сохранения и записи объектов в поток. Эти свойства объекта рассматриваются как ортогональный остальным вид на объект. Речь об этом виде пойдет в самом конце и он требует хорошего понимания более простых вещей. Пока эти сообщения можно благополучно пропустить. Иногда, если мне это кажется нужным, я буду уделять внимание внутреннему строению объекта, иногда нет.
При анализе этих классов надо набрать тот минимум техники С++, чтобы при работе с более сложными абстракциями не спотыкаться на незнании того, как описать класс в С++. В этих простых классах не решается главная задача - как его получить из задачи.
Самым раздражающим моментом в работе со строкой типа (char_p)
является невозможность удобно складывать или копировать такие строки. Приходится заниматься постоянно распределением памяти, что совершенно не относится к сути решаемой задачи. С другими проблемами вполне справляется стандартная библиотека из string.h. Я не хочу переназвать все функции из string.h новыми именами. Самой удобной и, видимо, самой используемой будет семейство функций printf()
.
Этот класс я назвал АТД, потому как идея его создания - именно образование АТД относительно операции сложения, вернее относительно операций, связанных с перераспределением памяти. Присваивание можно считать как такую последовательность: очистить и прибавить к себе.
Этот класс не создавался, чтобы его можно было наследовать. Он работает с однобайтовой u_char
строкой, что достаточно для многих применений.
Раз уж мы делаем АТД, то стоит добавить другой полезный класс операций - операции сравнения. Функция strcmp()
делает это не очень удобно для прямой проверки в if()
. Как вообще можно будет в этом классе сравнить строки?
Сравнение производится перегруженными операциями, аналогично применяемым для встроенных типов: == != < > <= >=
. По умолчанию, длина строки учитывается.
Разрешение использования длины строки для сравнения задается раздельно:
для операций == и !=
|
|
для операций < > <= >=
|
|
Этот класс развился из примера t_string
из раздела А-модель. Строка хранится внутри объекта как (char_p)
ограниченный \0. Длина строки не хранится, т.к. все операции над строкой производятся через string.h. Размер строки ограничен размером памяти, которую можно получить от ОС. Размер памяти, выделенный для строки может быть больше чем используемая длина строки. Контроль за размером памяти не для операций определенных в этом классе лежит на программисте.
Лучше изучить поведение этого класса ( с точки зрения технических подробностей вызовов конструкторов и т.п. ) можно сняв комментарий в строке
#define TSTRING_DEBUG
в файле tstring.cpp, и создав простые примеры использующие ссылки и объекты этого типа строки в обычных операциях и в возвратах функций, и читая описание С++, но для решения задачи и цепочки задача-модель-решение этого, я надеюсь, не надо.
Объект содержит защищенные, т.е. не доступные прямо при доступе через ссылку на экземпляр класса, элементы:
|
Первый указывает на выделенную область памяти и на строку. Второй используется для запоминания выделенного размера памяти и контроля перевыделения памяти, если строка не помещается туда. Последние хранят установки для использования длины при сравнении для двух групп операций.
Важное замечание. Реальный размер выделенной памяти чуть больше, чем нужно для строки. Это позволит снять проблему учета \0 символа при вычислениях размера и связанные с этим ошибки «налезания» на чужие области памяти. Память для концевого \0, но только для него, выделятся автоматически, что удобно.
Для создания строки используется набор конструкторов:
| ||
|
Первый набор конструкторов реализуется с помощью одного реального конструктора и параметров по умолчанию. Это «синтаксический» полиморфизм: эмуляция набора методов отличающихся подробностью задания параметров.
Второй параметр maxlen
используется для указания минимального размера памяти, который сразу будет выделен. Если для программы важна скорость выполнения и известно, что размер строки в большинстве случаев не превысит некоторый предел, например, имя файла всегда менее 1024 байт, то для ускорения работы, чтобы снизить перевыделения памяти при нехватке места для строки, можно сразу выделить нужный размер. Приравнять память по длине строки, т.е. скопировать в новую область, но меньшего размера, освободив не используемую память, всегда можно сообщением compact()
.
Конструктор копирования служит для дублирования данных строки, а не указателя на них, что делает конструктор копирования по умолчанию. Копированием просто называют создание объекта из другого объекта такого же класса.
Примеры:
если не использованы параметры, то конструктор по умолчанию |
|
создать из char_p |
|
зарезервировать память для строки |
|
создать объект из другого объекта такого же класса |
|
Я напомню, что конструктор это не оператор для выделения памяти, в свете чего его многие пытаются рассматривать. Конструктор - это создание объекта из чего-нибудь, в том числе и из ничего. Его можно рассматривать как оператор преобразования одного типа в другой, а не аналог оператора new
.
Для удаления объекта используется деструктор. В этом классе он просто освобождает память выделенную под строку, под данные строки data_p
. Поля самого объекта этого класса удаляются автоматически.
Деструктор не виртуальный и класс не предназначен для наследования. Не наследуйте этот класс.
Основными операциями являются, как я уже говорил, операции c распределением памяти
добавление к себе Tstring или char_p (+=) |
|
присваивание Tstring или char_p (=) |
|
сложение двух Tstring (+) |
|
В первом и втором случае возвращается ссылка на себя самого, чтобы можно было складывать конвейерно: (a+=(b+=c /*вернет b*/)/*вернет а*/)
. В последнем случае создается новый объект, который и возвращается.
Следует рассмотреть вариант возврата локального объекта, чтобы снять возможные недоумения по потерянным данным. Локальный объект ведет себя также, как и локальная переменная. Объект это грубо как минимум экземпляр АТД, а АТД это тип переменной.
При возврате локальной переменной, например int
, происходит сначала копирование ее ( из стека или из чего-нибудь ) в получатель, потом ссылка на нее теряется. По аналогии с этим, объект копируется в получатель (если он есть) затем он переводится в разряд «мусора», в самом простом случае для него сразу вызовется деструктор и ссылка на него будет утеряна. Не будем вдаваться в подробности и упрощенно скажем, что для возврата будут использоваться конструктор копирования (чтобы сменить контекст определения локального объекта, создать временный объект возврата доступный приемнику, что элементарно для int
) и оператор присваивания.
| ||
| ||
| ||
|
Первое сообщение приравняет память к реальной длине строки, а точнее скопирует в новую область, но меньшего размера.
Получение длины строки используется для проверки на NULL поля data_p
перед отправкой в strlen()
, иначе это на некоторых стандартных библиотеках вызовет сбой обращения к памяти, мы же считаем это строкой нулевой длины. Если нет уверенности, что память для строки была выделена, т.е. когда вы сами только что не проверили, лучше не использовать функции string.h прямо.
Преобразование в char_p
в этом классе на самом деле не преобразовывает ничего, а открывает внутреннюю строку для использования внешними функциями. Я уже вижу как ругаются знатоки С++ и борцы за надежность. Да. Открытие внутренних данных на запись, мягко говоря, плохо для объекта. Но, к большой радости, у нас не объект, а АТД. Для него это в порядке вещей, а для нас пока в порядке вещей простота в использовании.
Обычно, подобные преобразования из типа в тип делают с помощью оператора преобразования источника в тип приемника или конструктора приемника с параметром источника. Но у нас опять особый случай. Приемник-то у нас часто без типа - любимый printf()
.
Удобство оператора преобразования - автоматическое приведение типа. Как раз в случае параметра без типа это удобство работать не будет, надо будет явно задать тип, например так
printf("my string is: %s\n",(char_p)str);
Это крайне нудно, поверьте мне. Унарный минус для объекта - находка для получения из него чего-нибудь во всех случаях, когда объект не имеет логического определения как больше или меньше нуля. Он правильно работает как для ссылки, так и для указателя. В сомнительных случаях лучше ставить скобки.
|
Это юникоде, который не совсем юникоде, вернее, совсем не юникоде. Лучше было назвать его recode, но что выросло, то выросло. В данной версии Tunicode
объект переводит u_char
(размером в 8 бит) строки из одной кодировки в другую. Перекодировке подвергаются u_char
с включенным старшим битом, т.е. 0x80 и больше.
Доступны такие кодовые страницы перечислимого типа t_unicode_type
:
t_unicode_type | описание |
UNICODE_TYPE_DOS_866 | для DOS, ее еще называют альтернативная |
UNICODE_TYPE_WIN_1251 | для Windows |
UNICODE_TYPE_KOI8R | для Unix |
Тип входной и выходной кодировки задается такими сообщениями
| ||
|
Необходимость перекодировать символы возникает в этих примерах в следующих случаях:
Для каждого из этих действий можно создать свой объект Tunicode
. Причем для сообщений программы этот объект смело можно создать в глобальном контексте, назвав его, например, msg_unicode
и в функции main()
устанавливая нужные значения кодировок источника и приемника. Для каждого объекта программы можно как свойство включить Tunicode
объект и использовать его для локального, только для этого объекта, задания кодировки текста файла и его имени в файловой системе.
При создании объекта просто устанавливаются значения кодировок в допустимом диапазоне. Какие это кодировки не оговаривается. Перед использованием объекта надо их указать сообщениями source()
и target()
. Специально определять для этого объекта деструктор не надо.
Перекодировка производится посылкой сообщения convert()
. Это сообщение имеет две формы, два метода, отличающиеся типом параметра ( параметрический полиморфизм для сообщения convert()
).
| ||
|
Для символа, сообщение convert()
возвращает преобразованный символ. Для строки возвращается длина преобразованной строки. В принципе, возврат для строки и не нужен, т.к. реальное преобразование происходит в памяти, адресуемой указателем str
, но сообщение имеет один логический тип возврата.
Логичнее, может быть, было бы возвращать АТД, которое получено после преобразования. Но мы не будем делать сложно то, что можно сделать просто, поэтому вернем u_int
, который можно трактовать и как u_char
, и как длина строки, и как двухбайтовый код.
Форма сообщения с параметром char_p
имеет две подформы, т.е. помимо параметрического полиморфизма здесь еще подробности задания параметров - «синтаксический» по нашей классификации.
Параметр size
позволяет перекодировать области памяти, содержащие \0. Если size
равен нулю, то перекодирование идет до первого \0, иначе по длине size
.
Вспомогательные сообщения служат для определения текущего состояния объекта.
| ||
|
Сообщение get_unicode_type(1)
вернет установленный тип кодировки источника, а get_unicode_type(0)
вернет установленный тип кодировки после преобразования.
Второе сообщение возвращает строку с описанием t_unicode_type
, полученного в качестве параметра. Такое сообщение не связано с параметрами объекта, которое его получает, и лучше сделать его статическим или вообще исключить из описания класса и сделать внешней функцией, хотя это значительно хуже. По историческим причинам в данном описании это сообщение не статическое.
Вот простой пример перекодировки сообщения программы
|
Во многих случаях объекты удобно иметь в группированном виде. Такая группа может быть упорядоченная (ряд, последовательность) или нет (множество). Ряд фиксированного размера можно сделать объявив стандартный массив. Недостатком массива является невозможность после объявления просто изменить его размер. Вернее, неудобно стандартными операциями делать это. Массив сразу занимает размер памяти (и другие ресурсы) по объявленному числу элементов. Часто надо сделать массив, ресурсы под элементы которого выделяются динамически, при создании или удалении элемента массива, и иметь возможность добавлять и убирать элементы с сохранением упорядоченности остальных в произвольном месте массива, между любыми соседними элементами. Такой массив называют списком. Конечно, список может эмулировать динамическое выделение памяти, храня данные в массиве.
Список можно получить включив в любой объект поля связи с другими объектами и обрабатывая их методами объекта. Можно создать базовую абстракцию ( класс ) списка, все наследники которого получат возможность образовать список. Можно создать список, элементом которого будет любой объект или любой объект-потомок класса элемент_списка.
Не занимаясь их сравнениями, я скажу что будет использоваться третий вариант. Фактически, этот класс - список указателей на объекты списка. Этот класс есть прямое развитие списка из примера к А-модели.
Под состоянием списка понимаем:
Мы будем использовать список, в котором элемент связан и с последующим, и с предыдущим элементом. Для такого списка сложнее операции изменения его структуры ( удаление, вставка ), чем для варианта с одной связью, но существенно быстрее операция перемещения к началу.
Вот перечень допустимых операций над списком, которых нам будет достаточно:
Эти операции определены с помощью абстрактного класса Tplist_access
так:
Операции для доступа к списку |
|
Можно прочитать раздел "Объектная модель и С++:перегрузка операторов" и увидеть, что в данном случае использован вариант с изменением смысла операций. Операторы 3, 4 - постфиксная форма, о чем говорит параметр int
, который не используется, а служит только для указания формы. Оператор 7 - унарный минус. Он, как и 6, возвращает указатель на элемент списка. Индекс считается от нуля.
Вот классы списка.
| ||
| ||
|
Первый из них - пустой. Любой элемент наследник этого класса может быть добавлен в список. Т.к. этот класс в данном случае пустой, то можно его не наследовать, а использовать явное приведение типа.
Такой вариант "безтипового" элемента для хранения плох с точки зрения автоматического контроля типа и велика вероятность ошибочного использования элемента. Если все элементы списка однородные, то эта проблема менее острая. В любом случае, вы всегда можете наследовать из этого класса такой класс, который будет работать с вашими объектами без явных преобразований правильно. В этих примерах это не используется.
Что такое итератор, рассказано в разделе "Реализация объектной модели:Дружественные классы и функции". Как к списку, так и к итератору применимы операции описанные в классе Tplist_access
.
Организацией связей, упорядоченностью элементов списка занимается внутренний, прямо не используемый внешними программами, класс Tplist_item
. Он содержит указатели на следующий и предыдущий Tplist_item
и указатель на данные - пользовательский объект элемента списка Tlist_item
. Объекты класса Tplist_item
есть реальные элементы списка, каждый из которых обрамляет пользовательский объект полями связи.
Особенностью данного варианта списка является наличие итератора. Этот объект сохраняет копию состояния списка и поддерживает все операции со списком. С каждым списком может быть связан целый набор итераторов, т.е. итераторы сами образуют еще один скрытый список, но очень простой. Элементом этого скрытого списка итераторов тоже служит Tplist_item
, но поле prev
не используется. Подробности смотреть в листинге.
Создается итератор конструктором с параметром объекта списка. При создании, он добавляется к списку итераторов списка, а при удалении - исключается из него.
Даже если не включать в наш итератор команды, модернизирующие список (1, 2, 3), а допустить их только для объекта списка, то легко представить, что состояние итератора может стать недействительным, если будут добавлены или удалены элементы списка, за исключением малого количества случаев. Тот же эффект будет достигнут, если удалить объект списка, на который ссылается итератор. Для борьбы с этим злом я сделал итератору свойства, которые устанавливаются для всех итераторов связанных с изменяемым списком в такие состояния
| ||
|
Любая операция итератора, которая делает состояние других списков неверным вызывает сообщение для своего объекта-итератора
void send_new_list();
Это рассылка обновления списка по остальным итераторам, которое устанавливает всем другим итераторам состояние no_longer_valid
. Сбросить неопределенное состояние, если объект список не удален, можно и нужно выбрав индексированием элемент ноль [0].
Проверять корректность состояния итератора нельзя. Это не входит в список допустимых операций. Алгоритм должен быть составлен так, что он не пытается иметь доступ через итератор к уже удаленному списку или к списку с неопределенным состоянием, которое получается после модификации списка через другой итератор. Эта рассылка нарушения списка служит для индикации ошибки алгоритма и нашей тренировке описывать дружественные классы.
Класс списка Tplist
, как и класс итератора Tplist_iterator
, является потомком абстрактного класса допустимых операций для списка Tplist_access
. При доступе к списку через объект класса списка, а не через итератор, используется скрытый итератор, а определенные в пространстве имен по умолчанию для Tplist
операции просто транслируют сообщение во вложенный объект, поэтому объект списка можно прямо использовать без объявления дополнительных итераторов, если такой нужен только один.
Тут использовано наследование для задания равнозначности списка и его итератора для шаблона, работающего с объектом допустимых для списка операций Tplist_access
, вложение итератора как подобъект в список, трансляция полиморфных относительно списка сообщений в сообщения итератора. Эта трансляция не имеет побочных эффектов, так как итератор изначально предназначен для внешней работы с объектом списка и ему не нужен тот же контекст выполнения, что и для списка.
Этот класс не разрабатывался для того, чтобы его можно было произвольно наследовать. Его можно наследовать только чтобы обеспечить поддержку работы с потомками Tlist_item
( пользовательскими объектами ) для автоматической проверки и приведения типов элементов, а не менять существенно структуру классов списка и итератора. Но мы не будем использовать даже это.
Для создания списка используется конструктор по умолчанию, для удаления деструктор, который очищает список, удаляет скрытый итератор и рассылает всем итераторам сообщение о том, что объект списка удален.
Доступ к списку, допустимые команды, определяется с помощью абстрактного класса Tplist_access
, описывающего сообщения доступа как к объекту класса Tplist
, так и к Tplist_iterator
. Для Tplist
еще допустимо сообщение
void zap(char destruct_only=0);
которое только удаляет Tplist_item
элементы, т.е. добавленные списком поля связи для пользовательского объекта. Сам пользовательский объект должен удалять тот, кто его создал. В этом списке принято так. Поэтому, в некоторых случаях можно наследовать из этого списка такой, который будет автоматически удалять и/или создавать сами полезные элементы списка в своем конструкторе или деструкторе.