Sizeof value 9 страница

Під час передачі об'єкта функції створюється його копія (і ця копія стає параметром у функції). Створення копії означає появу нового об'єкта. Коли виконання функції завершується, копія аргументу (тобто параметр) руйнується. Тут виникає відразу два запитання. По-перше, чи викликається конструктор об'єкта під час створення копії? По-друге, чи викликається деструктор об'єкта під час руйнування копії? Відповіді на ці запитання можуть здивувати Вас.

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

Однак, коли функція завершує свою роботу, то руйнується копія об'єкта, який використовується як аргумент, для чого викликається деструктор цього об'єкта. Необхідність виклику деструктора пов'язана з виходом об'єкта з області видимості його функцією, у якій він використовується. Саме тому попередня програма мала два звернення до деструктора. Перше відбулося при виході з області видимості параметра функції display(), а друге – під час руйнування об'єкта A_ob у функції main() після завершення роботи програми.

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

13.5.2. Потенційні проблеми під час передачі параметрів

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

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

#include <vcl>

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

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

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

 

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

int *p;

public:

myClass(int izm);

~myClass();

int getVal() { return *p;}

};

 

myClass::myClass(int izm)

{

cout << "Виділення пам'яті, яка адресується покажчиком p.\n";

p = new int;

*p = izm;

}

 

myClass::~myClass()

{

cout << "Звільнення пам'яті, яка адресується покажчиком p.\n";

delete p;

}

 

// У процесі виконання цієї функції якраз і виникає проблема.

void display(myClass a_ob) // Звичайна передача об'єкта

{

cout << "*p= " << a_ob.getVal() << "\n";

}

 

int main()

{

myClass A_ob(10); // Створення об'єкта класу

 

display(A_ob);

 

getch(); return 0;

}

Ось як виглядають результати виконання цієї програми.

Виділення пам'яті, яка адресується покажчиком p.

*p= 10

Звільнення пам'яті, яка адресується покажчиком p.

Звільнення пам'яті, яка адресується покажчиком p.

Ця програма містить принципову помилку. І ось чому: під час створення у функції main() об'єкта A_ob виділяється область пам'яті, адреса якої присвоюється покажчику A_ob.р. Під час передачі функції display() об'єкт A_ob копіюється в параметр a_ob. Це означає, що обидва об'єкти (A_ob і a_ob) матимуть однакове значення для покажчика р.

Іншими словами, в обох об'єктах (в оригіналі та його копії) член даних р вказуватиме на одну і ту саму динамічно виділену область пам'яті. Після завершення роботи функції display() об'єкт a_ob руйнується, і його руйнування супроводжується викликом деструктора. Деструктор звільняє область пам'яті, яка адресується покажчиком a_ob.р. Але ж ця (вже звільнена) область пам'яті – та ж сама область, на яку все ще вказує член даних (початкового об'єкта) A_ob.р! Тобто, як на перший погляд – виникає серйозна помилка.

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

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

Код програми 13.10. Демонстрація механізму вирішення проблеми під час передачі об'єктів функціям

#include <vcl>

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

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

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

 

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

int *p;

public:

myClass(int izm);

~myClass();

int getVal() { return *p;}

};

 

myClass::myClass(int izm)

{

cout << "Виділення пам'яті, яка адресується покажчиком p.\n";

p = new int;

*p = izm;

}

 

myClass::~myClass()

{

cout << "Звільнення пам'яті, яка адресується покажчиком p.\n";

delete p;

}

 

// Ця функція НЕ створює проблем.

void display(myClass &a_ob) // Передача об'єкта за посиланням

{

cout << "*p= " << a_ob.getVal() << "\n";

}

 

int main()

{

myClass A_ob(10); // Створення об'єкта класу

 

display(A_ob);

 

getch(); return 0;

}

Оскільки об'єкт a_ob тепер передається за посиланням, то копія аргументу не створюється, а отже, об'єкт не виходить з області видимості після завершення роботи функції display(). Результати виконання цієї версії програми виглядають набагато краще від попередніх:

Виділення пам'яті, яка адресується покажчиком p.

*p= 10

Звільнення пам'яті, яка адресується покажчиком p.

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

