C++ — C++. Переопределение операторов.


Содержание

C++ — C++. Переопределение операторов.

Собственно, всё написано у Шилдта, это, скорее, шпаргалка.

Совсем нельзя перегружать: . :: .* ?

Нельзя перегружать не членами класса: = () [] ->

Не надо перегружать, если не какой-то особый случай: new delete -> ->* ,

Если функция перегрузки оператора — член класса, то при перегрузке бинарного оператора неявно (через указатель this ) передаётся первый операнд (слева от знака операции), а параметром функции явно передаётся второй операнд (справа от знак операции).

Для унарного оператора операнд передаётся неявно, а явный параметр не нужен.

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

Если функция перегрузки оператора — не член, но друг ( friend ) класса, то при перегрузке бинарного оператора явно передаются оба операнда (стоящие слева и справа от знака операции), а при перегрузке унарного оператора явно передаётся ссылка на его единственный операнд. При этом префиксная форма функции-друга принимает один параметр-ссылку (который и является операндом), а постфиксная форма — два параметра (вторым является целочисленное значение, которое не используется).

Перегруженные операторы отношения или логических операцией могут возвращать просто значение типа int или bool .

Вообще говоря, перегрузка бинарной операции, присваивания и унарных постфиксных операторов должны возвращать новый объект класса, созданный внутри операторной функции. Перегрузка унарного префиксного оператора может возвращать как объект класса значение *this . Можно возвращать *this и для перегруженных операций вроде *= , += и т.д. Делать операторные функции с типом возвращаемого значения void не нужно — над объектами не будет работать, например, присваивание по цепочке вида a=b=c .

Для класса с динамически выделяемой под некоторые свойства оперативной памятью нужен явный конструктор копирования, принятое по умолчанию побитовое копирование объектов не подходит (при этом могут быть скопированы адреса динамических объектов, но не их значения). Конструктор копирования должен иметь единственный параметр — const-ссылку на объект класса (копируемый объект; объект, куда копируем, доступен через this ).

Корректно переопределённый оператор [] (который может использоваться и слева, и справа от знака «=») должен возвращать ссылку на объект того типа, из которого состоит индексируемая последовательность. В принципе, [] перегружается как бинарный оператор.

Оператор вызова функций () создаёт не новый способ вызова функций, а операторную функцию, которой можно передать произвольное число параметров.

Пример на большую часть сказанного, простейший класс Class состоит только из числа n :

03.04.2014, 19:40; рейтинг: 12143

Перегрузка операций

Кроме перегрузки функций С++ позволяет организовать перегрузку операций. Механизм перегрузки операций позволяет обеспечить более традиционную и удобную запись действий над объектами. Для перегрузки встроенных операторов используется ключевое слово operator .

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


где @ — знак перегружаемой операции (-, +, * и т.д.),
тип — тип возвращаемого значения.

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

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

Любой перегруженный оператор можно вызвать с использованием функциональной формы записи (функции-операции):

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

Имеется два способа описания функции, соответствующей переопределяемой операции:

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

Можно описывать функции, определяющие значения следующих операций:

! = += -= *= /= %= ^= &= |= > >>= = && || ++ — [] () new delete

Операции, не допускающие перегрузки:

  • . прямой выбор члена объекта класса;
  • .* обращение к члену через указатель на него;
  • ? : условная тернарная операция;
  • :: операция указания области видимости (разрешение контекста);
  • sizeof операция вычисления размера в байтах;
  • # препроцессорная операция.

Правила перегрузки операций

  • Язык C++ не допускает определения для операций нового лексического символа, кроме уже определенных в языке. Например, нельзя определить в качестве знака операции @ .
  • Не допускается перегрузка операций для встроенных типов данных. Нельзя, например, переопределить операцию сложения целых чисел:

Перегрузка унарной операции

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

Если унарная операция перегружается дружественной функцией, то она должна иметь один аргумент – объект, для которого она выполняется.
Таким образом, для любой унарной операции @ aa@ или @aa может интерпретироваться или как aa.operator@() , или как operator @(aa) .

Если определена и та, и другая, то и aa@ и @aa являются ошибками.

Функция- член класса Дружественная функция

Перегрузка бинарной операции

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

Если бинарная операция перегружается дружественной функцией, то в списке параметров она должна иметь оба аргумента:

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

то необходимо переопределить операцию сложения дважды:

Перегрузка операций индексирования и вызова функции

Переопределение операции () позволяет использовать синтаксис вызова функции применительно к объекту класса (имя объекта с круглыми скобками). Количество операндов в скобках может быть любым. Переопределение операции [] позволяет использовать синтаксис доступа к элементам массива (имя объекта с квадратными скобками).

Перегрузка операции присваивания

Любой конструктор вызывается явно либо неявно в том случае, если необходимо создать новый объект какого-либо класса.
Рассматривая на примере создание нового объекта и его инициализацию, мы использовали конструктор копии. До проведения инициализации существовал только один объект. Второй был создан и инициализирован в результате работы конструктора копии. Однако если бы существовало несколько объектов одного типа, и была бы необходимость в присваивании значений одного объекта элементам другого, то никакой из конструкторов не вызывается, так как объект уже был создан. При выполнении операции присваивания по умолчанию копирование значений происходит «поверхностно», но такое копирование не всегда допустимо. Например, недопустимо копирование массивов или указателей.
Если необходимо осуществить присваивание, но поведение операции присваивания по умолчанию не устраивает, то операция присваивания может быть перегружена.
Дополним приведенный выше пример реализации класса String перегруженным оператором присваивания:

Перегрузка операций выделения памяти

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

где void * – указатель на область памяти, выделяемую под объект,
size – размер объекта в байтах,
size_t – тип размерности области памяти, int или long int .
Переопределение этих операций позволяет написать собственное распределение памяти для объектов класса.

переопределение чисто виртуальных операторов

Я пытаюсь создать интерфейс счетчика, который заставляет все производные классы реализовать этот интерфейс:

Однако, только включение этого определения класса приводит к ошибке ниже.

К сожалению, сообщение inc не может быть определено как ссылка.

Любые идеи, как решить эту проблему курица / яйцо?


Решение

Не повезло тебе. Вы хотите вернуть значение с динамический тип объекта вызывается функция. Вы не можете: (не указатель) значения в C ++ не могут быть ковариантными возвращаемыми типами, потому что возвращаемое значение в C ++ не может иметь динамический тип, отличный от его статического типа.

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

То, что вы могли бы сделать, это расширить дизайн. Напишите класс, который содержит (умный) указатель на экземпляр CounterInterface , Этот тип может быть возвращен по значению и, следовательно, может реализовывать интерфейс, который вы хотите. Это можно сделать, вызвав чисто виртуальную функцию CounterInterface *clone() (или же unique_ptr clone() ), который выделяет и возвращает новый экземпляр конкретного класса, который реализует интерфейс. Для операторов, которые работают как виртуальные функции, вы можете оставить их включенными CounterInterface и ваш класс-оболочка может вызывать через него, или вы можете переименовать их в виртуальном интерфейсе:

виртуальный operator== это отдельная проблема, которую я оставлю другим вопросам на сайте.

Btw, CounterInterface нужен виртуальный деструктор.

Другие решения

Вы не можете создать экземпляр класса с чисто виртуальными функциями-членами. Поскольку вы не можете создать их, вы также не можете вернуть их по значению, как вы это делаете в

Прежде всего вы должны заменить

Во-вторых, то же самое, что сказал «Олаф Дитче», было быстрее, чем я ��

Определение и перегрузка операторов класса в C++

В C++ можно определять пользовательские операторы для собственных типов данных. Оператор определяется, как обычная функция-член класса, только после определения возвращаемого типа ставится ключевое слово operator .

Пример определения оператора сложения:

Оператор может быть унарным или бинарным. Унарный оператор не принимает никаких аргументов. Например, оператор отрицания — «!». Бинарный оператор принимает дополнительный параметр. Например, в случае со сложением, принимается второе слагаемое.

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

Для операции деления, мы также сделали проверку деления на ноль.

Пример использования класса simple_fraction :

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

Задание: усовершенствуйте класс simple_fraction . Перегрузите операторы сложения, вычитания, умножения и деления так, чтобы можно было производить операции над двумя простыми дробями и получать новую простую дробь. Реализуйте приведение двух дробей к общему знаменателю.

15. Перегруженные операторы и определенные пользователем преобразования

В главе 15 мы рассмотрим два вида специальных функций: перегруженные операторы и определенные пользователем преобразования. Они дают возможность употреблять объекты классов в выражениях так же интуитивно, как и объекты встроенных типов. В этой главе мы сначала изложим общие концепции проектирования перегруженных операторов. Затем представим понятие друзей класса со специальными правами доступа и обсудим, зачем они применяются, обратив особое внимание на то, как реализуются некоторые перегруженные операторы: присваивание, взятие индекса, вызов, стрелка для доступа к члену класса, инкремент и декремент, а также специализированные для класса операторы new и delete. Другая категория специальных функций, которая рассматривается в этой главе, – это функции преобразования членов (конвертеры), составляющие набор стандартных преобразований для типа класса. Они неявно применяются компилятором, когда объекты классов используются в качестве фактических аргументов функции или операндов встроенных или перегруженных операторов. Завершается глава развернутым изложением правил разрешения перегрузки функций с учетом передачи объектов в качестве аргументов, функций-членов класса и перегруженных операторов.

15.1. Перегрузка операторов

В предыдущих главах мы уже показывали, что перегрузка операторов позволяет программисту вводить собственные версии предопределенных операторов (см. главу 4) для операндов типа классов. Например, в классе String из раздела 3.15 задано много перегруженных операторов. Ниже приведено его определение:

В классе String есть три набора перегруженных операторов. Первый – это набор операторов присваивания:

Сначала идет копирующий оператор присваивания. (Подробно они обсуждались в разделе 14.7.) Следующий оператор поддерживает присваивание C-строки символов объекту типа String:

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

Во втором наборе есть всего один оператор – взятия индекса:

Он позволяет программе индексировать объекты класса String точно так же, как массивы объектов встроенного типа:

(Детально этот оператор описывается в разделе 15.4.)

В третьем наборе определены перегруженные операторы равенства для объектов класса String. Программа может проверить равенство двух таких объектов или объекта и C-строки:

Перегруженные операторы позволяют использовать объекты типа класса с операторами, определенными в главе 4, и манипулировать ими так же интуитивно, как объектами встроенных типов. Например, желая определить операцию конкатенации двух объектов класса String, мы могли бы реализовать ее в виде функции-члена concat(). Но почему concat(), а не, скажем, append()? Выбранное нами имя логично и легко запоминается, но пользователь все же может забыть, как мы назвали функцию. Зачастую имя проще запомнить, если определить перегруженный оператор. К примеру, вместо concat() мы назвали бы новую операцию operator+=(). Такой оператор используется следующим образом:

