712
"tulip" == flower
Мы ведь не определили такой перегруженный оператор: bool operator==( const char *, const String & );
Но это необязательно. Когда перегруженный оператор является функцией в пространстве имен, то как для первого, так и для второго его параметра (для левого и правого операндов) рассматриваются возможные преобразования, т.е. компилятор интерпретирует второе использование оператора равенства как operator==( String("tulip"), flower ); и вызывает для выполнения сравнения следующий перегруженный оператор: bool operator==( const String &, const String & );
Но тогда зачем мы предоставили второй перегруженный оператор: bool operator==( const String &, const char * );
Преобразование типа из C-строки в класс String может быть применено и к правому операнду. Функция main() будет компилироваться без ошибок, если просто определить в пространстве имен перегруженный оператор, принимающий два операнда String: bool operator==( const String &, const String & );
Предоставлять ли только этот оператор или еще два: bool operator==( const String &, const char * ); зависит от того, насколько велики затраты на преобразование из C-строки в String во время выполнения, то есть от “стоимости” дополнительных вызовов конструктора в программах, пользующихся нашим классом String. Если оператор равенства будет часто использоваться для сравнения C-строк и объектов , то лучше предоставить все три варианта. (Мы вернемся к вопросу эффективности в разделе, посвященном друзьям.
Подробнее о приведении к типу класса с помощью конструкторов мы расскажем в разделе 15.9; в разделе 15.10 речь пойдет о разрешении перегрузки функций с помощью описанных преобразований, а в разделе 15.12 – о разрешении перегрузки операторов.)
Итак, на основе чего принимается решение, делать ли оператор членом класса или членом пространства имен? В некоторых случаях у программиста просто нет выбора:
•
если перегруженный оператор является членом класса, то он вызывается лишь при условии, что левым операндом служит член этого класса. Если же левый операнд имеет другой тип, оператор обязан быть членом пространства имен;
•
язык требует, чтобы операторы присваивания ("="), взятия индекса ("[]"), вызова ("()") и доступа к членам по стрелке ("->") были определены как члены класса. В противном случае выдается сообщение об ошибке компиляции: bool operator==( const char *, const String & );
С++ для начинающих
713char& operator[]( String &, int ix );
(Подробнее оператор присваивания рассматривается в разделе 15.3, взятия индекса – в разделе 15.4, вызова – в разделе 15.5, а оператор доступа к члену по стрелке – в разделе
15.6.)
В остальных случаях решение принимает проектировщик класса.
Симметричные операторы, например оператор равенства, лучше определять в пространстве имен, если членом класса может быть любой операнд (как в String).
Прежде чем закончить этот подраздел, определим операторы равенства для класса
String в пространстве имен:
}
15.1.2. Имена перегруженных операторов Перегружать можно только предопределенные операторы языка C++ (см. табл. 15.1).
Таблица 15.1. Перегружаемые операторы +
-
*
/
%
^
&
|
!
,
=
<
>
<=
>=
++
--
<<
>>
==
!=
&&
||
+=
-=
/=
%=
^=
&=
|=
*=
<<=
>>=
[]
()
->
->* new new[] delete delete[]
Проектировщик класса не вправе объявить перегруженным оператор с другим именем.
Так, при попытке объявить оператор ** для возведения в степень компилятор выдаст сообщение об ошибке.
Следующие четыре оператора языка C++ не могут быть перегружены:
:: .* . ?:
// ошибка: должен быть членом класса 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 ;
} inline bool operator==( const String &str, const char *s )
{ return strcmp( str.c_str(), s ) ? false : true ;
// неперегружаемые операторы
С++ для начинающих
714
Предопределенное назначение оператора нельзя изменить для встроенных типов.
Например, не разрешается переопределить встроенный оператор сложения целых чисел так, чтобы он проверял результат на переполнение. int operator+( int, int );
Нельзя также определять дополнительные операторы для встроенных типов данных, например добавить к множеству встроенных операций operator+ для сложения двух массивов.
Перегруженный оператор определяется исключительно для операндов типа класса или перечисления и может быть объявлен только как член класса или пространства имен, принимая хотя бы один параметр типа класса или перечисления (переданный по значению или по ссылке).
Предопределенные приоритеты операторов (см. раздел 4.13) изменить нельзя.
Независимо от типа класса и реализации оператора в инструкции x == y + z; всегда сначала выполняется operator+, а затем operator==; однако помощью скобок порядок можно изменить.
Предопределенная арность операторов также должна быть сохранена. К примеру, унарный логический оператор НЕ нельзя определить как бинарный оператор для двух объектов класса String. Следующая реализация некорректна и приведет к ошибке компиляции:
}
Для встроенных типов четыре предопределенных оператора ("+", "-", "*" и "&") используются либо как унарные, либо как бинарные. В любом из этих качеств они могут быть перегружены.
Для всех перегруженных операторов, за исключением operator(), недопустимы аргументы по умолчанию.
15.1.3.
Разработка перегруженных операторов
Операторы присваивания, взятия адреса и оператор “запятая” имеют предопределенный смысл, если операндами являются объекты типа класса. Но их можно и перегружать.
Семантика всех остальных операторов, когда они применяются к таким операндам, должна быть явно задана разработчиком. Выбор предоставляемых операторов зависит от ожидаемого использования класса.
// ошибка: нельзя переопределить встроенный оператор сложения int
// некорректно: ! - это унарный оператор bool operator!( const String &s1, const String &s2 )
{ return ( strcmp( s1.c_str(), s2.c_str() ) != 0 );
С++ для начинающих
715Начинать следует с определения его открытого интерфейса. Набор открытых функций- членов формируется с учетом операций, которые класс должен предоставлять пользователям. Затем принимается решение, какие функции стоит реализовать в виде перегруженных операторов.
После определения открытого интерфейса класса проверьте, есть ли логическое соответствие между операциями и операторами:
•
isEmpty()
становится оператором “ЛОГИЧЕСКОЕ НЕ”, operator!().
•
isEqual()
становится оператором равенства, operator==().
•
copy()
становится оператором присваивания, operator=().
У каждого оператора есть некоторая естественная семантика. Так, бинарный + всегда ассоциируется со сложением, а его отображение на аналогичную операцию с классом может оказаться удобной и краткой нотацией. Например, для матричного типа сложение двух матриц является вполне подходящим расширением бинарного плюса.
Примером неправильного использования перегрузки операторов является определение operator+()
как операции вычитания, что бессмысленно: не согласующаяся с интуицией семантика опасна.
Такой оператор одинаково хорошо поддерживает несколько различных интерпретаций.
Безупречно четкое и обоснованное объяснение того, что делает operator+(), вряд ли устроит пользователей класса String, полагающих, что он служит для конкатенации строк. Если семантика перегруженного оператора неочевидна, то лучше его не предоставлять.
Эквивалентность семантики составного оператора и соответствующей последовательности простых операторов для встроенных типов (например, эквивалентность оператора +, за которым следует =, и составного оператора +=) должна быть явно поддержана и для класса. Предположим, для String определены как operator+()
, так и operator=() для поддержки операций конкатенации и почленного копирования: s1 = s1 + s2; // s1 == "C++"
Но этого
недостаточно для поддержки составного оператора присваивания s1 += s2;
Его
следует определить явно, так, чтобы он поддерживал ожидаемую семантику.
Упражнение 15.1
Почему при выполнении следующего сравнения
не вызывается перегруженный оператор operator==(const String&, const String&)
:
"cobble" == "stone"
Упражнение 15.2
String s1( "C" );
String s2( "++" );
С++ для начинающих
716
Напишите перегруженные операторы неравенства, которые могут быть использованы в таких сравнениях:
C- строка != String
Объясните, почему вы решили реализовать один или несколько операторов.
Упражнение 15.3
Выявите те функции-члены класса Screen, реализованного в главе 13 (разделы 13.3, 13.4 и 13.6), которые можно перегружать.
Упражнение 15.4
Объясните, почему перегруженные операторы ввода и вывода, определенные для класса
String из раздела 3.15, объявлены как глобальные функции, а не функции-члены.
Упражнение 15.5
Реализуйте перегруженные операторы ввода и вывода для класса Screen из главы 13.
15.2.
Друзья
Рассмотрим еще раз перегруженные операторы равенства для класса String, определенные в области видимости пространства имен. Оператор равенства для двух объектов String выглядит следующим образом:
}
Сравните это определение с определением того же оператора как функции-члена:
}
Нам пришлось модифицировать способ обращения к закрытым членам класса String.
Поскольку новый оператор равенства – это глобальная функция, а не функция-член, у него нет доступа к закрытым членам класса String. Для получения размера объекта
String и лежащей в его основе C-строки символов используются функции-члены size() и c_str().
String != 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;
С++ для начинающих
717Альтернативной реализацией является объявление глобальных операторов равенства
друзьями класса String. Если функция или оператор объявлены таким образом, им предоставляется доступ к неоткрытым членам.
Объявление друга (оно начинается с ключевого слова friend) встречается только внутри определения класса. Поскольку друзья не являются членами класса, объявляющего дружественные отношения, то безразлично, в какой из секций – public, private или protected
– они объявлены. В примере ниже мы решили поместить все подобные объявления сразу после заголовка класса:
};
В этих трех строчках три перегруженных оператора сравнения, принадлежащие глобальной области видимости, объявляются друзьями класса String, а следовательно, в их определениях можно напрямую обращаться к закрытым членам данного класса:
}
// и т.д.
Можно возразить, что в данном случае прямой доступ к членам _size и _string необязателен, так как встроенные функции c_str() и size() столь же
эффективны и при этом сохраняют инкапсуляцию, а значит, нет особой нужды объявлять операторы равенства для класса String его друзьями.
Как узнать, следует ли сделать оператор, не являющийся членом класса, его другом или воспользоваться функциями доступа? В общем случае разработчик должен сократить до минимума число объявленных функций и операторов, которые имеют доступ к внутреннему представлению класса. Если имеются функции доступа, обеспечивающие равную эффективность, то предпочтение следует отдать им, тем самым изолируя операторы в пространстве имен от изменений представления класса, как это делается и для других функций. Если же разработчик класса не предоставляет функций доступа для некоторых членов, а объявленный в пространстве имен оператор должен к этим членам обращаться, то использование механизма друзей становится неизбежным. 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 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;
}
С++ для начинающих
718Наиболее часто такой механизм применяется для того, чтобы разрешить перегруженным операторам, не являющимся членами класса, доступ к его закрытым членам. Если бы не необходимость
обеспечить симметрию левого и правого операндов, то перегруженный оператор был бы функцией-членом с полными правами доступа.
Хотя объявления друзей обычно употребляются по отношению к операторам, бывают случаи, когда функцию в пространстве имен, функцию-член другого класса или даже целый класс приходится объявлять таким образом. Если один класс объявлен другом второго, то все функции-члены первого класса получают доступ к неоткрытым членам другого. Рассмотрим это на примере функций, не являющихся операторами.
Класс должен объявлять другом каждую из множества перегруженных функций, которой он хочет дать неограниченные права доступа:
};
Если функция манипулирует объектами двух разных классов и ей нужен доступ к их неоткрытым членам, то такую функцию можно либо объявить другом обоих классов, либо сделать членом одного и другом второго.
Объявление функции другом двух классов должно выглядеть так:
};
Если же мы решили сделать функцию членом одного класса и другом второго, то объявления будут построены следующим образом: 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 & );
// ...
С++ для начинающих
719Screen& Screen::copy( Window & ) { /* ... */ }
Функция-член одного класса не может быть объявлена другом второго, пока компилятор не увидел определения ее собственного класса. Это не всегда возможно. Предположим, что Screen должен объявить некоторые функции-члены Window своими друзьями, а
Window
– объявить таким же образом некоторые функции-члена Screen. В таком случае весь класс Window объявляется другом Screen:
};
К закрытым членам класса Screen теперь можно обращаться из любой функции-члена
Window
Упражнение 15.6
Реализуйте операторы ввода и вывода, определенные для класса Screen в упражнении
15.5, в виде друзей и модифицируйте их определения так, чтобы они напрямую обращались к закрытым членам. Какая реализация лучше? Объясните почему.
15.3.
Оператор =
Присваивание одного объекта другому объекту того же класса выполняется с помощью копирующего оператора присваивания. (Этот специальный случай был рассмотрен в разделе 14.7.)
Для класса могут быть определены и другие операторы присваивания. Если объектам
класса надо присваивать значения типа, отличного от этого класса, то разрешается определить такие операторы, принимающие подобные параметры. Например, чтобы поддержать присваивание C-строки объекту String: car = "Studebaker"; class Window; class Screen {
// copy() - член класса Screen
Screen& copy( Window & );
// ...
}; class Window {
// Screen::copy() - друг класса Window friend Screen& Screen::copy( Window & );
// ...
}; class Window; class Screen { friend class Window;
// ...
String car ("Volks");
С++ для начинающих
720
мы предоставляем оператор, принимающий параметр типа const char*. Эта операция уже была объявлена в нашем классе:
};
Такой оператор реализуется следующим образом. Если объекту String присваивается нулевой указатель, он становится “пустым”. В противном случае ему присваивается копия C-строки:
}
_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*. class String { public:
// оператор присваивания для char*
String& operator=( const char * );
// ... private: int _size; char *string;
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;
С++ для начинающих
721
Хранение в _string прямой ссылки на C-строку, адресуемую sobj, порождает и иные сложности. Мы не знаем, на что именно указывает sobj. Это может быть массив символов, который модифицируется способом, неизвестным объекту String. Например: ia[3] = 'g'; // а вот это нам не нужно:
// модифицируется и ia, и trap._string
Если trap._string напрямую ссылался на ia, то объект trap демонстрировал бы своеобразное поведение: его значение может изменяться без вызова функций-членов класса String. Поэтому мы полагаем, что выделение области памяти для хранения копии значения C-строки менее опасно.
Обратите внимание, что в операторе присваивания используется delete. Член _string содержит ссылку на массив символов, расположенный в хипе. Чтобы предотвратить утечку, память, выделенная под старую строку, освобождается с помощью delete до выделения памяти под новую. Поскольку _string адресует массив символов, следует использовать версию delete для массивов (см. раздел 8.4).
И последнее замечание об операторе присваивания. Тип возвращаемого им значения – это ссылка на класс String. Почему именно ссылка? Дело в том, что для встроенных типов операторы присваивания можно сцеплять: iobj = jobj = 63;
Они ассоциируются справа налево, т.е. в предыдущем примере присваивания выполняются так: iobj = (jobj = 63);
Это удобно и при работе с объектами класса String: поддерживается, к примеру, следующая конструкция: verb = noun = "count";
При первом присваивании из этой цепочки вызывается определенный ранее оператор для const char*
. Тип полученного результата должен быть таким, чтобы его можно было использовать как аргумент для копирующего оператора присваивания класса String.
Поэтому, хотя параметр данного оператора имеет тип const char *, возвращается все же ссылка на String.
Операторы присваивания бывают перегруженными. Например, в нашем классе String есть такой набор: char ia[] = { 'd', 'a', 'n', 'c', 'e', 'r' };
String trap = ia; // trap._string ссылается на ia
// сцепление операторов присваивания int iobj, jobj;
String ver, noun;