[ЗМІСТ]      [Далі]       [Назад]      [Початок розділу]

 

11.3 Динамічні об’єкти. Деструктори

 

      Об’єкти, як і звичайні змінні, можуть розміщуватись як у статичній, так і у динамічній пам’яті. В останньому випадку ми будемо називати об’єкти динамічними. Для динамічних об’єктів, як і для звичайних змінних, виконують команди виділення та звільнення пам’яті. Однак ці команди мають для об’єктів певну специфіку.

 

      Розглянемо реалізацію динамічних об’єктів у мові Паскаль. Вказівник на об’єкти деякого класу описують так само, як і вказівник на звичайні змінні. Наприклад,

 

      Type  PA = ^A;

            A = Object

                  constructor Create(i: word);

                  ...

            end;

            PB= ^B;

            B = Object(A)

                  ...

            end;

      Var p1: PA; p2: PB;

 

PA та PB – типи вказівників на об’єкти класів A та B відповідно. Для вказівників на об’єкти, так само, як і для звичайних вказівників, визначено розіменування, та порівняння. Що ж стосується присвоєння, то воно аналогічне присвоєнню для самих об’єктів. Тобто, вказівнику на об’єкт суперкласу можна присвоїти значення вказівника на об’єкт підкласу, наприклад, p1:=p2.

      Для виділення пам’яті під динамічний об’єкт використовують new. Можна користуватись стандартним синтаксисом new, наприклад, new(p1). Однак, більшість динамічних об’єктів має віртуальні методи та конструктори. Тому відразу після виділення пам’яті треба викликати конструктор. Тобто, для створення динамічного об’єкта класу A треба було б постійно виконувати ланцюг:

      new(p1); p1^.Create(n)

 

Для скорочення запису у Паскалі використовують новий синтаксис new:

 

      new(p1, Create(n))

 

У цьому випадку другим параметром new йде ім’я конструктора а також фактичні параметри для його виклику. Нарешті, часто новостворений динамічний об’єкт (вказівник на цей об’єкт) треба передавати як параметр у підпрограмах. Для подальшого скорочення запису у таких випадках використовують функціональний синтаксис new:

 

      p1:=new(PA, Create(n))

 

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

 

      Для звільнення пам’яті під динамічні об’єкти можна використовувати стандартний синтаксис dispose, наприклад, dispose(p1). Однак із звільненням пам’яті виникають деякі проблеми. Розглянемо ланцюг:

 

      p1:=p2; dispose(p1)

 

Очевидно, що об’єкт, на який вказує p2, і який належить класу B, займає більше пам’яті, ніж об’єкти класу A, на які звичайно вказує p1 (B є нащадком A). Тому dispose(p1) звільнить стільки пам’яті, скільки займають об’єкти класу A і частина динамічної пам’яті залишиться незвільненою. Для того, щоб уникнути некоректного використання пам’яті для динамічних об’єктів, застосовують спеціальні методи, які називають деструкторами. Деструктори у Паскалі мають синтаксис звичайних процедур, у яких замість слова procedure використовують слово destructor. Наприклад:

 

      destructor Done;

 

Деструктор вказують другим параметром при виклику dispose:

 

      dispose(p1, Done)

 

Деструктор вказує, скільки насправді пам’яті займає об’єкт, який треба видалити. Деструктори можуть бути і найчастіше бувають віртуальними. Звичайно, що тіло деструктора може вміщувати команди, які повинні виконуватись перед знищенням об’єкта, хоча часто деструктори взагалі порожні.

 

Рис. 11.1 – Дек довільних об’єктів

 

      Як приклад використання динамічних об’єктів та деструкторів розглянемо дек довільних об’єктів (Рис. 11.1). У попередньому розділі ми вже розглянули дек цілих чисел. Дек довільних об’єктів відрізняється від деку цілих чисел тим, що даними у елементах деку будуть не цілі числа, а вказівники на об’єкти. На Рис. 11.1 вказівники на об’єкти зображено грубими стрілками, а вказівники на елементи деку – тонкими.

      Побудуємо ієрархію класів від початкового порожнього об’єкта (AnyObject). У деку будуть зберігатись об’єкти класу AnyObject або його нащадків. Клас AnyObject, вказівник на нього AnyRef опишемо у модулі AnyThing. Клас AnyObject має тільки порожній конструктор Init та порожній віртуальний деструктор Done.

 