Передача об'єкта за посиланням – прекрасне вирішення описаної вище проблеми, але тільки у випадках, коли ситуація дає змогу прийняти таке рішення, що буває далеко не завжди. На щастя, є більш загальне рішення: можна створити власну версію конструктора копії. Це дасть змогу точно визначити, як саме потрібно створювати копію об'єкта і тим самим уникнути описаних вище проблем. Але перед тим, як ми займемося конструктором копії, є сенс розглянути ще одну ситуацію, у вирішенні якої ми також можемо отримати певний виграш завдяки створенню конструктора копії.

13.6. Повернення об'єктів функціями

Якщо об'єкти можна передавати функціям, то з таким самим успіхом функції можуть повертати об'єкти. Щоби функція могла повернути об'єкт, по-перше, необхідно оголосити значення, що повертається нею, типом відповідного класу. По-друге, потрібно забезпечити повернення об'єкта цього типу за допомогою звичайної настанови return. Розглянемо приклад функції, яка повертає об'єкт.

Код програми 13.11. Демонстрація механізму повернення об'єкта функцією

#include <vcl>

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

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

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

 

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

char sMas[80];

public:

void setStr(char *str){ strcpy(sMas, str);}

void showStr() { cout << "Рядок: " << sMas << "\n";}

};

 

// Ця функція повертає об'єкт типу 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.showStr();

 

getch(); return 0;

}

У наведеному прикладі функція input() створює локальний об'єкт s_ob класу strClass, а потім зчитує рядок з клавіатури. Цей рядок копіюється в рядок s_ob.sMas, після чого об'єкт s_ob повертається функцією input() і присвоюється об'єкту S_ob у функції main().

Потенційна проблема під час повернення об'єктів функціями

Щодо механізму повернення об'єктів функціями, то тут важливо розуміти таке. Якщо функція повертає об'єкт класу, то вона автоматично створює тимчасовий об'єкт, який зберігає повернуте значення. Саме цей тимчасовий об'єкт реально і повертає функція. Після повернення значення об'єкт руйнується. Руйнування тимчасового об'єкта в деяких ситуаціях може викликати непередбачені побічні ефекти. Наприклад, якщо повернутий функцією об'єкт має деструктор, який звільняє динамічно виділену пам'ять, то ця пам'ять буде звільнена навіть у тому випадку, якщо об'єкт, який отримує повернуте функцією значення, все ще цю пам'ять використовує. Розглянемо таку некоректну версію попередньої програми.

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

#include <vcl>

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

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

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

 

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

char *s;

public:

strClass() { s = 0;}

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

void setStr(char *str); // Завантаження рядка.

{ s = new char[strlen(str)+1]; strcpy(s, str); }

void showStr() { cout << "s= " << s << "\n";}

};

 

// Ця функція повертає об'єкт типу 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.showStr(); // Відображення "сміття".

 

getch(); return 0;

}

Результати виконання цієї програми виглядають таким чином.

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

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

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

s= тут появиться сміття

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

Звернемо Вашу увагу на те, що виклик деструктора класу strClass відбувається тричі! Вперше він викликається при виході локального об'єкта s_ob з області видимості у момент його повернення з функції input(). Другий виклик деструкторa ~strClass() відбувається тоді, коли руйнується тимчасовий об'єкт, який повертається функцією input(). Коли функція повертає об'єкт, то автоматично генерується невидимий (для Вас) тимчасовий об'єкт, який зберігає повернуте значення. У нашому випадку цей об'єкт просто є побітовою копією об'єкта s_ob, який міститиме значення, що повертається з функції. Отже, після повернення з функції виконується деструктор тимчасового об'єкта. Оскільки область пам'яті, що виділяється для зберігання рядка, який вводить користувач, вже була звільнена (причому двічі!), то під час виклику функції showStr() на екран виведеться "сміття"[52]. Нарешті, після завершення роботи програми викликається деструктор об'єкта S_ob (у функції main()). Ситуація тут ускладнюється тим, що під час першого виклику деструктора звільняється пам'ять, виділена для зберігання рядка, отримуваного функцією input(). Таким чином, у цій ситуації погано не тільки те, що решта два звернення до деструктора класу strClass спробують звільнити вже звільнену динамічно виділену область пам'яті, але вони також можуть зруйнувати систему динамічного розподілу пам'яті.

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

