Перегрузка унарных операций
Первый вариант перегрузки унарных операций состоит в том, что унарная функция-операция, определяемая в классе, должна быть представлена с помощью нестатического метода, не имеющего никаких параметров. При этом операндом является вызвавший её объект.
Пример 1. Перегрузка унарной операции с помощью функции-операции определённой в классе. <…> class Player{ <…> Player & operator --() { --currentLvlOfHealth; return *this; } }; Player myHero; cout << (--myHero). getHealth(); |
Второй вариант перегрузки состоит в том, что функция-операция определяется вне класса и имеет один параметр типа этого класса.
Пример 2. Перегрузка унарной операции с помощью функции-операции, определённой вне класса. <…> class Player{ <…> friend Player & operator --(Player &Pl) }; Player &operator --(Player &Pl){ --Pl.currentLvlOfHealth; return Pl; } |
Третий вариант состоит в том, что функция-операция может быть не описана внутри класса дружественная, однако в этом случае следует учитывать необходимость доступности изменяемых полей.
Добавим в класс дополнительный метод изменяющий количество очков игрока.
Пример 3. Перегрузка унарной операции с помощью функции, не описанной как дружественной внутри класса. class Player{ <…> public: void changePoints(int NOP){ numberOfPoints = NOP; } }; /*Теперь можно перегрузить операцию инкремента с помощью обычной функции, описанной вне класса*/ Payer & operator ++(Player &Pl){ int NP = Pl.getPoints(); NP++; Pl.changePoints(NP); return Pl; } |
Перегрузка операций постфиксного инкремента и декремента
Операции постфиксного инкремента и декремента должны иметь первый параметр типа int. Он, как правило, используется для того, чтобы их можно было отличить от префиксной формы записи.
Пример 4. Перегрузка унарной постфиксного декремента. <…> class Player{ <…> Player & operator --(int) { Player Pl(*this); currentLvlOfHealth--; return Pl; } }; Player myHero; cout << (myHero--).getHealth(); |
Перегрузка бинарных операций
Первый вариант перегрузки бинарных операций предполагает то, что бинарная функция-операция должна быть определена внутри класса и представлена с помощью нестатического метода с параметрами. Важно помнить о том, что экземпляр класса, вызвавший эту функцию-операцию, считается первым операндом.
Пример 5. Перегрузка бинарной операции с помощью функции-операции, определённой в классе. <…> class Player{ <…> booloperator > (constPlayer &Pl){ if(currentLvlOfHealth > maxHealth) return true; return false; } }; |
Второй вариант предполагает то, что функция-операция определяется вне класса. В таком случае она должна иметь два параметра типа класса.
Пример 6. Перегрузка бинарной операции с помощью функции-операции, определённой вне класса. booloperator > (constPlayer &Pl1, constPlayer &Pl2){ if(Pl1.getHealth() > Pl2.getHealth()) return true; return false; } |
Перегрузка операции присваивания
Операция присваивания определена в любом классе по умолчанию в качестве поэлементного копирования. Она вызывается каждый раз, когда одному существующему экземпляру класса присваивается значение другого. В том случае если класс содержит поля, под которые динамически выделяется память, необходимо определить собственную операцию присваивания. Для сохранения семантики присваивания, функция-операция всегда должна возвращать ссылку на экземпляр класса, для которого она вызвана. При этом она должна принимать в качестве параметра всего один аргумент – ссылку на присваиваемый объект.
Операцию присваивания можно определять исключительно в качестве метода класса. Также она не наследуется.
Пример 7. Перегрузка операции присваивания. class Player & operator = (const Player &Pl){ /*проверка на самоприсваивание*/ if (&Pl == this) return *this; currentLvlOfHealth = Pl.currentLvlOfHealth; presenceOfWeapins = Pl.presenceOfWeapins; impactForce = Pl.impactForce; } |
Перегрузка операций new и delete
Для обеспечения альтернативного варианта управления памятью, существует возможность определения собственных операций new и new[], предназначенных для выделения динамической памяти под экземпляр класса и массив объектов соответственно, и delete и delete[], предназначенных для её освобождения.
Существует ряд правил при создании функций-операций, используемых для перегрузки new и delete.
- Таким функциям-операциям не требуется передавать параметр типа класса.
- В качестве первого параметра функциям new и delete должен передаваться размер объекта типа size_t, возвращаемый функцией sizeof и при вызове передаваемый в качестве неявного параметра.
- Такие функции-операции должны быть определены с типом возвращаемого значения void*, даже если return возвращает указатель на иные типы данных.
- Операция delete должна иметь тип возвращаемого значения void и первый аргумент типа void*.
- Операции выделения и освобождения памяти являются статическими элементами класса.
Важно иметь ввиду то, что поведение этих перегруженных операций обязательно должно соответствовать действиям, которые они выполняют по умолчанию.
Операция new выполняет следующие действия:
- возвращение правильного значения;
- корректная обработка запроса на выделение памяти нулевого размера;
- порождение исключения при невозможности выполнения запроса.
Операция delete выполняет безопасное удаление нулевых указателей.
Для обеспечения описанного действия в операции delete внутри неё необходима проверка указателя на нуль или отсутствие каких-либо действиях в случае равенства.
Стандартные операции выделения и освобождения памяти могут использоваться в области действия класса наряду с перегруженными с помощью операции «::» для объектов этого класса и любых других.
Перегрузка операции выделения памяти применяется для экономии памяти, повышения быстродействия программы или размещения данных в некоторой конкретной области.
Пример 8. Перегрузка операции new. class Object { <…> }; class PtrObject{ <…> private: Object *ptr; }; PtrObject *ptrPO = new PtrObject; |
При выделении участка памяти под объект типа PtrObject с помощью стандартной операции new фактический объём памяти будет превышать тот, что возвращает sizeof(PtrObject). Чтобы операция delete отрабатывала правильно, new, как правило, записывает в начало выделяемого участка памяти его размер.
С целью экономии памяти можно определять собственную операцию new для класса PtrObject, которая будет выделять блок памяти достаточного размера, после чего будет выполнять размещение указателей на Object.
Теперь добавим в класс, описанный в примере 8, статическое поле, указывающее на начало выделенного блока памяти. Идентификатор такого поля – headOfFreeSpace. Прочие неиспользуемые ячейки определим списком. Для того, чтобы не занимать память под поле связи будем использовать объединение, которое позволит использовать одну и ту же ячейку памяти для размещения либо указателя на объект, либо указателя на следующую ячейку памяти.
Пример 9. Перегрузка операции new. Развитие примера 8. class PtrObject{ <…> private: Object *ptr; union{ Object *ptrO; //Указатель на сам объект PtrObject *next; /*Указатель на следующую ячейку памяти*/ }; static const int sizeOfBlock; //Размер блока памяти static PtrObject * headOfFreeSpace; public: static void * operator new(size_t size); }; void *PtrObject:: operator new(size_t size){ /*Отказ запросам с количеством памяти несоответствующим размеру PtrObject и их перенаправление стандартной операции new*/ if (size!= sizeof(PtrObject)) return PtrObject:: operator new(size); PtrObject *ptrO = headOfFreeSpace; /*Смещение указателя списка свободных ячеек.*/ if (ptrO) headOfFreeSpace- ptrO->next; /*Если свободная память отсутствует, необходимо выделение блока памяти.*/ else{ PtrObject *newBlock = static_cast< PtrObject*> (PtrObject:: operator new (sizeOfBlock * sizeof(PtrObject))); for (int i = 0; I < sizeOfBlock-1; i++) newBlock[i].next = &newBlock[i+1]; newBlock[sizeOfBlock-1].next = 0; /*Установка начала списка свободных ячеек.*/ headOfFreeSpace = &newBlock[i]; ptrO = newBlock; } return ptrO; /*Возвращение указателя на выделенную память*/ } |
Так как перегруженная операция new наследуется, она вызывается для всех дочерних объектов. В том случае если размер дочерних объектов не соответствует размеру базового, могут появиться непредвиденные ошибки, избежать которые можно, установив проверку на соответствие размеров. Её можно сформулировать следующим образом: «если размер экземпляра класса не равен тому, для которого была перегружена операция new, запрос на выделение участка памяти должен быть передан стандартной операции new ».
В программе, которая использует класс PtrObject, обязательно должна быть инициализация его статических полей.
Пример 10. Инициализация статических полей. PtrObject *PtrObject:: headOfFreeSpace; const int PtrObject:: sizeOfBlock = 1024; |
Если операция new перегружена, то же самое должно быть выполнено и для операции delete.
В соответствии с примерами выше, делаем вывод о том, что перегруженная операция delete должна добавлять освобождённую ячейку памяти к общему списку свободных ячеек.
Пример 11. Перегрузка операции delete. class PtrObject{ <…> private: <…> static void operator delete(void*freeSpace, size_t size){ /*Проверка на соответствие размеров*/ if(freeSpace == 0) return; if(size!= sizeOf(PtrObject)){ PtrObject:: operator delete(freeSpace); return; } PtrObject *ptrO = static_cast<PtrObject>(freeSpace); ptrO->next = headOfFreeSpace; headOfFreeSpace = ptrO; } }; PtrObject *ptrPO = new PtrObject; |
Как видите из примера, реализация проверки соответствия размеров в операции delete аналогична таковой в операции new.