public:
Account () const; // помилка
Account () volatile; // помилка
// ...
};
Це не означає, що об'єкти класу з такими специфікаторами заборонено ініціалізувати конструктором. Просто до об'єкта застосовується підходящий конструктор, причому без обліку специфікаторів в оголошенні об'єкта. Константність об'єкта класу встановлюється після того, як робота з його ініціалізації завершена, і пропадає в момент виклику деструктора. Таким чином, об'єкт класу зі специфікатором const уважається константним з моменту завершення роботи конструктора до моменту запуску деструктора. Те ж саме ставиться й до специфікатора volatile.
Розглянемо наступний фрагмент програми:
// у якімсь заголовному файлі
extern void print (const Account &acct);
// ...
int main ()
{
// перетворить рядок "oops" в об'єкт класу Account
// за допомогою конструктора Account:: Account ("oops", 0.0)
print ("oops");
// ...
}
За замовчуванням конструктор з одним параметром (або з декількома - за умови, що всі параметри, крім першого, мають значення за замовчуванням) відіграє роль оператора перетворення. У цьому фрагменті програми конструктор Account неявно застосовується компілятором для трансформації літерального рядка в об'єкт класу Account при виклику print (), хоча в даній ситуації таке перетворення не потрібно.
Ненавмисні неявні перетворення класів, наприклад трансформація "oops" в об'єкт класу Account, виявилися джерелом помилок, що виявляють важко. Тому в стандарт C++ було додано ключове слово explicit, що говорить компіляторові, що такі перетворення не потрібні:
class Account {
public:
explicit Account (const char*, double=0.0);
};
Даний модифікатор застосуємо тільки до конструктора.
Часто почленна ініціалізація не забезпечує коректну дію класу. Тому ми явно визначаємо конструктор копіювання. У нашому класі Account це необхідно, інакше два об'єкти будуть мати однакові номери рахунків, що заборонено специфікацією класу.
Конструктор копіювання приймає як формальний параметр посилання на об'єкт класу (рекомендовано зі специфікатором const). Його реалізація:
inline Account::
Account (const Account &rhs)
: _balance (rhs. _balance)
{
_name = new char [strlen (rhs. _name) + 1];
strcpy (_name, rhs. _name);
// копіювати rhs. _acct_nmbr не можна
_acct_nmbr = get_unique_acct_nmbr ();
}
Коли ми пишемо:
Account acct2 (acct1);
компілятор визначає, чи оголошений явний конструктор копіювання для класу Account. Якщо він оголошений і доступний, то він і викликається; а якщо недоступний, то визначення acct2 вважається помилкою. У випадку, що коли конструктор копіювання не об’явлений, виконується почленна ініціалізація за замовчуванням. Якщо згодом об’явлення конструктор копіювання буде додане або вилучене, ніяких змін у програми користувачів вносити не прийдеться. Однак перекомпілювати їх все-таки необхідно.
Одна із цілей, що ставляться перед конструктором, - забезпечити автоматичне виділення ресурсу. Ми вже бачили в прикладі із класом Account конструктор, де за допомогою оператора new виділяється пам'ять для масиву символів і привласнюється унікальний номер рахунку. Можна також представити ситуацію, коли потрібно одержати монопольний доступ до поділюваної пам'яті або до критичної секції потоку. Для цього необхідна симетрична операція, що забезпечує автоматичне звільнення пам'яті або повернення ресурсу після завершення часу життя об'єкта, - деструктор. Деструктор - це спеціальна обумовлена користувачем функція-член, що автоматично викликається, коли об'єкт виходить із області видимості або коли до покажчика на об'єкт застосовується операція delete. Ім'я цієї функції створено з імені класу з попереднім символом “тильда" (~). Деструктор не повертає значення й не приймає ніяких параметрів, а отже, не може бути перевантажений.
Хоча дозволяється визначати кілька таких функцій-членів, лише одна з них буде застосовуватися до всіх об'єктів класу. От, наприклад, деструктор для нашого класу Account:
class Account {
public:
Account ();
explicit Account (const char*, double=0.0);
Account (const Account&);
~Account ();
// ...
private:
char *_name;
unsigned int _acct_nmbr;
double _balance;
};
inline
Account:: ~Account ()
{
delete [] _name;
return_acct_number (_acct_nnmbr);
}
Зверніть увагу, що в нашому деструкторі не скидаються значення членів:
inline Account:: ~Account ()
{
// необхідно
delete [] _name;
return_acct_number (_acct_nnmbr);
// необов'язково
_name = 0;
_balance = 0.0;
_acct_nmbr = 0;
}
Робити це необов'язково, оскільки відведена під члени об'єкта пам'ять однаково буде звільнена. Розглянемо наступний клас:
class Point3d {
public:
// ...
private:
float x, y, z;
};
Конструктор тут необхідний для ініціалізації членів, що представляють координати точки. Чи потрібний деструктор? Немає. Для об'єкта класу Point3d не потрібно звільняти ресурси: пам'ять виділяється й звільняється компілятором автоматично на початку й наприкінці його життя.
В загальному випадку, якщо члени класу мають прості значення, скажімо, координати точки, то деструктор не потрібний. Не для кожного класу необхідний деструктор, навіть якщо в нього є один або більше конструкторів. Основною метою деструктора є звільнення ресурсів, виділених або в конструкторі, або під час життя об'єкта, наприклад звільнення пам'яті, виділеної оператором new.
Але функції деструктора не обмежені тільки звільненням ресурсів. Він може реалізовувати будь-яку операцію, що за задумом проектувальника класу повинна бути виконана відразу по закінченні використання об'єкта. Так, широко розповсюдженим прийомом для виміру продуктивності програми є визначення класу Timer, у конструкторі якого запускається та або інша форма програмного таймера. Деструктор зупиняє таймер і виводить результати вимірів. Об'єкт даного класу можна умовно визначати в критичних ділянках програми, які ми хочемо профілювати, у такий спосіб:
{
// початок критичної ділянки програми
#ifdef PROFILE
Timer t;
#endif
// критична ділянка
// t знищується автоматично
// відображається витрачений час...
}
Щоб переконатися в тім, що ми розуміємо поводження деструктора (та й конструктора теж), розберемо наступний приклад:
(1) #include "Account. h"
(2) Account global ("James Joyce");
(3) int main ()
(4) {
(5) Account local ("Anna Livia Plurabelle", 10000);
(6) Account &loc_ref = global;
(7) Account *pact = 0;
(8)
(9) {
(10) Account local_too ("Stephen Hero");
(11) pact = new Account ("Stephen Dedalus");
(12) }
(13)
(14) delete pact;
(15) }
Скільки тут викликається конструкторів? Чотири: один для глобального об'єкта global у рядку (2); по одному для кожного з локальних об'єктів local і local_too у рядках (5) і (10) відповідно, і один для об'єкта, розподіленого в купі, у рядку (11). Ні об’явлення посилання loc_ref на об'єкт у рядку (6), ні об’явлення вказівника pact у рядку (7) не приводять до виклику конструктора. Посилання - це псевдонім для вже сконструйованого об'єкта, у цьому випадку для global. Вказівника також лише адресує об'єкт, створений раніше (у цьому випадку розподілений у купі, рядок (11)), або не адресує ніякого об'єкта (рядок (7)).
Аналогічно викликаються чотири деструктори: для глобального об'єкта global, об’явленого в рядку (2), для двох локальних об'єктів і для об'єкта в купі при виклику delete у рядку (14). Однак у програмі немає інструкції, з якої можна зв'язати виклик деструктора. Компілятор просто вставляє ці виклики за останнім використанням об'єкта, але перед закриттям відповідної області видимості.
Конструктори й деструктори глобальних об'єктів викликаються на стадіях ініціалізації й завершення виконання програми. Хоча такі об'єкти нормально поводяться при використанні в тім файлі, де вони визначені, але їхнє застосування в ситуації, коли виробляються посилання через границі файлів, стає в C++ серйозною проблемою.
Деструктор не викликається, коли з області видимості виходить посилання або вказівник на об'єкт (сам об'єкт при цьому залишається).
С++ за допомогою внутрішніх механізмів перешкоджає застосуванню оператора delete до вказівника, що не адресує ніякого об'єкта, так що відповідні перевірки коду необов'язкові:
// необов'язково: неявно виконується компілятором
if (pact! = 0) delete pact;
Щораз, коли усередині функції цей оператор застосовується до окремого об'єкта, розміщеному в купі, краще використати об'єкт класу auto_ptr, а не звичайний вказівник. Це особливо важливо тому, що пропущений виклик delete (скажемо, у випадку, коли збуджується виключення) веде не тільки до витоку пам'яті, але й до пропуску виклику деструктора. Нижче приводиться приклад програми, переписаної з використанням auto_ptr (вона злегка модифікована, тому що об'єкт класу auto_ptr може бути явно із для адресації іншого об'єкта тільки присвоюванням його іншому auto_ptr):
#include <memory>
#include "Account. h"
Account global ("James Joyce");
int main ()
{
Account local ("Anna Livia Plurabelle", 10000);
Account &loc_ref = global;
auto_ptr<Account> pact (new Account ("Stephen Dedalus"));
{
Account local_too ("Stephen Hero");
}
// об'єкт auto_ptr знищується тут
}
Іноді викликати деструктор для деякого об'єкта доводиться явно. Особливо часто така необхідність виникає у зв'язку з оператором new. Розглянемо приклад.