Sizeof value 8 страница

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

Розглянемо короткий приклад, у якому функція-"друг" класу використовується для доступу до закритих членів класу myClass.

Код програми 13.1. Демонстрація механізму використання функції-"друга" класу для доступу
до закритих членів класу

#include <vcl>

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

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

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

 

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

int a, b;

public:

// Оголошення параметризованого конструктора

myClass(int izm, int jzm) {a = izm; b = jzm;}

 

// Функція fun_sum() – "друг" класу myClass.

friend int fun_sum(myClass s_ob);

};

 

// Функція fun_sum() не є членом ні одного класу.

int fun_sum(myClass s_ob)

{

/* Оскільки функція fun_sum() – "друг" класу myClass,

то вона має право на прямий доступ до його членів-даних a i b. */

return s_ob.a + s_ob.b;

}

 

int main()

{

myClass S_ob(3, 4); // Створення об'єкта класу

 

cout << "Suma= " << fun_sum(S_ob) << "\n";

 

getch(); return 0;

}

У наведеному прикладі функція fun_sum() не є членом класу myClass. Проте вона має повний доступ до private-членів класу myClass. Зокрема, вона може безпосередньо використовувати значення s_ob.а і s_ob.b. Зверніть також увагу на те, що функція fun_sum() викликається звичайним способом, тобто без прив'язування до імені об'єкта (і без використання оператора "крапка"). Оскільки вона не є функцією-членом класу, то під час виклику її не потрібно кваліфікувати з вказанням імені об'єкта[49]. Зазвичай "дружній" функції як параметр передається один або декілька об'єктів класу, для яких вона є "другом". Робиться це так само як і у випадку передачі параметра функції fun_sum().

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

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

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

#include <vcl>

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

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

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

 

const int IDLE=0;

const int INUSE=1;

 

class bClass; // Випереджувальне оголошення класу

 

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

int status; // IDLE, якщо повідомлення неактивне

// INUSE, якщо повідомлення виведене на екран.

//...

public:

void setStatus(int state) { status = state; }

friend int displey(aClass a_ob, bClass b_ob);

};

 

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

int status; // IDLE, якщо повідомлення неактивне

// INUSE, якщо повідомлення виведене на екран.

//...

public:

void setStatus(int state) { status = state; }

friend int displey(aClass a_ob, bClass b_ob);

};

 

// Функція displey() – "друг" для класів aClass і bClass.

int displey(aClass a_ob, bClass b_ob)

{

if(a_ob.status || b_ob.status) return 0;

else return 1;

}

 

int main()

{

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

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

 

X_ob.setStatus(IDLE); // IDLE = 0

Y_ob.setStatus(IDLE);

 

if(displey(X_ob, Y_ob)) cout << "Екран вільний.\n";

else cout << "Відображається повідомлення.\n";

 

X_ob.setStatus(INUSE); // INUSE = 1

 

if(displey(X_ob, Y_ob)) cout << "Екран вільний.\n";

else cout << "Відображається повідомлення.\n";

 

getch(); return 0;

}

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

Екран вільний.

Відображається повідомлення.

Оскільки функція displey() є "другом" як для класу aClass, так і для класу bClass, то вона має доступ до закритого члена status, визначеного в обох класах. Таким чином, стан об'єкта кожного класу одночасно можна перевірити всього одним зверненням до функції displey().

Випереджувальне оголошення призначене для оголошення імені класового типу ще до визначення самого класу.

Звернемо Вашу увагу на те, що у цій програмі використовується випереджувальне оголошення (також називається випереджувальним посиланням)для класу bClass. Потреба у ньому пов'язана з тим, що оголошення функції displey() у класі aClass використовує посилання на клас bClass ще до його оголошення. Щоб створити випереджувальне оголошення для будь-якого класу, достатньо використовувати такий формат, як це представлено у наведеному вище коді програми.

"Друг" одного класу може бути членом іншого класу. Перепишемо попередню програму так, щоби функція displey() стала членом класу aClass. Звернемо Вашу увагу на використання оператора дозволу області видимості (або оператора дозволу контексту) під час оголошення функції displey() як "друга" класу bClass.

Код програми 13.3. Демонстрація механізму використання функції – члена одного класу і одночасно "друга" – іншого класу

#include <vcl>

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

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

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

 

const int IDLE=0;

const int INUSE=1;

 

class bClass; // Випереджувальне оголошення класу

 

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

int status; // IDLE, якщо повідомлення неактивне

// INUSE, якщо повідомлення виведене на екран.

//...

public:

void setStatus(int state) { status = state; }

int displey(bClass b_ob); // тепер це член класу aClass

};

 

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