13.7. Створення і використання конструктора копії

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

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

Аналогічна ситуація виникає під час повернення об'єкта з функції. Компілятор генерує тимчасовий об'єкт, який зберігатиме копію значення, що повертається функцією[53]. Цей тимчасовий об'єкт виходить за межі області видимості відразу ж, як тільки ініціатору виклику цієї функції буде повернуте "обіцяне" значення, після чого негайно викликається деструктор тимчасового об'єкта. Але, якщо цей деструктор зруйнує що-небудь потрібне для виконуваного далі коду програми, то наслідки будуть непоправні.

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

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

Перш ніж грунтовніше ознайомитися з використанням конструктора копії, важливо зрозуміти, що у мові програмування C++ визначено два окремі види ситуацій, у яких значення одного об'єкта передається іншому. Першою такою ситуацією є присвоєння, а другою – ініціалізація. Ініціалізація може виконуватися трьома способами, тобто у випадках, коли:

● один об'єкт безпосередньо ініціалізував інший об'єкт, як, наприклад, в оголошенні;

● копія об'єкта передається параметру функції;

● генерується тимчасовий об'єкт (найчастіше як значення, що повертається функцією).

Конструктор копії застосовується тільки для ініціалізацій одного об'єкта іншим. Він не застосовується до присвоєнь.

Ось як виглядає найпоширеніший формат конструктора копії.

ім'я_класу (const ім'я_класу &obj)

{

// Тіло конструктора

}

У цьому записі елемент obj означає посилання на об'єкт, який використовують для ініціалізації іншого об'єкта. Наприклад, припустимо, у нас є клас myClass і об'єкт Y_ob типу myClass. Тоді у процесі виконання таких настанов буде викликаний конструктор копії класу myClass:

myClass X_ob = Y_ob; // Об'єкт Y_ob безпосередньо ініціалізував об'єкт X_ob.

fun_с1(Y_ob); // Об'єкт Y_ob передається як аргумент

Y_ob = fun_c2(); // Об'єкт Y_ob приймає об'єкт що повертається функцією.

У перших двох випадках конструктору копії буде передано посилання на об'єкт y, а в третьому – посилання на об'єкт, який повертається функцією fun_c2().

Варто знати! Конструктори копії не роблять ніякого впливу на операції присвоєння.

Щоб глибше зрозуміти призначення конструкторів копії, розглянемо грунтовніше їх значення у кожній з цих трьох ситуацій.

13.7.1. Використання конструктора копії для ініціалізації одного об'єкта іншим

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

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

#include <vcl>

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

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

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

 

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

int *p;

public:

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

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

~myClass();

int getVal() { return *p;}

};

 

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

myClass::myClass(int izm)

{

cout << "Виділення p-пам'яті звичайним конструктором.\n";

p = new int;

*p = izm;

}

 

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

myClass::myClass(const myClass &p_ob)

{

p = new int;

*p = *p_ob.p; // Значення копії

cout << "Виділення p-пам'яті конструктором копії.\n";

}

 

myClass::~myClass()

{

cout << "Звільнення p-пам'яті.\n";

delete p;

}

 

int main()

{

myClass A_ob(10); // Викликається звичайний конструктор.

 

myClass B_ob = A_ob; // Викликається конструктор копії.

 

getch(); return 0;

}

Результати виконання цієї програми є такі:

Виділення p-пам'яті звичайним конструктором.

Виділення p-пам'яті конструктором копії.

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

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

Як підтверджують результати виконання цієї програми, під час створення об'єкта A_ob викликається звичайний конструктор. Але коли об'єкт A_ob використовують для ініціалізації членів-даних об'єкта B_ob, викликається конструктор копії. Використання конструктора копії гарантує, що об'єкт B_ob виділить для своїх членів-даних власну область пам'яті. Без конструктора копії об'єкт B_ob просто був би точною копією об'єкта A_ob, а член A_ob.р указував би на ту ж саму область пам'яті, що і член B_ob.р.

