Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Конспект_ООП_КЛАССЫ.doc
Скачиваний:
9
Добавлен:
12.02.2016
Размер:
10.12 Mб
Скачать

Int a::getValue( ) const {return privateDateMember};

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

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

Программа на рис. 2.1 создает константный объект класса Time и пы­тается изменить объект неконстантными функциями-элементами setHour, setMinute и setSecond. Как результат показаны сгенерированные компиля­тором Borland С++ предупреждения. Опция компилятора была установлена такой, чтобы в случае появления любого предупреждения компилятор не создавал исполняемого файла.

Результат компиляции программного кода:

Рисунок 2.1. Использование класса Time с константными объектами и константными функциями-элементами

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

Ошибкой программирования является:

  • описание константной функции-элемента, которая изменяет данные-элементы объекта,

  • описание константной функции-элемента, которая вызывает неконстантную функцию-элемент,

  • вызов неконстантной функции-элемента для константного объекта,

  • попытка изменить константный объект.

Константный объект не может быть изменен с помощью присваивания, так что он должен получить начальное значение. Если данные-элементы клас­са объявлены как const, то надо использовать инициализатор элементов, чтобы обеспечить конструктор объекта этого класса начальными значением данных-элементов. Рис. 2.2 демонстрирует использование инициализатора элементов для задания начального значения константному элементу incre­ment класса Increment. Конструктор для Increment изменяется следующим образом:

Increment :: Increment (int с, int i)

: increment (i)

{ count = c; }

Запись : increment (i) вызывает задание начального значения элемента increment, равного i. Если необходимо задать начальные значения сразу не­скольким элементам, просто включите их в список после двоеточия, разделяя занятыми. Используя инициаторы элементов, можно присвоить начальные значения всем данным-элементам.

Результат выполнения программы:

Рисунок 2.2. Использование инициализаторов элементов для инициализации данных константного встроенного типа

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

2.2. Композиция: классы как элементы других классов

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

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

Программа на рис. 2.3 использует классы Employee и Date для демон­страции объектов как элементов других объектов. Класс Employee содержит закрытые данные-элементы lastName, firstName, birthDate и hireDate. Эле­менты birthDate и hireDate являются объектами класса Date, который со­держит закрытые данные-элементы month, day и year. Программа создает объект Employee, задает начальные значения его данным-элементам и ото­бражает их на экране. Приведем синтаксис заголовка функции в описании конструктора Employee:

Employee: : Employee (char *fname, char *lname,

int bmonth, int bday, int byear,

int hmonth, int: hday, int hyear)

: birthDate (bmonth, bday, byear), hireDay (hmonth, hday, hyear)

Этот конструктор принимает восемь аргументов (fname, lname, bmonth, bday, byear, hmonth, hday и hyear). Двоеточие в заголовке отделяет иници­ализаторы элементов от списка параметров. Инициализаторы элементов указывают, что аргументы Employee передаются конструкторам объектов-эле­ментов. В частности, bmonth, bday и byear передаются конструктору birthDate, a hmonth, hday и hyear – конструктору hireDate, Инициализаторы элементов в списке разделяются запятыми.

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

Результат выполнения программы:

Рисунок 2.3. Использование инициализаторов объектов-элементов

2.3. Дружественные функции и дружественные классы

Дружественные функции класса определяются вне области действия этого класса, но имеют право доступа к закрытым элементам private и к элементам protected данного класса. Функция или класс в целом могут быть объявлены другом (friend) другого класса.

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

Чтобы объявить функцию как друга (friend) класса, перед ее прототипом в описании класса ставится ключевое словоfriend. Чтобы объявить классClassTwoкак друга классаClassOne, следует записать объявление в форме

friend ClassTwo

в определение класса ClassOne.

Дружественность требует разрешения, т.е. чтобы класс В стал другом класса А, класс А должен объявить, что класс В – его друг. Таким образом дружественность не обладает ни свойством симметричности, ни свойством транзитивности, т.е. если класс А – друг класса В, а класс В – Друг класса С, то отсюда не следует, что класс В – друг класса А, что класс С – Друг класса В, или что класс А – Друг класса С.

