Методические указания Ухта 2006 удк 681 06(076) г 23

Вид материалаМетодические указания
Простой класс: MyString
Выбор способа представления абстрактных типов
Реализация конструкторов
Реализация “простых” методов
Реализация необходимых функций
Подобный материал:
1   2   3   4   5   6

Простой класс: MyString



Итак: текст любого класса в языке C++ состоит из двух файлов: файла заголовка и файла "тело класса". Как правило, файлы имеют имена, совпадающие с именем класса и расширения "h" ("hpp") и "cpp" соответственно. Если по тем или иным причинам файлы содержат тексты нескольких классов (практически любой файл из библиотеки INCLUDE - например, EXCEL_2K.h), то, безусловно, имена файлов могут не совпадать с именем класса. Тем не менее, принято, что имена заголовка и "тела" класса одинаковые.

В заголовке класса, обычно, помещаются само описание класса, структуры и переменные составляющие класс, а также необходимые константы. Все прототипы функций также помещаются в заголовок класса. В заголовке также находятся короткие функции, INLINE - функции и прототипы дружественных функций. Переходим на терминологию ООП: в заголовке находятся все члены – данные класса (свойства - в терминологии C++Builder) и прототипы всех членов - функций (методы). В теле класса помещается всё то, что не помещено в заголовок. Чаще всего, это значительные по размеру члены-функции (методы).

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

Это выполняется помещением директив:

#include "class.h"

#include "class.cpp"

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

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

Отсюда же видно, что класс должен сдаваться вместе с текстом класса. Конечно, он может быть оформлен как DLL - библиотека, набором объектных модулей (LIB - библиотека или набор отдельных объектных модулей) и включаться в программу по директиве #include "XXXXXXX.h", но при появлении нештатных ситуаций вам обязательно потребуется текст метода, вызвавшего сбой.

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

Класс MyString: класс "моя строка". Работа со строками, реализованная в языке C, с самого начала вызывала множество нареканий со стороны программистов. Всё вроде бы хорошо и даже эффективно, но, как это ни странно, большинство практикующих программистов имели собственные библиотеки работы со строками. Это признак дурно написанной системы программ, хотя любой, написавший собственную библиотеку работы со строками, скажет, что просто создатели стандартной библиотеки исходили из других идей. Тем не менее, почти во всех трансляторах языка C имелись собственные функции работы со строками, а в C++ собственные классы: тип AnsiString в C++Builder. Исходя из этого, вряд ли требуется написание ещё одного класса работы со строками. Однако работа со строками широко используется во всех языках, интуитивно понятна практически всем программистам и позволяет продемонстрировать все основные трудности, возникающие при написании собственных классов.

Постановка


Нам требуется такое представление строк, при котором программист не задумывался бы о наличии символа конца строки ("0x00") и длина которой динамически бы менялась в зависимости от содержания. Кроме того, хотелось бы, чтобы легко выполнялась операция конкатенации строк. Всё это естественно: почему программист уже на этапе освоения языка обязан знать внутреннее представление строк?

Необходимо отметить, что с одной стороны это правильно: программист должен написать "СТРОКА" имя, и далее просто использовать данное описание. С другой стороны, часть современных программистов, пользуясь вышеописанным принципом, совсем не понимает разницы между типами int и float и задаёт с их точки зрения законный вопрос: "А зачем нам два способа представления чисел? "

Чего ещё мы хотим от "нашего" типа "строка"? Очевидно, нам требуется возможность быстро перейти к обычному представлению строк и обратно, требуется сохранить общепринятый подход доступа к любому символу строки.

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

