А.В. Столяров - Введение в язык Си++ (1114949), страница 12
Текст из файла (страница 12)
В нашей упрощенной версии мы просто запретим присваиваниеи копирование, описав фиктивные конструктор копирования и операциюприсваивания в приватной части. Описание полноценного конструкторакопирования и полноценной операции присваивания оставим читателю вкачестве упражнения.Для начала напишем заголовок класса:c l a s s IntA rray {in t *р ;/ / указатель на хранилищеunsigned in t s i z e ; / / текущий размер хранилищаp u b l ic :In tA rray () {s iz e = 16;р = пей i n t [ s i z e ] ;53}"IntArrayО { d e le te [] p ; }int& o p e r a t o r [ ] (un sign ed in t i d x ) ;p r iv a te :vo id R e siz e (u n sig n e d in t r e q u ire d _ in d e x );/ / запретим копирование и присваиваниеvo id o p e ra to r= (c o n st IntArray& r e f ) { }In tA rray (c o n st IntArray& r e f ) { }};Тела конструктора и деструктора имеют сравнительно небольшой размер, поэтому мы совершенно спокойно можем оставить их внутри заголовка класса.
С другой стороны, тело операции индексирования будетсостоять из нескольких строк, поэтому мы опишем его отдельно. Длялучшей ясности его реализации мы предусмотрели также вспомогательную функцию R e s i z e (), которая будет осуществлять изменение размерамассива. Поскольку эта функция, очевидно, является деталью реализации и не предназначена для пользователя (с точки зрения пользователянаш массив представляется бесконечным), мы скрыли эту функцию вприватной части класса.Читатель, возможно, обратил внимание на тип возвращаемого значения операции индексирования.
Функция o p e r a t o r [ ] ( ) возвращаетс с ы л к у на соответствующий элемент массива, чтобы сделать возможным как выборку значения из массива, так и присваивание его элементамновых значений. Опишем теперь тело операции индексирования:int& In tA rra y : : o p e r a t o r [ ] (unsigned in t id x ) {i f ( i d x >= s iz e )R e s iz e ( id x ) ;re tu rn p [ i d x ] ;}Нам осталось описать функцию R e s i z e ( ) :vo id In tA rra y : : R e siz e (u n sig n e d in t req u ire d _ in d e x ) {unsigned in t new _size = s i z e ;w hile(new _size <= req u ired _ in d ex)new _size *= 2;in t *new _array = new in t[n e w _ s iz e ];fo r(u n sig n e d in t i = 0; i < s i z e ; i++)new_array [ i] = p [ i ] ;d e le te [] p;p = n ew .array ;s iz e = new _size;}54Теперь мы можем в программе использовать, например, такие операторы:IntArray a r r ;arr[500] = 15;a rr [1000] = 30;a rr [10] = a rr [500] + 1;a r r [10]++;Операция индексирования должна иметь строго один параметр, ноэтот параметр, вообще говоря, может быть любого типа.
Это позволяетсоздавать «массивы», использующие в качестве индекса текстовые строки или даже объекты других классов. Хранить элементы массива мытакже можем любым удобным нам способом, например, в виде списка(в некоторых случаях это может быть оправдано). Более того, возможноорганизовать объект, выглядящий как массив, но хранящий свои элементы в файле на диске, так что операция индексирования реально будетпредставлять собой операцию чтения или записи в файл.§2.19.3. Переопределение операций ++ и - Символы ++ ипредставляющие операции и н к р е м ен т а и дек р е м е н т а , имеют одну важную особенность. Можно было бы переопределять инкремент и декремент как обычные унарные операции (тоесть вне классов в виде функции от одного аргумента, либо в классе/структуре в виде метода без параметров), если бы не то обстоятельство, что каждая из этих операций имеет две формы: префиксную (++i)и постфиксную (i++).
Вообще говоря, эти две формы представляют собойразличные операции.Чтобы различать эти формы между собой, в С и + + принято достаточно неожиданно выглядящее соглашение. Поскольку префиксная формаболее «традиционна» для унарных операций, обычным способом переопределяется именно она. Иначе говоря, если мы переопределяем соответствующие операции введением метода в классе или структуре, то метод с именем operator++() или o perator— () без параметров определяетпрефиксную форму соответствующей операции (например, ++i); аналогично, префиксную форму операции инкремента (декремента) задаёт иглобальная функция (не метод), имеющая имя operator++ (operator—)и один параметр.
Что касается постфиксной формы, то она переопределяется функцией с тем же именем operator++ (operator--), но имеющейодин дополнительный (фиктивный) параметр типа in t. Наличие параметра благодаря перегрузке имён функций позволяет иметь две функциис одним и тем же именем; параметр, введённый исключительно ради этого различия, реально никогда не используется. Таким образом, для пе55реопределения постфиксной формы операции инкремента (декремента)мы можем воспользоваться либо методом класса/структуры с именемoperator++ (operator—) и одним параметром (фиктивным), либо глобальной функцией с таким же именем, имеющей два параметра (второйиз них фиктивный).
Приведём пример. Пусть класс А описан следующимобразом:c la s s А {p u b lic :void operator++() { p r i n t f ( " f i r s t \ n " ) ; }void o perator— () { p r in tf("s e c o n d \n "); }void op erator++(in t) { p r in t f ( " t h ir d \ n " ) ; }void o perator— (in t) { p r in t f (" fo u r t h \n " ); }};Рассмотрим теперь фрагмент кода:А а;++а;а++;--а;а—;////////firstth irdsecondf ourthВ результате выполнения этого фрагмента будут напечатаны (в столбик)слова f i r s t , th ird , second и fourth, именно в таком порядке.В рассмотренном примере наши операции ничего не возвращали;между тем, исходно префиксная и постфиксная формы этих операцийразличаются именно возвращаемым значением.
При этом операция впрефиксной форме возвращает повое значение переменной, к которойона применена, что позволяет выдержать семантику этой операции, вернув сам объект или (для оптимизации) константную ссылку на него. Соперацией в постфиксной форме дела обстоят несколько хуже: согласноклассической семантике она должна вернуть старое значение, в то время как объект уже имеет новое значение. Чтобы выдержать семантикув этой форме, приходится в теле функции op erator++(in t) создаватьвременную (локальную) копию объекта, причём возвращать значение,сохранённое в этой копии, приходится обязательно по значению, ведьвозвращать ссылку на локальную копию было бы ошибкой: копия исчезнет в момент завершения работы функции, то есть до того, как вызывающая функция успеет воспользоваться возвращённым результатом.Проиллюстрируем сказанное на примере операций инкремента для класса, инкапсулирующего целочисленную переменную и повторяющего еёсемантику:c la s s Mylnt {56in t i ;p u b lic :M ylnt(int x) : i( x ) -Qconst Mylnt& operator++() { i+ + ; return * t h i s ; }MyInt operator++(in t){ Mylnt tm p (*th is); i+ + ; return tmp; }// ...В этом примере объект класса занимает столько же памяти, сколько иобычная целочисленная переменная, так что двойное копирование объекта (при создании переменной tmp и при возврате значения) не приводитк существенным потерям.
Однако для более сложных классов педантичное следование классической семантике для постфиксных операций ++ и-- может обойтись неоправданно дорого.§ 2.19.4. Переопределение операции ->Пожалуй, наиболее экзотическим образом обстоят дела с операцией ->. Напомним, что эта операция в языке Си означает выборку поляиз структуры, на которую указывает заданный адрес.
В С и + + она означает практически то же самое; естественно, с её помощью можно такжеобращаться и к методам, а работать она может как со структурами, таки с классами. Перегрузка этой операции обычно требуется нам, если мыхотим создать объект, ведущий себя подобно указателю, но при этом выполняющий некоторые дополнительные действия.Отметим для начала один неочевидный факт.
Операция ->, несмотряна её внешний вид, является унарной, то есть имеет всего один аргумент(указатель). Действительно, пусть у нас описана структураstru c t s i {in t a, b;>;и имеется указатель на неё:s i *р = new s i ;Теперь обращение к полю а будет выглядеть так: р->а. Здесь как будтобы два операнда, р и а; но ведь а — это имя поля, которое никоим образом не является самостоятельным выражением, не имеет ни типа, низначения! Иначе говоря, на месте а не может стоять ничего, кроме имени поля. Язык не содержит никаких иных выражений, вычисляющихсяв значение, способное заменить имя поля.
Таким образом, единственнымполноценным операндом в выражении р->а является адрес, представленный указателем р. Заметим, в отличие от а, на месте р может стоять57выражение произвольной сложности, вычисляющее значение типа s i *(адрес структуры s i). Итак, мы имеем дело с унарной операцией, полным именем которой можно считать ->а (операция выборки поля а).Таким образом, можно при желании считать, что символ -> задаёт целое семейство операций. Впрочем, будучи семантически безупречным, напрактике такое рассмотрение нам не понадобится; здесь мы приводим еготолько для иллюстрации всего вышесказанного.Вернёмся к вопросу о переопределении операции ->.