Программа на рис. 2.4 демонстрирует объявление и использование дружественной функции setXдля установки закрытого элемента данныхxклассаcount. Причем, объявлениеfriend появляется первым (по соглашению) в объявлении класса, раньше объявления закрытых функций-элементов.

Результат выполнения программы:

Рисунок 2.4. Механизм использования дружественной функции для доступа

к закрытым элементам класса

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

2.4. Использование указателя this

Каждый объект сопро­вождается указателем па самого себя – называемым указателемthis– это неявный аргумент во всех ссылках па элементы внутри этого объекта. Ука­зательthisможно использовать также и явно. Каждый объект может определить свой собственный адрес с помощью ключевого словаthis.

Указатель thisнеявно используется для ссылки как на данные-элементы, так и на функции-элементы объекта. Тип указателяthisзависит от типа объекта и от того, объявлена ли функция-элемент, в которой используетсяthis, какconst. В неконстантной функции-элементе классаEmployeeуказа­тельthis имеет типEmployee*const(константный указатель на объектEm­ployee). В константной функции-элементе классаEmployeeуказательthisимеет типconst Employee*const(константный указатель на объектEmployee, который тоже константный).

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

Результат выполнения программы:

Рисунок 2.5. Использование указателя this

На рис.2.5 функция-элемент printсначала печатаетхнепосредственно. Затем программа использует две различных записи для доступа кхпосредством указателяthis– операцию стрелки (–>), примененную к указателюthis, и операцию точка (.) для разыменовывания указателяthis.

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

*this.x

оценивалось бы так же, как если бы были круглые скобки следующего вида

*(this.x)

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

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

Другим применением указателя thisявляется возможность сцепленных вызовов функций элементов. Программа на рис. 2.6 иллюстрирует возвра­щение ссылки да объектTime, которое дает возможность сцепления вызовов функций-элементов классаTime. Каждая из функций-элементовsetTime,setHour,setMinuteиsetSecondвозвращает*thisс типом возвратаTime &.

Результат выполнения программы:

Рисунок 2.6. Сцепление вызовов функций-элементов

Почему возвращение *thisработает как ссылка? Операция точка (.) имеет ассоциативность слева направо, так что выражение

t.setHour(18).setMinute(30).setSecond(22);

сначала вычисляет t.setHour(18), а затем возвращает ссылку на объектt как значение вызова этой функции.

Оставшееся выражение затем интерпретируется как

t.setMinute(30).setSecond(22);

Вызов t.setMinute(30)выполняется и возвращает эквивалентt. Оставшееся выражение интерпретируется как

t.setSecond(22);

Вызовы

t.setTime(20, 20, 20).printStandard( )

также используют особенности сцепления. Эти вызовы должны появляться именно в указанной последовательности, потому что printStandard, как описано в классе, не возвращает ссылку наt. Расположение вызоваprintStandardв предыдущем операторе перед вызовомsetTimeприводит к синтаксической ошибке.

2.5. Динамическое распределение памяти с помощью операций new и delete

Операции newиdeleteобеспечивают более удобные средства для реали­зации динамического распределения памяти (для любых встроенных или оп­ределенных пользователем типов) по сравнению с вызовами функцийmallocиfreeв С. Рассмотрим следующее предложение:

TypeName *typeNamePtr;

В Си ANSI, чтобы динамически создать объект типа TypeName вы долж­ны написать:

typeNamePtr =malloc(sizeof());

Это требует вызова функции mallocи явной ссылки на операциюsizeof. В версии С, предшествующей С ANSI, вы должны были бы также привести тип указателя, возвращенногоmalloc, операцией приведения типа(TypeName*). Функцияmallocне обеспечивает никакого метода инициализации выде­ляемого блока памяти.В C++ вы просто пишете

typeNamePtr = new TypeName;

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

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

delete typeNamePtr;

C++ позволяет использовать инициализатор для только что создан­ного объекта

