Наш класс String должен поддерживать инициализацию объектом класса String, строковым литералом и встроенным строковым типом, равно как и операцию присваивания ему значений этих типов. Мы используем для этого конструкторы класса и перегруженную операцию присваивания. Доступ к отдельным символам String будет реализован как перегруженная операция взятия индекса. Кроме того, нам понадобятся: функция size() для получения информации о длине строки; операция сравнения объектов типа String и объекта String со строкой встроенного типа; а также операции ввода/вывода нашего объекта. В заключение мы реализуем возможность доступа к внутреннему представлению нашей строки в виде строки встроенного типа.
Определение класса начинается ключевым словом class, за которым следует идентификатор – имя класса, или типа. В общем случае класс состоит из секций, предваряемых словами public (открытая) и private (закрытая). Открытая секция, как правило, содержит набор операций, поддерживаемых классом и называемых методами или функциями-членами класса. Эти функции-члены определяют открытый интерфейс класса, другими словами, набор действий, которые можно совершать с объектами данного класса. В закрытую секцию обычно включают данные-члены, обеспечивающие внутреннюю реализацию. В нашем случае к внутренним членам относятся _string – указатель на char, а также _size типа int. _size будет хранить информацию о длине строки, а _string – динамически выделенный массив символов. Вот как выглядит определение класса:
#include iostream
class String;
istream operator( istream, String );
ostream operator( ostream, const String );
class String {
public:
// набор конструкторов
// для автоматической инициализации
// String strl; // String()
// String str2( "literal" ); // String( const char* );
// String str3( str2 ); // String( const String );
String();
String( const char* );
String( const String );
// деструктор
~String();
// операторы присваивания
// strl = str2
// str3 = "a string literal"
String operator=( const String );
String operator=( const char* );
// операторы проверки на равенство
// strl == str2;
// str3 == "a string literal";
bool operator==( const String );
bool operator==( const char* );
// перегрузка оператора доступа по индексу
// strl[ 0 ] = str2[ 0 ];
char operator[]( int );
// доступ к членам класса
int size() { return _size; }
char* c_str() { return _string; }
private:
int _size;
char *_string;
}
Класс String имеет три конструктора. Как было сказано в разделе 2.3, механизм перегрузки позволяет определять несколько реализаций функций с одним именем, если все они различаются количеством и/или типами своих параметров. Первый конструктор
String();
является конструктором по умолчанию, потому что не требует явного указания начального значения. Когда мы пишем:
String str1;
для str1 вызывается такой конструктор.
Два оставшихся конструктора имеют по одному параметру. Так, для
String str2("строка символов");
вызывается конструктор
String(const char*);
а для
String str3(str2);
конструктор
String(const String);
Тип вызываемого конструктора определяется типом фактического аргумента. Последний из конструкторов, String(const String), называется копирующим, так как он инициализирует объект копией другого объекта.
Если же написать:
String str4(1024);
то это вызовет ошибку компиляции, потому что нет ни одного конструктора с параметром типа int.
Объявление перегруженного оператора имеет следующий формат:
return_type operator op (parameter_list);
где operator – ключевое слово, а op – один из предопределенных операторов: +, =, ==, [] и так далее. (Точное определение синтаксиса см. в главе 15.) Вот объявление перегруженного оператора взятия индекса:
char operator[] (int);
Этот оператор имеет единственный параметр типа int и возвращает ссылку на char. Перегруженный оператор сам может быть перегружен, если списки параметров отдельных конкретизаций различаются. Для нашего класса String мы создадим по два различных оператора присваивания и проверки на равенство.
Для вызова функции-члена применяются операторы доступа к членам – точка (.) или стрелка (-). Пусть мы имеем объявления объектов типа String:
String object("Danny");
String *ptr = new String ("Anna");
String array[2];
Вот как выглядит вызов функции size() для этих объектов:
vectorint sizes( 3 );
// доступ к члену для objects (.);
// objects имеет размер 5
sizes[ 0 ] = object.size();
// доступ к члену для pointers (-)
// ptr имеет размер 4
sizes[ 1 ] = ptr-size();
// доступ к члену (.)
// array[0] имеет размер 0
sizes[ 2 ] = array[0].size();
Она возвращает соответственно 5, 4 и 0.
Перегруженные операторы применяются к объекту так же, как обычные:
String namel( "Yadie" );
String name2( "Yodie" );
// bool operator==(const String)
if ( namel == name2 )
return;
else
// String operator=( const String )
namel = name2;
Объявление функции-члена должно находиться внутри определения класса, а определение функции может стоять как внутри определения класса, так и вне его. (Обе функции size() и c_str() определяются внутри класса.) Если функция определяется вне класса, то мы должны указать, кроме всего прочего, к какому классу она принадлежит. В этом случае определение функции помещается в исходный файл, допустим, String.C, а определение самого класса – в заголовочный файл (String.h в нашем примере), который должен включаться в исходный:
// содержимое исходного файла: String.С
// включение определения класса String
#include "String.h"
// включение определения функции strcmp()
#include cstring
bool // тип возвращаемого значения
String:: // класс, которому принадлежит функция
operator== // имя функции: оператор равенства
(const String rhs) // список параметров
{
if ( _size != rhs._size )
return false;
return strcmp( _strinq, rhs._string ) ?
false : true;
}
Напомним, что strcmp() – функция стандартной библиотеки С. Она сравнивает две строки встроенного типа, возвращая 0 в случае равенства строк и ненулевое значение в случае неравенства. Условный оператор (?:) проверяет значение, стоящее перед знаком вопроса. Если оно истинно, возвращается значение выражения, стоящего слева от двоеточия, в противном случае – стоящего справа. В нашем примере значение выражения равно false, если strcmp() вернула ненулевое значение, и true – если нулевое. (Условный оператор рассматривается в разделе 4.7.)
Операция сравнения довольно часто используется, реализующая ее функция получилась небольшой, поэтому полезно объявить эту функцию встроенной (inline). Компилятор подставляет текст функции вместо ее вызова, поэтому время на такой вызов не затрачивается. (Встроенные функции рассматриваются в разделе 7.6.) Функция-член, определенная внутри класса, является встроенной по умолчанию. Если же она определена вне класса, чтобы объявить ее встроенной, нужно употребить ключевое слово inline:
inline bool
String::operator==(const String rhs)
{
// то же самое
}
Определение встроенной функции должно находиться в заголовочном файле, содержащем определение класса. Переопределив оператор == как встроенный, мы должны переместить сам текст функции из файла String.C в файл String.h.
Ниже приводится реализация операции сравнения объекта String со строкой встроенного типа:
inline bool
String::operator==(const char *s)
{
return strcmp( _string, s ) ? false : true;
}
Имя конструктора совпадает с именем класса. Считается, что он не возвращает значение, поэтому не нужно задавать возвращаемое значение ни в его определении, ни в его теле. Конструкторов может быть несколько. Как и любая другая функция, они могут быть объявлены встроенными.
#include cstring
// default constructor
inline String::String()
{
_size = 0;
_string = 0;
}
inline String::String( const char *str )
{
if ( ! str ) {
_size = 0; _string = 0;
}
else {
_size = str1en( str );
_string = new char[ _size + 1 ];
strcpy( _string, str );
}
// copy constructor
inline String::String( const String rhs )
{
size = rhs._size;
if ( ! rhs._string )
_string = 0;
else {
_string = new char[ _size + 1 ];
strcpy( _string, rhs._string );
}
}
Поскольку мы динамически выделяли память с помощью оператора new, необходимо освободить ее вызовом delete, когда объект String нам больше не нужен. Для этой цели служит еще одна специальная функция-член – деструктор, автоматически вызываемый для объекта в тот момент, когда этот объект перестает существовать. (См. главу 7 о времени жизни объекта.) Имя деструктора образовано из символа тильды (~) и имени класса. Вот определение деструктора класса String. Именно в нем мы вызываем операцию delete, чтобы освободить память, выделенную в конструкторе:
inline String: :~String() { delete [] _string; }
В обоих перегруженных операторах присваивания используется специальное ключевое слово this.
Когда мы пишем:
String namel( "orville" ), name2( "wilbur" );
namel = "Orville Wright";
this является указателем, адресующим объект name1 внутри тела функции операции присваивания.
this всегда указывает на объект класса, через который происходит вызов функции. Если
ptr-size();
obj[ 1024 ];
то внутри size() значением this будет адрес, хранящийся в ptr. Внутри операции взятия индекса this содержит адрес obj. Разыменовывая this (использованием *this), мы получаем сам объект. (Указатель this детально описан в разделе 13.4.)
inline String
String::operator=( const char *s )
{
if ( ! s ) {
_size = 0;
delete [] _string;
_string = 0;
}
else {
_size = str1en( s );
delete [] _string;
_string = new char[ _size + 1 ];
strcpy( _string, s );
}
return *this;
}
При реализации операции присваивания довольно часто допускают одну ошибку: забывают проверить, не является ли копируемый объект тем же самым, в который происходит копирование. Мы выполним эту проверку, используя все тот же указатель this:
inline String
String::operator=( const String rhs )
{
// в выражении
// namel = *pointer_to_string
// this представляет собой name1,
// rhs - *pointer_to_string.
if ( this != rhs ) {
Вот полный текст операции присваивания объекту String объекта того же типа:
inline String
String::operator=( const String rhs )
{
if ( this != rhs ) {
delete [] _string;
_size = rhs._size;
if ( ! rhs._string )
_string = 0;
else {
_string = new char[ _size + 1 ];
strcpy( _string, rhs._string );
}
}
return *this;
}
Операция взятия индекса практически совпадает с ее реализацией для массива Array, который мы создали в разделе 2.3:
#include cassert
inline char
String::operator[] ( int elem )
{
assert( elem = 0 elem _size );
return _string[ elem ];
}
Операторы ввода и вывода реализуются как отдельные функции, а не члены класса. (О причинах этого мы поговорим в разделе 15.2. В разделах 20.4 и 20.5 рассказывается о перегрузке операторов ввода и вывода библиотеки iostream.) Наш оператор ввода может прочесть не более 4095 символов. setw() – предопределенный манипулятор, он читает из входного потока заданное число символов минус 1, гарантируя тем самым, что мы не переполним наш внутренний буфер inBuf. (В главе 20 манипулятор setw() рассматривается детально.) Для использования манипуляторов нужно включить соответствующий заголовочный файл:
#include iomanip
inline istream
operator( istream io, String s )
{
// искусственное ограничение: 4096 символов
const int 1imit_string_size = 4096;
char inBuf[ limit_string_size ];
// setw() входит в библиотеку iostream
// он ограничивает размер читаемого блока до 1imit_string_size-l
io setw( 1imit_string_size ) inBuf;
s = mBuf; // String::operator=( const char* );
return io;
}
Оператору вывода необходим доступ к внутреннему представлению строки String. Так как operator не является функцией-членом, он не имеет доступа к закрытому члену данных _string. Ситуацию можно разрешить двумя способами: объявить operator дружественным классу String, используя ключевое слово friend (дружественные отношения рассматриваются в разделе 15.2), или реализовать встраиваемую (inline) функцию для доступа к этому члену. В нашем случае уже есть такая функция: c_str() обеспечивает доступ к внутреннему представлению строки. Воспользуемся ею при реализации операции вывода:
inline ostream
operator( ostream os, const String s )
{
return os s.c_str();
}