Надо упомянуть об одной методической проблеме, с которой сталкиваются программисты, осваивающие ООП: степень общности реализуемого класса. Класс можно реализовать очень узко, так что использовать его можно будет только в той задаче, для решения которой он создавался. Нередко это можно сделать достаточно быстро и просто. Но такие классы почти не модифицируемы и при реализации следующей задачи, вам придётся снова (!) писать ещё один очень похожий на предыдущий класс - это классы однодневки. Написание же класса, который можно использовать в более широком контексте требует большего времени как на постановку, так и на отладку, зато обычно, такие классы могут широко использоваться при программировании других задач. Программист стоит перед проблемой: следует ли обобщать реализуемый класс, а если следует, то в какой степени. Важно понять, что обобщение класса должно выполняться не ради самого обобщения, а ради более удобного и быстрого достижения поставленных целей.

Итак, наши цели при написании класса MyString:

  • Не задумываться о длине строки и не считать символы при присвоении строк.
  • Не "знать" о наличии концевого символа, фактически, мы не хотим знакомиться с внутренним представлением строки.
  • Просто выполнять операцию конкатенации строк.
  • Сохранить доступ к любому символу строки по номеру символа.
  • Достаточно просто переходить от MyString к char и обратно.
  • Быстро писать дополнительные функции работы со строками.

Другими словами, мы хотим работать со строками, а не с массивом символов.

Выбор способа представления абстрактных типов


На этом шаге программист строит члены-данные класса и все необходимые структуры.

Очевидно, что внутри класса MyString строка всё-таки останется обычным типом char. Вряд ли стоит чего-нибудь выдумывать. Фактически, мы хотим всего лишь более удобного интерфейса для строк. Но мы не знаем заранее длины строки, что неизбежно приводит нас к указателю на тип char и получению памяти по оператору new, очевидно, что освобождать память мы будем оператором delete.

Из всего вышесказанного вырисовывается следующий текст класса:


// класс "Моя строка"

// контрольном примере

class MyString {

char * ps; // собственно указатель на строку.

};


Такой вот первый вариант класса. Принципиально мы уже написали класс.
Этот текст гарантировано пройдёт трансляцию. Самое интересное, что мы даже можем объявлять объекты этого класса (переменные этого типа), другой разговор, что ничего с этими объектами мы не сможем сделать. Нет, принципиально мы можем выполнять побитное копирование одного непроинициализированного объекта MyString в другой, но вряд ли это достигаемые нами цели.

Рассуждая далее, мы приходим к выводу, что неплохо бы где-то хранить объём выделенной памяти, чтобы при заполнении "нашей" строки не выйти за выделенные нам границы. Таким образом, у нас появляется ещё один член класса:


class MyString {

char * ps; // собственно указатель на строку.

int Ls; // объём выделеной памяти (в байтах) под строку.

};

Здесь и далее мы будем писать объём выделенной памяти в байтах. В нашем случае, когда реализация типа char выполнена однобайтовыми символами, по оператору new мы получаем Ls байт. Тем не менее, в свете перехода к двубайтовому представлению симоволов, следует помнить, что всё зависит от реализации типа char. Рекомендуется проверить реализацию, выполнив утверждение sizeof(char), поскольку оператор new выполняет выделение памяти под Ls символов, а не байт.

Таким образом, получив под "нашу строку", например, 50 байт, мы можем затем работать далее со строками в 10 байт, 20 байт и другими меньшими длинами. Если мы будем при любом изменении длины выполнять перезапрос памяти именно под запрашиваемую длину, то наш класс будет работать медленно. Хотим мы того или нет, но запрос памяти это всегда обращение к операционной системе и на его выполнение идёт много накладных расходов. Кроме того, если вам сейчас выделили 50 байт, то никто не гарантирует, что через несколько секунд у операционной системы найдётся для вас 10 байт. Наконец, такая схема работы с памятью обязательно приводит к фрагментации памяти. Тогда, необходимо хранить реальную длину строки. Теперь имеем следующий текст класса:


class MyString {

char * ps; // собственно указатель на строку.

int Ls; // объём выделеной памяти (в байтах) под строку.

int Tl; // текущая длина строки (в байтах).

};