Перегруженный оператор объявляется в теле класса точно так же, как обычная функция-член, только его имя состоит из ключевого слова operator, за которым следует один из множества предопределенных в языке C++ операторов (см. табл. 15.1). Так можно объявить operator+=() в классе String:

и определить его следующим образом:

15.1.1. Члены и не члены класса

Рассмотрим операторы равенства в нашем классе String более внимательно. Первый оператор позволяет устанавливать равенство двух объектов, а второй – объекта и C-строки:

При первом использовании оператора равенства в main() вызывается перегруженный operator==(const char *) класса String. Однако на второй инструкции if компилятор выдает сообщение об ошибке. В чем дело?

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

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

Причина в его неэффективности. Перегруженные операторы не требуют, чтобы оба операнда имели один и тот же тип. К примеру, в классе Text определяются следующие операторы равенства:

и выражение в main() можно переписать так:

Следовательно, чтобы найти подходящий для сравнения оператор равенства, компилятору придется просмотреть все определения классов в поисках конструктора, способного привести левый операнд к некоторому типу класса. Затем для каждого из таких типов нужно проверить все ассоциированные с ним перегруженные операторы равенства, чтобы понять, может ли хоть один из них выполнить сравнение. А после этого компилятор должен решить, какая из найденных комбинаций конструктора и оператора равенства (если таковые нашлись) лучше всего соответствует операнду в правой части! Если потребовать от компилятора выполнения всех этих действий, то время трансляции программ C++ резко возрастет. Вместо этого компилятор просматривает только перегруженные операторы, определенные как члены класса левого операнда (и его базовых классов, как мы покажем в главе 19).

Разрешается, однако, определять перегруженные операторы, не являющиеся членами класса. При анализе строки в main(), вызвавшей ошибку компиляции, подобные операторы принимались во внимание. Таким образом, сравнение, в котором C-строка стоит в левой части, можно сделать корректным, если заменить операторы равенства, являющиеся членами класса String, на операторы равенства, объявленные в области видимости пространства имен:

Обратите внимание, что эти глобальные перегруженные операторы имеют на один параметр больше, чем операторы-члены. Если оператор является членом класса, то первым параметром неявно передается указатель this. То есть для операторов-членов выражение

переписывается компилятором в виде:

и на левый операнд flower в определении перегруженного оператора-члена можно сослаться с помощью this. (Указатель this введен в разделе 13.4.) В случае глобального перегруженного оператора параметр, представляющий левый операнд, должен быть задан явно.

Непонятно, какой оператор вызывается для второго случая использования оператора равенства:

Мы ведь не определили такой перегруженный оператор:

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

и вызывает для выполнения сравнения следующий перегруженный оператор:

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

Но тогда зачем мы предоставили второй перегруженный оператор:

Преобразование типа из C-строки в класс String может быть применено и к правому операнду. Функция main() будет компилироваться без ошибок, если просто определить в пространстве имен перегруженный оператор, принимающий два операнда String:

Предоставлять ли только этот оператор или еще два:

зависит от того, насколько велики затраты на преобразование из C-строки в String во время выполнения, то есть от “стоимости” дополнительных вызовов конструктора в программах, пользующихся нашим классом String. Если оператор равенства будет часто использоваться для сравнения C-строк и объектов , то лучше предоставить все три варианта. (Мы вернемся к вопросу эффективности в разделе, посвященном друзьям.

Подробнее о приведении к типу класса с помощью конструкторов мы расскажем в разделе 15.9; в разделе 15.10 речь пойдет о разрешении перегрузки функций с помощью описанных преобразований, а в разделе 15.12 – о разрешении перегрузки операторов.)

Итак, на основе чего принимается решение, делать ли оператор членом класса или членом пространства имен? В некоторых случаях у программиста просто нет выбора:

  • если перегруженный оператор является членом класса, то он вызывается лишь при условии, что левым операндом служит член этого класса. Если же левый операнд имеет другой тип, оператор обязан быть членом пространства имен;
  • язык требует, чтобы операторы присваивания («=»), взятия индекса («[]»), вызова («()») и доступа к членам по стрелке («->») были определены как члены класса. В противном случае выдается сообщение об ошибке компиляции:

(Подробнее оператор присваивания рассматривается в разделе 15.3, взятия индекса – в разделе 15.4, вызова – в разделе 15.5, а оператор доступа к члену по стрелке – в разделе 15.6.)

В остальных случаях решение принимает проектировщик класса. Симметричные операторы, например оператор равенства, лучше определять в пространстве имен, если членом класса может быть любой операнд (как в String).

Прежде чем закончить этот подраздел, определим операторы равенства для класса String в пространстве имен:


15.1.2. Имена перегруженных операторов

Перегружать можно только предопределенные операторы языка C++ (см. табл. 15.1).

Таблица 15.1. Перегружаемые операторы

Проектировщик класса не вправе объявить перегруженным оператор с другим именем. Так, при попытке объявить оператор ** для возведения в степень компилятор выдаст сообщение об ошибке.

Следующие четыре оператора языка C++ не могут быть перегружены:

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

Нельзя также определять дополнительные операторы для встроенных типов данных, например добавить к множеству встроенных операций operator+ для сложения двух массивов.

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

Предопределенные приоритеты операторов (см. раздел 4.13) изменить нельзя. Независимо от типа класса и реализации оператора в инструкции

всегда сначала выполняется operator+, а затем operator==; однако помощью скобок порядок можно изменить.

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

Для встроенных типов четыре предопределенных оператора («+», «-«, «*» и «&») используются либо как унарные, либо как бинарные. В любом из этих качеств они могут быть перегружены.

Для всех перегруженных операторов, за исключением operator(), недопустимы аргументы по умолчанию.

15.1.3. Разработка перегруженных операторов

Операторы присваивания, взятия адреса и оператор “запятая” имеют предопределенный смысл, если операндами являются объекты типа класса. Но их можно и перегружать. Семантика всех остальных операторов, когда они применяются к таким операндам, должна быть явно задана разработчиком. Выбор предоставляемых операторов зависит от ожидаемого использования класса.

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

После определения открытого интерфейса класса проверьте, есть ли логическое соответствие между операциями и операторами:

  • isEmpty() становится оператором “ЛОГИЧЕСКОЕ НЕ”, operator!().
  • isEqual() становится оператором равенства, operator==().
  • copy() становится оператором присваивания, operator=().

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

Примером неправильного использования перегрузки операторов является определение operator+() как операции вычитания, что бессмысленно: не согласующаяся с интуицией семантика опасна.

Такой оператор одинаково хорошо поддерживает несколько различных интерпретаций. Безупречно четкое и обоснованное объяснение того, что делает operator+(), вряд ли устроит пользователей класса String, полагающих, что он служит для конкатенации строк. Если семантика перегруженного оператора неочевидна, то лучше его не предоставлять.

Эквивалентность семантики составного оператора и соответствующей последовательности простых операторов для встроенных типов (например, эквивалентность оператора +, за которым следует =, и составного оператора +=) должна быть явно поддержана и для класса. Предположим, для String определены как operator+(), так и operator=() для поддержки операций конкатенации и почленного копирования:

Но этого недостаточно для поддержки составного оператора присваивания

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

Почему при выполнении следующего сравнения не вызывается перегруженный оператор operator==(const String&, const String&):

Напишите перегруженные операторы неравенства, которые могут быть использованы в таких сравнениях:

Объясните, почему вы решили реализовать один или несколько операторов.

Выявите те функции-члены класса Screen, реализованного в главе 13 (разделы 13.3, 13.4 и 13.6), которые можно перегружать.

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

Реализуйте перегруженные операторы ввода и вывода для класса Screen из главы 13.

15.2. Друзья

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

15.3. Оператор =

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

15.4. Оператор взятия индекса

Оператор взятия индекса operator[]() можно определять для классов, представляющих абстракцию контейнера, из которого извлекаются отдельные элементы. Примерами таких контейнеров могут служить наш класс String, класс IntArray, представленный в главе 2, или шаблон класса vector, определенный в стандартной библиотеке C++. Оператор взятия индекса обязан быть функцией-членом класса.

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

Оператор взятия индекса может появляться как слева, так и справа от оператора присваивания. Чтобы быть в левой части, он должен возвращать l-значение индексируемого элемента. Для этого мы возвращаем ссылку:

В следующем фрагменте нулевому элементу массива color присваивается символ ‘V’:

Обратите внимание, что в определении оператора проверяется выход индекса за границы массива. Для этого используется библиотечная C-функция assert(). Можно также возбудить исключение, показывающее, что значение elem меньше 0 или больше длины C-строки, на которую ссылается _string. (Возбуждение и обработка исключений обсуждались в главе 11.)

15.5. Оператор вызова функции

Оператор вызова функции может быть перегружен для объектов типа класса. (Мы уже видели, как он используется, при рассмотрении объектов-функций в разделе 12.3.) Если определен класс, представляющий некоторую операцию, то для ее вызова перегружается соответствующий оператор. Например, для взятия абсолютного значения числа типа int можно определить класс absInt:

Перегруженный оператор operator() должен быть объявлен как функция-член с произвольным числом параметров. Параметры и возвращаемое значение могут иметь любые типы, допустимые для функций (см. разделы 7.2, 7.3 и 7.4). operator() вызывается путем применения списка аргументов к объекту того класса, в котором он определен. Мы рассмотрим, как он используется в одном из обобщенных алгоритмов, описанных в главе 12. В следующем примере обобщенный алгоритм transform() вызывается для применения определенной в absInt операции к каждому элементу вектора ivec, т.е. для замены элемента его абсолютным значением.

Первый и второй аргументы transform() ограничивают диапазон элементов, к которым применяется операция absInt. Третий указывает на начало вектора, где будет сохранен результат применения операции.

Четвертый аргумент – это временный объект класса absInt, создаваемый с помощью конструктора по умолчанию. Конкретизация обобщенного алгоритма transform(), вызываемого из main(), могла бы выглядеть так:

func – это объект класса, который предоставляет операцию absInt, заменяющую число типа int его абсолютным значением. Он используется для вызова перегруженного оператора operator() класса absInt. Этому оператору передается аргумент *iter, указывающий на тот элемент вектора, для которого мы хотим получить абсолютное значение.

15.6. Оператор «стрелка»

