Назад | Начало урока | Вперед
Содержание

Глава 13

Джесс Либерти "С++ за 21 день"

Строковые классы

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


//Listing 13.12 Using a String class
#include <iostream>
#include <string.h>
using namespace std;

// Rudimentary string class
class String
{

public:
// constructors
String();
String(const char *const);
String(const String &);
~String();

// overloaded operators
char & operator[](unsigned short offset);
char operator[](unsigned short offset) const;
String operator+(const String&);
void operator+=(const String&);
String & operator= (const String &);

// General accessors
unsigned short GetLen()const {

return itsLen;
}
const char * GetString() const {
return itsString;
}

private:
28: String (unsigned short); // private constructor

char * itsString;
unsigned short itsLen;

};

//1

// default constructor creates string of 0 bytes
String::String()
{

itsString = new char[1];
itsString[0] = '\0';
itsLen=0;
}

//2

// private (helper) constructor, used only by
// class methods for creating a new string of
// required size. Null filled.

String::String(unsigned short len)
{

itsString = new char[len+1];
for (unsigned short i = 0; i<=len; i++)
itsString[i] = '\0';
itsLen=len;
}

//3

// Converts a character array to a String
String::String(const char * const cString)
{

itsLen = strlen(cString);
itsString = new char[itsLen+1];
for (unsigned short i = 0; i < itsLen; i++)
itsString[i] = cString[i];
itsString[itsLen]='\0';
}

//4