С таким текстом уже можно работать. Как мы уже отмечали, трансляция пройдёт, и если вы объявите в программе переменные типа MyString, то они обязательно будут построены: транслятор применит конструктор по умолчанию, то есть просто отведёт память под наши три переменные. Реально работать с таким классом нельзя: ни одна переменная не проинициализирована. Однако, ситуация, в которой нам понадобится знать состояние объекта, будет постоянно возникать в процессе работы. Мы можем запросить память, но не заполнить её строкой. Хорошо, эта ситуация просто обходится обследованием переменных: при ней Ls > 0 & Tl == 0. Но, во-первых, эти переменные, скорее всего, будут скрыты от пользователя, а во-вторых, они отвечают за выделенные и реальные длины, а не за состояние нашего объекта. Не следует нагружать переменные дополнительными функциями, если для этого нет существенных причин. Примерно по этим же причинам не следует использовать состояние указателя (ps) в целях отображения состояния объекта.

Самое правильное будет добавить переменную, отвечающую за состояние объекта: int flag; и положить, что flag = –1, при созданном, но не проинициализированном объекте; flag = 0, когда под объект получена память, но значение строки не задано; flag = 1, когда у нас нормальная строка.


class MyString {

char * ps; // собственно указатель на строку.

int Ls; // объём выделеной памяти (в байтах) под строку.

int Tl; // текущая длина строки (в байтах).

int flag; // флажки строки: -1 - под строку даже не выделено

памяти

// 0 - память выделена, но её

// значение не задано;

// 1 - нормальная строка.

};


Теперь самое время обсудить наши структуры, которые мы собираемся использовать.

Первый вопрос, который обычно возникает: можно ли это сделать по-другому? Несмотря на очевидный ответ, всегда следует задумываться над этим. Если поставленную задачу можно реализовать единственным образом, то это свидетельствует либо об уникальности или узости решаемой задачи, либо об ограниченности мышления программиста. Однако, чаще всего правильный ответ на этот вопрос – "да". В нашем случае достаточно посмотреть реализацию AnsiString.

Тогда следующий возникающий вопрос – "Чем наша реализация хуже или лучше?" Ответ на этот вопрос зависит от преследуемых целей. Даже такой ответ как: "Это мой класс и я в нём могу поменять всё, что хочу" – имеет право на жизнь.

Третьим пунктом нужно обсудить технические вопросы.

Первое: Какая максимальная длина строки может быть у MyString? Так как под длины строк отведена переменная типа int, то при двухбайтном int это 32867 байт, а при четырёхбайтном int - 2 147 483 647. Принципиально мы могли увеличить длину строки вдвое, приняв тип unsigned int. Если при длине int два байта это ещё и может привлечь, то при четырёхбайтовом int вряд ли. Практика показывает, что длины строки до 32 000 байт более чем достаточно для большинства применений.

Второе: из только что изложенного выплывает не менее трёх представлений класса. При каком из этих представлений будет наиболее быстрая работа? Как сэкономить память понятно из вышеизложенного – достаточно перейти на тип unsigned short. Кроме того, переменную flag можно представить битовым полем из восьми бит (меньше просто не получится) и работать с битами, а не с int. Однако если нам понадобится экономить время работы с нашим классом, то всё упрётся в знание скоростных характеристик команд машины (вот где "сишнику" понадобится знание Assemblera). Если команды работы с полусловами выполняются быстрее команд работы со словами, то конечно следует перейти на полуслова.

Можно и "не знать Assemblera". Достаточно сгенерировать, написав шаблонный класс, несколько представлений класса и программно сравнить скоростные характеристики. Если вас не лимитирует длина строки, то эти скоростные характеристики, скорее всего и определят окончательный тип представления класса.

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

Реализация конструкторов


Следует помнить, что как только транслятор обнаруживает в тексте класса хоть один конструктор, он не строит конструктора по умолчанию. Конструктор по умолчанию имеет очень простую форму: Class () {};. Если у вас все конструкторы с параметрами, то полезно иметь в тексте класса такой "пустой" конструктор, он позволит вам просто объявлять объекты класса, не задумываясь об их начальном состоянии, что требуется довольно часто. В нашем случае (см. ниже) мы строим собственный конструктор без параметров.