float *thingPtr = new float (3.14159);

который задает начальное значение вновь созданному объекту типа float, равное 3.14159.

Массив можно создать и присвоить его chessBoardPtr в следующем виде:

int *chessBoardPtr = new int{8}{8);

Этот массив может быть уничтожен с помощью оператора

delete { }chessBoardPtr;

Операция new автоматически активизирует кон­структор, adeleteавтоматически активизирует деструктор класса.

Типичной ошибкой программирования является смешивание способов динамического распределения памяти в стиле new-delete со стилем malloc-free: пространство, созданное с помощью malloc, не может быть освобождено с помощью delete; объекты, созданные с помощью new, не могут быть уничтожены с помощью free.

2.6. Статические элементы класса

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

Хотя может показаться, что статические элементы-данные похожи на глобальные переменные, тем не менее, они имеют областью действия класс. Статические элементы могут быть открытыми, закрытыми или защищенны­ми (protected). Статическим данным-элементам можно задать начальные зна­ченияодин (и только один) разв области действия файл. Доступ к открытым статическим элементам класса возможен посредством любого объекта класса или посредством имени класса, с помощью бинарной операции разрешения области действия. Закрытые и защищенные статические элементы класса должны быть доступны открытым функциям-элементам этого класса или дру­зьям класса. Статические элементы класса существуют даже тогда, когда не существует никаких объектов этого класса. Чтобы в этом случае обеспечить доступ к открытому статическому элементу, просто поставьте перед элементом данных имя класса и бинарную операцию разрешения области действия. Для обеспечения доступа в указанном случае к закрытому или защищенному элементу класса должна быть предусмотрена открытая статическая функция-элемент, которая должна вызываться с добавлением перед ее именем имени класса и бинарной операции разрешения области действия.

Программа на рис.2.7 демонстрирует использование закрытого стати­ческого элемента данных и открытой статической функции-элемента. Эле­менту данных countзадается нулевое начальное значение в области действия файл с помощью оператора

int Employee::count = 0;

Элемент данных countобслуживает подсчет количества объектов классаEmployee,которые были созданы. Если объекты классаEmployeeсуществуют, элементcount может быть вызван посредством любой функции-элемента объ­ектаEmployee (в данном примере посредством как конструктора, так и де­структора). Если никаких объектов классаEmployeeне существует, элементcountтакже может быть вызван, но только посредством вызова статической функции-элементаgetCountследующим образом:

Employee::getCount( )

В этом примере функция getCountиспользована для определения теку­щего числа созданных объектов классаEmployee. Отметим, что когда в про­грамме еще не создано ни одного объекта, используется вызов функции Employee::getCount( ). Нo когда объекты уже созданы, функцияgetCountможет быть вызвана из одного из объектов оператором

elPtr->getCount( )

Результат выполнения программы:

Рисунок 2.7. Использование статического элемента данных

для подсчета количества объектов

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

Отметим использование assertв функции конструктореEmployee, функ­цииgetFirstNameи функцииgetLastName. Утилитаassert, определенная в заголовочном файлеassert.h, проверяет значение выражения. Если значение выражения равно 0 (ложь), тоassertпечатает сообщение об ошибке и вы­зывает функциюabort(из общей библиотеки утилит –stdlib.h), которая завершает выполнение программы. Это полезное отладочное средство для про­верки, имеет ли переменная правильное значение. В этой программеassertопределяет, способна ли операцияnewосуществить динамическое выделение необходимого объема памяти. Например, в функции конструктореEmployeeследующая строка (называемая такжеоператором контроля)

assert (firstName ! = 0) ;

проверяет указатель firstName, чтобы определить, не равен ли он0. Если условие в операторе контроля истинно, то программа продолжается без преры­вания. Если же это условие ложно, то выдастся сообщение об ошибке, содержащее номер строки, проверяемое условие, печатается имя файла, в котором проявил себя оператор контроля, и программа завершается. Програм­мист может сосредоточить свое внимание на этой части кода, чтобы найти причину ошибки.

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