63:// copy constructor
String::String (const String & rhs)
{

itsLen=rhs.GetLen();
itsString = new char[itsLen+1];
for (unsigned short i = 0; i itsString[i] = rhs[i];
itsString[itsLen] = '\0';
}

//5

// destructor, frees allocated memory
String::~String ()
{

delete [] itsString;
itsLen = 0;
}

//6

// operator equals, frees existing memory
// then copies string and size

String& String::operator=(const String & rhs)
{

if (this == &rhs)
return *this;
delete [] itsString;
itsLen=rhs.GetLen();
itsString = new char[itsLen+1];
for (unsigned short i = 0; i itsString[i] = rhs[i];
itsString[itsLen] = '\0';
return *this;
}

//7

//nonconstant offset operator, returns
// reference to character so it can be
// changed!

char & String::operator[](unsigned short offset)
{

if (offset > itsLen)
return itsString[itsLen-1];
else
return itsString[offset];
}

//8

// constant offset operator for use
// on const objects (see copy constructor!)

char String::operator[](unsigned short offset) const
{

if (offset > itsLen)
return itsString[itsLen-1];
else
return itsString[offset];
}

//9

// creates a new string by adding current
// string to rhs

String String::operator+(const String& rhs)
{

unsigned short totalLen = itsLen + rhs.GetLen();
String temp(totalLen);
unsigned short i;
for ( i= 0; i temp[i] = itsString[i];
for (unsigned short j = 0; j temp[i] = rhs[j];
temp[totalLen]='\0';
return temp;
}

//10

// changes current string, returns nothing

void String::operator+=(const String& rhs)
{

unsigned short rhsLen = rhs.GetLen();
unsigned short totalLen = itsLen + rhsLen;
String temp(totalLen);
unsigned short i;
for (i = 0; i temp[i] = itsString[i];
for (unsigned short j = 0; j temp[i] = rhs[i-itsLen];
temp[totalLen]='\0';
*this = temp;
}

// * * * * *

int main()
{

String s1("initial test");
cout << "S1:\t" << s1.GetString() << endl;

char * temp = "Hello World";
s1 = temp;
cout << "S1:\t" << s1.GetString() << endl;

char tempTwo[20];
strcpy(tempTwo, "; nice to be here!");
s1 += tempTwo;
cout << "tempTwo:\t" << tempTwo << endl;
cout << "S1:\t" << s1.GetString() << endl;

cout << "S1[4]:\t" << s1[4] << endl;
s1[4]='x';
cout << "S1:\t" << s1.GetString() << endl;

cout << "S1[999]:\t" << s1[999] << endl;

String s2(" Another string");
String s3;
s3 = s1+s2;
cout << "S3:\t" << s3.GetString() << endl;

String s4;
s4 = "Why does this work?";
cout << "S4:\t" << s4.GetString() << endl;
return 0;

}


* * * * * * * *

Результат:

S1: initial test
S1: Hello World
tempTwo: ; nice to be here!
S1: Hello World; nice to be here!
S1[4]: o
S1: Hellx World; nice to be here!
S1[999]: !
S3: Hellx World; nice to be here! Another string
S4: Why does this work?

Анализ программы:

Мы видим в программе объявление класса String.
Строки 11-13 содержат объявления трех конструкторов:
-стандартного,
-конструктора копий,
-конструктора, принимающего строку стиля С, которая завершается символом null.

// constructors
String();
String(const char *const);
String(const String &);

Далее класс String перегружает оператор индекса [], оператор плюс (+),
и оператор плюс-эквалс (+=) (присвоение с суммой)

// overloaded operators
char & operator[](unsigned short offset);
char operator[](unsigned short offset) const;
String operator+(const String&);
void operator+=(const String&);
String & operator= (const String &);

Как видим оператор индекса [] перегружен дважды.
Один раз как постоянная функция, возвращающая значение типа char,

char operator[](unsigned short offset) const;

а второй раз как не постоянная функция, возвращающая ссылку на значение
типа char.

char & operator[](unsigned short offset);

Не постоянная версия используется в операторах типа (строка 161):

SomeString[4]='x';

Это означает поместить значение типа x в пятый элемент массива SomeString.

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

cout << "S1[4]:\t" << s1[4] << endl;
s1[4]='x'; //изменяем значение пятого элемента массива s1
cout << "S1:\t" << s1.GetString() << endl;

Эти строки из главной программы изменяют значение символа под индексом 4
в символьном массиве s1.

Постоянная версия оператора [] используется в тех случаях, когда необходимо
получить доступ к постоянному объекту класса String , например в реализации
конструктора копий (строка 63)

63:// copy constructor
String::String (const String & rhs)
{

itsLen=rhs.GetLen();

itsString = new char[itsLen+1];

for (unsigned short i = 0; i < itsLen;i++)
itsString[i] = rhs[i];
itsString[itsLen] = '\0';

}

Обратите внимание что rhs[i] доступен, хотя rhs был объявлен как const String &.
К этому объекту не возможно получить доступ, используя непостоянные функции-члены.
Следовательно оператор индекса необходимо перегружать как постоянный.

В этом конструкторе учитывая то что внутри класса был объявлен как private
указатель на переменную типа char и здесь в конструкторе этот указатель
переопределен на динамическую память

char * itsString;
itsString=new char[itsLen]

Значит строка будет расположениа в динамической памяти, ее размер будет itsLen.

Если возвращаемый объект окажется слишком большим, то придется вернуть не сам объект,
а постоянную ссылку на него. Но поскольку один символ занимает только один байт,
то нет смысла так поступать.

-.-.-.- 1.

Стандартный конструктор реализован в строках 31-39
Стандартный конструктор создает строку нулевой длины

// default constructor creates string of 0 bytes
String::String()
{

itsString = new char[1];
itsString[0] = '\0';
itsLen=0;
}

Общепринято что в классе String длина строки измеряется без учета концевого
символа null. Таким образом строка созданная по умолчанию содержит лишь
концевой символ null.
Строка стандартного конструктора создана тоже в динамической памяти

char * itsString;
itsString=new char[1]

Таким образом если в главной программе мы объявим объект типа String
без параметров(то есть никак не инициализированный) например:

String s1;

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

-.-.-.- 2.

Конструктор копий реализован в строках 63-70

63:// copy constructor
String::String (const String & rhs)
{

itsLen=rhs.GetLen();
itsString = new char[itsLen+1];

for (unsigned short i = 0; i < itsLen;i++)
itsString[i] = rhs[i];

itsString[itsLen] = '\0';

}

Здесь параметр rhs -исходная строка
itsString -указатель на вновь создаваемую строку.

Конструктор сначала определяет длину исходной строки используя функцию-член
GetLen()

itsLen=rhs.GetLen();

Затем Конструктор переопределяет указатель на массив в динамической памяти
и устанавливает длину новой строки на единицу больше длины исходной

char * itsString;
itsString=new char[itsLen+1]

(дополнительный символ необходим для завершающего null),

а затем копирует каждый символ исходной строки во вновь созданную и

for (unsigned short i = 0; i < itsLen;i++)
itsString[i] = rhs[i];

завершает ее символом null.
itsString[itsLen] = '\0';

Все! Копия исходной строки создана в динамической памяти!
Напомню что копирующий конструктор вызывается автоматически.

-.-.-.- 3.

В строках 53-60 реализован конструктор, принимающий исходную строку в стиле С
(с концевым null)Этот конструктор подобен конструктору копий. Длина исходной
строки исчисляется с помощью функции strlen() из стандартной библиотеки String.

Конструктор преобразует символьный массив в строку

// Converts a character array to a String
String::String(const char * const cString)
{

itsLen = strlen(cString);

itsString = new char[itsLen+1];

for (unsigned short i = 0; i itsString[i] = cString[i];
itsString[itsLen]='\0';

}

То есть если мы создадим объект типа String с параметром в скобках
например как в данной программе

String s1("initial test");

То будет вызван данный конструктор. Сначала он определит длину данной строки
при помощи функции strlen()

itsLen = strlen(cString);

Затем в динамической памяти создаст место на один элемент больше
нашей введенной строки

char * itsString;
itsString=new char[itsLen+1]

а затем скопирует каждый символ введенной нами строки во вновь созданную и

for (unsigned short i = 0; i < itsLen;i++)
itsString[i] = rhs[i];

завершает ее символом null.

itsString[itsLen] = '\0';

Все! Введенная нами строка находится в динамической памяти и на нее указыает
указатель itsString расположенный в соответствующем поле вновь созданного
нами объекта типа String

-.-.-.- 4.

В строке 28 объявлен еще один конструктор, String (unsigned short)

String (unsigned short); // private constructor

Он объявлен как закрытая функция-член. Таково было намерение автора,
чтобы ни один из клиентов этого класса не мог создать строку произвольной
длины. Этот конструктор создает строки для внутреннего пользования,
как например operator += в строке 130. Более подробная информация об операторе
operator += приведена далее в этой главе.

Конструктор

String (unsigned short); // private constructor

заполняет все элементы строки символом null.
Поэтому условием выхода из цикла for будет i <= len , а не i < len.

// private (helper) constructor, used only by
// class methods for creating a new string of
// required size. Null filled.

String::String(unsigned short len)
{

itsString = new char[len+1];

for (unsigned short i = 0; i<=len; i++)
itsString[i] = '\0';
itsLen=len;

}

Конструктор переорентирует указатель на массив созданный в динамической памяти
длиной len+1

char * itsString;
itsString=new char[itsLen+1]

Затем заполняет все элементы массива символом null

for (unsigned short i = 0; i <= len; i++)
itsString[i] = '\0';

И присваивает переменной itsLen значение len , чтобы нельзя было выйти за границы
массива

itsLen=len;

Вопрос:где у нас в программе пример использования этого массива?

-.-.-.- 5.

Деструктор, реализованный в строках 73-77, освобождает память, занимаемую
строкой объекта класса String. Удостоверьтесь что при вызове оператора
delete[] не забыты квадратные скобки, иначе вместо всего массива будет
удален только его первый элемент.

// destructor, frees allocated memory
String::~String ()
{

delete [] itsString;
itsLen = 0;
}

-.-.-.- 6.

Оператор присвоения:

// operator equals, frees existing memory
// then copies string and size
String& String::operator=(const String & rhs)
{

if (this == &rhs)
return *this;
delete [] itsString;
itsLen=rhs.GetLen();
itsString = new char[itsLen+1];
for (unsigned short i = 0; i itsString[i] = rhs[i];
itsString[itsLen] = '\0';
return *this;
}

Оператор присвоения сначала проверяет, не равна ли строка справа от оператора
строке слева.

if (this == &rhs)
return *this;

Если это не так, то удалив прежнюю строку, он создает новую

delete [] itsString;

itsLen=rhs.GetLen();
itsString = new char[itsLen+1];

и копирует в нее исходную.

for (unsigned short i = 0; i < itsLen;i++)
itsString[i] = rhs[i];
itsString[itsLen] = '\0';

В качестве результата возвращается ссылка на
новую строку,

return *this;

что позволяет осуществить присвоение типа:

String1=String2=String3;

Вот как например этот перегруженный оператор используется в главной
программе:

char * temp = "Hello World";
s1 = temp;
cout << "S1:\t" << s1.GetString() << endl;

На экране мы увидим:

S1: Hello World

-.-.-.- 7.

Оператор индекса [] перегружен дважды. И в обоих случаях осуществляется
проверка пределов массивов. Если пользователь попытается осуществить доступ
к символу вне пределов массива, то последним возвращаемым символом окажется
последний символ массива, то есть len-1 й элемент

//nonconstant offset operator, returns
// reference to character so it can be
// changed!

char & String::operator[](unsigned short offset)
{

if (offset > itsLen)
return itsString[itsLen-1];
else
return itsString[offset];
}

// constant offset operator for use
// on const objects (see copy constructor!)
char String::operator[](unsigned short offset) const
{

if (offset > itsLen)
return itsString[itsLen-1];
else
return itsString[offset];
}

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

Вот это место в главной программе:

cout << "S1[999]:\t" << s1[999] << endl;

Попытка обратиться за пределы массива
На экране мы увидим только знак ! последний элемент массива как
и было задумано.

-.-.-.- 8.

В строках 117-127 оператор суммы (+) перегружается в оператор конкатенации.

Было бы очень удобно иметь возможность осуществлять конкатенацию
строк аналогично простому сложению:

String3=String1+String2;

// creates a new string by adding current
// string to rhs

String String::operator+(const String& rhs)
{

unsigned short totalLen = itsLen + rhs.GetLen();
String temp(totalLen);
unsigned short i;
for ( i= 0; i temp[i] = itsString[i];
for (unsigned short j = 0; j temp[i] = rhs[j];
temp[totalLen]='\0';
return temp;
}

Для реализации этой возможности функция заменяющая оператор плюс (+)
вычисляет суммарную длину обеих строк

unsigned short totalLen = itsLen + rhs.GetLen();

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

String temp(totalLen); //вызывается закрытый конструктор

Впоследствии эти пустые символы будут заменены содержимым двух исходных строк.
Сначала во временную строку копируется левая исходная строка (+this),

unsigned short i;
for ( i= 0; i < itsLen; i++)
temp[i] = itsString[i];

а затем -правая (rhs).

for (unsigned short j = 0; j < rhs.GetLen(); j++, i++)
temp[i] = rhs[j];
temp[totalLen]='\0';

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

Оператор суммы возвращает временную строку как значение, которое присваивается
строке, расположенной слева от оператора (string1).

Вот строки из главной программы в которых используется этот перегруженный
оператор:

String s2(" Another string");
String s3;
s3 = s1+s2;
cout << "S3:\t" << s3.GetString() << endl;

Результат на экране:

Две строки соединяются в одну

-.-.-.- 9.

Оператор += работает с уже существующими строками, находящимися с левой
стороны оператора

String1 += String2;

Этот оператор работает аналогично оператору суммы , за исключением того ,
что временное значение присваивается текущей строке

+this=temp (строка 142)

// changes current string, returns nothing
void String::operator+=(const String& rhs)
{

unsigned short rhsLen = rhs.GetLen();
unsigned short totalLen = itsLen + rhsLen;
String temp(totalLen);
unsigned short i;
for (i = 0; i temp[i] = itsString[i];
for (unsigned short j = 0; j temp[i] = rhs[i-itsLen];
temp[totalLen]='\0';
*this = temp;
}

Вот строки из главной программы которые используют данный перегруженный
оператор

char tempTwo[20]; //объявлен массив
strcpy(tempTwo, "; nice to be here!"); //он инициализирован
s1 += tempTwo; //применен оператор +=
cout << "tempTwo:\t" << tempTwo << endl;
cout << "S1:\t" << s1.GetString() << endl;//строки соединились!

* * * * *

Функция main() проверяет работоспособность созданного нами класса.

int main()
{

String s1("initial test");
cout << "S1:\t" << s1.GetString() << endl;

char * temp = "Hello World";
s1 = temp;
cout << "S1:\t" << s1.GetString() << endl;

char tempTwo[20];
strcpy(tempTwo, "; nice to be here!");
s1 += tempTwo;
cout << "tempTwo:\t" << tempTwo << endl;
cout << "S1:\t" << s1.GetString() << endl;

cout << "S1[4]:\t" << s1[4] << endl;
s1[4]='x';
cout << "S1:\t" << s1.GetString() << endl;

cout << "S1[999]:\t" << s1[999] << endl;

String s2(" Another string");
String s3;
s3 = s1+s2;
cout << "S3:\t" << s3.GetString() << endl;

String s4;
s4 = "Why does this work?";
cout << "S4:\t" << s4.GetString() << endl;
return 0;

}

В строке 147 создается объект класса String с помощью конструктора,
принимающего строку в стиле С с пустым символом в конце.
Строка 148 выводит на экран ее содержимое используя функцию доступа
GetString().

String s1("initial test");
cout << "S1:\t" << s1.GetString() << endl;

В строке 150 создается другая строка стиля С. Строка 151 проверяет
оператор присвоения, а строка 152 выводит на экран результаты.

char * temp = "Hello World"; //какой здесь использован конструктор?
151 s1 = temp; //использован оператор присвоения
cout << "S1:\t" << s1.GetString() << endl; // вывод на экран

В строке 154 создается третья строка в стиле С-tempTwo.
В строке 155 вызов функции strcpy() заполняет буфер символами:
nice to be here!
Строка 156 вызывает оператор += и добавляет к строке s1 строку tempTwo.
Строка 158 выводит результаты на экран.

char tempTwo[20];
strcpy(tempTwo, "; nice to be here!");
156 s1 += tempTwo;
cout << "tempTwo:\t" << tempTwo << endl;
cout << "S1:\t" << s1.GetString() << endl;

В строке 160 возвращается и выводится на экран пятый символ строки s1.
В строке 161 с помощью оператора индекса [] ему присваивается новое
значение, а в строке 162 на экран выводится результат, демонстрирующий
факт внесения изменений.

cout << "S1[4]:\t" << s1[4] << endl;
s1[4]='x';
cout << "S1:\t" << s1.GetString() << endl;

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

cout << "S1[999]:\t" << s1[999] << endl;

В строках 166 , 167 создаются еще два объекта класса String,
а в строке 168 происходит их сложение.
Строка 169 выводит результаты на экран.

String s2(" Another string");
String s3;
s3 = s1+s2;
cout << "S3:\t" << s3.GetString() << endl;

В строке 171 создается новый объект класса String -s4.
В строке 172 вызывается оператор присвоениия, в строке 173
выводится результат на экран.

String s4;
s4 = "Why does this work?";
cout << "S4:\t" << s4.GetString() << endl;

Можно было бы засомневаться :"В строке 21 оператор присвоения определен
так, чтобы принимать постоянную ссылку на объект класса String, а здесь
передается строка в стиле С. Допустимо ли это?"

Хоть компилятор и ожидает объект класса String , но получив символьный
массив, он проверяет можно ли преобразовать его в строку. А в строке 12
как раз объявлен конструктор , который создает объекты класса String из
символьных массивов . Компилятор создает из символьного массива временную
строку и передает ее оператору присвоения. Этот процесс известен под
названием "неявное приведение (implicit casting или promotion).
Если бы не был объявлен соответствующий конструктор, для реализации
подобной функции, такое присвоение привело бы к ошибке компиляции.
return 0;

Задача:

Модифицировать класс String так чтобы каждый конструктор выводил
свое сообщение и посмотреть когда какой конструктор будет вызван при
создании новых объектов.

Использовать этот класс для программы которая из данного массива
выбирает слова с одинаковой длительностью и помещает слова с одинаковой
длительностью в один файл, с другой длительностью в другой файл ,
с иной -в третий файл и т.д.
Использовать готовую функцию сортировки из программы ...
и использовать функции образования на жестком диске файлов.

Выводы:

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

String s1("initial test");
String s2(" Another string");
cout << "S1:\t" << s1.GetString() << endl;
cout << "S1:\t" << s2.GetString() << endl;

char * temp = "Hello World";
s1 = temp;
cout << "S1:\t" << s1.GetString() << endl;

char tempTwo[20];
strcpy(tempTwo, "; nice to be here!");
cout << "tempTwo:\t" << tempTwo << endl;

String s4;
s4 = "Why does this work?";
cout << "S4:\t" << s4.GetString() << endl;

Этот класс позволяет нам использовать эффективно и удобно следующие
операторы:
(=), для присвоения

(+), (+=) для соединения двух и более строк в одну

[] -оператор индекса для доступа к элементам массива и ограничения
доступа за пределы массива

Оператор присвоения содержимого одного массива другому:

char * temp = "Hello World";
s1 = temp; //оператор присвоения
cout << "S1:\t" << s1.GetString() << endl;

Оператор сложения двух строк и присвоения результата третьей

String s2(" Another string");
String s3;
s3 = s1+s2; //операторы сложения и присвоения
cout << "S3:\t" << s3.GetString() << endl;

Оператор (+=)

char tempTwo[20];
strcpy(tempTwo, "; nice to be here!");
s1 += tempTwo; //оператор (+=)
cout << "tempTwo:\t" << tempTwo << endl;
cout << "S1:\t" << s1.GetString() << endl;

Оператор []

cout << "S1[4]:\t" << s1[4] << endl;
s1[4]='x';
cout << "S1:\t" << s1.GetString() << endl;

Здесь произошло присвоение одному элементу массива
нового значения.


Назад | Начало урока | Вверх | Вперед
Содержание

Hosted by uCoz