int status; // IDLE, якщо повідомлення неактивне

// INUSE, якщо повідомлення виведене на екран.

//...

public:

void setStatus(int state) { status = state; }

friend int aClass::displey(bClass b_ob); // функція-"друг" класу

};

 

// Функція displey() -- член класу aClass і "друг" класу bClass.

int aClass::displey(bClass b_ob)

{

if(status || b_ob.status) return 0;

else return 1;

}

 

int main()

{

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

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

 

X_ob.setStatus(IDLE); // IDLE = 0

Y_ob.setStatus(IDLE);

 

if(X_ob.displey(Y_ob)) cout << "Екран вільний.\n";

else cout << "Відображається повідомлення.\n";

 

X_ob.setStatus(INUSE); // INUSE = 1

 

if(X_ob.displey(Y_ob)) cout << "Екран вільний.\n";

else cout << "Відображається повідомлення.\n";

 

getch(); return 0;

}

Оскільки функція displey() є членом класу aClass, то вона має прямий доступ до змінної status об'єктів типу aClass. Отже, як параметр необхідно передавати функції displey() тільки об'єкти типу bClass.

13.2. Перевантаження конструкторів

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

clock_t clock();

Тип clock_t є різновидом довгого цілочисельного типу. Операція ділення значення, що повертається функцією clock(), на значення CLOCKS_PER_SEC дає змогу перетворити отриманий результат в секунди. Як прототип для функції clock(), так і визначення константи CLOCKS_PER_SEC належать заголовку <ctime>.

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

#include <vcl>

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

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

#include <ctime> // Для використання системного часу і дати

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

 

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

int seconds;

public:

// Задавання секунд у вигляді рядка

timerClass(char *t) { seconds = atoi(t);}

 

// Задавання секунд у вигляді цілого числа

timerClass(int t) { seconds = t;}

 

// Час, що задається в хвилинах і секундах

timerClass(int xv, int sec) { seconds = xv*60 + sec;}

// Час, що задається в годинах, хвилинах і секундах

timerClass(int hod, int xv, int sec)

{ seconds = 60*(hod*60 + xv) + sec;}

 

void run_timer(); // Таймер відліку часу

};

 

void timerClass::run_timer()

{

clock_t t1;

 

t1 = clock();

while((clock()/CLOCKS_PER_SEC – t1/CLOCKS_PER_SEC)< seconds);

cout << "\a"; // Подання звукового сигналу

}

 

int main()

{

timerClass A_ob(10), B_ob("20"), C_ob(1, 10), D_ob(0, 2, 8);

 

A_ob.run_timer(); // Відлік 10 секунд

B_ob.run_timer(); // Відлік 20 секунд

C_ob.run_timer(); // Відлік 1 хвилини і 10 секунд

D_ob.run_timer(); // відлік 0 годин 2 хвилини і 8 секунд

 

getch(); return 0;

}

Під час створення у функції main() об'єктів A_ob, B_ob, C_ob i D_ob класу timerClass він надає члену даних seconds початкові значення чотирма різними способами, що підтримуються перевантаженими функціями конструкторів. У кожному випадку викликається той конструктор, який відповідає заданому переліку параметрів, і тому належним чином ініціалізує "свій" об'єкт.

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

13.3. Динамічна ініціалізація конструктора

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

int n = strlen(str);

double arc = sin(theta);

float d = 1.02 * pm/deltax;

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

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

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

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

#include <vcl>

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

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

#include <ctime> // Для використання системного часу і дати

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

 

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

int seconds;

public:

// Задавання секунд у вигляді рядка

timerClass(char *t) { seconds = atoi(t);}

 

// Задавання секунд у вигляді цілого числа

timerClass(int t) { seconds = t;}

 

// Час, що задається в хвилинах і секундах

timerClass(int xv, int sec) { seconds = xv*60 + sec;}

// Час, що задається в годинах, хвилинах і секундах

timerClass(int hod, int xv, int sec)

{ seconds = 60*(hod*60 + xv) + sec;}

 

void run_timer(); // Таймер відліку часу

};

 

void timerClass::run_timer()

{

clock_t t1;

 

t1 = clock();

while((clock()/CLOCKS_PER_SEC – t1/CLOCKS_PER_SEC)< seconds);

cout << "\a"; // Подання звукового сигналу

}

 

int main()

