Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Липпман.doc
Скачиваний:
8
Добавлен:
14.08.2019
Размер:
7.54 Mб
Скачать

15.2. Друзья

Рассмотрим еще раз перегруженные операторы равенства для класса String, определенные в области видимости пространства имен. Оператор равенства для двух объектов String выглядит следующим образом:

bool operator==( const String &str1, const String &str2 )

{

if ( str1.size() != str2.size() )

return false;

return strcmp( str1.c_str(), str2.c_str() ) ? false : true;


}

Сравните это определение с определением того же оператора как функции-члена:

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

{

if ( _size != rhs._size )

return false;

return strcmp( _string, rhs._string ) ? false : true;


}

Нам пришлось модифицировать способ обращения к закрытым членам класса String. Поскольку новый оператор равенства – это глобальная функция, а не функция-член, у него нет доступа к закрытым членам класса String. Для получения размера объекта String и лежащей в его основе C-строки символов используются функции-члены size() и c_str().

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

Объявление друга (оно начинается с ключевого слова friend) встречается только внутри определения класса. Поскольку друзья не являются членами класса, объявляющего дружественные отношения, то безразлично, в какой из секций – public, private или protected – они объявлены. В примере ниже мы решили поместить все подобные объявления сразу после заголовка класса:

class String {

friend bool operator==( const String &, const String & );

friend bool operator==( const char *, const String & );

friend bool operator==( const String &, const char * );

public:

// ... остальная часть класса String


};

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

// дружественные операторы напрямую обращаются к закрытым членам

// класса String

bool operator==( const String &str1, const String &str2 )

{

if ( str1._size != str2._size )

return false;

return strcmp( str1._string, str2._string ) ? false : true;


}

inline bool operator==( const String &str, const char *s )

{

return strcmp( str._string, s ) ? false : true;

}


// и т.д.

Можно возразить, что в данном случае прямой доступ к членам _size и _string необязателен, так как встроенные функции c_str() и size() столь же эффективны и при этом сохраняют инкапсуляцию, а значит, нет особой нужды объявлять операторы равенства для класса String его друзьями.

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

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

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

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

extern ostream& storeOn( ostream &, Screen & );

extern BitMap& storeOn( BitMap &, Screen & );

// ...

class Screen

{

friend ostream& storeOn( ostream &, Screen & );

friend BitMap& storeOn( BitMap &, Screen & );

// ...


};

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

Объявление функции другом двух классов должно выглядеть так:

class Window; // это всего лишь объявление

class Screen {

friend bool is_equal( Screen &, Window & );

// ...

};

class Window {

friend bool is_equal( Screen &, Window & );

// ...


};

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

class Window;

class Screen {

// copy() - член класса Screen

Screen& copy( Window & );

// ...

};

class Window {

// Screen::copy() - друг класса Window

friend Screen& Screen::copy( Window & );

// ...

};


Screen& Screen::copy( Window & ) { /* ... */ }

Функция-член одного класса не может быть объявлена другом второго, пока компилятор не увидел определения ее собственного класса. Это не всегда возможно. Предположим, что Screen должен объявить некоторые функции-члены Window своими друзьями, а Window – объявить таким же образом некоторые функции-члена Screen. В таком случае весь класс Window объявляется другом Screen:

class Window;

class Screen {

friend class Window;

// ...


};

К закрытым членам класса Screen теперь можно обращаться из любой функции-члена Window.

Упражнение 15.6

Реализуйте операторы ввода и вывода, определенные для класса Screen в упражнении 15.5, в виде друзей и модифицируйте их определения так, чтобы они напрямую обращались к закрытым членам. Какая реализация лучше? Объясните почему.

15.3. Оператор =

Присваивание одного объекта другому объекту того же класса выполняется с помощью копирующего оператора присваивания. (Этот специальный случай был рассмотрен в разделе 14.7.)

Для класса могут быть определены и другие операторы присваивания. Если объектам класса надо присваивать значения типа, отличного от этого класса, то разрешается определить такие операторы, принимающие подобные параметры. Например, чтобы поддержать присваивание C-строки объекту String:

String car ("Volks");


car = "Studebaker";

мы предоставляем оператор, принимающий параметр типа const char*. Эта операция уже была объявлена в нашем классе:

class String {

public:

// оператор присваивания для char*

String& operator=( const char * );

// ...

private:

int _size;

char *string;


};

Такой оператор реализуется следующим образом. Если объекту String присваивается нулевой указатель, он становится “пустым”. В противном случае ему присваивается копия C-строки:

String& String::operator=( const char *sobj )

{

// sobj - нулевой указатель

if (! sobj ) {

_size = 0;

delete[] _string;

_string = 0;

}

else {

_size = strlen( sobj );

delete[] _string;

_string = new char[ _size + 1 ];

strcpy( _string, sobj );

}

return *this;


}

_string ссылается на копию той C-строки, на которую указывает sobj. Почему на копию? Потому что непосредственно присвоить sobj члену _string нельзя:

_string = sobj; // ошибка: несоответствие типов

sobj – это указатель на const и, следовательно, не может быть присвоен указателю на “не-const” (см. раздел 3.5). Изменим определение оператора присваивания:

String& String::operator=( const *sobj ) { // ... }

Теперь _string прямо ссылается на C-строку, адресованную sobj. Однако при этом возникают другие проблемы. Напомним, что C-строка имеет тип const char*. Определение параметра как указателя на не-const делает присваивание невозможным:

car = "Studebaker"; // недопустимо с помощью operator=( char *) !

Итак, выбора нет. Чтобы присвоить C-строку объекту типа String, параметр должен иметь тип const char*.

Хранение в _string прямой ссылки на C-строку, адресуемую sobj, порождает и иные сложности. Мы не знаем, на что именно указывает sobj. Это может быть массив символов, который модифицируется способом, неизвестным объекту String. Например:

char ia[] = { 'd', 'a', 'n', 'c', 'e', 'r' };

String trap = ia; // trap._string ссылается на ia


ia[3] = 'g'; // а вот это нам не нужно:

// модифицируется и ia, и trap._string

Если trap._string напрямую ссылался на ia, то объект trap демонстрировал бы своеобразное поведение: его значение может изменяться без вызова функций-членов класса String. Поэтому мы полагаем, что выделение области памяти для хранения копии значения C-строки менее опасно.

Обратите внимание, что в операторе присваивания используется delete. Член _string содержит ссылку на массив символов, расположенный в хипе. Чтобы предотвратить утечку, память, выделенная под старую строку, освобождается с помощью delete до выделения памяти под новую. Поскольку _string адресует массив символов, следует использовать версию delete для массивов (см. раздел 8.4).

И последнее замечание об операторе присваивания. Тип возвращаемого им значения – это ссылка на класс String. Почему именно ссылка? Дело в том, что для встроенных типов операторы присваивания можно сцеплять:

// сцепление операторов присваивания

int iobj, jobj;


iobj = jobj = 63;

Они ассоциируются справа налево, т.е. в предыдущем примере присваивания выполняются так:

iobj = (jobj = 63);

Это удобно и при работе с объектами класса String: поддерживается, к примеру, следующая конструкция:

String ver, noun;


verb = noun = "count";

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

Операторы присваивания бывают перегруженными. Например, в нашем классе String есть такой набор:

// набор перегруженных операторов присваивания

String& operator=( const String & );


String& operator=( const char * );

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