|
Программирование >> Синтаксис инициирования исключений
String s1 = Hello ; String s2 = Goodbye ; String s3 = s1 + s2; Любой оператор может быть перегружен в форме функции класса. Если оператор может перегружаться как внешней функцией, так и функцией класса, какую из двух форм выбрать? Ответ: используйте перегрузку в форме функции класса, если только у вас не найдется веских причин для перегрузки внешней функцией. Из этих причин наиболее распространены следующие: 1. Первый аргумент относится к базовому типу (например, int или double). 2. Тип первого аргумента определен в коммерческой библиотеке, которую нежелательно модифицировать. Компилятор ищет перегрузку в форме функций класса, просматривая левую часть бинарных операторов и единственный аргумент унарных. Если ваш тип указывается справа и вы хотите воспользоваться перегрузкой в форме функции класса, вам не повезло. Самый распространенный пример перегрузки в форме внешней функции - оператор << в библиотеке ostream. ostream& operator (ostream& os, const String& s) os << str.s; Предполагается, что данная функция является другом return os; Перегрузка должна осуществляться в форме внешней функции, поскольку ваш тип, String, находится справа - если, конечно, вы не хотите залезть в готовые заголовки iostream.h и включить в класс ostream перегрузку в форме функции класса для своего класса String. Наверное, все-таки не хотите. Примечание: предыдущий пример может не работать в вашем компиляторе, если функции strlen и strcat, как это часто бывает, по недосмотру разработчиков получают char* вместо const char*. Вы можете решить, что игра не стоит свеч, и объявить функцию неконстантной, но это выглядит слишком жестоко. Лучше избавиться от константности посредством преобразования типов, если вы абсолютно уверены, что библиотечная функция не модифицирует свои аргументы, и готовы смириться с предупреждениями компилятора. String& String::operator+(const String& s1) const char* s2 = new char[str1en((char*)s1.s) + strlen(s) + 1]; strcat(s2, (char*)s1.s, s); String newStr(s2); delete s2; return newStr; Видите, что происходит, если кто-то забывает о константности? Операторы преобразования Оператор преобразования - особый случай. Если конструктор представляет собой отображение аргументов на домен вашего класса, то оператор преобразования делает прямо противоположное: по экземпляру вашего класса он создает другой тип данных. class String { private: char* s; public: operator 1ong(); Использует atol для преобразования к типу long String::operator 1ong() Вероятно, здесь следует проверить, что строка представляет собой число, принадлежащее к диапазону длинных целых return atol1(s); String s( 1234 ); long x = s; Вызывается функция operator 1ong() Операторы преобразования должны быть функциями класса. Как видно из показанного фрагмента, операторы преобразования хороши тем, что компилятор обычно сам может определить, когда они должны вызываться. Если ему понадобится длинное целое, он ищет оператор 1ong(). Если ему понадобится объект Foo, он ищет в классе Foo либо конструктор с аргументом String, либо operator Foo(). Возникает интересный вопрос: если оператор преобразования делает фактически то же, что и конструктор, почему бы не обойтись чем-нибудь одним? Преимущество конструкторов состоит в том, что они обеспечивают инкапсуляцию результирующего класса. Чтобы сконструировать объект другого класса, оператор преобразования должен очень много знать о нем. Вот почему для перехода от одного типа к другому обычно используются конструкторы. А если осуществляется переход к базовому типу вроде int? Вряд ли вы будете требовать, чтобы компилятор создавал для int новые конструкторы, которые знают о существовании ваших пользовательских типов. А если и будете, то не рискнете признаться в этом вслух. Только оператор преобразования может автоматически перейти к базовому типу. Даже если результирующий тип не является базовым, он может быть частью готовой библиотеки, которую нежелательно модифицировать. И снова оператор преобразования справляется с задачей. Операторы преобразования можно объявлять для любых типов данных. Они вызываются без аргументов, а тип возвращаемого значения определяется по имени оператора. Операторы преобразования, как и все остальные операторы, бывают константными или неконстантными. Часто определяется как константная, так и неконстантная версии одного оператора. Как правило, константная версия работает более эффективно, поскольку неконстантная версия обычно выполняет копирование данных. class String { private: char* s; public: operator const char*() const { return s; } operator char*(): String::operator char*() char* newStr = new char[str1en(s) + 1]; strcpy(newStr, s); return newStr; Клиентский код, использующий неконстантную версию, должен взять на себя ответственность за удаление дубликата. Порядок поиска и неоднозначность Если во время обработки программы компилятор C++ находит оператор, он выполняет описанные ниже действия в указанном порядке, чтобы решить, как его компилировать. Описание относится к бинарному оператору, но та же самая логика используется и для унарных операторов: 1 . Если оба аргумента относятся к базовым типам, используется встроенный оператор. 2. Если слева указан пользовательский тип, компилятор ищет перепруженный оператор в форме функции данного класса, подходящей для всей сигнатуры подвыражения оператора. Если такой оператор будет найден, он используется при компиляции. 3. Если все остальные варианты испробованы, компилятор ищет перегрузку в форме внешней функции. Неоднозначность может возникнуть лишь в том случае, если она присутствует в левостороннем классе или в глобальном пространстве, и никогда - из-за совпадения перегруженных операторов в форме функции класса и внешней функции. При наличии неоднозначности сообщение об ошибке выдается лишь после вашей попытки реально использовать оператор. Так компилятор внушает ложное чувство безопасности и ждет, пока вы утратите бдительность, чтобы огреть вас дубиной по голове. Виртуальные операторы Операторы классов можно объявлять виртуальными, как и все остальные функции классов. Компилятор динамически обрабатывает перегруженный левосторонний оператор, как и любую другую функцию класса. Такая возможность особенно полезна в ситуациях, когда вы пытаетесь создать семейство классов, но открываете внешнему миру лишь их общий базовый класс. С точки зрения синтаксиса все выглядит просто, но логика программы может стать довольно запутанной. Виртуальные операторы являются одной из важнейших тем части 3, поэтому сейчас мы не будем вдаваться в подробности. Оператор -> Оператор -> занимает особое место среди операторов. Для начала рассмотрим его базовый синтаксис. class Pointer { private: Foo* f; public: Pointer(Foo* foo) : f(foo) {} Foo* operator->() const { return f; } Pointer p(new Foo); p->MemberOfFoo(); В приведенном фрагменте р используется для косвенного вызова функции класса Foo. Компилятор интерпретирует любой указатель на структуру или класс (*-переменная) как базовый тип >, а для всех базовых типов указателей существует встроенный оператор ->. Встретив ->, компилятор смотрит на левостороннее выражение; если оно представляет собой указатель па структуру или указатель на класс, для обращения к членам используется встроенный оператор ->. Если левостороннее выражение представляет собой пользовательский тип, этот тип должен перегрузить оператор ->. Перегруженный вариант должен возвращать либо указатель на структуру/класс, либо какой-нибудь другой пользовательский тип, который также перегружает оператор ->. Если возвращаемое значение относится к пользовательскому типу, компилятор заменяет левостороннее выражение возвращаемым значением оператора -> (в нашем примере Foo*) и продолжает свои попытки до тех пор, пока не доберется до встроенного указателя. Таким образом, следующее двухшаговое косвенное обращение также будет работать. class Pointer2 { private: Pointer p; public: Pointer(Foo* foo) : p(foo) {} Pointer operator->() const { return p; } Pointer2 p(new Foo); p->MemberOfFoo();
|
© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки. |