#define NDEBUG

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

2.7. Абстракция данных и скрытие информации

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

Представим стек в виде стопки тарелок. Когда тарелка ставится на стопку, она всегда помещается на ее вершину (это называется поместить в стек – pushing into the stack), а когда тарелка убирается из стопки, то всегда убирается тарелка с ее вершины (это называется вытолкнуть из стека – popping off the stack). Стеки известны как структуры данных типа последним вошел – первым вышел (last-in, first-out – LIFO) – посланий элемент, помещенный (вставленный) в стек, является первым элементом, выталкиваемым (удаляемым) из стека.

Программист может создать класс стек и скрыть от его клиентов ре­ализацию стека. Стеки можно легко реализовать с помощью массивов и других способов (таких как связные списки). Клиенту класса стекне нужно знать, как реализован стек. Кли­енту только надо знать, что когда он разместил элементы данных в стеке, то к ним можно обращаться только в последовательности последним вошел – первым вышел. Такой подход называетсяабстракция данных, а классы С++ определяютабстрактные типы данных (АТД). Хотя может оказаться, что пользователь знает детали реализации класса, но он может писать программу, не обращая внимания на эти детали. Это означает, что какой-то класс, например, тот, который реализует стек и его операцииpush(поместить) иpop(вытолкнуть), можно заменить другой его версией, не затрагивая остальной части системы, пока не изменен открытый интерфейс этого класса.

Задача языка высокого уровня – создать представление, удобное для использования программистом. Не существует единственного приемлемого стандартного представления и это одна из причин того, что существует так много языков программирования. Объектно-ориентированное программиро­вание на C++ дает еще одно представление.

Большинство языков программирования делает акцент на действия. В этих языках данные существуют для поддержки действий, необходимых про­грамме. Так или иначе, данные «менее интересны», чем действия. Данные в этих языках «негибки». Существует всего несколько встроенных типов данных и создание программистом своих собственных новых типов данных представляет определенные трудности.

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

Для продвижения в этом направлении среда языков программирования нуждается в формализации некоторых записей, относящихся к данным. Мы рассматриваем формализацию как запись абстрактных типов данных (АТД). АТД уделяют сейчас так же много внимания, как структурному програм­мированию два десятилетия назад. АТД не заменяют структурное програм­мирование. Скорее, АТД обеспечивают дополнительную формализацию, которая может улучшить процесс разработки программ.

Что такое абстрактный тип данных? Рассмотрим встроенный тип int.В голову приходит целое число в математике, однакоintв компьютере–это не то же самое, что целое в математикe.В частности, компьютерные значенияintобычно жестко ограничены по размеру. Например, на32-pазрядной машине значениеintможет быть ограничено диапазоном от +2 миллиардов до -2 миллиардов. Если результат вычислений выходит из этого диапазона, происходит ошибка и машина реагирует каким-либо машинно-ориентированным способом, включая возможность получения «втихомолку» неправильного результата. Для математических целых чисел этой проблемы не существует. Таким образом, компьютерная записьintна самом деле лишь приблизительно соответствует реально существующим целым числам. То же самое справедливо и по отношению к числам с плавающей десятичной запятойfloat.

Приближением является даже тип char; значенияcharобычно пред­ставляют собой 8-битовые образы, ничего общего не имеющие с символами, которые они отображают, такими как заглавная буква Z, строчная z, знак доллара ($), цифра (5) и т.д. Значения тинаchar в большинстве компьютеров жестко ограничены по сравнению с диапазоном реально существующих сим­волов. 7-битовый набор символов ANSI обеспечивает представление лишь 127 различных значений символов. Это совершенно неадекватно представ­лению таких языков как японский и китайский, которые требуют тысяч символов.

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

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