Как правило, конструкторов несколько:
  1. Создадим конструктор без параметров, который будет вызываться вместо конструктора по умолчанию:


public:

// самый простой конструктор

MyString () {ps = NULL; Ls = -1; Tl = -1; flag = -1;}


Здесь только задаются начальные параметры "пустой" строки. Нужна ли нам пустая строка? Очень даже возможно, что нет. Тем не менее, у нас все параметры заданы правильно, и мы можем работать с этой строкой. Почему мы задаём длины -1, а не нулями, станет ясно позже, при реализации простых методов.
  1. Следующий напрашивающийся конструктор: конструктор, задающий начальный размер строки:


// конструирование строки заданной длины

MyString (int A) {

if (A <= 0) A = 256;

Ls = A+1; // не забудьте про концевой символ

ps = NULL;

ps = new char [Ls];

if (ps == NULL) { Ls = -1; Tl = -1; flag = -1; return; }

Tl = 0; flag = 0;

return;

}


Обратим внимание, что мы не задаём начальных значений строки. Собственно зачем, если по Tl мы всегда можем понять, что строка пустая.
  1. Безусловно, необходим конструктор, которым мы сразу можем задавать начальное значение строки.


// конструирование строки с одновременной инициализацией.

MyString (char * U) {

int i, ls;

ls = strlen (U);

Ls = ls + 10;

ps = NULL;

ps = new char [Ls];

if (ps == NULL) { Ls = -1; Tl = -1; flag = -1; return; }

for ( i = 0; i < ls; i++) ps[i] = U[i];

for (; i < Ls; i++) ps[i] = 0x00;

Tl = ls; flag = 1;

return;

}

Здесь уже требуются некоторые разъяснения. Обратим внимание, что мы используем функцию strlen – следовательно, в текст заголовка обязательно потребуется внести директиву #include . Переменную ls заводим для того, чтобы дважды не обращаться к функции strlen. Однако, по тексту мы запрашиваем на 10 байт больше длины строки, инициализирующей MyString. Нам обязательно надо прибавить к strlen(U) единичку, чтобы подставить в конец строки символ 0x00. С другой стороны, текущая работа со строкой часто немного меняет её длину, поэтому, чтобы сразу не сталкиваться с перезапросом памяти в случае нехватки длины строки, отведём под её текущую длину чуть больше. Мы остановились на десяти байтах, хотя это число может быть практически любым, лишь бы не меньше единицы. Пустое место строки заполняем символом конца строки - 0x00, тогда мы всегда можем вставить в конец строки символ, оставив признак конца строки.

Теперь мы смело в своей тестирующей программе можем писать:


MyString First_object; // Принципиально так мы могли писать,

// не написав ни одного конструктора.

Обращаем ваше внимание на то, что это выполнимый оператор! При выполнении этого оператора вызывается конструктор без параметров.


MyString Second_object = MyString (125);

или First_object = MyString (857);

или MyString Third_object; int lh = 4389;

Third_object = MyString (lh);


MyString F_object = MyString (" Первая инициализация. ");

First_object = MyString (" Привет от старых штиблет. ");


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

// Д Е С Т Р У К Т О Р .

~MyString () { if (flag < 0) return;

delete [] ps;

}

Однако, при работе в C++ Builder деструктор писать не обязательно, более того его появление в тексте класса может привести к ошибкам при работе класса.

Переходим к шагу номер четыре.

Реализация “простых” методов


Очевидно, что очень часто нам необходимо знать длину строки:


// программа, возвращающая длину строки

int LenMyStr () { return Tl; }


Здесь, что мы просто возвращаем Tl, даже не проверив флаг. При этом, если строка нормальная, то нам вернётся реальная длина строки. Если же строка "пустая", то нам вернётся -1. Это объясняет, почему при "неудачах" конструирования MyString мы заполняли значения Ls и Tl минус единичками.


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


