А.В. Столяров - Введение в язык Си++ (1114949), страница 13
Текст из файла (страница 13)
Наряду с многими другими операциями, операция -> переопределяется исключительнометодами класса или структуры, то есть не может быть переопределенаотдельной функцией. Чтобы избежать рутинной работы по переопределению отдельных операций для выборки каждого поля, в С и + + принятонеочевидное, но вполне работающее соглашение: операция -> определяется методом operator-> (), не имеющим параметров (как и обычныеунарные операции), но при этом метод обязан возвращать либо указатель на некую другую структуру или класс, либо объект (или ссылкупа объект), для которого, в свою очередь, операция -> переопределена как метод. При использовании перегруженной операции -> компилятор вставляет в код последовательно вызовы методов operator-> (),пока очередной метод не окажется возвращающим обычный указательна структуру или класс; именно по этому указателю и производится витоге выборка заданного поля.Попробуем проиллюстрировать сказанное на примере.
Пусть у насесть структура s i и нам необходим класс, объекты которого будут вести себя как указатели на s i, но при уничтожении такого «указателя»(например, при завершении функции, в которой он описан как локальная переменная) объект, на который он указывает, будет уничтожаться.Начнём описание класса:c la s s P o in ter_ sl {s i *р ;p u b lic :Будем предполагать, что хранящийся внутри объекта простой указательна s i может иметь и нулевое значение, что будем расценивать обычнымобразом как отсутствие объекта; естественно, в этом случае удалять ничего не будем.
Опишем теперь конструктор и деструктор:P o in te r _ s l(sl *p tr = 0 ) { р = p tr ; }~ P o in te r_ sl() { i f (р) d e le te р; }Для удобства работы введём операцию присваивания обычного адреса:s i * o p e ra to r= (sl *p tr ) {i f (р) d elete p;58Р = p tr;}Заметим, что копирование и присваивание объектов класса P o in ter_slзаведомо приведёт к ошибкам, так как один и тот же объект типа s i вэтих случаях будет удаляться дважды.
В связи с этим запретим присваивание и копирование объектов класса P o in ter_ sl, убрав конструкторкопирования и соответствующий оператор присваивания в приватнуючасть:p r iv a te :P o in te r_ sl(co n st Pointer_sl& ) -Q/ / copying prohibitedvoid operator=(const Pointer_sl& ) -Q/ / assignments prohibitedНаконец, опишем операции, которые превратят объекты нашего класса вподобие указателя на s i, а именно, операции разыменования (унарную *)и выборки поля (->), и завершим описание класса:p u b lic :sl& o p e ra to r*() { return *р ; }s i * o p erator->() { return p; }>;Такой класс удобно использовать, например, внутри функций.
Так, еслив начале некоторой функции описать объект класса P o in ter_ sl, то емуможно будет присваивать адреса новых экземпляров s i , при этом онбудет каждый раз удалять старый экземпляр, а когда работа функциизавершится, автоматически удалит последний из экземпляров s i:in t f () {P o in ter_sl р;р = new s i ;р->а = 25;p->b = р->а + 36;И .../ / при завершении f память будет освобождена}§2.19.5.
Переопределение операции вы зова функцииОперация вызова функции, синтаксически представляющая собойпостфиксную операцию, символом которой служат круглые скобки (возможно, содержащие список параметров), может быть, как и другие операции, переопределена. Поскольку вызов функции обозначается круглыми скобками, имя функции, которая переопределяет эту операцию, будет59состоять из слова operator и круглых скобок; как обычно, после именифункции записывается список формальных параметров. Например, операция вызова функции без параметров записывается так:void o p erato rQ Q { / * тело * / }Подобно операциям присваивания и индексирования, вызов функции переопределяется только методом класса.Приведём пример.
Опишем класс Fun, для которого переопределеныоперации вызова функции без параметров, а также с одним и двумя целочисленными параметрами:c la s s Fun {p u b lic :void o p eratorQ Q{ p rin tf("fu n O \n "); }void o p e rato r( ) (in t a){ p r in t f ( " f u n l: %d\n", a ) ; }void o p e rato r( ) (in t a, in t b){ p rin tf("fu n 2 : %d %d\n", a , b ) ; }>;Теперь мы можем написать следующий фрагмент кода:Fun f ;f О ; f (100); f (25, 36);В результате выполнения этого фрагмента будет напечатано:funOfu n l: 100fun2: 25 36§ 2.19.6. Переопределение операции преобразованиятипаРассмотрим следующий фрагмент кода:in t i ;double d;// ...i = d;В строчке, содержащей присваивание, задействована (неявно) операция преобразования т и п а в ы р а ж е н и я , позволяющая в данном случае построить значение типа in t на основе имеющегося значения типа double.
Аналогичную возможность неявного преобразования можно60предусмотреть и для классов, введённых программистом. Один из способов этого мы уже знаем — это конструктор преобразования (см. § 2.11).В некоторых случаях применить конструктор преобразования не удаётся. В частности, конструктором можно задать преобразование из базового типа в тип, описанный пользователем, но не наоборот. Кроме того, иногда бывает по каким-то причинам невозможно изменить описаниенекоторого класса, но в программе нужно преобразование в объекты этого класса. Бывают и другие (довольно экзотические) ситуации, когда мыне можем задать способ преобразования путём модификации того типа,к квторому производится преобразование, но при этом у нас есть возможность изменить (дополнить) описание того типа, значения которогоподлежат преобразованию.
В таких случаях применяют переопределениеоперации преобразования типа.Операция неявного преобразования типов определяется методом, имякоторого состоит из слова operator и имени типа, к которому необходимо преобразовывать тип выражения. Тип возвращаемого значения длятакой функции не указывается, так как он определяется именем. Например:c la s s А {I I ...p u b lic :И ...operator in t( ) { / * . . . * / }};Наличие в классе А операции преобразования к in t делает возможным,например, такой код:А а;in t х;И ...х = а;Естественно, операция преобразования может быть переопределена только как метод класса или структуры.Учтите, что чрезмерное увлечение переопределениями операции преобразования приводит, как правило, к тому, что компилятор находитбольше одного способа преобразования из одного типа к другому и врезультате не применяет ни одного из них, выдавая ошибку.
Поэтому напрактике переопределение операции преобразования типа используетсякрайне редко; для получения значений нужного типа из объектов классовили структур обычно применяют специально для этого вводимые методы (обыкновенные, без слова operator), вызываемые явно. Пример си61туации, когда операция преобразования типа оказывается действительнонужна, рассмотрен в § 2.20.§2.20.
П рим ер: р азр еж ен н ы й м ассивПод р а зр е ж е н н ы м м асси во м понимается массив относительнобольшого объёма (например, состоящий из нескольких миллионов элементов), большинство элементов которого равны одному и тому же значению (чаще всего нулю) и лишь некоторые элементы, количество которых ничтожно в сравнении с размером массива, от этого значения отличаются. Естественно, в такой ситуации целесообразно хранить в памятилишь те элементы массива, значения которых отличны от нуля (или другого значения, которому равно большинство элементов).Попробуем написать на С и + + такой класс, который ведёт себя как целочисленный массив потенциально бесконечного размера9 в том смысле,что к объекту этого класса применима операция индексирования (квадратные скобки), однако объект реально хранит только те элементы, значения которых отличаются от нуля.
Назовём этот класс SparseA rraylnt.Для простоты картины будем считать, что количество ненулевых элементов массива настолько незначительно, что даже линейный поиск среди них может нас устроить по быстродействию. Это позволит хранитьненулевые элементы массива в виде списка пар «индекс/значение». Вседетали реализации, включая и структуру, представляющую звено списка, скроем в приватной части класса. Это позволит при необходимостисменить реализацию на более сложную.Основная проблема при реализации разреженного массива возникаетв связи с поведением операции индексирования.
Дело в том, что выражение «элемент массива» может встречаться как в правой, так и влевой части присваиваний, и в обоих случаях используется одна и таже функция-метод класса SparseA rraylnt, называемая operator [] . Если бы все элементы массива хранились в памяти (как это было в примереиз §2.19.2), можно было бы просто возвратить ссылку на соответствующее место в памяти, где хранится элемент, соответствующий заданномуиндексу.
С разреженным массивом так действовать не получится, поскольку в большинстве случаев элемент в памяти не хранится. Однакоже просто возвратить нулевое значение (без всякой ссылки) нельзя, ведьэто означало бы, что выражение, содержащее нашу операцию индексирования, нельзя применять в левой части присваивания.Итак, что же делать в случае, если операция индексирования вызванадля индекса, для которого соответствующий элемент в памяти не хра9На самом деле мы будем использовать в качестве индекса тип unsigned long;значения этого типа могут достигать 232 - 1, т.