Абстрактные типы данных на самом деле охватывают два понятия, а именно, представление данных и операции, которые разрешены над этими данными. Например, запись intопределяет в C++ операции сложения, вы­читания, умножения, деления и модуля, но деление на нуль не определено; эти разрешенные операции выполняются способом, чувствительным к пара­метрам машины, таким, как размер фиксированного слова используемой ком­пьютерной системы. Другим примером является запись отрицательных целых чисел, для которых операции и представление данных ясны, но операция вычисления квадратного корня из отрицательного числа не определена. В C++ программист для реализации абстрактных типов данных использует классы.

2.8. Классы контейнеры и итераторы

К наиболее популярным типам классов относятся классы контейнеры(называемые такжеклассы совокупностей), т.е. классы, спроектированные для хранения в них совокупностей объектов. Классы контейнеры обычно снабжены такими возможностями, как вставка, удаление, поиск, сортировка, проверка наличия элемента в классе и тому подобное. Массивы, стеки, оче­реди, связные списки – это примеры классов контейнеров.

Принято ассоциировать объекты итераторы, или, короче –итераторы, с классами контейнерами. Итератор – это объект, который возвращает сле­дующий элемент совокупности (или определяет некоторое действие над сле­дующим элементом совокупности). Когда написан итератор класса, легко по­лучить следующий элемент этого класса. Итераторы обычно пишутся как друзья классов, с которыми они работают. Это предоставляет итераторам возможность прямого доступа к закрытым данным этих классов. Подобно тому, как книга, читаемая несколькими людьми, могла бы иметь в себе сразу несколько закладок, класс контейнер может иметь несколько одновре­менно работающих итераторов. Каждый итератор поддерживает свою собст­венную «позицию» информации.

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

1. Заполните пробелы в следующих утверждениях:

a) Для задания начальных значений постоянных элементов класса используется ___________________ .

b) Функция, не являющаяся элементом, которая должна иметь до­ступ к закрытым данным-элементам класса, должна быть объявлена как _________________ этого класса.

c) Операция ______ динамически выделяет память для объекта ука­занного типа и возвращает ____________ на этот тип.

d) Константный объект должен быть ___________; он не может быть из­менен после своего создания.

e) _____________ элемент данных имеет одну копию для всех объектов клас­са.

f) Функции-элементы объекта поддерживают указатель на объект, называемый указатель ____________.

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

h) Если объект-элемент класса не снабжен инициализатором, вы­зывается ________ этого класса.

i) Функция-элемент может быть объявлена как static, если она не должна иметь доступ к __________________ элементам класса.

j) Дружественные функции могут иметь доступ к элементам класса с доступом _________ и ___________ .

k) Объекты-элементы создаются _________ , чем объект включающего их класса.

l) Операция ______ освобождает память, выделенную перед этим с по­мощьюnew.

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

  1. сlass Example {

public:

Example(int у = 10) {data = у;}

int getIncrementedData( ) const {return ++data;}

static int getCount( )

{

cout<<"Data is " << data << endl;

return count;

}

private:

int data;

static int count;

};

b) char *string;

string = new char[20];

free(string);

3. ПЕРЕГРУЗКА ОПЕРАЦИЙ

3.1. Основы перегрузки операций

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

Операция << используется в С++для многих целей: и как операция поместить в поток, и как операция сдвига влево. Это пример перегрузки операции. Подобным же образом перегружается операция>>; она исполь­зуется и как операция взять из потока, и как операция сдвига вправо. Каждая из этих операций перегружена в библиотеке классов C++. Язык C++ сам по себе перегружает + и -. Эти операции выполняются по-разному, в зави­симости от того, входят ли они в выражения целочисленной арифметики, арифметики с плавающей запятой или арифметики указателей.

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

Программирование на C++ – процесс, чувствительный к типам и осно­ванный на типах. Программист может использовать встроенные типы, а может определить и новые типы. Встроенные типы можно использовать с богатым набором операций С++. Операции обеспечивают программиста краткими средствами записи для выражения манипуляций с объектами встроенного типа.

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

Хотя перегрузка операций звучит экзотически, большинство програм­мистов неявно регулярно ее используют. Например, операция сложения (+) выполняется для целых чисел, чисел с плавающей запятой и с удвоенной точностью совершенно по-разному. Но, тем не менее, сложение прекрасно работает с любыми типами – int,float,doubleи многими другими встро­енными типами, потому что операция сложения (+) перегружена в самом C++.

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

