Перевантаження операцій new і delete
Щоб забезпечити альтернативні варіанти управління пам'яттю, можна визначати власні варіанти операцій new і new[] для виділення динамічної пам'яті під об'єкт і масив об'єктів відповідно, а також операції delete і delete[] для її звільнення.
Ці функції-операції повинні відповідати наступним правилам:
їм не вимагається передавати параметр типу класу;
першим параметром функціям new і new[] повинен передаватися розмір объекта типу size_t (це тип, який повертається операцією sizeof, він визначається в заголовочному файлі <stddef.h>); при виклику він передається у функції неявним чином;
вони повинні визначатися з типом значення void*, який повертається, навіть якщо return повертає покажчик на інші типи (частіше всього на клас);
операція delete повинна мати тип повернення void і перший аргумент типу
void*;
операції виділення і звільнення пам'яті є статичними элементами класу.
Поведінка перевантажених операцій повинна відповідати діям, які виконуються ними за замовчуванням. Для операції new це означає, що вона повинна повертати правильне значення, коректно обробляти запит на виділення пам'яті нульового розміру і породжувати виключення при неможливості виконання запиту
Для операції delete слід дотримувати умову, що видалення нульового покажчика повинне бути безпечним, тому всередині операції необхідна перевірка покажчика на нуль і відсутність яких-небудь дій у разі рівності.
Стандартні операції виділення і звільнення пам'яті можуть використовуватися в області дії класу разом з перевантаженими (за допомогою операції доступа до області видимості :: для об'єктів цього класу і безпосередньо — для будь-кого інших).
Перевантаження операції виділення пам'яті застосовується для економії пам'яті, підвищення швидкодії програми або для розміщення даних в деякій конкретній області. Наприклад, нехай описується клас, який містить покажчик на деякий об'єкт:
class Obj {...};
class pObj{
private:
Obj *p;
};
При виділенні пам'яті під об'єкт типу pObj за допомогою стандартної операції new
pObj *р = new pObj;
фактична кількість байтів перевищуватиме sizeof(pObj), оскільки new звичайно записує в початок області, яка виділяється, її розмір (для того, щоб правильно відпрацьовувала операція delete). Для невеликих об'єктів ці невигідні витрати можуть виявитися вельми впливовими. Для економії пам'яті можна написати власну операцію new класу pObj, яка виділятиме великий блок пам'яті, а потім розміщуватиме в ньому покажчики на Obj. Для цього в об'єкт pObj вводиться статичне поле headOfFree, в якому зберігається покажчик на першу вільну комірку блоку для розміщення чергового об'єкту. Комірки які не використовуються зв'язуються в список. Щоб не позичати пам'ять під поле зв'язку, використовується об'єднання (union), за допомогою якого одна і таж комірка використовується або для розміщення покажчика на об'єкт, або для зв'язку з наступною вільною коміркою:
class pObj
{
public:
static void * operator new(size_t size);
....
private:
union
{
Obj *p; // Покажчик на об'єкт
pObj *next; // Покажчик на наступну вільну комірку
};
static const int BLOCK_SIZE; // Розмір блоку
// Заголовок списку вільних комірок:
static pObj *headOfFree;
};
void * pObj::operator new(size_t size)
{
// Перенаправити запити невірної кількості пам'яті
// стандартної операції new:
if (size != sizeof(pObj)) return ::operator new(size);
pObj *p headOfFree; // Покажчик на першу вільну комірку
// Перемістити покажчик списку вільних комірок:
if (p) headOfFree = р -> next;
// Якщо вільної пам'яті немає, виділяємо черговий блок:
else {
pObj *newblock = static_cast<pObj*>
(::operator new(BLOCK_SIZE * sizeof(pObj)));
// Всі комірки вільні, окрім першої (вона буде
// зайнята), зв'язуємо їх:
for (int i=1; і<BLOCK_SIZE-1;++j) newblock[i].next = &newblock[i+1];
newblock[BLOCK_SIZE - l].next = 0;
// Встановлюємо початок списку вільних комірок:
headOfFree = &newblock[l];
р = newblock;
} return p; // Повертаємо покажчик на виділену пам'ять
}
Перевантажена операція new успадковується, тому вона викликається для похідних об'єктів. Якщо їх розмір не відповідає розміру базового (а так, скорше всього, і є), це може викликати проблеми. Щоб їх уникнути, на початку операції перевіряється відповідність розмірів. Якщо розмір об'єкту не рівний тому, для якого перевантажена операція new, запит на виділення пам'яті передається стандартній операції new.
В програмі, яка використовує клас pObj, повинна бути присутня ініціалізація його статичних полів
pObj *pObj::headOfFree; // Встановлюється в 0 за замовчуванням
const int pObj::BLOCK_SIZE = 1024;
Як видно з цього прикладу, крім економії пам'яті досягається ще і висока швидкодія, адже в більшості випадків виділення пам'яті зводиться до декількох простих операторів.
Природно, що якщо операція new перевантажена, то ж саме повинне бути зроблено і для операції delete (наприклад, в нашому випадку стандартна операція delete не знайде на початку об'єкту вірної інформації про його розміри що приведе до невизначеної поведінки програми).
В розглянутому прикладі операція delete повинна додавати звільнений елемент пам'яті до списку вільних комірок:
void pObj::operator delete(void * ObjToDie. size_t size)
{
if (ObjToDie == 0) return;
if (size != sizeof(pObj))
{
::operator delete(ObjToDie);
return;
}
pObj *p= static_cast<pObj*>(ObjToDie);
p->next = headOfFree;
headOfFree = p;
}
В операції delete виконана перевірка відповідності розмірів об'єктів, аналогічна приведеної в операції new.
Перевантаження операції приведення типу
Можна визначити функції-операції, які здійснюватимуть перетворення об'єкту класу до іншого типу. Формат:
operator ім'я_нового_типу();
Тип значення, яке повертається, і параметри вказувати не вимагається. Можна визначати віртуальні функції перетворення типу. Приклад:
monstr::operator int(){return health;}
monstr Vasia;
cout << int(Vasia);
Перевантаження операції виклику функції
Клас, в якому визначена операція виклику функції, називається функціональним. Від такого класу не вимагається наявності
інших полів і методів:
class if_greater
{
public:
int operator() (int а, int b) const
{
return а > b;
}
};
Використання такого класу має вельми специфічний синтаксис. Наприклад:
if_greater x;
cout << х(1,5)<< endl; // Результат - 0
cout << if_greater()(5,1) << endl; // Результат - 1
Оскільки в класі if_greater визначена операція виклику функції з двома параметрами, вираз х(1,5) є допустимим (те ж саме можна записати у вигляді х.operator()(1, 5)). Як видно з прикладу об'єкт функціонального класу використовується так, якби він був функцією.
В другому операторі виведення вираз if_greater() використовується для виклику конструктора за замовчуванням класу if_greater.
Результатом виконання цього виразу є об'єкт класу if_greater. Далі, як і в попередньому випадку, для цього об'єкту викликається функція з двома аргументами, записаними в круглих дужках.
Операцію () можна визначати тільки як метод класу. Можна визначити перевантажені операції виклику функції з різною кількістю аргументів. Функціональні об'єкти широко застосовуються в стандартній бібліотеці C++.
Перевантаження операції індексації
Операція індексації [ ] звичайно перевантажується, коли тип класу представляє безліч значень, для якої індексація має сенс. Операція індексації повинна повертати посилання на елемент, який міститься в множині.Приклад класу Vect, призначеного для зберігання массива цілих чисел і безпечної роботи з ним:
#iinclude <iostream.h>
#iinclude <stdlib.h>
class Vect
{
public:
explicit Vect(int n = 10);
Vect(const int а[ ], int n); //ініціалізація масивом
~Vect() { delete [ ] p; }
int &operator [ ](int i);
void Print();
private:
int *p;
int size;
};
Vect::Vect(int n):Size(n)
{
p = new int[size];
};
Vect::Vect(const int a[], int n) : size(n)
{
p = new int[size];
for (int і = 0; і < size; i++)
p[i] - а[i];
}
// Перевантаження операції індексації:
int& Vect::operator [] (int i)
{
if(i < 0 || і >= size)
{
cout << "Невірний індекс (і = " << і << ")" << endl;
cout << "Завершення програми" << endl;
exit(0);
};
return p[i];
};
void Vect::Print()
{
for (int і = 0; і < size; i++)
cout << p[i]<< " "; cout << endl;
}
int main()
{
int arr[10]= {1, 2, 3, 4, 5, 6, 7, 8. 9, 10};
Vect а(arr,10);
а.Print();
cout << а[5]<< endl;
cout << а[12]<< endl;
return 0;
}
Результат роботи програми:
1 2 3 4 5 6 7 8 9 10
6
Невірний індекс (і = 12)
Завершення програми
Перевантажена операція індексації одержує цілий аргумент і перевіряє, чи лежить його значення в межах діапазону масиву. Якщо так, то повертається адрес елемента, що відповідає семантиці стандартної операції індексації.
В даному прикладі конструктор з параметром за замовчуванням оголошений як explicit для того, щоб він не був конструктором перетворення типу який викликається неявно. Ключове слово explicit вказує на те, що цей конструктор викликатиметься тільки явним чином. Операцію [] можна визначати тільки як метод класу.