Простые классы


Детальное описание примеров

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

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


АТД Tstring (tstring.h)

Простые классы

Описание

АТД Tstring (tstring.h)

Самым раздражающим моментом в работе со строкой типа (char_p) является невозможность удобно складывать или копировать такие строки. Приходится заниматься постоянно распределением памяти, что совершенно не относится к сути решаемой задачи. С другими проблемами вполне справляется стандартная библиотека из string.h. Я не хочу переназвать все функции из string.h новыми именами. Самой удобной и, видимо, самой используемой будет семейство функций printf().

Этот класс я назвал АТД, потому как идея его создания - именно образование АТД относительно операции сложения, вернее относительно операций, связанных с перераспределением памяти. Присваивание можно считать как такую последовательность: очистить и прибавить к себе.

Этот класс не создавался, чтобы его можно было наследовать. Он работает с однобайтовой u_char строкой, что достаточно для многих применений.

Раз уж мы делаем АТД, то стоит добавить другой полезный класс операций - операции сравнения. Функция strcmp() делает это не очень удобно для прямой проверки в if(). Как вообще можно будет в этом классе сравнить строки?

Сравнение производится перегруженными операциями, аналогично применяемым для встроенных типов: == != < > <= >= . По умолчанию, длина строки учитывается.

Разрешение использования длины строки для сравнения задается раздельно:
для операций == и !=
    void    equ_min_size( char ms=1 );
для операций < > <= >=
    void    less_min_size( char ms=1 );

Этот класс развился из примера t_string из раздела А-модель. Строка хранится внутри объекта как (char_p) ограниченный \0. Длина строки не хранится, т.к. все операции над строкой производятся через string.h. Размер строки ограничен размером памяти, которую можно получить от ОС. Размер памяти, выделенный для строки может быть больше чем используемая длина строки. Контроль за размером памяти не для операций определенных в этом классе лежит на программисте.

Лучше изучить поведение этого класса ( с точки зрения технических подробностей вызовов конструкторов и т.п. ) можно сняв комментарий в строке

    #define TSTRING_DEBUG

в файле tstring.cpp, и создав ╚синтетические╩ примеры использующие ссылки и объекты этого типа строки в обычных операциях и в возвратах функций, и читая описание С++, но для решения задачи и цепочки задача-модель-решение этого, я надеюсь, не надо.

Организация объекта

АТД Tstring (tstring.h)

Объект содержит защищенные, т.е. не доступные прямо при доступе через ссылку на экземпляр класса, элементы:
    char_p  data_p;
    u_int   mem_size;
    char    compare_equ_min_size;
    char    compare_less_min_size;

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

Важное замечание. Реальный размер выделенной памяти чуть больше, чем нужно для строки. Это позволит снять проблему учета \0 символа при вычислениях размера и связанные с этим ошибки ╚налезания╩ на чужие области памяти. Память для концевого \0, но только для него, выделятся автоматически, что удобно.

Создание и удаление объекта

АТД Tstring (tstring.h)

Для создания строки используется набор конструкторов:
по умолчанию, из строки, с резервированием памяти
    Tstring( char_p src=NULL, u_int maxlen=DEF_TSTRING_MAXLEN );
копирования
    Tstring( Tstring& src );

Первый набор конструкторов реализуется с помощью одного реального конструктора и параметров по умолчанию. Это ╚синтаксический╩ полиморфизм: эмуляция набора методов отличающихся подробностью задания параметров.

Второй параметр maxlen используется для указания минимального размера памяти, который сразу будет выделен. Если для программы важна скорость выполнения и известно, что размер строки в большинстве случаев не превысит некоторый предел, например, имя файла всегда менее 1024 байт, то для ускорения работы, чтобы снизить перевыделения памяти при нехватке места для строки, можно сразу выделить нужный размер. Приравнять память по длине строки, т.е. скопировать в новую область, но меньшего размера, освободив не используемую память, всегда можно сообщением compact().

Конструктор копирования служит для дублирования данных строки, а не указателя на них, что делает конструктор копирования по умолчанию. Копированием просто называют создание объекта из другого объекта такого же класса.

Примеры:
если не использованы параметры, то конструктор по умолчанию
    Tstring    fname, str[5];
    Tstring_p  p= new Tstring;
создать из char_p
    Tstring    fname(╚autoexec.bat╩), str(╚win.ini╩,1024);
    Tstring_p  p=new Tstring(╚config.sys╩,1024);
зарезервировать память для строки
    Tstring    fn(0,1024), str(╚vc.com╩,1024);
    Tstring_p  p=new Tstring(0,1024);
создать объект из другого объекта такого же класса
    Tstring    fname(╚system.ini╩,1024), str(fname);
    Tstring_p  p= new Tstring(fname);

    Tstring    returned_str();

    Tstring    r( returned_str() );