Unit AnyThing;

 

interface

 

Type

  AnyRef = ^AnyObject;

  AnyObject = Object                       { початковий обєкт }

                Constructor Init;          { конструктор }

                Destructor Done; Virtual;  { деструктор }

              end;

 

implementation

 

  Constructor AnyObject.Init;

    begin end;

 

  Destructor AnyObject.Done;

    begin end;

 

end.

 

 

      Сам дек довільних об’єктів опишемо у модулі AnyDeque. Описи вказівника на елемент деку Dref та елемента деку Delem майже дослівно повторюють описи для деку цілих чисел (у полі даних елемента деку dt зберігається вказівник на об’єкт класу AnyObject). До дій над деком додамо дію “очистити дек” – видалити всі елементи деку. .Дек – це клас, який має властивості bg, en – вказівники на початок та кінець деку, та методи Init, Empty, PutBg, GetBg, PutEn, GetEn, деструктор Clear.

      Оскільки сам дек є об’єктом, він не є параметром методів, а передається як неявний параметр у всі методи роботи з деком. Дії GetBg та GetEn реалізовані за допомогою функцій, які повертають вказівник типу AnyRef. У наведеній нижче реалізації модуля AnyDeque немає підпрограм GetEn та PutEn, оскільки їх можна формально отримати з підпрограм GetBg та PutBg аналогічно тому, як це було зроблено у попередньому розділі для деку цілих чисел. Очищення деку не тільки видаляє всі елементи, але й об’єкти що зберігаються у деку (Dispose(p^.dt,Done)).

 

 

{ Дек довільних об’єктів }

Unit AnyDeque;

 

interface

 

Uses AnyThing;

 

Type

  Dref = ^Delem;

  Delem = Record                  { елемент деку }

            dt: AnyRef;           { вказівник на обєкт }

            next,prev: Dref

          end;

  Deque = Object                       { Дек }

           private

            bg,en: Dref;               { вказівники на початок та кінець деку }

           public

            Procedure  Init;            { Почати роботу }

            Function   Empty: Boolean;  { Чи порожній дек ? }

            Procedure  PutBg(D: AnyRef);{ Додати елемент до початку деку }

            Function   GetBg: AnyRef;   { Взяти елемент з початку деку }

            Procedure  PutEn(D: AnyRef);{ Додати елемент до кінця деку }

            Function   GetEn: AnyRef;   { Взяти елемент з кінця деку }

            Destructor Clear;           { Очистити дек }

          end;

 

implementation

 

  Procedure Deque.Init;

    begin

      bg:=nil; en:=nil

    end;

 

  Function Deque.Empty;

    begin

      Empty:=(bg = nil) and (en = nil)

    end;

 

  Procedure Deque.PutBg;

    var p: Dref;

    begin

      New(p); p^.dt:=D; p^.prev:=nil; p^.next:=bg;

      if bg <> nil then bg^.prev:=p

      else en:=p;

      bg:=p

    end;

 

  Function Deque.GetBg;

    var p: Dref;

    begin

      if bg = nil then begin

        writeln('Deque.GetBg: дек порожній'); halt

      end;

      p:=bg; GetBg:=p^.dt; bg:=p^.next;

      if bg = nil then en:=nil

      else bg^.prev:=nil;

      dispose(p)

    end;

 

      .................

 

  Destructor Deque.Clear;

    var p,p1: Dref;

    begin

      p:=bg;

      while p <> nil do begin

        Dispose(p^.dt,Done);

        p1:=p; p:=p^.next;

        Dispose(p1)

      end;

      bg:=nil; en:=nil

    end;

 

