Тривалість життя об’єктів


4.10. Тривалість життя об’єктів: автоматичні, статичні і динамічні об’єкти, регістрові змінні, оператори управління пам’яттю (new, delete)

 

Звичайні об’єкти живуть протягом терміну активності їх області видимості: створюються при вході в блок і знищуються при виході з нього. Такий спосіб управління пам’яттю називається автоматичним і, відповідно, об’єкти автоматичними. Альтернативою автоматичному служить статичне управління пам’яттю і статичні об’єкти. Статичні об’єкти створюються компонувальником і живуть весь час виконання програми. Проміжне становище займають динамічні об’єкти. Вони створюються і знищуються в довільні моменти часу в результаті виконання відповідних інструкцій.

 

Глобальні об’єкти завжди статичні. Локальні об’єкти теж можуть бути статичними. Якщо блок, в якому їх видно, активізується багатократно, то при кожному повторному вході в блок зберігається значення статичного об’єкту, яке він набув при виході з блоку.

 

int gcd (int v1, int v2)

{

// лічильник глибини рекурсії

static int depth =1;

cout <<”recursion depth: ”<<depth++<<endl;

return v2? gcd (v2, v1%v2): v1;

}

 

Тепер легко підрахувати кількість викликів при прямому обчисленні чисел Фібоначчі

 

int BadFib(int n)

{

// лічильник рекурсивних викликів

static int counter =1;

bool outward = false;

if (counter++ ==1) outward =true;

int result;

switch (n)

{

case 0:

result=0; break;

case 1:

result=1; break;

default:

result = BadFib(n-1)+BadFib(n-2);

}

if (outward)

{

cout<<counter<<" calls"<<endl;

counter = 1;

};

return result;

}

 

Викликавши функцію, ми помітимо, що для обчислення десятого числа Фібоначчі буде зроблено 178 рекурсивних викликів (замість 10). 100-е число в такий спосіб, вочевидь, взагалі не вдасться обрахувати. Тепер можна порівняти ефективність іншого способу очислення

 

void fib(double &f1, double &f2, int n)

{

// лічильник рекурсивних викликів

static int counter =1;

bool outward = false;

if (counter++ ==1) outward =true;

double f;

if (n>=2)

{

f=f2; f2+=f1; f1=f;

fib(f1, f2, n-1);

}

if (outward)

{

cout<<counter<<" calls"<<endl;

counter = 1;

}

};

 

В цьому прикладах використовується принципова відмінність між статичною змінною counter і автоматичною змінною outward. Подумайте, чому при виході із функції значення counter скидається до 1. За якої умови?

 

Швидке обчислення чисел Фібоначчі легко запрограмувати за їх матричним визначенням,

якщо помітити, що

 

??????????????????????????????????????????????????????

 

та скористатися швидким піднесенням до степеня.

 

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

 

Tree* createTreePtr (int node, Tree *left, Tree *right)

{

Tree *aTree;

aTree = new Tree;

aTree -> node = node;

aTree -> left = left;

aTree -> right = right;

return aTree;

}

 

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

 

Створити можна один об’єкт або цілий масив об’єктів, наприклад,

 

int *iPtr = new int; // неіціціалізований об’єкт

int *p = new int(0); // об’єкт, іціціалізований нулем

int *ar = new int[n]; // створення динамічного масиву

 

Ознака успішності створення обєкту — ненульовий указник. Це корисно одразу перевіряти.

 

if (iPtr == 0) // пам’ять не виділена

if (!p) // те ж саме

 

На жаль, немає засобів ініціалізації динамічних масивів, тому динамічний масив не може бути масивом констант. Окремий динамічний об’єкт може бути сталим

 

const int *zero = new const int(0);

 

Парним оператором для new служить delete. Розрізняють знищення окремого об’єкту і масиву

 

delete iPtr;

delete [] ar;

 

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

 

int *pint = new int(1);

cout<<pint<<':'<<*pint<<endl;

delete pint;

int *fint = new int(3);

cout<<fint<<':'<<*fint<<endl;

*pint=2;

cout<<pint<<':'<<*pint<<endl;

cout<<fint<<':'<<*fint<<endl;

 

Деяких неприємностей можна уникнути, розміщуючи динамічні змінні у буфері

 

char *buf = new char [sizeof(Tree)*n];

Tree *node = new (buf) Tree;

 

При такому способі розміщення звільнити можна лише цілком весь буфер

 

delete [] buf;