Я напомню, что конструктор это не оператор для выделения памяти, в свете чего его многие пытаются рассматривать. Конструктор - это создание объекта из чего-нибудь, в том числе и из ничего. Его можно рассматривать как оператор преобразования одного типа в другой, а не аналог оператора new.

Для удаления объекта используется деструктор. В этом классе он просто освобождает память выделенную под строку, под данные строки data_p. Поля самого объекта этого класса удаляются автоматически.

Деструктор не виртуальный и класс не предназначен для наследования. Не наследуйте этот класс.

Основные операции

АТД Tstring (tstring.h)

Основными операциями являются, как я уже говорил, операции c распределением памяти
добавление к себе Tstring или char_p (+=)
    Tstring&   operator +=( Tstring& srс );
    Tstring&   operator +=( char_p src );
присваивание Tstring или char_p (=)
    Tstring&   operator =( Tstring& srс );
    Tstring&   operator =( char_p src );
сложение двух Tstring (+)
    Tstring    operator +( Tstring& str );

В первом и втором случае возвращается ссылка на себя самого, чтобы можно было складывать конвейерно: (a+=(b+=c /*вернет b*/)/*вернет а*/). В последнем случае создается новый объект, который и возвращается.

Следует рассмотреть вариант возврата локального объекта, чтобы снять возможные недоумения по потерянным данным. Локальный объект ведет себя также, как и локальная переменная. Объект это грубо как минимум экземпляр АТД, а АТД это тип переменной.

При возврате локальной переменной, например int, происходит сначала копирование ее ( из стека или из чего-нибудь ) в получатель, потом ссылка на нее теряется. По аналогии с этим, объект копируется в получатель (если он есть) затем он переводится в разряд ╚мусора╩, в самом простом случае для него сразу вызовется деструктор и ссылка на него будет утеряна. Не будем вдаваться в подробности и упрощенно скажем, что для возврата будут использоваться конструктор копирования (чтобы сменить контекст определения локального объекта, создать временный объект возврата доступный приемнику, что элементарно для int) и оператор присваивания.

Другие операции

АТД Tstring (tstring.h)
Установить память по длине строки
    void      compact();
Очистить строку и память под нее
    Tstring&  operator ~();
Получить длину строки
    int       len();
Преобразовать в char_p ( унарный минус )
    char_p    operator -();

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

Получение длины строки используется для проверки на NULL поля data_p перед отправкой в strlen(), иначе это на некоторых стандартных библиотеках вызовет сбой обращения к памяти, мы же считаем это строкой нулевой длины. Если нет уверенности, что память для строки была выделена, т.е. когда вы сами только что не проверили, лучше не использовать функции string.h прямо.

Преобразование в char_p в этом классе на самом деле не преобразовывает ничего, а открывает внутреннюю строку для использования внешними функциями. Я уже вижу как ругаются знатоки С++ и борцы за надежность. Да. Открытие внутренних данных на запись, мягко говоря, плохо для объекта. Но, к большой радости, у нас не объект, а АТД. Для него это в порядке вещей, а для нас пока в порядке вещей простота в использовании.

Обычно, подобные преобразования из типа в тип делают с помощью оператора преобразования источника в тип приемника или конструктора приемника с параметром источника. Но у нас опять особый случай. Приемник-то у нас часто без типа - любимый printf().

Удобство оператора преобразования - автоматическое приведение типа. Как раз в случае параметра без типа это удобство работать не будет, надо будет явно задать тип, например так

    printf(╚my string is: %s\n╩,(char_p)str);

Это крайне нудно, поверьте мне. Унарный минус для объекта - находка для получения из него чего-нибудь во всех случаях, когда объект не имеет логического определения как больше или меньше нуля. Он правильно работает как для ссылки, так и для указателя. В сомнительных случаях лучше ставить скобки.
Tstring    str(╚str_1╩), *p=&str;

    printf(╚Как к ссылке %s\n╩,-str);	
    printf(╚Как к указателю %s\n╩,-*p);


Класс Tunicode (unicode.h)

Простые классы

Описание

Класс Tunicode (unicode.h)

Это юникоде, который не совсем юникоде, вернее, совсем не юникоде. Лучше было назвать его 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

Тип входной и выходной кодировки задается такими сообщениями
тип кодировки для преобразования
    t_err    source(t_unicode_type type);
тип кодировки после преобразования
    t_err    target(t_unicode_type type);

Необходимость перекодировать символы возникает в этих примерах в следующих случаях:

Для каждого из этих действий можно создать свой объект Tunicode. Причем для сообщений программы этот объект смело можно создать в глобальном контексте, назвав его, например, msg_unicode и в функции main() устанавливая нужные значения кодировок источника и приемника. Для каждого объекта программы можно как свойство включить Tunicode объект и использовать его для локального, только для этого объекта, задания кодировки текста файла и его имени в файловой системе.

Создание и удаление объекта

Класс Tunicode (unicode.h)

При создании объекта просто устанавливаются значения кодировок в допустимом диапазоне. Какие это кодировки не оговаривается. Перед использованием объекта надо их указать сообщениями source() и target(). Специально определять для этого объекта деструктор не надо.

Основные операции

Класс Tunicode (unicode.h)

Перекодировка производится посылкой сообщения convert(). Это сообщение имеет две формы, два метода, отличающиеся типом параметра ( параметрический полиморфизм для сообщения convert() ).
Преобразовать символ
    u_int    convert(u_int ch);
Преобразовать строку
    u_int    convert(char_p str,u_int size=0);

Для символа, сообщение convert() возвращает преобразованный символ. Для строки возвращается длина преобразованной строки. В принципе, возврат для строки и не нужен, т.к. реальное преобразование происходит в памяти, адресуемой указателем str, но сообщение имеет один логический тип возврата.

Логичнее, может быть, было бы возвращать АТД, которое получено после преобразования. Но мы не будем делать сложно то, что можно сделать просто, поэтому вернем u_int, который можно трактовать и как u_char, и как длина строки, и как двухбайтовый код.

Форма сообщения с параметром char_p имеет две подформы, т.е. помимо параметрического полиморфизма здесь еще подробности задания параметров - ╚синтаксический╩ по нашей классификации.

Параметр size позволяет перекодировать области памяти, содержащие \0. Если size равен нулю, то перекодирование идет до первого \0, иначе по длине size.

Другие операции

Класс Tunicode (unicode.h)

Вспомогательные сообщения служат для определения текущего состояния объекта.
Получить установленное значение кодовой страницы
    t_unicode_type    get_unicode_type(char src);
Cтрока описания типа кодировки
    char_p    type_name(t_unicode_type type);

Сообщение get_unicode_type(1) вернет установленный тип кодировки источника, а get_unicode_type(0) вернет установленный тип кодировки после преобразования.

Второе сообщение возвращает строку с описанием t_unicode_type, полученного в качестве параметра. Такое сообщение не связано с параметрами объекта, которое его получает, и лучше сделать его статическим или вообще исключить из описания класса и сделать внешней функцией, хотя это значительно хуже. По историческим причинам в данном описании это сообщение не статическое.

Примеры

Класс Tunicode (unicode.h)

Вот простой пример перекодировки сообщения программы
    //перекодировка заголовка
    sprintf(s,"%s", ╚Сообщение набранное в редакторе на русском языке╩);
    ::msg_unicode.convert(s);
    printf(╚%s\n╩, s);


Класс Tplist (plist.h)

Простые классы

Список

Класс Tplist (plist.h)

Во многих случаях объекты удобно иметь в группированном виде. Такая группа может быть упорядоченная (ряд, последовательность) или нет (множество). Ряд фиксированного размера можно сделать объявив стандартный массив. Недостатком массива является невозможность после объявления просто изменить его размер. Вернее, неудобно стандартными операциями делать это. Массив сразу занимает размер памяти (и другие ресурсы) по объявленному числу элементов. Часто надо сделать массив, ресурсы под элементы которого выделяются динамически, при создании или удалении элемента массива, и иметь возможность добавлять и убирать элементы с сохранением упорядоченности остальных в произвольном месте массива, между любыми соседними элементами. Такой массив называют списком. Конечно, список может эмулировать динамическое выделение памяти, храня данные в массиве.

Список можно получить включив в любой объект поля связи с другими объектами и обрабатывая их методами объекта. Можно создать базовую абстракцию ( класс ) списка, все наследники которого получат возможность образовать список. Можно создать список, элементом которого будет любой объект или любой объект-потомок класса элемент_списка.

Не занимаясь их сравнениями, я скажу что будет использоваться третий вариант. Фактически, этот класс - список указателей на объекты списка. Этот класс есть прямое развитие списка из примера к А-модели.

Состояние списка

Класс Tplist (plist.h)

Под состоянием списка понимаем:

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

Операции со списком

Класс Tplist (plist.h)

Вот перечень допустимых операций над списком, которых нам будет достаточно:

  1. добавить элемент после выбранной позиции
  2. добавить элемент перед выбранной позицией
  3. удалить выбранный элемент
  4. перемещение выбранной позиции не циклически на 1 элемент в конец
  5. перемещение выбранной позиции не циклически на 1 элемент в начало
  6. установить выбранную позицию ( индексация )
  7. получить выбранный элемент
  8. получить номер ( индекс ) выбранного элемента
  9. получить количество элементов