end.

 

 

      Для тестування деку довільних об’єктів розв’яжемо задачу „лічилка”, яка формулюється наступним чином. По колу стоять n гравців, які мають номери від 1 до n. У лічилці m слів. Починають лічити з першого гравця. n-ий гравець вибуває, після чого знову починають лічити з гравця, що є наступним за тим, що вибув, і т. д. Треба показати послідовність номерів, що вибувають, при заданих значеннях n та m.

      Визначимо клас Player (гравець) як нащадок класу AnyObject. Цей клас має одну властивість No – номер гравця, конструктор Init – створити гравця з номером n, метод Show – показати гравця та віртуальний порожній деструктор Done. Об’єкти класу Player будемо зберігати у деку довільних об’єктів.

      Програма Counter використовує модулі опису початкового об’єкта AnyThing та деку довільних об’єктів AnyDeque. Спочатку вводяться значення кількості гравців (n) та кількості слів (m). Потім у кінець деку додаються n об’єктів класу Player з номерами від 1 до n. При цьому New(PlayerRef,Init(i)) створює новий об’єкт класу Player, викликає для нього конструктор Init(i) та повертає вказівник на новостворений об’єкт. Цей вказівник передається у метод GetEn об’єкта dq як параметр-аргумент. Тобто, під час виклику виконується присвоєння вказівнику типу AnyRef значення вказівника типу PlayerRef. Таке присвоєння допускається, оскільки Player є нащадком AnyObject. Після цього, поки дек не спорожніє перекладаємо (m-1) раз елементи з початку деку у його кінець, а потім отримуємо m-ий за ліком елемент.

      Зверніть увагу на присвоєння pl:=PlayerRef(dq.GetBg). За допомогою dq.GetBg ми беремо перший елемент деку та отримуємо вказівник на цей елемент типу AnyRef. Ми не можемо напряму виконати присвоєння pl:=dq.GetBg, оскільки pl є вказівником на клас Player, який є нащадком AnyObject. Не можна також описати pl з типом AnyRef, оскільки в останньому випадку після присвоєння pl:=dq.GetBg pl^ буде належати класу AnyObject та не буде мати властивостей та методів класу Player. Тому ми використовуємо явне приведення типів: PlayerRef(dq.GetBg) повертає вже вказівник типу PlayerRef, який ми можемо присвоїти pl. Таке приведення коректне, оскільки ми знаємо, що у деку знаходяться об’єкти класу Player. Ця техніка явного приведення типів часто застосовується у ієрархіях класів для роботи з динамічними об’єктами.

      Після отримання значення pl ми показуємо номер гравця та звільняємо пам’ять під об’єкт, викликавши dispose(pl, Done).

 

 

Program Counter;

 

Uses AnyThing,AnyDeque;

 

TYPE

  PlayerRef = ^Player;

  Player = Object(AnyObject)                    {гравець}

             No: Integer;                       {номер гравця}

             Constructor Init(n: Integer);      {створити гравця}

             Procedure Show;                    {показати гравця}

             Destructor Done; Virtual;          {деструктор}

           end;

 

  Constructor Player.Init;

    begin

      No:=n

    end;

 

  Procedure Player.Show;

    begin

      writeln(No)

    end;

 

  Destructor Player.Done;

    begin

    end;

 

  var

    dq: Deque; pl: PlayerRef; i,m,n: Integer;

 

  begin

    write('Введіть кількість гравців: '); Readln(n);

    write('Введіть кількість слів: '); Readln(m);

    dq.Init;

    for i:=1 to n do begin

      dq.PutEn(New(PlayerRef,Init(i)));

    end;

    writeln('Порядок номерів, що вибувають');

    while not dq.Empty do begin

      for i:=1 to m-1 do begin

        dq.PutEn(dq.GetBg)

      end;

      pl:=PlayerRef(dq.GetBg);

      pl^.Show;

      dispose(pl,Done)

    end;

  end.

 

 

      У мові C++ вказівники на об’єкти описують як звичайні вказівники, а динамічні об’єкти створюються за допомогою ключового слова new. Наприклад, опишемо клас A з одноіменним конструктором та вказівник на об’єкти цього класу pa.

 

class A {

      A(int n);

      .................

      };

.................

 

A *pa;

 

Тоді

 

      pa = new A(2);

 

створює новий об’єкт класу A, викликає для нього конструктор, повертає вказівник на новостворений об’єкт та присвоює значення цього вказівника pa.

      Звільнення пам’яті, раніше виділеної під динамічний об’єкт здійснюється за допомогою delete, наприклад,

 

      delete pa;

 

