Namespace CounterNameSpace 1 страница

Else

Else

Else

Else

Else

Else

Else

Else

Else

Else

Else

Else

return false;

}

Якщо вважати, що операторна функція operator==() вже реалізована, то такий код програми є абсолютно коректним:

kooClass A_ob, B_ob;

//...

if(A_ob == B_ob) cout << "A_ob = B_ob\n";

else cout << "A_ob не дорівнює B_ob\n";

Оскільки операторна функція operator==() повертає результат типу bool, то її можна використовувати для керування настановою if. Як вправу рекомендуємо самостійно реалізувати і інші оператори відношення та логічні оператори для класу kooClass.

14.3. Особливості реалізації оператора присвоєння

У попередньому розділі ми розглянули потенційну проблему, пов'язану з передачею об'єктів функціям і поверненням об'єктів з функцій. У обох випадках проблема була викликана використанням конструктора за замовчуванням, який створює побітову копію об'єкта. Згадаймо, що вирішення цього питання полягає у створенні власного конструктора копії, який точно визначає, як повинна бути створена копія об'єкта.

Подібна проблема може виникати і під час присвоєння одного об'єкта іншому. За замовчуванням об'єкт, який знаходиться з лівого боку від оператора присвоєння, отримує побітову копію об'єкта, що знаходиться справа. До негативних наслідків це може призвести у випадках, коли під час створення об'єкт виділяє певний ресурс (наприклад, пам'ять), а потім змінює його або звільняє. Якщо після виконання операції присвоєння об'єкт змінює або звільняє цей ресурс, то другий об'єкт також змінюється, оскільки він все ще використовує його. Вирішення цього питання полягає в перевантаженні оператора присвоєння.

Щоб до кінця зрозуміти суть описаної проблеми, розглянемо таку (некоректну) програму.

Код програми 14.7. Демонстрація виявлення помилки, що генерується під час повернення об'єкта з функції

#include <iostream>// Для потокового введення-виведення

#include <cstring> // Для роботи з рядковими типами даних

#include <cstdlib> // Для використання бібліотечних функцій

using namespace std; // Використання стандартного простору імен

 

class strClass { // Оголошення класового типу

char *s;

public:

strClass() { s = 0;}

strClass(const strClass &obj); // Оголошення конструктора копії

~strClass() {if(s) delete[]s; cout << "Звільнення s-пам'яті.\n";}

void showRez(char *s) { cout << "s= " << s << "\n";}

void setStr(char *str);

};

 

// Визначення конструктора копії.

strClass::strClass(const strClass &obj)

{

s = new char[strlen(obj.s)+1];

strcpy(s, obj.s);

}

 

// Завантаження рядка.

void strClass::setStr(char *str)

{

s = new char[strlen(str)+1];

strcpy(s, str);

}

 

// Ця функція повертає об'єкт типу strClass.

strClass input()

{

char strMas[80];

strClass s_ob;

 

cout << "Введіть рядок: "; cin >> strMas;

 

s_ob.setStr(strMas);

 

return s_ob;

}

 

int main()

{

strClass S_ob; // Створення об'єкта класу

 

//Присвоюємо об'єкт, повернутий функцією input(), об'єкту S_ob.

S_ob = input(); // Ця настанова генерує помилку!!!!

S_ob.showRez();

 

getch(); return 0;

}

Можливі результати виконання цієї програми мають такий вигляд:

Введіть рядок: Привіт

Звільнення s-пам'яті.

Звільнення s-пам'яті.

s= тут "сміття"

Звільнення s-пам'яті.

Залежно від використовуваного компілятора, Ви можете побачити "сміття" або ні. Програма може також згенерувати помилку тривалості виконання. У будь-якому випадку помилки не минути. І ось чому.

У цій програмі конструктор копії коректно обробляє повернення об'єкта функцією input(). Згадаймо, що у разі, коли функція повертає об'єкт, для зберігання значення, що повертається нею, створюється тимчасовий об'єкт. Оскільки під час створення об'єкта-копії конструктор копії виділяє нову область пам'яті, то член s початкового об'єкта і член s об'єкта-копії вказуватимуть на різні області пам'яті, які, як наслідок, не стануть псувати один одного.

Проте помилки не минути, якщо об'єкт, який повертається функцією, присвоюється об'єкту S_ob, оскільки у процесі виконання операції присвоєння за замовчуванням створюється побітова його копія. У цьому випадку тимчасовий об'єкт, який повертається функцією input(), копіюється в об'єкт S_ob. Як наслідок, член obj.s вказує на ту ж саму область пам'яті, що і член s тимчасового об'єкта. Але після виконання операції присвоєння в процесі руйнування тимчасового об'єкта ця пам'ять звільняється. Отже, член obj.s тепер вказуватиме на вже звільнену пам'ять! Понад це, пам'ять, яка адресується членом obj.s, повинна бути звільнена і після завершення роботи програми, тобто удруге. Щоб запобігти цьому, необхідно перевантажити оператор присвоєння так, щоб об'єкт, який розташовується зліва від оператора присвоєння, виділяв власну область пам'яті.

Реалізацію цього рішення покажемо у такій відкоректованій програмі.

Код програми 14.8. Демонстрація коректної роботи програми

#include <vcl>

#include <iostream>// Для потокового введення-виведення

#include <cctype> // Для роботи з символьними аргументами

#include <cstring> // Для роботи з рядковими типами даних

#include <conio>// Для консольного режиму роботи

using namespace std; // Використання стандартного простору імен

 

class strClass { // Оголошення класового типу

char *s;

public:

strClass(); // Оголошення звичайного конструктора

strClass(const strClass &obj); // Оголошення конструктора копії

~strClass() { if(s) delete[]s; cout << "Звільнення s-пам'яті.\n";}

void showRez(char *s) { cout << "s= " << s << "\n";}

void setStr(char *str);

strClass operator=(const strClass &obj); // Перевантажений

// оператор присвоєння

};

 

// Визначення звичайного конструктора.

strClass::strClass()

{

s = new char ('\0'); // Член s вказує на NULL-рядок.

}

 

// Визначення конструктора копії.

strClass::strClass(const strClass &obj)

{

s = new char[strlen(obj.s)+1];

strcpy(s, obj.s);

}

 

// Завантаження рядка.

void strClass::setStr(char *str)

{

s = new char[strlen(str)+1];

strcpy(s, str);

}

 

// Перевантаження оператора присвоєння "=".

strClass strClass::operator=(const strClass &obj)

{

/* Якщо виділена область пам'яті має недостатній

розмір, виділяється нова область пам'яті. */

if(strlen(obj.s) > strlen(s)) {

delete[]s;

s = new char[strlen(obj.s)+1];

}

strcpy(s, obj.s);

 

// Повернення модифіков. об'єкта операнда, адресованого покажчиком

return *this;

}

 

// Ця функція повертає об'єкт типу strClass.

strClass input()

{

char strMas[80];

strClass s_ob;

 

cout << "Введіть рядок: "; cin >> strMas;

s_ob.setStr(strMas);

 

return s_ob;

}

 

int main()

{

strClass S_ob; // Створення об'єкта класу

 

// Присвоюємо об'єкт, повернутий функцією input(), об'єкту S_ob

S_ob = input(); // Тепер тут все гаразд!

S_ob.showRez();

 

getch(); return 0;

}

Ця програма тепер відображає такі результати (у припущенні, що на пропозицію "Введіть рядок: " Ви введете "Привіт").

Введіть рядок: Привіт

Звільнення s-пам'яті.

Звільнення s-пам'яті.

Звільнення s-пам'яті.

Привіт

Звільнення s-пам'яті.

Як бачимо, ця програма тепер працює коректно. Спробуйте детально проаналізувати програму і зрозуміти, чому виводиться кожне з повідомлень "Звільнення s-пам'яті.".[59]

14.4. Перевантаження оператора індексації масивів ([])

На додаток до традиційних операторів мова програмування C++ дає змогу перевантажувати і більш "екзотичні", наприклад, оператор індексації масивів ([]). У мові програмування C++ (з погляду механізму перевантаження) оператор "[]" вважається бінарним. Його можна перевантажувати тільки для класу і тільки з використанням функції-члена класу. Ось як виглядає загальний формат операторної функції-члена класу operator[]().

тип ім'я_класу::operator[](int індекс)

{

//...

}

Формально параметр індекс необов'язково повинен мати тип int, але операторна функція operator[]() зазвичай використовують для забезпечення індексації масивів, тому в загальному випадку як аргумент цієї функції передається цілочисельне значення.

Оператор "[]" перевантажується як бінарний оператор.

Припустимо, нехай визначено об'єкт A_ob, тоді вираз A_ob[-3]перетвориться в такий виклик операторної функції operator[]():

A_ob.operator[](3);

Іншими словами, значення виразу, що задається в операторі індексації, передається операторній функції operator[]() як безпосередньо заданий аргумент. При цьому покажчик this вказуватиме на об'єкт A_ob, тобто об'єкт, який генерує виклик цієї функції.

У наведеному нижче коді програми в класі aClass оголошується масив для зберігання трьох int-значень. Його конструктор ініціалізує кожного члена цього масиву. Перевантажена операторна функція operator[]()| повертає значення елемента, що задається його параметром.

Код програми 14.9. Демонстрація механізму перевантаження оператора індексації масиву "[]"

#include <iostream>// Для потокового введення-виведення

using namespace std; // Використання стандартного простору імен

 

const int size = 3;

classaClass { // Оголошення класового типу

int aMas[size];

public:

aClass() { register int i; for(i=0; i<size; i++) aMas[i]= i;}

int operator[](int i) {return aMas[i];}

};

 

int main()

{

aClass A_ob;

cout << A_ob[2]; // Відображає число 2

 

getch(); return 0;

}

У цій програмі функція operator[]() повертає значення 1-го елемента масиву aMas. Таким чином, вираз A_ob[2] повертає число 2, яке відображається настановою cout. Ініціалізація масиву aMas за допомогою конструктора (у цій і наступній програмах) виконується тільки з ілюстративною метою.

Можна розробити операторну функцію operator[]() так, щоб оператор "[]" можна було використовувати як зліва, так і праворуч від оператора присвоєння. Для цього достатньо вказати, що значення, яке повертається операторною функцією operator[](), є посиланням. Цю можливість продемонстровано у наведеному нижче коді програми.

Код програми 14.10. Демонстрація повернення посилання з операторної функції operator()[]

#include <iostream>// Для потокового введення-виведення

using namespace std; // Використання стандартного простору імен

 

const int size = 3;

classaClass { // Оголошення класового типу

int aMas[size];

public:

aClass() { register int i; for(i=0; i<size; i++) aMas[i] = i;}

int &operator[](int i) {return aMas[i];}

};

 

int main()

{

aClass A_ob;

cout << A_ob[2]; // Відображається число 2.

cout << " ";

 

A_ob[2] = 25; // Оператор "[]" знаходиться зліва від оператора присвоєння "=".

cout << A_ob[2]; // Тепер відображається число 25.

 

getch(); return 0;

}

У процесі виконання ця програма відображає на екрані такі результати:

2 25

Оскільки функція operator[]() тепер повертає посилання на елемент масиву, індексований параметром i, то оператор "[]" можна використовувати зліва від оператора присвоєння, що дасть змогу модифікувати будь-який елемент масиву[60].

Одна з наявних переваг перевантаження оператора "[]" полягає у тому, що за його допомогою ми можемо забезпечити засіб реалізації безпечної індексації масивів. Як уже зазначалося вище, у мові програмування C++ можливий вихід за межі масиву у процесі виконання програми без відповідного повідомлення (тобто без генерування повідомлення про динамічну помилку). Але, якщо створити клас, який містить масив, і надати доступ до цього масиву тільки через перевантажений оператор індексації "[]", то можливе перехоплення індексу, значення якого вийшло за дозволені межі. Наприклад, наведений нижче код програми (в основу якої покладений програмний код попередньої) оснащена засобом контролю попадання в допустимий інтервал.

Код програми 14.11. Демонстрація прикладу організації безпечного масиву

#include <iostream>// Для потокового введення-виведення

#include <cstdlib> // Для використання бібліотечних функцій

using namespace std; // Використання стандартного простору імен

 

const int size = 3;

classaClass { // Оголошення класового типу

int aMas[size];

public:

aClass() { register int i; for(i=0; i<size; i++) aMas[i] = i;}

int &operator[](int i);

};

 

// Забезпечення контролю попадання в допустимий інтервал для класу aClass.

int &аClass::operator[](int i)

{

if(i<0 || i> size-1) {

cout << "\n Значення| індексу " << i

<< " виходить за межі масиву.\n";

exit(1);

}

return aMas[i];

}

 

int main()

{

aClass A_ob;

cout << A_ob[2]; // Відображається число 2.

cout << " ";

 

A_ob[2] = 25; // Оператор "[]" знаходиться в лівій частині.

cout << A_ob[2]; // Відображається число 25.

 

A_ob[3] = 44; // Генерується помилка часу виконання.

// оскільки значення 3 виходить за межі масиву.

getch(); return 0;

}

У процесі виконання ця програма виводить такі результати:

2 25

Значення індексу 3 виходить за межі масиву.

У процесі виконання настанови

A_ob[3] = 44;

операторною функцією operator[]() перехоплюється помилка порушення меж масиву, після чого програма відразу завершується, щоб не допустити ніяких потенційно можливих руйнувань.

14.5. Перевантаження оператора "()"

Можливо, найбільш інтригуючим оператором, якого можна перевантажувати, є оператор "()" (оператор виклику функцій). Під час його перевантаження створюється не новий спосіб виклику функцій, а операторна функція, якій можна передати довільну кількість параметрів. Почнемо з такого прикладу. Припустимо, що певний клас містить наведене нижче оголошення перевантаженої операторної функції:

int operator()(float f, char *p);

І якщо у програмі створюється об'єкт obj цього класу, то настанова

obj(99.57, "перевантаження");

перетвориться в такий виклик операторної функції operator():

operator()(99.57, "перевантаження");

У загальному випадку під час перевантаження оператора "()" визначаються параметри, які необхідно передати функції operator(). Під час використання оператора "()" у програмі задані аргументи копіюються в ці параметри. Як завжди, об'єкт, який генерує виклик операторної функції (obj у наведеному прикладі), адресується покажчиком this.

Розглянемо приклад перевантаження оператора "()" для класу kooClass. Тут створюється новий об'єкт класу kooClass, координати якого є результати підсумовування відповідних значень координат об'єкта і значень, що передаються як аргументи.

Код програми 14.12. Демонстрація механізму перевантаження оператора "()"

#include <iostream>// Для потокового введення-виведення

using namespace std; // Використання стандартного простору імен

 

class kooClass { // Оголошення класового типу

int x, y, z; // Тривимірні координати

public:

kooClass() { x = y = z = 0;}

kooClass(int izm, int jzm, int kzm) {x = izm; y = jzm; z = kzm;}

kooClass operator()(int a, int b, intc);

void showRez(char *s);

};

 

// Перевантаження оператора "()".

kooClass kooClass::operator()(int a, int b, intc)

{

kooClass tmp; // Створення тимчасового об'єкта

tmp.x = x + а;

tmp.y = y + b;

tmp.z = z + c;

 

return tmp; // Повертає модифікований тимчасовий об'єкт

}

 

// Відображення тривимірних координат x, y, z.

void kooClass::showRez(char *s)

{

cout << "Координати об'єкта <" << s << ">:\n";

cout << "\tx= " << x << ", y= " << y << ", z= " << z << "\n";

}

 

int main()

{

kooClass A_ob(1, 2, 3), B_ob;

 

B_ob = A_ob(10, 11, 12); // Виклик функції operator()

cout << "A_ob: ";

A_ob.showRez();

cout << "B_ob: ";

B_ob.showRez();

 

getch(); return 0;

}

Внаслідок виконання цієї програми на моніторі буде відображено такі результати:

A_ob: 1, 2, 3

B_ob: 11, 13, 15

Не забувайте, що під час перевантаження оператора "()" можна використовувати параметри будь-якого типу, та і сама операторна функція operator() може повертати значення будь-якого типу. Вибір типу повинен диктуватися потребами конкретних програм.

14.6. Перевантаження інших операторів

За винятком таких операторів, як new, delete, ->, ->* і "кома", решту С++-оператори можна перевантажувати таким самим способом, як було показано в попередніх прикладах. Перевантаження операторів new і delete вимагає застосування спеціальних методів, повний опис яких наведено в розд. 18 (він присвячений обробленню виняткових ситуацій). Оператори ->, ->* і "кома" – це спеціальні оператори, детальний перегляд яких виходить за рамки цього навчального посібника. Читачі, яких цікавлять інші приклади перевантаження операторів, можуть звернутися до такої книги [27].

14.6.1. Приклад перевантаження операторів класу рядків

Завершуючи тему перевантаження операторів, розглянемо приклад, який часто називають квінтесенцією прикладів, присвячених перевантаженню операторів, а саме клас рядків. Незважаючи на те, що С++-підхід до рядків (які реалізуються у вигляді символьних масивів, що завершуються нулем, а не як окремий тип) є дуже ефективним і гнучким, проте початківці С++-програмісти часто випробовують недолік у понятійній ясності реалізації рядків, яка наявна в таких мовах, як BASIC. Звичайно ж, цю ситуацію неважко змінити, оскільки у мові програмування C++ існує можливість визначити клас рядків, який забезпечуватиме реалізацію рядків подібно до того, як це зроблено в інших мовах програмування. Правду кажучи, на початкових етапах розвитку мови програмування C++ реалізація класу рядків була забавою для програмістів. І хоча стандарт мови програмування C++ тепер визначає рядковий клас, який описано далі у цьому навчальному посібнику, проте спробуйте самостійно реалізувати простий варіант такого класу. Ця вправа наочно ілюструє потужність механізму перевантаження операторів.

Спочатку визначимо "класовий" тип strClass:

// Перевантаження оператора "string"

#include <iostream>// Для потокового введення-виведення

#include <cstring> // Для роботи з рядковими типами даних

using namespace std; // Використання стандартного простору імен

 

class strClass { // Оголошення класового типу

char string[80];

public:

strClass(char *str = "") { strcpy(string, str);}

strClass operator+(strClass s_ob); // Конкатенація рядків

strClass operator=(strClass s_ob); // Присвоєння рядків

void showStr() { cout << string;} // Виведення рядка

};

Як бачимо, в класі strClass оголошується закритий символьний масив string, призначений для зберігання рядка. У наведеному прикладі домовимося, що розмір рядків не перевищуватиме 79 байтів. У реальному ж класі рядків пам'ять для їх зберігання повинна виділятися динамічно, однак це обмеження зараз діяти не буде. Окрім того, щоб не захаращувати логіку цього прикладу, ми вирішили звільнити цей клас (і його функції-члени) від контролю виходу за межі масиву. Безумовно, в будь-якій справжній реалізації подібного класу повинен бути забезпечений повний контроль за помилками.

Цей клас має один конструктор, який можна використовувати для ініціалізації масиву string з використанням заданого значення або для присвоєння йому порожнього рядка у разі відсутності ініціалізації. У цьому класі також оголошуються два перевантажені оператори, які виконують конкатенацію і присвоєння. Нарешті, клас strClass містить функцію showStr(), яка виводить рядок на екран. Ось як виглядають коди операторних функцій operator+() і operator=():

// Конкатенація двох рядків

strClass strClass::operator+(strClass s_ob)

{

strClass tmp; // Створення тимчасового об'єкта

strcpy(tmp.string, string);

strcpy(tmp.string, s_ob.string);

 

return tmp; // Повертає модифікований тимчасовий об'єкт

}

 

// Присвоєння одного рядка іншому

strClass strClass::operator=(strClass s_ob)

{

strcpy(string, s_ob.string);

// Повернення модифіков. об'єкта операнда, адресованого покажчиком

return *this;

}

Маючи визначення цих функцій, продемонструємо, як їх можна використовувати, на прикладі наведеної нижче функції main():

int main()

{

strClass A_ob("Всім "), B_ob("привіт"), C_ob;

 

C_ob = A_ob + B_ob;

C_ob.showStr();

 

getch(); return 0;

}

У процесі виконання ця програма виводить на екран рядок "Всім привіт". Спочатку вона конкатенує рядки (об'єкти класу strClass) A_ob і B_ob, а потім присвоює результат конкатенації рядку C_ob.

Потрібно мати на увазі, що оператори "=" і "+" визначено тільки для об'єктів типу strClass. Наприклад, така настанова нероботоздатна, оскільки вона є спробою присвоїти об'єкту A_ob рядок, що завершується нульовим символом:

A_ob = "Цього поки робити не можна.";

Але клас strClass, як буде показано далі, можна удосконалити і дати змогу виконання таких настанов.

Для розширення кола операцій, підтримуваних класом strClass(наприклад, щоб можна було об'єктам типу strClass присвоювати рядки, що мають завершальний нуль-символ, або конкатенувати рядок, що завершується нульовим символом, з об'єктом типу strClass), необхідно перевантажити оператори "=" і "+" ще раз. Спочатку змінимо оголошення класу:

// Перевантаження рядкового класу: остаточний варіант

class strClass { // Оголошення класового типу

char string[80];

public:

strClass(char *str = "") { strcpy(string, str);}

strClass operator+(strClass s_ob); /* Конкатенація об'єктів

типу strClass */

strClass operator+(char *str); /* Конкатенація об'єкта типу strClass

з рядком, що завершується нулем */

strClass operator=(strClass s_ob); /* Присвоєння одного об'єкта

типу strClass іншому */

char *operator=(char *str); /* Присвоєння рядка об'єкту типу

strClass, що завершується нулем */

void showStr() { cout << string;}

};

Потім реалізуємо перевантаження операторних функцій operator+() і operator=():

// Присвоєння рядка об'єкту типу strClass, що завершується нулем

strClass strClass::operator=(char *str)

{

strClass tmp; // Створення тимчасового об'єкта