Чтобы использовать операцию над объектами классов, эта операция должна быть перегружена, но есть два исключения. Операция присваивания (=) может быть использована с каждым классом без явной перегрузки. По умолчанию операция присваивания сводится к побитовому копированиюданных-элементов класса. Такое побитовое копи­рование опасно для классов с элементами, которые указывают на динами­чески выделенные области памяти; для таких классов мы будем явно перегружать операцию присваивания. Операция адресации (&) также может быть использована с объектами любых классов без перегрузки; она просто возвращает адрес объекта в памяти. Но операцию адресации можно также и перегружать.

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

Цель перегрузки операций состоит в том, чтобы обеспечить такие же краткие выражения для типов, определенных пользователем, какие C++ обес­печивает с помощью богатого набора операций для встроенных типов. Однако перегрузка операций не выполняется автоматически; чтобы выполнить тре­буемые операции, программист должен написать функции, осуществляющие перегрузки операций. Иногда, эти функции лучше сделать функциями-эле­ментами, иногда – функциями-друзьями, а случается, что лучше не делать их ни элементами, ни друзьями.

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

3.2. Ограничения на перегрузку операций

Большинство операций С++ перегружать можно. Они показаны в таблице 3.1. В таблице 3.2 показаны операции, которые перегружать нельзя.

Таблица 3.1.

Таблица 3.2.

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

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

Изменить количество операндов, которое берет операция, невозможно: перегруженные унарные операции остаются унарными, перегруженные бинарные операции остаются бинарными. В C++ не может быть перегружена единственная тернарная операция ?:. Каждая из операций &, *, + и – может иметь унарный и бинарный варианты; эти унарные и бинарные варианты могут перегружаться раздельно.

Создавать новые операции невозможно; перегружать можно только уже существующие операции. Это запрещает программисту использовать попу­лярные нотации, подобные операции **, используемой в BASIC для экспо­ненты.

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

Перегрузка операций присваивания и сложения, разрешающая такие опе­раторы, как

object2 = object2 + object1;

не означает, что операция += также перегружается с целью разрешить такие операторы, как

оbject2 += object1;

Это может быть достигнуто только путем явной перегрузки операции += для данного класса.

3.3. Функции-операции как элементы класса и как дружественные функции

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

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

Реализована ли функция-операция как функция-элемент или нет, опе­рация в выражении реализуемся одинаково. Так какая же реализация лучше?

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

Перегруженная операция << должна иметь левый операнд типаostream &(такой, какcoutв выраженииcout << classObject), так что она не может быть функцией-элементом. Аналогично, перегруженная операция>> должна иметь левый операнд типаistream &(такой, какcinв выраженииcin << classObject), так что она тоже не может быть функцией-элементом. К тому же каждая из этих перегруженных функций-операций может потребовать доступа к закрытым элементам данным объекта класса, являющегося вход­ным или выходным потоком, так что эти перегруженные функции-операции делают иногда функциями-друзьями класса из соображений эффективности.

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

3.4. Перегрузка операций поместить в поток и взять из потока

C++ способен вводить и выводить стандартные типы данных, используя операцию поместить в поток >> и операцию ваять из потока<<. Эти операции уже перегружены в библиотеках классов, которыми снабжены компиляторы C++, чтобы обрабатывать каждый стандартный тип данных, включая строки и адреса памяти. Операции поместить в поток и взять из потока можно также перегрузить для того, чтобы выполнять ввод и вывод типов, опреде­ленных пользователем. Программа на рисунке 3.1 демонстрирует перегрузку операций поместить в поток и взять из потока для обработки данных определенного пользователем класса телефонных номеровPhoneNumber. В этой программе предполагается, что телефонные номера вводятся правильно.