// программа возвращающая "объём" строки:

int MyStrSize () { return Ls; }


Наверняка нам понадобится знать, есть ли в строке "законно" присвоенные символы или другими словами надо часто знать, пуста ли наша строка?


// проверка на пустоту строки

bool IsMyStrEmpty () { if (flag < 1) return true;

else return false; }


Обратим внимание, что у нас появилась масса коротких программ. Необходимость каждой из них пока чисто умозрительная. Возникает вопрос: стоит ли вообще их писать? Поскольку они очень короткие и написать их ничего не стоит, то лучше их написать.

Одна из слабых отрицательных сторон ООП это появление множества таких коротких программок. Связано это с инкапсуляцией. Поскольку у нас есть закрытые члены, то появляются программки "Get" – программки законного получения этих свойств. Соответственно появляются и обратные программки: программы "Set" – установления этих свойств. Дабы их явно не показывать, в визуальном программировании ввели новое понятие – свойство (property): это в общем закрытая переменная значения которой пользователь может устанавливать обычной операцией присвоения ("=") и значениями которых можно пользоваться, указывая имя свойства.


Несколько более сложная процедура "очищение нашей строки".
Очевидно, что очищение следует делать символами конца строки ("0x00").


// очищение нашей строки концами строк из C++

void ClearMyStr ();

Этот текст помещается в файл заголовока. Текст самой программы
помещается в файл "MyString.cpp":


// очищение нашей строки концами строк из C++

void MyString::ClearMyStr () { if (flag < 1) return;

for (int i = 0; i < Ls; i++) ps[i] = 0x00;

Tl = 0; flag = 0; return; }


Обращаем ваше внимание на указание области видимости перед именем функции. Когда наши функции помещались в объявлении класса, область видимости указывать не было необходимости, так как функция и так была в этой области видимости.


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


Переходим к шагу пять.

Реализация необходимых функций


Нам следует научиться делать самые обыденные операции с данными типа MyString. Под обыденными операциями мы здесь понимаем, прежде всего, операцию присвоения – «=». Так как у нас в представлении класса имеется указатель, то мы не можем воспользоваться побитной копией объекта, которую сгенерирует транслятор, когда обнаружит в тексте присвоение одной переменной типа MyString другой переменной этого же типа.

Ещё раз:

Если у нас имеются MyString A, B; то A.ps и B.ps указывают на разные области памяти. Если мы выполняем A = B; побитной копией, то получаем в результате A.ps = B.ps; то есть оба указателя имеют одно и тоже значение и указывают на одну область памяти. Таким образом, мы, во-первых "потеряли" часть памяти, ту на которую ссылался A.ps. Во-вторых, теперь изменяя значения A, мы меняем одновременно и значение B.

Отсюда следует, что нам придётся перегрузить операцию присвоения и написать конструктор копирования. Нам надо так реализовать эти функции, чтобы выполнялось реальное копирование строк, а именно *(B.ps) пересылалось в *(A.ps). Но длина A может оказаться меньше длины B. Следовательно, перед копированием необходимо увеличить длину A, для чего нам потребуется запросить новый объём памяти под A, скопировать в новый объём строку из A, освободить предыдущий объём памяти. Очевидно, что лучше это делать некоторой функцией - назовём её Redefenition - переопределение. Причём, все вышеописанные манипуляции с памятью должны быть скрыты от среднего, пользователя. Значит, функцию Redefinition поместим в раздел private.


private:

bool Redefinition (int);


Этот текст будет в заголовке класса.


Текст функции будет в файле "class.cpp":


// перераспределение памяти под уже построенную строку!

bool MyString::Redefinition (int NM) {

char * ti;

int i;

if (NM < 1) return false;

if (Ls > 0)

if (NM <= Ls) return true;

ti = NULL;

ti = new char [NM];

if (ti == NULL) return false;

if (Tl > 0) {

for (i = 0; i < Ls; i++) ti[i] = ps[i];

for (; i < NM; i++) ti[i] = 0x00;

}

else

{ ti[0] = 0x00; Tl = 0; }

delete [] ps;

Ls = NM;

if (flag < 0) flag = 0;

ps = ti;

return true;

}