Эти операции определены с помощью абстрактного класса Tplist_access так:
Операции для доступа к списку
  1. virtual void         operator+=(Tlist_item_p pointer)=0;
  2. virtual void         operator-=(Tlist_item_p pointer)=0;
  3. virtual t_err        operator~()=0;
  4. virtual t_err        operator++(int)=0;
  5. virtual t_err        operator--(int)=0;
  6. virtual Tlist_item_p operator[](u_int index)=0;
  7. virtual Tlist_item_p operator-()=0;
  8. virtual u_int        get_index()=0;
  9. virtual u_int        get_items()=0;

Можно прочитать раздел ╚Объектная модель и С++:перегрузка операторов╩ и увидеть, что в данном случае использован вариант с изменением смысла операций. Операторы 3, 4 - постфиксная форма, о чем говорит параметр int, который не используется, а служит только для указания формы. Оператор 7 - унарный минус. Он, как и 6, возвращает указатель на элемент списка. Индекс считается от нуля.

Описание

Класс Tplist (plist.h)

Вот классы списка.
Элемент списка, то что мы туда положили
    class    Tlist_item;
Списoк
    class    Tplist;
Доступ к списку - итератор
    class    Tplist_iterator;

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

Такой вариант ╚безтипового╩ элемента для хранения плох с точки зрения автоматического контроля типа и велика вероятность ошибочного использования элемента. Если все элементы списка однородные, то эта проблема менее острая. В любом случае, вы всегда можете наследовать из этого класса такой класс, который будет работать с вашими объектами без явных преобразований правильно. В этих примерах это не используется.

Что такое итератор, рассказано в разделе "Реализация объектной модели:Дружественные классы и функции". Как к списку, так и к итератору применимы операции описанные в классе Tplist_access.

Организация объекта

Класс Tplist (plist.h)

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

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

Создается итератор конструктором с параметром объекта списка. При создании, он добавляется к списку итераторов списка, а при удалении - исключается из него.

Даже если не включать в наш итератор команды, модернизирующие список (1, 2, 3), а допустить их только для объекта списка, то легко представить, что состояние итератора может стать недействительным, если будут добавлены или удалены элементы списка, за исключением малого количества случаев. Тот же эффект будет достигнут, если удалить объект списка, на который ссылается итератор. Для борьбы с этим злом я сделал итератору свойства, которые устанавливаются для всех итераторов связанных с изменяемым списком в такие состояния
список невосстановимо нарушен
    char    no_longer_valid;
список не существует
    char    no_longer_exist;

Любая операция итератора, которая делает состояние других списков неверным вызывает сообщение для своего объекта-итератора

    void    send_new_list(); 

Это рассылка обновления списка по остальным итераторам, которое устанавливает всем другим итераторам состояние no_longer_valid. Сбросить неопределенное состояние, если объект список не удален, можно и нужно выбрав индексированием элемент ноль [0].

Проверять корректность состояния итератора нельзя. Это не входит в список допустимых операций. Алгоритм должен быть составлен так, что он не пытается иметь доступ через итератор к уже удаленному списку или к списку с неопределенным состоянием, которое получается после модификации списка через другой итератор. Эта рассылка нарушения списка служит для индикации ошибки алгоритма и нашей тренировке описывать дружественные классы.

Класс списка Tplist, как и класс итератора Tplist_iterator, является потомком абстрактного класса допустимых операций для списка Tplist_access. При доступе к списку через объект класса списка, а не через итератор, используется скрытый итератор, а определенные в пространстве имен по умолчанию для Tplist операции просто транслируют сообщение во вложенный объект, поэтому объект списка можно прямо использовать без объявления дополнительных итераторов, если такой нужен только один.

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

Этот класс не разрабатывался для того, чтобы его можно было произвольно наследовать. Его можно наследовать только чтобы обеспечить поддержку работы с потомками Tlist_item ( пользовательскими объектами ) для автоматической проверки и приведения типов элементов, а не менять существенно структуру классов списка и итератора. Но мы не будем использовать даже это.

Создание и удаление объекта

Класс Tplist (plist.h)

Для создания списка используется конструктор по умолчанию, для удаления деструктор, который очищает список, удаляет скрытый итератор и рассылает всем итераторам сообщение о том, что объект списка удален.

Основные операции

Класс Tplist (plist.h)

Доступ к списку, допустимые команды, определяется с помощью абстрактного класса Tplist_access, описывающего сообщения доступа как к объекту класса Tplist, так и к Tplist_iterator. Для Tplist еще допустимо сообщение

    void    zap(char destruct_only=0);

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


Простые классы