На рис. 3.1 функция-операция взять из потока (operator>>)получает как аргументы ссылкуinputтипаistream, и ссылку, названнуюnum, на определенный пользователем типPhoneNumber; функция возвращает ссылку типаistream. Функция-операция (operator>>) используется для ввода номе­ров телефонов в виде

(800) 555-1212

в объекты класса PhoneNumber. Когда компилятор видит выражение

cin >> phone

в main, он генерирует вызов функции

operator>>(cin, рhone);

После выполнения этого вызова параметр inputстановится псевдонимом дляcin, а параметрnumстановится псевдонимом дляphone.

Результат компиляции программного кода:

Рисунок 3.1. Определенные пользователем операции

«поместить в поток» и «взять из потока»

Функция-опе­рация использует функцию-элемент getlineклассаistream, чтобы прочитать как строки три части телефонного номера вызванного объекта классаPhoneNmnber(numв функции-операции иphoneвmain) вareaCode(код местности),exchange(коммутатор) иline(линия). Символы круглых скобок, пробела и дефиса пропус­каются при вызове функции-элементаignoreклассаistream, которая отбра­сывает указанное количество символов во входном потоке (один символ по умолчанию). Функцияoperator>>возвращает ссылкуinputтипаistream(т.е.cin).Это позволяет операциям ввода объектовPhoneNumberбыть сцепленными с операциями ввода других объектовPhoneNumber или объектов других типов данных. Например, два объектаPhoneNumberмогли бы быть введены следующим образом:

cin >>phonel>>phone2;

Сначала было бы выполнено выражение cin >> phonel путем вызова

operator>>(cin, phonel);

Этот вызов мог бы затем вернуть ссылку на cinкак значение cin>>phonel, так что оставшаяся часть выражения была бы интерпретирована просто как cin>>phone2. Это было бы выполнено путем вызова

operator>>(cin,phone2);

Операция поместить в поток получает как аргументы ссылку output типаostream и ссылкуnumна определенный пользователем типPhoneNumberи возвращает ссылку типаostream.Функцияoperator<<выводит на экран объекты типаPhoneNumber. Когда компилятор видит выражение

cout <<phone

в main, он генерирует вызов функции

operator<<(cout, phone);

Функция operator<<выводит на экран части телефонного номера как строки, потому что они хранятся в формате строки (функция-элементgetlineклассаistream сохраняет нулевой символ после завершения ввода).

Заметим, что функции operator>>иoperator<<объявлены вclass PhoneNumberне как функции-элементы, а как дружественные функции. Эти операции не могут быть элементами, так как объект классаPhoneNumberпоявляется в каждом случае как правый операнд операции; а для перегру­женной операции, записанной как функция-элемент, операнд класса должен появляться слева. Перегруженные операции поместить в поток и взять из потока должны объявляться как дружественные, если они должны иметь прямой доступ к закрытым элементам класса по соображениям производи­тельности.

3.5. Перегрузка унарных операций

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

Перегрузим унарную операцию !, чтобы проверять, пуст ли объект класса String. Если унарная операция, такая, как !, пере­гружена как нестатическая функция-элемент без аргументов и еслиs– объект классаStringили ссылка на объект классаString, то, когда компи­лятор видит выражение!s, он генерирует вызовs.operator!( ). Операндs–это объект класса, для которого вызывается функция-элементoperator!клас­саString.Функция объявляется в описании класса следующим образом:

class String {

public:

int operator!() const;

};

Унарная операция, такая, как !, может быть перегружена как функция с одним аргументом, не являющаяся элементом, двумя различными способами: либо с аргументом, который является объектом (это требует копирования объекта, чтобы побочные эффекты функции не оказывали влияния на исходный объект), либо с аргументом, который является ссылкой на объект (никакой копии исходного объекта при этом не делается, но все побочные эффекты этой функции оказывают влияние на исходный объект). Если s– объект классаString(или ссылка на объект классаString), то!sтрактуется как вызовореrаtor!(s), активизирующий дружественную функцию, не являющуюся элементом классаString, но объявленную в нем следующим образом:

class String {

friendint operator! (const String &);

};

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

3.6. Перегрузка бинарных операций

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

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

class String {

public:

String &operator+= (const Stcing &);

};

Бинарная операция +=может быть перегружена как функция, не являющаяся элементом, с двумя аргументами, один из которых должен быть объектом класса или ссылкой на объект класса. Еслиуиz–объекты классаStringили ссылки на объект классаString, тоу += zрассматривается как вызовoperator+=(y,z), активизирующий объявленную ниже дружествен­ную функциюoperator+=, не являющуюся элементом:

class String {

friend String &operator+=(String &, const String &0);

};

3.7. Пример: класс массив

Запись массива в C++ является альтернативой указателям, так что мас­сивы могут служит источником множества ошибок. Например, программа может легко «выйти за пределы» массива, поскольку C++ не проверяет, не вышли ли индексы из допустимых пределов. Массивы размера nдолжны иметь номера 0, ... , n-1; иных индексов не может быть. Массив целиком не может быть введен или выведен сразу: каждый элемент массива должен быть считан или записан индивидуально. Два массива не могут быть сравнены друг с другом с помощью операций проверки на равенство или операций отношения. Когда массив передастся функции общего назначения, обраба­тывающей массивы произвольного размера, размер массива должен переда­ваться как дополнительный аргумент. Один массив не может быть присвоен другому с помощью операции присваивания. Эти и другие возможности, не­сомненно, кажутся «естественными» для работы с массивами, но C++ не обеспечивает таких возможностей. Однако, C++ обеспечивает средства для реализации этих возможностей посредством механизмов перегрузки опера­ций.

В этом примере мы разработаем класс массив, который выполняет про­верку диапазона, чтобы гарантировать, что индексы остаются в пределах границ массива. Класс допускает присваивание одного объекта массива дру­гому с помощью операции присваивания. Объекты этого класса автомати­чески узнают свой размер, так что при передаче массива функции передавать отдельно размер массива в качестве аргумента не требуется. Массив можно целиком выводить или вводить с помощью операции поместить в поток и взять из потока соответственно. Сравнение массивов можно осуществить с помощью операций = = и !=. Наш класс массив использует статический эле­мент, чтобы отследить количество объектов массивов, которые были созданы в программе. Этот пример отточит понимание абстракции данных.

Программа на рис. 3.2 демонстрирует класс Arrayи его перегруженные операции. Сначала мы проследим программу драйвер вmain. Затем рассмот­рим определение класса, каждую функцию-элемент класса и определения дружественных функций.

Статический элемент данных arrayCountклассаArrayсодержит коли­чество объектов, образованных во время выполнения программы. Программа начинается с использования статической функции-элементаgеtArrayCount, которая дает возможность получить количество созданных ранее массивов. Далее программа создает два объекта классаArray:integers1с семью эле­ментами иintegers2с десятью элементами по умолчанию (значение по умол­чанию указано конструкторомArray). Чтобы получить новое количество объ­ектов снова используется статическая функция-элементgetArrayCount. Функция-элементgetSizeвозвращает размер массиваintegers1. Программа выводит на экран размер массиваintegers1, затем выводит сам массив, используя перегруженную операцию поместить в поток, чтобы удостовериться, что конструктор задал элементам массива правильные начальные значения. Далее выводится размер массиваintegers2, а затем с помощью операции поместить в поток выводится сам массив.

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

cin >>integers1>>integers2;

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

Далее программа проверяет перегруженную операцию проверки на не­равенство путем оценки условия

integers1 != integers2

и сообщает, что массивы действительно не равны.

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

Далее программа проверяет перегруженную операцию присваивания с помощью выражения integers1 = integers2. Печатается содержимое обоих массивов, чтобы удостовериться, что присваивание было правильным. Инте­ресно отметить, чтоintegers1первоначально содержал 7 целых чисел и по­требовалось изменить его размер, чтобы вместить копию 10 элементов массиваintegers2. Как мы увидим, перегруженная операция присваивания легко вы­полняет это изменение размера.