Заметим, что мы не только получаем дополнительную память, но ещё и сохраняем состояние исходной строки. Конечно, при выполнении операции присвоения можно обойтись без сохранения предыдущего состояния строки, но функция Redefinition будет использоваться всегда, когда потребуется увеличить длину строки, а увеличение длины строки предполагает сохранение исходного состояния строки.

Теперь, когда у нас имеется управление длиной собственной строки можно взяться за перегрузку операции присвоения. Сначала перегрузим присвоение MyString в MyString, то есть, если имеются MyString A, B, то мы выполняем A = B.

В тексте заголовка помещаем:


// перегрузка оператора =

MyString & operator = (MyString);


в файле "MyString.cpp" :


// перегрузка оператора =

MyString & MyString::operator = (MyString Oth) {

int i;

if (Ls < Oth.Ls) Redefinition(Oth.Ls+1);

for (i = 0; i < Oth.Ls; i++) ps[i] = Oth.ps[i];

Oth.ps[i] = 0x00;

Tl = i; flag = 1;

if (Ls > Oth.Ls) for (; i < Ls; i++) ps[i] = 0x00;

return *this;

}


Из приведённого текста видно, что основное выполняемое действо – пересылка символов из одной строки в другую. Для этого можно было использовать стандартную функцию strcmp. Приведённый тескт не зависит от strcpm, но работает всё-таки медленней. Что реально использовать: цикл for или функцию strcmp дело вкуса программиста.

Очевидно, что сразу имеет смысл реализовать и перегрузку операции присвоения для типа char.


Имеем: char St[15] = "Новая строка. "; MyString A;

Необходимо выполнить: A = St; или A = "Ещё одна строка"; Это выполняется почти такой же процедурой, как и приведённая выше, но входной параметр имеет тип char.


В заголовок класса помещаем:


// перегрузка оператора = char *

MyString operator = (char * Ot);


в файл "MyString.cpp" :


// перегрузка оператора = char *

MyString & MyString::operator = (char * Oth) {

int i, k;

k = strlen(Oth);

if (Ls < k) Redefinition(k+1);

for (i = 0; i < k; i++) ps[i] = Oth[i];

Oth[i] = 0x00;

if (Ls > k) for (; i < Ls; i++) ps[i] = 0x00;

Tl = k; flag = 1;

return *this;

}


Процедуры, практически, идентичны.

Точно так же, как и присвоение типа char, можно перегрузить операцию присвоения для типов int, float и т.д. Читатель может выполнить это как самостоятельное упражнение.

Теперь можно вернуться и к конструктору копирования. После вышеприведённых текстов ясно как его писать:


// конструктор копирования.

MyString (const MyString & Qs) {

int i;

ps = NULL;

ps = new char [Qs.Ls];

if (ps == NULL) { Ls = -1; Tl = -1; flag = -1; return; }

Ls = Qs.Ls; Tl = Qs.Tl; flag = Qs.flag;

for (i = 0; i < Ls; i++) ps[i] = Qs.ps[i];

}


Ввиду важности этого конструктора целиком поместим его в заголовок класса.


Из необходимых функций y нас осталась операция конкатенации (сцепления) строк и операция сравнения строк.


Операцию конкатенации будем рассматривать как перегрузку операции "+" для MyString.


В заголовок класса помещаем:


// перегрузка оператора +

MyString operator + (MyString &);


в файл "MyString.cpp":


// перегрузка оператора + фактически выполняем конкатенацию наших строк.

