Int a::getValue( ) const {return privateDateMember};
которая просто возвращает значение одного из данных-элементов объекта. Если константная функция-элемент описывается вне определения класса, то как объявление функции-элемента, так и ее описание должны включать const.
Здесь возникает интересная проблема для конструкторов и деструкторов, которые обычно должны изменять объект. Для конструкторов и деструкторов константных объектов объявление const не требуется. Конструктор должен иметь возможность изменять объект с целью присваивания ему соответствующих начальных значений. Деструктор должен иметь возможности выполнять подготовку завершения работ перед уничтожением объекта.
Программа на рис. 2.1 создает константный объект класса Time и пытается изменить объект неконстантными функциями-элементами setHour, setMinute и setSecond. Как результат показаны сгенерированные компилятором Borland С++ предупреждения. Опция компилятора была установлена такой, чтобы в случае появления любого предупреждения компилятор не создавал исполняемого файла.
Результат компиляции программного кода:
Рисунок 2.1. Использование класса Time с константными объектами и константными функциями-элементами
Хорошим стилем программирования считается объявление как const всех функций-элементов, которые предполагается использовать с константными объектами.
Ошибкой программирования является:
описание константной функции-элемента, которая изменяет данные-элементы объекта,
описание константной функции-элемента, которая вызывает неконстантную функцию-элемент,
вызов неконстантной функции-элемента для константного объекта,
попытка изменить константный объект.
Константный объект не может быть изменен с помощью присваивания, так что он должен получить начальное значение. Если данные-элементы класса объявлены как const, то надо использовать инициализатор элементов, чтобы обеспечить конструктор объекта этого класса начальными значением данных-элементов. Рис. 2.2 демонстрирует использование инициализатора элементов для задания начального значения константному элементу increment класса 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(константный указатель на объектEmployee). В константной функции-элементе класса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. Найдите ошибку или ошибки в каждом из следующих фрагментов программ и объясните, как их исправить.
с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. Как мы увидим, перегруженная операция присваивания легко выполняет это изменение размера.