{

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

 

A_ob.run_timer();

 

char strMas[80];

cout << "Введіть кількість секунд: "; cin >> strMas;

timerClass B_ob(strMas); // Динамічна ініціалізація конструктора

B_ob.run_timer();

 

int xv, sec;

cout << "Введіть хвилини і секунди: "; cin >> xv >> sec;

timerClass C_ob(xv, sec); // Динамічна ініціалізація конструктора

C_ob.run_timer();

 

int hod;

cout << "Введіть години, хвилини і секунди: ";

cin >> hod >> xv >> sec;

timerClass D_ob(hod, xv, sec);// Динамічна ініціалізація конструктора

D_ob.run_timer();

 

getch(); return 0;

}

Як бачимо, об'єкт A_ob створюється, використавши ініціалізацію цілочисельною константою. Проте основою для побудови об'єктів B_ob і C_ob слугує інформація, що вводиться користувачем. Оскільки для об'єкта B_ob користувач вводить рядок, то є сенс перевантажити конструктор timerClass() для прийняття рядкової змінної. Об'єкт C_ob також створюється у процесі виконання програми з використанням даних, які вводяться користувачем. Оскільки у цьому випадку час вводиться у вигляді хвилин і секунд, то для побудови об'єкта C_ob логічно використовувати формат конструктора, що приймає два цілочисельні аргументи. Аналогічно створюється об'єкт D_ob, для якого час вводиться у вигляді годин, хвилин і секунд, тобто використовується формат конструктора, що приймає три цілочисельних аргументи. Важко не погодитися з тим, що наявність декількох форматів ініціалізації конструктора позбавляє програміста від виконання додаткових перетворень під час ініціалізації членів-даних об'єктів.

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

Варто знати!Розробляючи перевантажені конструктори, необхідно визначитися у тому, які ситуації важливо передбачити, а які можна і не враховувати.

13.4. Присвоєння об'єктів

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

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

#include <vcl>

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

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

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

 

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

int a, b;

public:

void setAB(int izm, int jzm) {a = izm, b = jzm;}

void showAB() { cout << "a = " << a << "; b = " << b << "\n";}

};

 

int main()

{

myClass A_ob, B_ob; // Створення об'єктів класу

 

A_ob.setAB(10, 20);

B_ob.setAB(0, 0);

cout << "Об'єкт A_ob до присвоєння:\n";

A_ob.showAB();

cout << "Об'єкт B_ob до присвоєння:\n";

B_ob.showAB();

cout << "\n";

 

B_ob = A_ob; // Присвоюємо об'єкт A_ob об'єкту B_ob.

 

cout << "Об'єкт A_ob після виконання операції присвоєння:\n";

A_ob.showAB();

cout << "Об'єкт B_ob після виконання операції присвоєння:\n";

B_ob.showAB();

 

getch(); return 0;

}

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

Об'єкт A_ob до присвоєння:

a = 10; b = 20

Об'єкт B_ob до присвоєння:

a = 0; b = 0

 

Об'єкт A_ob після виконання операції присвоєння:

a = 10; b = 20

Об'єкт B_ob після виконання операції присвоєння:

a = 10; b = 20

За замовчуванням усі значення членів-даних з одного об'єкта присвоюються іншому шляхом створення порозрядної копії[50]. Але, як буде показано далі, оператор присвоєння можна перевантажувати, визначивши власні операції присвоєння.

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

13.5. Передача об'єктів функціям

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

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

#include <vcl>

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

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

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

 

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

int izm;

public:

void setIzm(int x) { izm = x;}

void getIzm(char *t) { cout << t << izm << "\n";}

};

 

void fun_f(tClass t_ob) // Визначення функції не члена класу

{

t_ob.getIzm("t2="); // Виведення числа 10.

t_ob.setIzm(100); // Встановлює тільки локальну копію.

t_ob.getIzm("t3="); // Виведення числа 100.

}

 

int main()

{

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

 

T_ob.setIzm(10);

T_ob.getIzm("t1="); // Виведення числа 10.

 

fun_f(T_ob); // Передача об'єкта функції не члена класу

 

T_ob.getIzm("t4="); // Як і раніше, виводиться число 10,

// значення змінної "i" не змінилося.

getch(); return 0;

}

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

t1= 10

t2= 10

t3= 100

t4= 10

Як підтверджують ці результати, модифікування об'єкта t_ob у функції fun_f() не впливає на об'єкт T_ob у функції main().

13.5.1. Конструктори, деструктори і передача об'єктів

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

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

#include <vcl>

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

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

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

 

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

int num;

public:

myClass(int izm) { num = izm; cout << "Створення\n";}

~myClass() { cout << "Руйнування\n";}

int getVal() { return num;}

};

void display(myClass a_ob)

{

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

}

 

int main()

{

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

 

display(A_ob);

 

getch(); return 0;

}

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

Створення

num= 10

Руйнування

Руйнування

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