MyString MyString::operator + (MyString & Oth) {

int L, i, j;

L = LenMyStr() + Oth.LenMyStr() + 1;

MyString temp = MyString (L);

if (Tl > 0) {

for (i = 0; i < Tl; i++) temp.ps[i] = ps[i];

if (temp.ps[i-1] == 0x00) i--;

}

else i = 0;

if (Oth.Tl < 1) { temp.Tl = i-1; return temp; }

for (j = 0; j < Oth.Tl; j++, i++)

temp.ps[i] = Oth.ps[j];

temp.ps[L] = 0x00;

temp.Tl = L-1; temp.flag = 1;

return temp;

}

Почти точно также перегружается операция "+" для типа char:


// перегрузка оператора + char *

MyString MyString::operator + (char * Ot) {

int L, i, j, k;

k = strlen(Ot);

L = LenMyStr() + k + 1;

MyString temp = MyString (L);

if (Tl > 0) {

for (i = 0; i < Tl; i++) temp.ps[i] = ps[i];

if (temp.ps[i-1] == 0x00) i--;

}

else i = 0;

if (k < 1) { temp.Tl = i-1; return temp; }

for (j = 0; j < k; j++, i++)

temp.ps[i] = Ot[j];

temp.ps[i] = 0x00;

temp.Tl = i-1; temp.flag = 1;

return temp;

}


Теперь мы можем выполнять: A+"удлиннение строки", где MyString A; Но всё-таки чуть по-другому выполняется перегрузка для операции "+" для выражения - " удлиннение строки. "+A. Обнаруживаем, что такую перегрузку можно выполнить только дружественными функциями, откуда:

в заголовок класса помещаем:


// ещё одна перегрузка оператора + char *

friend MyString operator + (char *, MyString &);


в файл "MyString.cpp":


// ещё одна перегрузка оператора + char *

MyString operator + (char * Ot, MyString & Se)

{

int L, i, j, k;

k = strlen(Ot);

L = Se.LenMyStr() + k + 1;

MyString temp = MyString (L);

if (k > 0) {

for (i = 0; i < k; i++) temp.ps[i] = Ot[i];

if (Ot[i-1] == 0x00) i--;

}

else i = 0;

if (Se.Tl < 1) { temp.Tl = i; return temp; }

for (j = 0; j < Se.Tl; j++, i++)

temp.ps[i] = Se.ps[j];

temp.ps[i] = 0x00;

temp.Tl = i-1; temp.flag = 1;

return temp;

}


Обратите внимание на отсутствие при имени функции "MyString::" – эта функция дружественная и не является членом класса. Принципально её текст может быть помещён где угодно, а не обязательно в файле "MyString.cpp".


Согласно постановки нам следует перегрузить операцию []. Несмотря на простоту реализации, перегрузка этой операции часто вызывает у студентов трудности. Дело в том, что это операция доступа к данным, а не получения данных. На практике, программист не так часто различает эти две операции. Когда мы где-либо в тексте программы пишем имя переменной, то транслятор генерирует два шага: щаг вычисления адреса переменной – доступ к данным, и шаг собственно использования значения переменной – получение данных, но программист - практик обычно не задумывается об этом, ему это просто незачем. Операция [] в языке C++, тот редкий случай когда приходится явным образом различать операции доступа к данным и получения данных.

Ввиду краткости программы в заголовок класса помещаем:


// перегрузка операции []

char operator [] (int i) { if (i < 1) return 0x00;

if (i > Tl) return 0x00;

return ps[i-1]; }


При такой реализации операции мы не можем помещать результат выполнения операции слева от знака присавивания, а именно:

Mystring U;

U = “присвоение строки”;

В этом случае мы не можем написать U[2] = ‘ш’;


Если нам необходим подобный доступ к символам строки, то следует при перегрузке операции [] возвращать адресную ссылку на тип char. Кроме того, изменяется и оператор return. Более подробное описание этого можно найти в [4]. Реализация подобного похода оставляется читателю в качестве упражнения.


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


В заголовок класса помещаем:


// перегрузка сравнения : равно : ==

int operator == (MyString &);


в файл "MyString.cpp":


// перегрузка сравнения : равно : ==