Потрібно мати на увазі, що конструктор копії викликається тільки у разі виконання ініціалізації. Наприклад, така послідовність настанов не викличе конструктора копії, визначеного у попередній програмі:

myClass A_ob(2), B_ob(3);

//...

B_ob = A_ob;

У цьому записі настанова B_ob = A_ob виконує операцію присвоєння, а не операцію копіювання.

13.7.2. Використання конструктора копії для передачі об'єкта функції

Під час передачі об'єкта функції як аргумент створюється копія цього об'єкта. Якщо у класі визначено конструктор копії, то саме він і викликається для створення копії. Розглянемо програму, у якій використовується конструктор копії для належного оброблення об'єктів типу myClass під час їх передачі функції як аргументів. Нижче наведемо коректну версію некоректної програми, представленої у розд. 13.5.2.

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

#include <vcl>

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

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

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

 

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

int *p;

public:

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

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

~myClass();

int getVal() { return *p;}

};

 

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

myClass::myClass(int izm)

{

cout << "Виділення пам'яті, яка адресується покажчиком p.\n";

p = new int;

*p = izm;

}

 

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

myClass::myClass(const myClass &p_ob)

{

p = new int;

*p = *p_ob.p; // значення копії

cout << "Викликаний конструктор копії.\n";

}

 

myClass::~myClass()

{

cout << "Звільнення пам'яті, яка адресується покажчиком p.\n";

delete p;

}

// Ця функція приймає один об'єкт-параметр.

void display(myClass a_ob)

{

cout << "*p= " << a_ob.getVal() << "\n";

}

 

int main()

{

myClass A_ob(10); // Створення об'єкта класу

 

display(A_ob);

 

getch(); return 0;

}

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

Виділення пам'яті, яка адресується покажчиком p.

Викликаний конструктор копії.

*p= 10

Звільнення пам'яті, яка адресується покажчиком p.

Звільнення пам'яті, яка адресується покажчиком p.

У процесі виконання цієї програми тут відбувається таке: коли у функції main() створюється об'єкт A_ob, "стараннями" звичайного конструктора виділяється пам'ять, і адреса цієї області пам'яті присвоюється покажчику A_ob.р. Потім об'єкт A_ob передається функції display(), а саме – її параметру a_ob. У цьому випадку викликається конструктор копії, який для об'єкта A_ob створює копію з іменем a_ob. Конструктор копії виділяє пам'ять для цієї копії, а значення покажчика на виділену область пам'яті присвоює члену р об'єкта-копії. Потім значення, яка адресується покажчиком р початкового об'єкта, записується в область пам'яті, адреса якої зберігається в покажчику р об'єкта-копії. Таким чином, області пам'яті, що адресуються покажчиками A_ob.р і p_ob.р, роздільні та незалежні одна від одної, але значення (на які вказують A_ob.р і p_ob.р), що зберігаються в них, однакові. Якби конструктор копії не був визначений, то внаслідок створення за замовчуванням побітової копії члени A_ob.р і p_ob.р указували б на одну і ту саму область пам'яті.

Після завершення роботи функції display() об'єкт p_ob виходить з області видимості. Цей вихід супроводжується викликом його деструктора, який звільняє область пам'яті, яка адресується покажчиком p_ob.p. Нарешті, після завершення роботи функції main() виходить з області видимості об'єкт A_ob, що також супроводжується викликом його деструктора і відповідним звільненням області пам'яті, яка адресується покажчиком A_ob.р. Як бачимо, використання конструктора копії усуває деструктивні побічні ефекти, пов'язані з передачею об'єкта функції.

13.7.3. Використання конструктора копії під час повернення функцією об'єкта

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

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

#include <vcl>

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

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

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

 

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

public:

myClass() { cout << "Звичайний конструктор.\n";}

myClass(const myClass &obj) {cout << "Конструктор копії.\n";}

};

 

myClass fun_ob()

{

myClass a_ob; // Викликається звичайний конструктор.

return a_ob; // Опосередковано викликається конструктор копії.

}

 

int main()

{

myClass A_ob; // Викликається звичайний конструктор.

 

A_ob = fun_ob(); // Викликається конструктор копії.

 

getch(); return 0;

}

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

Звичайний конструктор.