Оператор «стрелка», разрешающий доступ к членам, может перегружаться для объектов класса. Он должен быть определен как функция-член и обеспечивать семантику указателя. Чаще всего этот оператор используется в классах, которые предоставляют «интеллектуальный указатель» (smart pointer), ведущий себя аналогично встроенным, но поддерживают и некоторую дополнительную функциональность.

Допустим, мы хотим определить тип класса для представления указателя на объект Screen (см. главу 13):

Определение ScreenPtr должно быть таким, чтобы объект этого класса гарантировано указывал на объект Screen: в отличие от встроенного указателя, он не может быть нулевым. Тогда приложение сможет пользоваться объектами типа ScreenPtr, не проверяя, указывают ли они на какой-нибудь объект Screen. Для этого нужно определить класс ScreenPtr с конструктором, но без конструктора по умолчанию (детально конструкторы рассматривались в разделе 14.2):

В любом определении объекта класса ScreenPtr должен присутствовать инициализатор – объект класса Screen, на который будет ссылаться объект ScreenPtr:

Чтобы класс ScreenPtr вел себя как встроенный указатель, необходимо определить некоторые перегруженные операторы – разыменования (*) и “стрелку” для доступа к членам: Оператор доступа к членам унарный, поэтому параметры ему не передаются. При использовании в составе выражения его результат зависит только от типа левого операнда. Например, в инструкции point->action(); исследуется тип point. Если это указатель на некоторый тип класса, то применяется семантика встроенного оператора доступа к члену. Если же это объект или ссылка на объект, то проверяется, есть ли в этом классе перегруженный оператор доступа. Когда перегруженный оператор «стрелка» определен, он вызывается для объекта point, иначе инструкция неверна, поскольку для обращения к членам самого объекта (в том числе по ссылке) следует использовать оператор «точка». Перегруженный оператор «стрелка» должен возвращать либо указатель на тип класса, либо объект класса, в котором он определен. Если возвращается указатель, то к нему применяется семантика встроенного оператора «стрелка». В противном случае процесс продолжается рекурсивно, пока не будет получен указатель или определена ошибка. Например, так можно воспользоваться объектом ps класса ScreenPtr для доступа к членам Screen: ps->move( 2, 3 ); Поскольку слева от оператора «стрелка» находится объект типа ScreenPtr, то употребляется перегруженный оператор этого класса, который возвращает указатель на объект Screen. Затем к полученному значению применяется встроенный оператор «стрелка» для вызова функции-члена move(). Ниже приводится небольшая программа для тестирования класса ScreenPtr. Объект типа ScreenPtr используется точно так же, как любой объект типа Screen*: #include #include #include «Screen.h» void printScreen( const ScreenPtr &ps ) < cout height() w >height(); ++ix ) < for ( int iy = 1; iy width(); ++iy ) cout height(); ++ix ) for ( int iy = 1; iy width(); ++iy ) < ps->move( ix, iy ); ps->set( init[ initpos++ ] ); > // Вывести содержимое экрана printScreen( ps ); return 0; >

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

15.7. Операторы инкремента и декремента

Продолжая развивать реализацию класса ScreenPtr, введенного в предыдущем разделе, рассмотрим еще два оператора, которые поддерживаются для встроенных указателей и которые желательно иметь и для нашего интеллектуального указателя: инкремент (++) и декремент (—). Чтобы использовать класс ScreenPtr для ссылки на элементы массива объектов Screen, туда придется добавить несколько дополнительных членов.

Сначала мы определим новый член size, который содержит либо нуль (это говорит о том, что объект ScreenPtr указывает на единственный объект), либо размер массива, адресуемого объектом ScreenPtr. Нам также понадобится член offset, запоминающий смещение от начала данного массива:

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

С помощью этого аргумента задается размер массива. Чтобы сохранить прежнюю функциональность, предусмотрим для него значение по умолчанию, равное нулю. Таким образом, если второй аргумент конструктора опущен, то член size окажется равен 0 и, следовательно, такой объект будет указывать на единственный объект Screen. Объекты нового класса ScreenPtr можно определять следующим образом:

Теперь мы готовы определить в ScreenPtr перегруженные операторы инкремента и декремента. Однако они бывают двух видов: префиксные и постфиксные. К счастью, можно определить оба варианта. Для префиксного оператора объявление не содержит ничего неожиданного:

Такие операторы определяются как унарные операторные функции. Использовать префиксный оператор инкремента можно, к примеру, следующим образом: const int arrSize = 10; Screen *parray = new Screen[ arrSize ]; ScreenPtr parr( *parray, arrSize ); for ( int ix = 0; ix () (см. раздел 15.6) так, чтобы указатель ни при каком условии не адресовал элемент перед началом или за концом массива. Совет: в этих операторах следует воспользоваться новыми членами size и offset.

15.8. Операторы new и delete

По умолчанию выделение объекта класса из хипа и освобождение занятой им памяти выполняются с помощью глобальных операторов new() и delete(), определенных в стандартной библиотеке C++. (Мы рассматривали эти операторы в разделе 8.4.) Но класс может реализовать и собственную стратегию управления памятью, предоставив одноименные операторы-члены. Если они определены в классе, то вызываются вместо глобальных операторов с целью выделения и освобождения памяти для объектов этого класса.

Определим операторы new() и delete() в нашем классе Screen.

Оператор-член new() должен возвращать значение типа void* и принимать в качестве первого параметра значение типа size_t, где size_t – это typedef, определенный в системном заголовочном файле . Вот его объявление:

Когда для создания объекта типа класса используется new(), компилятор проверяет, определен ли в этом классе такой оператор. Если да, то для выделения памяти под объект вызывается именно он, в противном случае – глобальный оператор new(). Например, следующая инструкция

создает объект Screen в хипе, а поскольку в этом классе есть оператор new(), то вызывается он. Параметр size_t оператора автоматически инициализируется значением, равным размеру Screen в байтах.

Добавление оператора new() в класс или его удаление оттуда не отражаются на пользовательском коде. Вызов new выглядит одинаково как для глобального оператора, так и для оператора-члена. Если бы в классе Screen не было собственного new(), то обращение осталось бы правильным, только вместо оператора-члена вызывался бы глобальный оператор.

С помощью оператора разрешения глобальной области видимости можно вызвать глобальный new(), даже если в классе Screen определена собственная версия:

Оператор delete(), являющийся членом класса, должен иметь тип void, а в качестве первого параметра принимать void*. Вот как выглядит его объявление для Screen:

Когда операндом delete служит указатель на объект типа класса, компилятор проверяет, определен ли в этом классе оператор delete(). Если да, то для освобождения памяти вызывается именно он, в противном случае – глобальная версия оператора. Следующая инструкция

освобождает память, занятую объектом класса Screen, на который указывает ps. Поскольку в Screen есть оператор-член delete(), то применяется именно он. Параметр оператора типа void* автоматически инициализируется значением ps. Добавление delete() в класс или его удаление оттуда никак не сказываются на пользовательском коде. Вызов delete выглядит одинаково как для глобального оператора, так и для оператора-члена. Если бы в классе Screen не было собственного оператора delete(), то обращение осталось бы правильным, только вместо оператора-члена вызывался бы глобальный оператор.

С помощью оператора разрешения глобальной области видимости можно вызвать глобальный delete(), даже если в Screen определена собственная версия:

В общем случае используемый оператор delete() должен соответствовать тому оператору new(), с помощью которого была выделена память. Например, если ps указывает на область памяти, выделенную глобальным new(), то для ее освобождения следует использовать глобальный же delete().

Оператор delete(), определенный для типа класса, может содержать два параметра вместо одного. Первый параметр по-прежнему должен иметь тип void*, а второй – предопределенный тип size_t (не забудьте включить заголовочный файл ):

Если второй параметр есть, компилятор автоматически инициализирует его значением, равным размеру адресованного первым параметром объекта в байтах. (Этот параметр важен в иерархии классов, когда оператор delete() может наследоваться производным классом. Подробнее наследование обсуждается в главе 17.)

Рассмотрим реализацию операторов new() и delete() в классе Screen более детально. В основе нашей стратегии распределения памяти будет лежать связанный список объектов Screen, на начало которого указывает член freeStore. При каждом обращении к оператору-члену new() возвращается следующий объект из списка. При вызове delete() объект возвращается в список. Если при создании нового объекта список, адресованный freeStore, пуст, то вызывается глобальный оператор new(), чтобы получить блок памяти, достаточный для хранения screenChunk объектов класса Screen.

Как screenChunk, так и freeStore представляют интерес только для Screen, поэтому мы сделаем их закрытыми членами. Кроме того, для всех создаваемых объектов нашего класса значения этих членов должны быть одинаковыми, а следовательно, нужно объявить их статическими. Чтобы поддержать структуру связанного списка объектов Screen, нам понадобится третий член next:

Вот одна из возможных реализаций оператора new() для класса Screen:

Оператор new() можно объявить в классе и без соответствующего delete(). В таком случае объекты освобождаются с помощью одноименного глобального оператора. Разрешается также объявить и оператор delete() без new(): объекты будут создаваться с помощью одноименного глобального оператора. Однако обычно эти операторы реализуются одновременно, как в примере выше, поскольку разработчику класса, как правило, нужны оба.

Они являются статическими членами класса, даже если программист явно не объявит их таковыми, и подчиняются обычным ограничениями для подобных функций-членов: им не передается указатель this, а следовательно, напрямую они могут получить доступ только к статическим членам. (См. обсуждение статических функций-членов в разделе 13.5.) Причина, по которой эти операторы делаются статическими, заключается в том, что они вызываются либо перед конструированием объекта класса (new()), либо после его уничтожения (delete()).

Выделение памяти с помощью оператора new(), например:

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

Иными словами, сначала вызывается определенный в классе оператор new(), чтобы выделить память для объекта, а затем этот объект инициализируется конструктором. Если new() неудачно завершает работу, то возбуждается исключение типа bad_alloc и конструктор не вызывается.

Освобождение памяти с помощью оператора delete(), например:

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

Таким образом, при уничтожении объекта сначала вызывается деструктор класса, а затем определенный в классе оператор delete() для освобождения памяти. Если значение ptr равно 0, то ни деструктор, ни delete() не вызываются.

15.8.1. Операторы new[ ] и delete [ ]



Оператор new(), определенный в предыдущем подразделе, вызывается только при выделении памяти для единичного объекта. Так, в данной инструкции вызывается new() класса Screen:

тогда как ниже вызывается глобальный оператор new[]() для выделения из хипа памяти под массив объектов типа Screen:

В классе можно объявить также операторы new[]() и delete[]() для работы с массивами.

Оператор-член new[]() должен возвращать значение типа void* и принимать в качестве первого параметра значение типа size_t. Вот его объявление для Screen:

Когда с помощью new создается массив объектов типа класса, компилятор проверяет, определен ли в классе оператор new[](). Если да, то для выделения памяти под массив вызывается именно он, в противном случае – глобальный new[](). В следующей инструкции в хипе создается массив из десяти объектов Screen:

В этом классе есть оператор new[](), поэтому он и вызывается для выделения памяти. Его параметр size_t автоматически инициализируется значением, равным объему памяти в байтах, необходимому для размещения десяти объектов Screen.

Даже если в классе имеется оператор-член new[](), программист может вызвать для создания массива глобальный new[](), воспользовавшись оператором разрешения глобальной области видимости:

Оператор delete(), являющийся членом класса, должен иметь тип void, а в качестве первого параметра принимать void*. Вот как выглядит его объявление для Screen:

Чтобы удалить массив объектов класса, delete должен вызываться следующим образом:

Когда операндом delete является указатель на объект типа класса, компилятор проверяет, определен ли в этом классе оператор delete[](). Если да, то для освобождения памяти вызывается именно он, в противном случае – его глобальная версия. Параметр типа void* автоматически инициализируется значением адреса начала области памяти, в которой размещен массив.

Даже если в классе имеется оператор-член delete[](), программист может вызвать глобальный delete[](), воспользовавшись оператором разрешения глобальной области видимости:

Добавление операторов new[]() или delete[]() в класс или удаление их оттуда не отражаются на пользовательском коде: вызовы как глобальных операторов, так и операторов-членов выглядят одинаково.

При создании массива сначала вызывается new[]() для выделения необходимой памяти, а затем каждый элемент инициализируется с помощью конструктора по умолчанию. Если у класса есть хотя бы один конструктор, но нет конструктора по умолчанию, то вызов оператора new[]() считается ошибкой. Не существует синтаксической конструкции для задания инициализаторов элементов массива или аргументов конструктора класса при создании массива подобным образом.

При уничтожении массива сначала вызывается деструктор класса для уничтожения элементов, а затем оператор delete[]() – для освобождения всей памяти. При этом важно использовать правильный синтаксис. Если в инструкции

ps указывает на массив объектов класса, то отсутствие квадратных скобок приведет к вызову деструктора лишь для первого элемента, хотя память будет освобождена полностью.

У оператора-члена delete[]() может быть не один, а два параметра, при этом второй должен иметь тип size_t:

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

15.8.2. Оператор размещения new() и оператор delete()

Оператор-член new() может быть перегружен при условии, что все объявления имеют разные списки параметров. Первый параметр должен иметь тип size_t:

Остальные параметры инициализируются аргументами размещения, заданными при вызове new:

Та часть выражения, которая находится после ключевого слова new и заключена в круглые скобки, представляет аргументы размещения. В примере выше вызывается оператор new(), принимающий два параметра. Первый автоматически инициализируется значением, равным размеру класса Screen в байтах, а второй – значением аргумента размещения start.

Можно также перегружать и оператор-член delete(). Однако такой оператор никогда не вызывается из выражения delete. Перегруженный delete() неявно вызывается компилятором, если конструктор, вызванный при выполнении оператора new (это не опечатка, мы действительно имеем в виду new), возбуждает исключение. Рассмотрим использование delete() более внимательно.

Последовательность действий при вычислении выражения

такова:

  1. Вызывается определенный в классе оператор new(size_t, Screen*).
  2. Вызывается конструктор по умолчанию класса Screen для инициализации созданного объекта.

Переменная ps инициализируется адресом нового объекта Screen.

Предположим, что оператор класса new(size_t, Screen*) выделяет память с помощью глобального new(). Как разработчик может гарантировать, что память будет освобождена, если вызванный на шаге 2 конструктор возбуждает исключение? Чтобы защитить пользовательский код от утечки памяти, следует предоставить перегруженный оператор delete(), который вызывается только в подобной ситуации.

Если в классе имеется перегруженный оператор с параметрами, типы которых соответствуют типам параметров new(), то компилятор автоматически вызывает его для освобождения памяти. Предположим, есть следующее выражение с оператором размещения new:

Если конструктор по умолчанию класса Screen возбуждает исключение, то компилятор ищет delete() в области видимости Screen. Чтобы такой оператор был найден, типы его параметров должны соответствовать типам параметров вызванного new(). Поскольку первый параметр new() всегда имеет тип size_t, а оператора delete() – void*, то первые параметры при сравнении не учитываются. Компилятор ищет в классе Screen оператор delete() следующего вида:

Если такой оператор будет найден, то он вызывается для освобождения памяти в случае, когда new() возбуждает исключение. (Иначе – не вызывается.)

Разработчик класса принимает решение, предоставлять ли delete(), соответствующий некоторому new(), в зависимости от того, выделяет ли этот оператор new() память самостоятельно или пользуется уже выделенной. В первом случае delete() необходимо включить для освобождения памяти, если конструктор возбудит исключение; иначе в нем нет необходимости.

Можно также перегрузить оператор размещения new[]() и оператор delete[]() для массивов:

Оператор new[]() используется в случае, когда в выражении, содержащем new для распределения массива, заданы соответствующие аргументы размещения:

Если при работе оператора new конструктор возбуждает исключение, то автоматически вызывается соответствующий delete[]().

Объясните, какие из приведенных инициализаций ошибочны:

Что происходит в следующих выражениях, содержащих new и delete?

Измените эти выражения так, чтобы вызывались глобальные операторы new() и delete().

Объясните, зачем разработчик класса должен предоставлять оператор delete().

15.9. Определенные пользователем преобразования

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

Операнды ch и sh расширяются до типа int. При выполнении операции складываются два значения типа int. Расширение типа неявно выполняется компилятором и для пользователя прозрачно.

В этом разделе мы рассмотрим, как разработчик может определить собственные преобразования для объектов типа класса. Такие определенные пользователем преобразования также автоматически вызываются компилятором по мере необходимости. Чтобы показать, зачем они нужны, обратимся снова к классу SmallInt, введенному в разделе 10.9.

Напомним, что SmallInt позволяет определять объекты, способные хранить значения из того же диапазона, что unsigned char, т.е. от 0 до 255, и перехватывает ошибки выхода за его границы. Во всех остальных отношениях этот класс ведет себя точно так же, как unsigned char.

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

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

разрешается в два шага:

  1. Константа 3.14159 типа double преобразуется в целое число 3.
  2. Вызывается operator+(const SmallInt &,int), который возвращает значение 6.

Если мы хотим поддержать битовые и логические операции, а также операции сравнения и составные операторы присваивания, то сколько же необходимо перегрузить операторов? Сразу и не сосчитаешь. Значительно удобнее автоматически преобразовать объект класса SmallInt в объект типа int.

В языке C++ имеется механизм, позволяющий в любом классе задать набор преобразований, применимых к его объектам. Для SmallInt мы определим приведение объекта к типу int. Вот его реализация:

Оператор int() – это конвертер, реализующий определенное пользователем преобразование, в данном случае приведение типа класса к заданному типу int. Определение конвертера описывает, что означает преобразование и какие действия компилятор должен выполнить для его применения. Для объекта SmallInt смысл преобразования в int заключается в том, чтобы вернуть число типа int, хранящееся в члене value.

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

разрешается двумя шагами:

  1. Вызывается конвертер класса SmallInt, который возвращает целое число 3.
  2. Целое число 3 расширяется до 3.0 и складывается с константой двойной точности 3.14159, что дает 6.14159.

Такое поведение больше соответствует поведению операндов встроенных типов по сравнению с определенными ранее перегруженными операторами. Когда значение типа int складывается со значением типа double, то выполняется сложение двух чисел типа double (поскольку тип int расширяется до double) и результатом будет число того же типа.

В этой программе иллюстрируется применение класса SmallInt:

Откомпилированная программа выдает следующие результаты:

Введите SmallInt, пожалуйста: 127

Прочитано значение 127

Введите SmallInt, пожалуйста (ctrl-d для выхода): 126

Оно меньше, чем 127


Введите SmallInt, пожалуйста (ctrl-d для выхода): 128

Оно больше, чем 127

Введите SmallInt, пожалуйста (ctrl-d для выхода): 256

*** Ошибка диапазона SmallInt: 256 ***

В реализацию класса SmallInt добавили поддержку новой функциональности:

Ниже приведены определения функций-членов, находящиеся вне тела класса:

15.9.1. Конвертеры

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

Имя, находящееся за ключевым словом, не обязательно должно быть именем одного из встроенных типов. В показанном ниже классе Token определено несколько конвертеров. В одном из них для задания имени типа используется typedef tName, а в другом – тип класса SmallInt.

Обратите внимание, что определения конвертеров в типы SmallInt и int одинаковы. Конвертер Token::operator int() возвращает значение члена val. Поскольку val имеет тип SmallInt, то неявно применяется SmallInt::operator int() для преобразования val в тип int. Сам Token::operator int() неявно употребляется компилятором для преобразования объекта типа Token в значение типа int. Например, этот конвертер используется для неявного приведения фактических аргументов t1 и t2 типа Token к типу int формального параметра функции print():

После компиляции и запуска программа выведет такие строки:

Общий вид конвертера следующий:

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

Конвертер вызывается в результате явного преобразования типов. Если преобразуемое значение имеет тип класса, у которого есть конвертер, и в операции приведения указан тип этого конвертера, то он и вызывается:

У конвертера Token::operator tName() может быть нежелательный побочный эффект. Попытка прямого обращения к закрытому члену Token::name помечается компилятором как ошибка:

Однако наш конвертер, разрешая пользователям непосредственно изменять Token::name, делает как раз то, от чего мы хотели защититься. Скорее всего, это не годится. Вот, например, как могла бы произойти такая модификация:

Мы намереваемся разрешить доступ к преобразованному объекту класса Token только для чтения. Следовательно, конвертер должен возвращать тип const char*:

Другое решение – заменить в определении Token тип char* на тип string из стандартной библиотеки C++:

Семантика конвертера Token::operator string() состоит в возврате копии значения (а не указателя на значение) строки, представляющей имя лексемы. Это предотвращает случайную модификацию закрытого члена name класса Token.

Должен ли целевой тип точно соответствовать типу конвертера? Например, будет ли в следующем коде вызван конвертер int(), определенный в классе Token?

Если целевой тип (в данном случае double) не точно соответствует типу конвертера (в нашем случае int), то конвертер все равно будет вызван при условии, что существует последовательность стандартных преобразований, приводящая к целевому типу из типа конвертера. (Эти последовательности описаны в разделе 9.3.) При обращении к функции calc() вызывается Token::operator int() для преобразования tok из типа Token в тип int. Затем для приведения результата от типа int к типу double применяется стандартное преобразование.

Вслед за определенным пользователем преобразованием допускаются только стандартные. Если для достижения целевого типа необходимо еще одно пользовательское преобразование, то компилятор не применяет никаких преобразований. Предположим, что в классе Token не определен operator int(), тогда следующий вызов будет ошибочным:

Если конвертер Token::operator int() не определен, то приведение tok к типу int потребовало бы вызова двух определенных пользователем конвертеров. Сначала фактический аргумент tok надо было бы преобразовать из типа Token в тип SmallInt с помощью конвертера

а затем результат привести к типу int – тоже с помощью пользовательского конвертера

Вызов calc(tok) помечается компилятором как ошибка, так как не существует неявного преобразования из типа Token в тип int.

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

Какое значение должен вернуть конвертер int() класса Date? Сколь бы основательными ни были причины для того или иного решения, читатель останется в недоумении относительно того, как пользоваться объектами класса Date, поскольку между ними и целыми числами нет явного логического соответствия. В таких случаях лучше вообще не определять конвертер.

15.9.2. Конструктор как конвертер

Набор конструкторов класса, принимающих единственный параметр, например, SmallInt(int) класса SmallInt, определяет множество неявных преобразований в значения типа SmallInt. Так, конструктор SmallInt(int) преобразует значения типа int в значения типа SmallInt. При вызове calc(i) число i преобразуется в значение типа SmallInt с помощью конструктора SmallInt(int), вызванного компилятором для создания временного объекта нужного типа. Затем копия этого объекта передается в calc(), как если бы вызов функции был записан в форме:

Фигурные скобки в этом примере обозначают время жизни данного объекта: он уничтожается при выходе из функции.

Типом параметра конструктора может быть тип некоторого класса:

В таком случае значение типа SmallInt можно использовать всюду, где допустимо значение типа Number:

Если конструктор используется для выполнения неявного преобразования, то должен ли тип его параметра точно соответствовать типу подлежащего преобразованию значения? Например, будет ли в следующем коде вызван SmallInt(int), определенный в классе SmallInt, для приведения dobj к типу SmallInt?

Если необходимо, к фактическому аргументу применяется последовательность стандартных преобразований до того, как вызвать конструктор, выполняющий определенное пользователем преобразование. При обращении к функции calc()употребляется стандартное преобразование dobj из типа double в тип int. Затем уже для приведения результата к типу SmallInt вызывается SmallInt(int).

Компилятор неявно использует конструктор с единственным параметром для преобразования его типа в тип класса, к которому принадлежит конструктор. Однако иногда удобнее, чтобы конструктор Number(const SmallInt&) можно было вызывать только для инициализации объекта типа Number значением типа SmallInt, но ни в коем случае не для выполнения неявных преобразований. Чтобы избежать такого употребления конструктора, объявим его явным (explicit):

Компилятор никогда не применяет явные конструкторы для выполнения неявных преобразований типов:

Однако такой конструктор все же можно использовать для преобразования типов, если оно запрошено явно в форме оператора приведения типа:

15.10. Выбор преобразования A

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

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

Последовательность стандартных преобразований ->

Определенное пользователем преобразование ->

Последовательность стандартных преобразований

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

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

В классе разрешается определять много конвертеров. Например, в нашем классе Number их два: operator int() и operator float(), причем оба способны преобразовать объект типа Number в значение типа float. Естественно, можно воспользоваться конвертером Token::operator float() для прямой трансформации. Но и Token::operator int() тоже подходит, так как результат его применения имеет тип int и, следовательно, может быть преобразован в тип float с помощью стандартного преобразования. Является ли трансформация неоднозначной, если имеется несколько таких последовательностей? Или какую-то из них можно предпочесть остальным?

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

  1. operator float() -> точное соответствие
  2. operator int() -> стандартное преобразование

Как было сказано в разделе 9.3, точное соответствие лучше стандартного преобразования. Поэтому первая последовательность лучше второй, а значит, выбирается конвертер Token::operator float().

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

Здесь в классе SmallInt определено два конструктора – SmallInt(int) и SmallInt(double), которые можно использовать для изменения значения типа double в объект типа SmallInt: SmallInt(double) трансформирует double в SmallInt напрямую, а SmallInt(int) работает с результатом стандартного преобразования double в int. Таким образом, имеются две последовательности определенных пользователем преобразований:

  1. точное соответствие -> SmallInt( double )
  2. стандартное преобразование -> SmallInt( int )

Поскольку точное соответствие лучше стандартного преобразования, то выбирается конструктор SmallInt(double).

Не всегда удается решить, какая последовательность лучше. Может случиться, что все они одинаково хороши, и тогда мы говорим, что преобразование неоднозначно. В таком случае компилятор не применяет никаких неявных трансформаций. Например, если в классе Number есть два конвертера:

то невозможно неявно преобразовать объект типа Number в тип long. Следующая инструкция вызывает ошибку компиляции, так как выбор последовательности определенных пользователем преобразований неоднозначен:

Для трансформации num в значение типа long применимы две такие последовательности:

  1. operator float() -> стандартное преобразование
  2. operator int() -> стандартное преобразование

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

С помощью явного приведения типов программист способен задать нужное изменение:


Вследствие такого указания выбирается конвертер Token::operator int(), за которым следует стандартное преобразование в long.

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

Аргумент num преобразуется в тип SmallInt двумя разными способами: с помощью конструктора SmallInt::SmallInt(const Number&) либо с помощью конвертера Number::operator SmallInt(). Поскольку оба изменения одинаково хороши, вызов считается ошибкой.

Для разрешения неоднозначности программист может явно вызвать конвертер класса Number:

Однако для разрешения неоднозначности не следует использовать явное приведение типов, поскольку при отборе преобразований, подходящих для приведения типов, рассматриваются как конвертер, так и конструктор:

Как видите, наличие большого числа подобных конвертеров и конструкторов небезопасно, поэтому их. следует применять с осторожностью. Ограничить использование конструкторов при выполнении неявных преобразований (а значит, уменьшить вероятность неожиданных эффектов) можно путем объявления их явными.

15.10.1. Еще раз о разрешении перегрузки функций

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

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

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

15.10.2. Функции-кандидаты

Функцией-кандидатом называется функция с тем же именем, что и вызванная. Предположим, что имеется такой вызов:

Функция-кандидат должна иметь имя add. Какие из объявлений add() принимаются во внимание? Те, которые видимы в точке вызова.

Например, обе функции add(), объявленные в глобальной области видимости, будут кандидатами для следующего вызова:

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

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

Аргумент si имеет тип SmallInt, т.е. тип класса, объявленного в пространстве имен NS. Поэтому к множеству функций-кандидатов добавляется add(const String &, const String &), объявленная в этом пространстве имен;

  • если фактический аргумент – это объект типа класса, указатель или ссылка на класс либо указатель на член класса и у этого класса есть друзья, имеющие то же имя, что и вызванная функция, то они добавляются к множеству функций-кандидатов:
  • Аргумент функции si имеет тип SmallInt. Функция-друг класса SmallInt add(SmallInt, int) – член пространства имен NS, хотя непосредственно в этом пространстве она не объявлена. При обычном поиске в NS функция-друг не будет найдена. Однако при вызове add() с аргументом типа класса SmallInt принимаются во внимание и добавляются к множеству кандидатов также друзья этого класса, объявленные в списке его членов.

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

    Рассмотрим следующий пример:

    Здесь кандидатами являются:

    • глобальные функции:
    • функция из пространства имен:
    • функция-друг:

    При разрешении перегрузки выбирается функция-друг класса SmallInt NS::add( SmallInt, int ) как наилучшая из устоявших: оба фактических аргумента точно соответствуют заданным формальным параметрам.

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

    15.10.3. Функции-кандидаты для вызова функции в области видимости класса

    Когда вызов функции вида

    встречается в области видимости класса (например, внутри функции-члена), то первая часть множества кандидатов, описанного в предыдущем подразделе (т.е. множество, включающее объявления функций, видимых в точке вызова), может содержать не только функции-члены класса. Для построения такого множества применяется разрешение имени. (Эта тема детально разбиралась в разделах 13.9 – 13.12.)

    Как отмечалось в разделе 13.11, квалификаторы NS::myClass:: просматриваются в обратном порядке: сначала поиск видимого объявления для имени, использованного в определении функции-члена mf(), ведется в классе myClass, а затем – в пространстве имен NS. Рассмотрим первый вызов:

    При разрешении имени h() в определении функции-члена mf() сначала просматриваются функции-члены myClass. Поскольку функции-члена с таким именем в области видимости этого класса нет, то далее поиск идет в пространстве имен NS. Функции h()нет и там, поэтому мы переходим в глобальную область видимости. Результат – глобальная функция h(char), единственная функция-кандидат, видимая в точке вызова.

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

    Сначала поиск ведется в области видимости класса myClass. При этом найдены две функции-члена k(int) и k(char*). Поскольку множество кандидатов содержит лишь функции, объявленные в той области, где разрешение успешно завершилось, то пространство имен NS не просматривается и функция k(double) в данное множество не включается.

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

    15.10.4. Ранжирование последовательностей определенных пользователем преобразований

    Фактический аргумент функции может быть неявно приведен к типу формального параметра с помощью последовательности определенных пользователем преобразований. Как это влияет на разрешение перегрузки? Например, если имеется следующий вызов calc(), то какая функция будет вызвана?

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

    Последовательность стандартных преобразований всегда лучше последовательности определенных пользователем преобразований. Так, при вызове calc() из примера выше обе функции calc() являются устоявшими. calc(double) устояла потому, что существует стандартное преобразование типа фактического аргумента int в тип формального параметра double, а calc(SmallInt) – потому, что имеется определенное пользователем преобразование из int в SmallInt, которое использует конструктор SmallInt(int). Следовательно, наилучшей из устоявших функций будет calc(double).

    А как сравниваются две последовательности определенных пользователем преобразований? Если в них используются разные конвертеры или разные конструкторы, то обе такие последовательности считаются одинаково хорошими:

    Устоявшими окажутся и calc(int), и calc(SmallInt); первая – поскольку конвертер Number::operator int()преобразует фактический аргумент типа Number в формальный параметр типа int, а вторая потому, что конвертер Number::operator SmallInt() преобразует фактический аргумент типа Number в формальный параметр типа SmallInt. Так как последовательности определенных пользователем преобразований всегда имеют одинаковый ранг, то компилятор не может выбрать, какая из них лучше. Таким образом, этот вызов функции неоднозначен и приводит к ошибке компиляции.

    Есть способ разрешить неоднозначность, указав преобразование явно:

    Явное приведение типов заставляет компилятор преобразовать аргумент num в тип int с помощью конвертера Number::operator int(). Фактический аргумент тогда будет иметь тип int, что точно соответствует функции calc(int), которая и выбирается в качестве наилучшей.

    Допустим, в классе Number не определен конвертер Number::operator int(). Будет ли тогда вызов

    по-прежнему неоднозначен? Вспомните, что в SmallInt также есть конвертер, способный преобразовать значение типа SmallInt в int.

    Можно предположить, что функция calc() вызывается, если сначала преобразовать фактический аргумент num из типа Number в тип SmallInt с помощью конвертера Number::operator SmallInt(), а затем результат привести к типу int с помощью SmallInt::operator SmallInt(). Однако это не так. Напомним, что в последовательность определенных пользователем преобразований может входит несколько стандартных преобразований, но лишь одно пользовательское. Если конвертер Number::operator int() не определен, то функция calc(int) не считается устоявшей, поскольку не существует неявного преобразования из типа фактического аргумента num в тип формального параметра int.

    Поэтому в отсутствие конвертера Number::operator int() единственной устоявшей функцией будет calc(SmallInt), в пользу которой и разрешается вызов.

    Если в двух последовательностях определенных пользователем преобразований употребляется один и тот же конвертер, то выбор наилучшей зависит от последовательности стандартных преобразований, выполняемых после его вызова:

    Как manip(int), так и manip(char) являются устоявшими функциями; первая – потому, что конвертер SmallInt::operator int() преобразует фактический аргумент типа SmallInt в тип формального параметра int, а вторая – потому, что тот же конвертер преобразует SmallInt в int, после чего результат с помощью стандартного преобразования приводится к типу char. Последовательности определенных пользователем преобразований выглядят так:

    Поскольку в обеих последовательностях используется один и тот же конвертер, то для определения лучшей из них анализируется ранг последовательности стандартных преобразований. Так как точное соответствие лучше преобразования, то наилучшей из устоявших будет функция manip(int).

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

    И compute(float), и compute(int) – устоявшие функции. compute(float) – потому, что конвертер SmallInt::operator float()преобразует аргумент типа SmallInt в тип параметра float, а compute(char) – потому, что SmallInt::operator int() преобразует аргумент типа SmallInt в тип int, после чего результат стандартно приводится к типу char. Таким образом, имеются последовательности:

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

    В классах стандартной библиотеки C++ нет определений конвертеров, а большинство конструкторов, принимающих один параметр, объявлены явными. Однако определено множество перегруженных операторов. Как вы думаете, почему при проектировании было принято такое решение?

    Почему перегруженный оператор ввода для класса SmallInt, определенный в начале этого раздела, реализован не так:

    Приведите возможные последовательности определенных пользователем преобразований для следующих инициализаций. Каким будет результат каждой инициализации?

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

    Какая из функций calc() выбирается в качестве наилучшей из устоявших в данном случае? Покажите последовательности преобразований, необходимых для вызова каждой функции, и объясните, почему одна из них лучше другой.

    15.11. Разрешение перегрузки и функции-члены A

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

    1. Отбор функций-кандидатов.
    2. Отбор устоявших функций.
    3. Выбор наилучшей из устоявших функции.

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


    15.11.1. Объявления перегруженных функций-членов

    Функции-члены класса можно перегружать:

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

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

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

    Множество перегруженных функций-членов может содержать как статические, так и нестатические функции:

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

    15.11.2. Функции-кандидаты

    Рассмотрим два вида вызовов функции-члена:

    где mc – выражение типа myClass, а pmc – выражение типа «указатель на тип myClass». Множество кандидатов для обоих вызовов составлено из функций, найденных в области видимости класса myClass при поиске объявления mf().

    Аналогично для вызова функции вида

    множество кандидатов также состоит из функций, найденных в области видимости класса myClass при поиске объявления mf(). Например:

    Кандидатами для вызова функции в main() являются все три функции-члена mf(), объявленные в myClass:

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

    15.11.3. Устоявшие функции

    Устоявшей называется функция из множества кандидатов, которая может быть вызвана с данными фактическими аргументами. Чтобы она устояла, должны существовать неявные преобразования между типами фактических аргументов и формальных параметров. Например: ); static void mf( int* ); // . >; int main() < myClass mc; int iobj; mc.mf( iobj ); // какая именно функция-член mf()? Неоднозначно >

    В этом фрагменте для вызова mf() из main() есть две устоявшие функции:

    • mf(double) устояла потому, что у нее только один параметр и существует стандартное преобразование аргумента iobj типа int в параметр типа double;
    • mf(char,char) устояла потому, что для второго параметра имеется значение по умолчанию и существует стандартное преобразование аргумента iobj типа int в тип char первого формального параметра.

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

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

    Независимо от вида вызова функции, в множество устоявших могут быть включены как статические, так и нестатические члены:

    Здесь функция-член mf() вызывается с указанием имени класса и оператора разрешения области видимости myClass::mf(). Однако не задан ни объект (с оператором «точка»), ни указатель на объект (с оператором «стрелка»). Несмотря на это, нестатическая функция-член mf(char) все же включается в множество устоявших наряду со статическим членом mf(int).

    Затем процесс разрешения перегрузки продолжается: на основе ранжирования преобразований типов, примененных к фактическим аргументам, чтобы выбрать наилучшую из устоявших функций. Аргумент cobj типа char точно соответствует формальному параметру mf(char) и может быть расширен до типа формального параметра mf(int). Поскольку ранг точного соответствия выше, то выбирается функция mf(char).

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

    Еще одна особенность функций-членов, которую надо принимать во внимание при формировании множества устоявших функций, – это наличие спецификаторов const или volatile у нестатических членов. (Они рассматривались в разделе 13.3.) Как они влияют на процесс разрешения перегрузки? Пусть в классе myClass есть следующие функции-члены:

    Тогда и статическая функция-член mf(int*), и константная функция mf(int), и неконстантная функция mf(double) включаются в множество кандидатов для показанного ниже вызова. Но какие из них войдут в множество устоявших?

    Исследуя преобразования, которые надо применить к фактическим аргументам, мы обнаруживаем, что устояли функции mf(double) и mf(int). Тип double фактического аргумента dobj точно соответствует типу формального параметра mf(double) и может быть приведен к типу параметра mf(int) с помощью стандартного преобразования.

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

    mc – это константный объект, для которого можно вызывать только нестатические константные функции-члены. Следовательно, неконстантная функция-член mf(double) исключается из множества устоявших, и остается в нем единственная функция mf(int), которая и вызывается.

    А если константный объект использован для вызова статической функции-члена? Ведь для такой функции нельзя задавать спецификатор const или volatile, так можно ли ее вызывать через константный объект?

    Статические функции-члены являются общими для всех объектов одного класса. Напрямую они могут обращаться только к статическим членам класса. Так, нестатические члены константного объекта mc недоступны статической mf(int). По этой причине разрешается вызывать статическую функцию-член для константного объекта с помощью операторов «точка» или «стрелка».

    Таким образом, статические функции-члены не исключаются из множества устоявших и при наличии спецификаторов const или volatile у объекта, для которого они вызваны. Статические функции-члены рассматриваются как соответствующие любому объекту или указателю на объект своего класса.

    В примере выше mc – константный объект, поэтому функция-член mf(char) исключается из множества устоявших. Но функция-член mf(int) в нем остается, так как является статической. Поскольку это единственная устоявшая функция, она и оказывается наилучшей.

    15.12. Разрешение перегрузки и операторы A

    В классах могут быть объявлены перегруженные операторы и конвертеры. Предположим, при инициализации встретился оператор сложения:

    Как компилятор решает, что следует сделать: вызвать перегруженный оператор для класса SomeClass или конвертировать операнд sc во встроенный тип, а затем уже воспользоваться встроенным оператором?

    Ответ зависит от множества перегруженных операторов и конвертеров, определенных в SomeClass. При выборе оператора для выполнения сложения применяется процесс разрешения перегрузки функции. В данном разделе мы расскажем, как этот процесс позволяет выбрать нужный оператор, когда операндами являются объекты типа класса.

    При разрешении перегрузки используется все та же процедура из трех шагов, представленная в разделе 9.2:

    • Отбор функций-кандидатов.
    • Отбор устоявших функций.
    • Выбор наилучшей из устоявших функции.

    Рассмотрим эти шаги более детально.

    Разрешение перегрузки функции не применяется, если все операнды имеют встроенные типы. В таком случае гарантированно употребляется встроенный оператор. (Использование операторов с операндами встроенных типов описано в главе 4.) Например:

    Поскольку операнды i1 и i2 имеют тип int, а не тип класса, то при сложении используется встроенный оператор +. Перегруженный operator+(const SmallInt &, const SmallInt &) игнорируется, хотя операнды можно привести к типу SmallInt с помощью определенного пользователем преобразования в виде конструктора SmallInt(int). Описанный ниже процесс разрешения перегрузки в таких ситуациях не применяется.

    Кроме того, разрешение перегрузки для операторов употребляется только в случае использования операторного синтаксиса:

    Если вместо этого использовать синтаксис вызова функции:

    int res = operator+( si, iobj ); // синтаксис вызова функции

    то применяется процедура разрешения перегрузки для функций в пространстве имен (см. раздел 15.10). Если же использован синтаксис вызова функции-члена:

    то работает соответствующая процедура для функций-членов (см. раздел 15.11).

    15.12.1. Операторные функции-кандидаты

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

    операторной функцией-кандидатом является operator+. Какие объявления operator+ принимаются во внимание?

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

    • множество операторов, видимых в точке вызова. Объявления функции operator+(), видимые в точке использования оператора, являются кандидатами. Например, operator+(), объявленный в глобальной области видимости, – кандидат в случае применения operator+() внутри main():
    • множество операторов, объявленных в пространстве имен, в котором определен тип операнда. Если операнд имеет тип класса и этот тип объявлен в пользовательском пространстве имен, то операторные функции, объявленные в том же пространстве и имеющие то же имя, что и использованный оператор, считаются кандидатами:

    Операнд si имеет тип класса SmallInt, объявленного в пространстве имен NS. Поэтому перегруженный operator+(const SmallInt, double), объявленный в том же пространстве, добавляется к множеству кандидатов;

  • множество операторов, объявленных друзьями классов, к которым принадлежат операнды. Если операнд принадлежит к типу класса и в определении этого класса есть одноименные применяемому оператору функции-друзья, то они добавляются к множеству кандидатов:
  • Операнд si имеет тип SmallInt. Операторная функция operator+(const SmallInt&, int), являющаяся другом этого класса, – член пространства имен NS, хотя непосредственно в этом пространстве она не объявлена. При обычном поиске в NS эта операторная функция не будет найдена. Однако при использовании operator+() с аргументом типа SmallInt функции-друзья, объявленные в области видимости этого класса, включаются в рассмотрение и добавляются к множеству кандидатов. Эти три множества операторных функций-кандидатов формируются точно так же, как и для вызовов обычных функций с аргументами типа класса. Однако при использовании операторного синтаксиса строятся еще два множества:

      множество операторов-членов, объявленных в классе левого операнда. Если такой операнд оператора operator+() имеет тип класса, то в множество функций-кандидатов включаются объявления operator+(), являющиеся членами этого класса:

    Оператор-член SmallInt::operator+(const myFloat &), определенный в SmallInt, включается в множество функций-кандидатов для разрешения вызова operator+() в main();

  • множество встроенных операторов. Учитывая типы, которые можно использовать со встроенным operator+(), кандидатами являются также:
  • Первое объявление относится к встроенному оператору для сложения двух значений целых типов, второе – к оператору для сложения значений типов с плавающей точкой. Третье и четвертое соответствуют встроенному оператору сложения указательных типов, который используется для прибавления целого числа к указателю. Два последних объявления представлены в символическом виде и описывают целое семейство встроенных операторов, которые могут быть выбраны компилятором на роль кандидатов при обработке операций сложения.


    Любое из первых четырех множеств может оказаться пустым. Например, если среди членов класса SmallInt нет функции с именем operator+(), то четвертое множество будет пусто.

    Все множество операторных функций-кандидатов является объединением пяти подмножеств, описанных выше:

    В эти пять множеств входят семь операторных функций-кандидатов на роль operator+() в main():


      первое множество пусто. В глобальной области видимости, а именно в ней употреблен operator+() в функции main(), нет объявлений перегруженного оператора operator+();
    • второе множество содержит операторы, объявленные в пространстве имен NS, где определен класс SmallInt. В этом пространстве имеется один оператор:
    • четвертое множество содержит операторы, объявленные членами SmallInt. Такой тоже есть:
    • пятое множество содержит встроенные бинарные операторы:

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

    15.12.2. Устоявшие функции

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

    Левый операнд имеет тип SmallInt, а правый – double.

    Первый кандидат является устоявшей функцией для данного использования operator+():

    Левый операнд типа SmallInt в качестве инициализатора точно соответствует формальному параметру-ссылке этого перегруженного оператора. Правый, имеющий тип double, также точно соответствует второму формальному параметру.

    Следующая функция-кандидат также устоит:

    Левый операнд si типа SmallInt в качестве инициализатора точно соответствует формальному параметру-ссылке перегруженного оператора. Правый имеет тип int и может быть приведен к типу второго формального параметра с помощью стандартного преобразования.

    Устоит и третья функция-кандидат:

    Левый операнд si имеет тип SmallInt, т.е. тип того класса, членом которого является перегруженный оператор. Правый имеет тип int и приводится к типу класса myFloat с помощью определенного пользователем преобразования в виде конструктора myFloat(double).

    Четвертой и пятой устоявшими функциями являются встроенные операторы:

    Класс SmallInt содержит конвертер, который может привести значение типа SmallInt к типу int. Этот конвертер используется вместе с первым встроенным оператором для преобразования левого операнда в тип int. Второй операнд типа double трансформируется в тип int с помощью стандартного преобразования. Что касается второго встроенного оператора, то конвертер приводит левый операнд от типа SmallInt к типу int, после чего результат стандартно преобразуется в double. Второй же операнд типа double точно соответствует второму параметру.

    Лучшей из этих пяти устоявших функций является первая, operator+(), объявленная в пространстве имен NS:

    Оба ее операнда точно соответствуют параметрам.

    15.12.3. Неоднозначность

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

    и такое использование оператора operator==:

    Тогда при сравнении

    вызывается оператор равенства класса String:

    Для трансформации правого операнда pf из типа const char* в тип String параметра operator==() применяется определенное пользователем преобразование, которое вызывает конструктор:

    Если добавить в определение класса String конвертер в тип const char*:

    то показанное использование operator==() становится неоднозначным:

    Из-за добавления конвертера operator const char*() встроенный оператор сравнения

    тоже считается устоявшей функцией. С его помощью левый операнд flower типа String может быть преобразован в тип const char *.

    Теперь для использования operator==() в foo() есть две устоявших операторных функции. Первая из них

    требует применения определенного пользователем преобразования правого операнда pf из типа const char* в тип String. Вторая

    требует применения пользовательского преобразования левого операнда flower из типа String в тип const char*.

    Таким образом, первая устоявшая функция лучше для левого операнда, а вторая – для правого. Поскольку наилучшей функции не существует, то вызов помечается компилятором как неоднозначный.

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

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

    Какой из операторов operator+() будет выбран в качестве наилучшего из устоявших для оператора сложения в main()? Перечислите все функции-кандидаты, все устоявшие функции и преобразования типов, которые надо применить к аргументам для каждой устоявшей функции. Назад Вперед

    C++ — C++. Переопределение операторов.

    Нужна помощь в решении задачи. Нужно реализовать два оператора: унарный минус и унарный плюс. Сигнатура обязательно такая — Rational operator-() const < /* Реализация */ >.

    Условие:
    Дан класс Rational, который описывает рациональное число. В нем определены методы add, sub, mul и div, которые прибавляют к нему число, отнимают число, умножают на число и делят на число соответственно. Кроме того в нем определен метод neg, который меняет знак на противоположный.

    Лекция 7. Перегрузка операторов

    Что такое перегрузка?

    Перегрузка — это возможность поддерживать несколько функций с одним названием, но разными сигнатурами вызова. Рассмотрим пример:

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

    &nbsp
    Что такое операторы?

    Оператор в С++ — это некоторое действие или функция обозначенная специальным символом. Для того что бы распространять эти действия на новые типы данных, при этом сохраняя естественный синтаксис, в С++ была введена возможность перегрузки операторов.

    + — * / % //Арифметические операторы
    += -= *= /= %=
    +a -a //Операторы знака
    ++a a++ —a a— //Префиксный и постфиксный инкременты
    && || ! //Логические операторы
    & |

    ^
    &= |= ^=
    > >= //Битовый сдвиг
    = //Оператор присваивания
    == != //Операторы сравнения
    >= a->*
    () []
    (type)
    . , (a ? b : c)

    Не все операторы можно переопределять. Операторы » . » и » a?b:c «(тернарный оператор) переопределить нельзя.

    Так же нужно отметить, что переопределяя операторы » , » » && » » || » теряются их «ленивые» свойства.

    Операторы » a-> «, » [] «, » () «, » = » и » (type) » можно переопределить только как методы класса.

    &nbsp
    Как переопределить оператор?

    Рассмотрим как переопределять операторы на примере нашего класса длинной арифметики BigInt.

    Бинарный оператор

    Бинарный оператор — это функция от двух параметров, параметрами которой являются левый и правый операнды оператора.

    Унарный оператор

    Унарный оператор — это оператор от одного параметра. Если он объявлен внутри класса, то этим параметром (неявным) является this .

    При перегрузке операторов » >> » и » «, для ввода и вывода через потоки нужно подключить заголовочный файл iostream .

    Переопределение префиксных и постфиксных операторов:

    Фактически параметра size_t size у операции нет — он фиктивен. Это хак для того чтобы внести различия в сигнатуры

    Переопределение операторов внутри класса:

    Обратите внимание, что при переопределении » -> » необходимо вернуть именно указатель на объект.

    Как вы могли заметить, оператор » [] » переопределен как константный и не константный. Во втором случае мы возвращаем ссылку на объект, а не сам объект. Это может быть полезно при определении массивов.


    &nbsp
    Как правильно перегружать оператор «=» ?

    Для перегрузки оператора » = » есть специальная идиома, которая облегчает присваивание сложных объектов.
    Рассмотрим следующий код:

    Функция std::swap(a,b) — меняет значение a и b местами.

    Создав временный объект равный num , поменяем его значения с текущими значениями объекта * this . Выйдя из функции временный объект удалится, а в * this останутся новые значения.

    &nbsp
    Приведение типов

    Часто при разработке новых классов появляется желание приводить уже существующие типы к новому, и наоборот.

    Оператор приведения — это противоположность конструктору с одним параметром.

    Важно понять, что в случае

    и во всех подобных случаях, когда необходимо одно значение привести к другому и оператор приведения не определен, то будет вызываться конструктор копирования A::A( int b) от соответствующего типа. Если определить и оператор приведения, и конструктор копирования как не explicit , то при вызове произойдет ошибка компиляции.

    Как переопределить пост- и пре- фиксные операторы?

    Хорошим тоном является определение постфиксного оператора через префиксный:

    (В этом примере операторы определялись внутри класса BigInt .)

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

    При определении операторов сравнения нет смысла определять их все. Проще определить операторы > или += , -= , *= и т.д.) лучше делать внутри класса. А бинарные операторы( + , — , * и т.д.) на их основе, но уже снаружи класса.

  • Быть осторожнее с explicit конструкторами.
  • Нельзя определять новые операторы и нельзя менять приоритет операторов, т.к. система парсинга языка С++ этого не позволяет.
  • C++ — C++. Переопределение операторов.

    Собственно, всё написано у Шилдта, это, скорее, шпаргалка.

    Совсем нельзя перегружать: . :: .* ?

    Нельзя перегружать не членами класса: = () [] ->

    Не надо перегружать, если не какой-то особый случай: new delete -> ->* ,

    Если функция перегрузки оператора — член класса, то при перегрузке бинарного оператора неявно (через указатель this ) передаётся первый операнд (слева от знака операции), а параметром функции явно передаётся второй операнд (справа от знак операции).

    Для унарного оператора операнд передаётся неявно, а явный параметр не нужен.

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

    Если функция перегрузки оператора — не член, но друг ( friend ) класса, то при перегрузке бинарного оператора явно передаются оба операнда (стоящие слева и справа от знака операции), а при перегрузке унарного оператора явно передаётся ссылка на его единственный операнд. При этом префиксная форма функции-друга принимает один параметр-ссылку (который и является операндом), а постфиксная форма — два параметра (вторым является целочисленное значение, которое не используется).

    Перегруженные операторы отношения или логических операцией могут возвращать просто значение типа int или bool .

    Вообще говоря, перегрузка бинарной операции, присваивания и унарных постфиксных операторов должны возвращать новый объект класса, созданный внутри операторной функции. Перегрузка унарного префиксного оператора может возвращать как объект класса значение *this . Можно возвращать *this и для перегруженных операций вроде *= , += и т.д. Делать операторные функции с типом возвращаемого значения void не нужно — над объектами не будет работать, например, присваивание по цепочке вида a=b=c .

    Для класса с динамически выделяемой под некоторые свойства оперативной памятью нужен явный конструктор копирования, принятое по умолчанию побитовое копирование объектов не подходит (при этом могут быть скопированы адреса динамических объектов, но не их значения). Конструктор копирования должен иметь единственный параметр — const-ссылку на объект класса (копируемый объект; объект, куда копируем, доступен через this ).

    Корректно переопределённый оператор [] (который может использоваться и слева, и справа от знака «=») должен возвращать ссылку на объект того типа, из которого состоит индексируемая последовательность. В принципе, [] перегружается как бинарный оператор.

    Оператор вызова функций () создаёт не новый способ вызова функций, а операторную функцию, которой можно передать произвольное число параметров.

    Пример на большую часть сказанного, простейший класс Class состоит только из числа n :

    03.04.2014, 19:40; рейтинг: 12143

    Перегрузка операторов

    С++ поддерживает перегрузку операторов (operator overloading). За небольшими исключениями большинство операторов С++ могут быть перегружены, в результате чего они получат специаль­ное значение по отношению к определенным классам. Например, класс, определяющий связан­ный список, может использовать оператор + для того, чтобы добавлять объект к списку. Другой класс может использовать оператор + совершенно иным способом. Когда оператор перегружен, ни одно из его исходных значений не теряет смысла. Просто для определенного класса объектов определен новый оператор. Поэтому перегрузка оператора + для того, чтобы обрабатывать свя­занный список, не изменяет его действия по отношению к целым числам.

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

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

    тип имя_класса::operator#(список_аргументов)
    <
    // действия, определенные применительно к классу
    >

    Здесь перегруженный оператор подставляется вместо символа #, а тип задает тип значений, воз­вращаемых оператором. Для того, чтобы упростить использование перегруженного оператора в
    сложных выражениях, в качестве возвращаемого значения часто выбирают тот же самый тип, что и класс, для которого перегружается оператор. Характер списка аргументов определяется не­сколькими факторами, как будет видно ниже.

    Чтобы увидеть, как работает перегрузка операторов, начнем с простого примера. В нем созда­ется класс three_d, содержащий координаты объекта в трехмерном пространстве. Следующая про­грамма перегружает операторы + и = для класса three_d:

    #include
    class three_d <
    int x, y, z; // трехмерные координаты
    public:
    three_d operators+(three_d t);
    three_d operator=(three_d t);
    void show ();
    void assign (int mx, int my, int mz);
    >;
    // перегрузка +
    three_d three_d::operator+(three_d t)
    <
    three_d temp;
    temp.x = x+t.x;
    temp.у = y+t.y;
    temp.z = z+t.z;
    return temp;
    >
    // перегрузка =
    three_d three_d::operator=(three_d t)
    <
    x = t.x;
    y = t.y;
    z = t.z;
    return *this;
    >
    // вывод координат X, Y, Z
    void three_d::show ()
    <
    cout x, где х ассоциировано с объектом, который вызывает функцию-оператор. Во всех случаях именно объект слева от знака операции вызывает функцию-оператор. Объект, стоящий справа от знака операции, передается функции.

    При перегрузке унарной операции функция-оператор не имеет параметров, а при перегрузке бинарной операции функция-оператор имеет один параметр. (Нельзя перегрузить триадный опе­ратор . ) Во всех случаях объект, активизирующий функцию-оператор, передается неявным об­разом с помощью указателя this.

    Чтобы понять, как работает перегрузка операторов, тщательно проанализируем, как работа­ет предыдущая программа, начиная с перегруженного оператора +. Когда два объекта типа three_d подвергаются воздействию оператора +, значения их соответствующих координат скла­дываются, как это показано в функции operator+(), ассоциированной с данным классом. Обра­тим, однако, внимание, что функция не модифицирует значений операндов. Вместо этого она возвращает объект типа three_d, содержащий результат выполнения операции. Чтобы понять, почему оператор + не изменяет содержимого объектов, можно представить себе стандартный арифметический оператор +, примененный следующим образом: 10 + 12. Результатом этой опе­рации является 22, однако ни 10 ни 12 от этого не изменились. Хотя не существует правила о том, что перегруженный оператор не может изменять значений своих операндов, обычно име­ет смысл следовать ему. Если вернуться к данному примеру, то нежелательно, чтобы оператор + изменял содержание операндов.

    Другим ключевым моментом перегрузки оператора сложения служит то, что он возвращает объект типа three_d. Хотя функция может иметь в качестве значения любой допустимый тип язы­ка С++, тот факт, что она возвращает объект типа three_d, позволяет использовать оператор + в более сложных выражениях, таких, как a+b+с. Здесь а+b создает результат типа three_d. Это значение затем прибавляется к с. Если бы значением суммы а+b было значение другого типа, то мы не могли бы затем прибавить его к с.

    В противоположность оператору +, оператор присваивания модифицирует свои аргументы. (В этом, кроме всего прочего, и заключается смысл присваивания.) Поскольку функция operator=() вызывается объектом, стоящим слева от знака равенства, то именно этот объект модифицируется
    при выполнении операции присваивания. Однако даже оператор присваивания обязан возвра­щать значение, поскольку как в С++, так и в С оператор присваивания порождает величину, стоящую с правой стороны равенства. Так, для того, чтобы выражение следующего вида

    было допустимым, необходимо, чтобы оператор operator=() возвращал объект, на который ука­зывает указатель this и который будет объектом, стоящим с левой стороны оператора присваива­ния. Если сделать таким образом, то можно выполнить множественное присваивание.

    Можно перегрузить унарные операторы, такие как ++ или —. Как уже говорилось ранее, при перегрузке унарного оператора с использованием функции-члена, эта функция-член не имеет аргументов. Вместо этого операция выполняется над объектом, осуществляющим вызов функции-оператора путем неявной передачи указателя this. В качестве примера ниже рассмотрена расши­ренная версия предыдущей программы, в которой определяется оператор-инкремент для объекта типа three_d:

    #include
    class three_d <
    int x, y, z; // трехмерные координаты
    public:
    three_d operator+(three_d op2); // op1 подразумевается
    three_d operator=(three_d op2); // op1 подразумевается
    three_d operator++ (); // op1 также подразумевается
    void show();
    void assign (int mx, int my, int mz);
    >;
    // перегрузка +
    three_d three_d::operator+(three_d op2)
    <
    three_d temp;
    temp.x = x+op2.x; // целочисленное сложение
    temp.у = y+op2.y; // и в данном случае + сохраняет
    temp.z = z+op2.z; // первоначальное значение
    return temp;
    >
    // перегрузка =
    three_d three_d::operator=(three_d op2)
    <
    x = op2.x; // целочисленное присваивание
    у = op2.y; // и в данном случае = сохраняет
    z = op2.z; // первоначальное значение
    return *this;
    >
    // перегрузка унарного оператора
    three_d three_d::operator++()
    <
    х++;
    у++;
    z++;
    return *this;
    >
    // вывести координаты X, Y, Z
    void three_d::show()
    <
    cout > применительно к cout и cin имеют мало общего с их действием на переменные целого типа. Однако, исходя из стремления сделать код более легко читаемым и хорошо структурированным, желательно, чтобы перегруженные опера­торы соответствовали, там где это возможно, смыслу исходных операторов. Например, оператор + по отношению к классу three_d концептуально сходен с оператором + для переменных целого типа. Мало пользы, например, можно ожидать от такого оператора +, действие которого на
    соответствующий класс будет напоминать действие оператора ||. Хотя можно придать перегру­женному оператору любой смысл по своему выбору, но для ясности его применения желательно, чтобы его новое значение соотносилось с исходным значением.

    Имеются некоторые ограничения на перегрузку операторов. Во-первых, нельзя изменить при­оритет оператора. Во-вторых, нельзя изменить число операндов оператора. Наконец, за исклю­чением оператора присваивания, перегруженные операторы наследуются любым производным классом. Каждый класс обязан определить явным образом свой собственный перегруженный опе­ратор =, если он требуется для каких-либо целей. Разумеется, производные классы могут пере­грузить любой оператор, включая и тот, который был перегружен базовым классом. Следующие операторы не могут быть перегружены:
    . :: * ?

    Перегрузка операций в C++. ООП

    В C++ существует возможность перегрузить не только функции, но и операции внутри класса, например, можно добиться того, чтобы операция * при работе с матрицами осуществляла умножение матриц, а при работе с комплексными числами — умножение комплексных чисел.

    Для перегрузки операций внутри класса нужно написать специальную функцию — метод класса. При перегрузке операций следует помнить следующее:

    • нельзя поменять приоритет операций;
    • нельзя изменить тип операции (из унарной операции нельзя сделать бинарную или наоборот);
    • перегруженная операция является членом класса и может использоваться только в выражениях с объектами своего класса;
    • нельзя создавать новые операции;
    • запрещено перегружать операции: . (доступ к членам класса), унарную операцию * (значение по адресу указателя), :: (расширение области видимости), ?: (операция if);
    • допустима перегрузка следующих операций: +, , *, /, %, =, , +=, -=, *=, /=, &&, ||, ++, , (), [], new, delete.

    Для перегрузки бинарной операции внутри класса необходимо создать функцию-метод:

    Здесь type — тип возвращаемого операцией значения, operator — служебное слово, symbols — перегруженная операция, type1 — тип второго операнда, первым операндом является экземпляр текущего класса, parametr — имя переменной второго операнда.

    В качестве примера рассмотрим класс complex с перегруженными операциями + (сложение комплексных чисел) и — (вычитание комплексных чисел).

    Задача

    Создать класс для работы с комплексными числами, в котором перегрузить операции сложения и вычитания. Текст программы с комментариями приведен ниже.

    Для перегрузки унарной операции внутри класса необходимо создать функцию-метод:

    Как Вы помните, в C++ унарные операции ++ и —. Действие этих операций различается при расположении с лева и справа от операнда. Рассмотрим, как реализуется перегрузка операции ++x и x++ на примере класса комплексных чисел. Пусть операция ++x увеличивает действительную и мнимую части комплексного числа x на 1, а x++ увеличивает на 1 только действительную часть комплексного числа x.

    Как видно операторы ++x и x++ были перегружены по-разному. Вы можете сами дописать в класс complex методы, реализующие перегрузку операций умножения и деления комплексных чисел.

    Цукерберг рекомендует:  Unicode - Уважаемые знатоки PHP, внимание вопрос ...
    Понравилась статья? Поделиться с друзьями:
    Все языки программирования для начинающих
    Функция- член класса Дружественная функция