bool MyString::operator == (MyString & Oth)

{

int i, k, l;

if (IsMyStrEmpty() & Oth.IsMyStrEmpty()) return true;

if (IsMyStrEmpty() | Oth.IsMyStrEmpty()) return false;

k = LenMyStr();

l = Oth.LenMyStr();

if (k != l) return false;

for (i = 0; i < k; ++i)

if (ps[i] != Oth.ps[i]) return false;

return true;

}


Перегрузка ещё одной операции сравнения: < - меньше:


// перегрузка сравнения : меньше : <

bool MyString::operator < (MyString & Oth)

{

int i, j, k, l;

if (IsMyStrEmpty() & Oth.IsMyStrEmpty()) return false;

k = LenMyStr();

l = Oth.LenMyStr();

if (k < l) j = k; else j = l;

for (i = 0; i < j; ++i) {

if (ps[i] > Oth.ps[i]) return false;

if (ps[i] < Oth.ps[i]) return true;

}

if (k < l) return true; else return false;

}


Все остальные операции сравнения можно выполнить, имея эти две. Как это сделать приведено в [3] стр. 66. Обе операции сравнения легко модифицировать для сравнения объектов типа MyString и строк типа char. Если мы задумаемся, как будем проверять наш класс, или каким образом будем писать тестирующую программку, то придём к выводу, что нам потребуется ещё и программа преобразования типа нашей строки MyString в тип AnsiString. Всё дело в том, что для оперативной распечатки лучше использовать функции вывода диалоговых окон, в частности, мы используем функцию MessageDlg, которая на входе "понимает" только AnsiString. Отсюда: программа преобразования типа MyString в тип AnsiString.

Ввиду краткости программы текст программы помещаем в заголовок класса:


// программа преобразования в AnsiString. Гатин Г.Н. для MessageDlg

operator AnsiString () { AnsiString U = ps; return U; }


На этом, в общем, реализация класса MyString может быть закончена. Наличие или отсутствие других функций в классе зависит от нужд и потребностей "заказчика". В электронном примере класса, например, реализованы перегрузка операции инкремент, причём как в префиксной, так и в постфиксной форме. Для примера приведём ещё реализацию функции Substr: – функция выделяет из MyString подстроку, начиная с позиции i, длиной l символов:


// пример реализации функции Substr Гатин Г.Н.

MyString MyString::Substr (int i, int l)

{

MyString pM = MyString(l+1);

int k = LenMyStr(), j;

if (i < 1) return pM;

if (i > k) return pM;

if (l < 1) return pM;

for (j = 0; l > 0; --l, ++i, ++j) {

if (i >= k) break;

pM.ps[j] = ps[i];

}

pM.ps[j+1] = 0x00;

pM.Tl = j;

return pM;

}


На этом реализацию класса можно завершить.


Как и что писать в тестирующей программе ясно из её текста – читайте.


После первой отладки класса, обязательно обдумайте, как можно улучшить реализацию класса, какие методы надо добавить, а какие всё-таки лишние. Углублённый непредвзятый анализ выявляет множество недостатков реализации, а иногда и явные ошибки.

В случае класса MyString, например, первое, что бросается в глаза, что мы нигде, кроме одного случая (найдите), не использовали член класса flag. Скорее всего, от этого члена можно (и нужно) избавиться. Кроме того, в некоторых случаях мы по старинке, в стиле языка C, вызывали методы класса. То есть передавали в метод не ссылку на объект, а сам объект, что приводило к ненужному построению временного объекта, а затем его удалению, а на всё это необходимо время. Как именно лучше всего передавать параметры в методы можно выяснить, прочитав замечательную книгу [5]. Наконец, внутри класса, возможно, надо шире использовать функции работы с C-строками, что обязательно ускорит работу методов класса. В наши задачи не входило детальное описание синтаксиса объектно-ориентированного программирования в C++. Желающим получить детальное описание синтаксиса и подхода рекомендуем книгу Герберта Шилдта [4].