звільняє пам’ять під об’єкт, на який вказує pa.

      Деструктори у C++ використовуються для звільнення пам’яті, але явно не вказуються у delete. У цьому немає потреби, оскільки деструктори у C++, як і конструктори, мають фіксовані імена. Деструктор класу A завжди має ім’я ~A. Деструктор неявно викликається під час знищення динамічного об’єкта.

 

      Для демонстрації роботи із динамічними об’єктами розглянемо, як і у Паскалі, побудову деку довільних об’єктів та задачу “лічилка”. Початковий клас AnyObject опишемо та повністю реалізуємо у файлі anything.h. Цей клас має тільки порожній конструктор та порожній деструктор. У мові C++ немає потреби описувати окремий тип вказівника на об’єкти класу AnyObject.

 

/* anything.h */

 

class AnyObject {             // початковий обєкт

   public:

     AnyObject() { };

     ~AnyObject() { };

     };

 

      Дек довільних об’єктах реалізуємо у модулі anydeque (файли anydeque.h та anydeque.с). Клас Deque має один конструктор Deque (аналог Init у Паскалі) та деструктор ~Deque (аналог Clear у Паскалі).

 

/* anydeque.h */

 

#include "anything.h"

 

typedef struct delem *dref;

struct delem {    //

            AnyObject *dt;    // елемент деку

            dref next, prev;

           };

 

class Deque {                 // Дек

      dref bg, en;            // вказівники на початок та кінець деку

  public:

      Deque();                      // Почати роботу

      ~Deque();                     // Очистити дек

      int empty();                  // Чи порожній дек ?

      void put_bg(AnyObject *d);    // Додати елемент до початку деку

      AnyObject *get_bg();          // Взяти елемент з початку деку

      void put_en(AnyObject *d);    // Додати елемент до кінця деку

      AnyObject *get_en();          // Взяти елемент з кінця деку

      };

 

 

/* anydeque.cpp */

 

#include <stdio.h>

#include <stdlib.h>

#include "anydeque.h"

 

Deque::Deque()

{

  bg = en = NULL;

}

 

int Deque::empty()

{

  return  bg == NULL && en == NULL;

}

 

void Deque::put_bg(AnyObject *d)

{

  dref p;

 

  p = (dref) malloc(sizeof(struct delem));

  p -> dt = d;

  p -> prev = NULL;

  p -> next = bg;

  if (bg == NULL) en = p;

  else bg -> prev = p;

  bg = p;

}

 

AnyObject *Deque::get_bg()

{

  dref p;

  AnyObject *d;

 

 

  if (bg ==NULL)

    {

     printf("get_bg: Дек порожнiй\n");

     exit(1);

    }

  p = bg;

  d = p -> dt;

  bg = bg -> next;

  if (bg == NULL) en = NULL;

  else bg -> prev = NULL;

  free(p);

  return d;

 }

 

      .................

 

Deque::~Deque()

{

 dref p,p1;

 AnyObject *d;

 

 

 p = bg;

 while (p != NULL)

 {

  d = p -> dt;

  delete d;

  p1 = p; p = p -> next;

  free(p1);

 }

 bg = en = NULL;

}

 

      Програма Counter містить опис класу Player як нащадка AnyObject. Всі методи Player реалізовано як inline-функції. Як і у Паскалі, для оторимання m-го по порядку гравця ми виконуємо явне приведення типів

 

      pl = (Player *) dq.get_bg();

 

(Player *) позначає тип вказівника на об’єкти класу Player.

 

/* Counter */

 

#include <stdio.h>

#include "anydeque.h"

 

class Player: public AnyObject {                // гравець

        int no;                                 // номер гравця

  public:

        Player(int n){ no=n; };                 // створити гравця

        void show() { printf("%d\n", no); };    // показати гравця

        ~Player() { };                          // деструктор

  };

 

main()

{

  Deque dq;

  Player *pl;

  int i,m,n;

 

  printf("'Введіть кількість гравців: "); scanf("%d", &n);

  printf("'Введіть кількість слів: "); scanf("%d", &m);

  for (i=1; i<=n; i++)

    dq.put_en(new Player(i));

  printf("'Порядок номерів, що вибувають\n");

  while (!dq.empty())

   {

    for (i=1; i<=m-1; i++)

      dq.put_en(dq.get_bg());

    pl = (Player *) dq.get_bg();

    (*pl).show();

    delete pl;

   }

}

 

[ЗМІСТ]      [Далі]       [Назад]      [Початок розділу]