Конструкторы и деструкторы при наследовании

Инструкция avt av("книга 1",123," автор 1") в примере программы предыдущего пункта приводит к формированию объекта av и вызова конструктора avt производного класса и конструктора book базового класса (предыдущая программа):

void avt::avt(char *s1,int i,char *s2) : book(s1,i)

При этом вначале вызывается конструктор базового класса book (выполняется инициализация компонент-данных naz и kl), затем конструктор производного класса avt (инициализация компоненты fm). Поскольку базовый класс ничего не знает про производные от него классы, его инициализация (вызов его конструктора) производится перед инициализацией (активизацией конструктора) производного класса.

В противоположность этому деструктор производного класса вызывается перед вызовом деструктора базового класса. Это объясняется тем, что уничтожение объекта базового класса влечет за собой уничтожение и объекта производного класса, следовательно, деструктор производного класса должен выполняться перед деструктором базового класса.

Ниже приведена программа вычисления суммы двух налогов, в которой использована следующая форма записи конструктора базового и производного классов.

имя_конструктора(тип переменной_1 имя_переменной_1,…,

тип переменной_n имя_переменной_n) :

имя_конструктора_базового_класса(имя_переменной_1,…, имя_переменной_k),

компонент_данное_1(имя_переменной_m),…,

компонент_данное_n(имя_переменной_n);

#include "iostream.h"

class A // базовый класс

{ protected:double pr1,pr2; // protected для видимости pr в классе B

public:

A(double prc1,double prc2): pr1(prc1),pr2(prc2) {};

void a_prnt(){cout << "% налога = " << pr1 << " и " << pr2 << endl;}

};

class B : public A // производныйкласс

{ int sm;

public:

B(double prc1,double prc2,int sum): A(prc1,prc2),sm(sum) {};

void b_prnt()

{ cout << " налоги на сумму = " << sm << endl;

cout << "первый = " << pr1 <<"\n втрой = " << pr2 << endl;

}

double rashet() {return pr1*sm/100+pr2*sm/100;}

};

void main()

{ A aa(9,5.3); // описание объекта аа (базового класса) и инициа-

// лизация его компонент с использованием

// конструктора А()

B bb(7.5,5,25000); // описание объекта bb (производного класса)

// и инициализация его компонент (вызов конструк-

// тора B() и конструктора А() (первым))

aa.a_prnt();

bb.b_prnt();

cout << "Сумма налога = " << bb.rashet() << endl;

}

В приведенном примере использованы функции-конструкторы следующего вида:

public: A(double prc1,double prc2): pr1(prc1),pr2(prc2) {};

public: B(double prc1,double prc2,int sum): A(prc1,prc2),sm(sum) {};

Конструктор А считывает из стека 2 double значения prc1 и prc2, которые далее используются для инициализации компонент класса А pr1(prc1),pr2(prc2). Аналогично конструктор В считывает из стека 2 double значения prc1 и prc2 и одно значение int, после чего вызывается конструктор класса A(prc1,prc2), затем выполняется инициализация компоненты sm класса В.

Производный класс может служить базовым классом для создания следующего производного класса. При этом, как отмечалось выше, конструкторы выполняются в порядке наследования, а деструкторы в обратном порядке. Если при наследовании переопределена хотя бы одна перегруженная функция, то все остальные варианты этой функции будут скрыты. Если необходимо, чтобы они оставались доступными в производном классе, все их надо переопределить. Если в производном классе создается функция с тем же именем, типом возвращаемого значения и сигнатурой, как и у функции–компоненты базового класса, но с новой реализацией, то эта функция считается переопределенной. Под сигнатурой понимают имя функции со списком параметров, заданных в ее прототипе, а также слово const. Типы возвращаемых значений могут различаться. Распространенная ошибка: пытаясь переопределить функцию базового класса, забывают включить слово const, в результате чего функция оказывается скрыта, а не переопределена.

Ниже приведен текст еще одной программы, в которой также используется наследование. В программе выполняется преобразование арифметического выражения из обычной (инфиксной) формы в форму польской записи. Для этого используется стек, в который заносятся арифметические операции. Алгоритм преобразования выражения здесь не рассматривается.

#include<iostream.h>

#include<stdlib.h>

class st // описание класса – элемент стека операций

{ public :

char c;

st *next;

public:

st(){} // конструктор

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

};

Class cl : public st

{ char *a; // исходная строка (для анализа)

char outstr[80]; // выходная строка

public :

cl() : st() {} // конструктор

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

st *push(st *,char); // занесение символа в стек

char pop(st **); // извлечение символа из стека

int PRIOR(char); // определение приоритета операции

char *ANALIZ(char *); // преобразование в польскую запись

};

char * cl::ANALIZ(char *aa)

{ st *OPERS; //

OPERS=NULL; // стек операций пуст

int k,p;

a=aa;

k=p=0;

while(a[k]!='\0'&&a[k]!='=') // пока не дойдем до символа '='

{ if(a[k]==')') // если очередной символ ')'

{ while((c=pop(&OPERS))!='(') // считываем из стека в выходную

outstr[p++]=c; // строку все знаки операций до символа

// ‘(‘ и удаляем из стека ‘(‘

}

if(a[k]>='a'&&a[k]<='z') // если символ – буква, то

outstr[p++]=a[k]; // заносим ее в выходную строку

if(a[k]=='(') // если очередной символ '(' , то

OPERS=push(OPERS,'('); // помещаем его в стек

if(a[k]=='+'||a[k]=='-'||a[k]=='/'||a[k]=='*')

{ // если следующий символ – знак операции, то

while((OPERS!=NULL)&&(PRIOR(c)>=PRIOR(a[k])))

outstr[p++]=pop(&OPERS); // переписываем в выходную строку все

// находящиеся в стеке операции с большим

// или равным приоритетом

OPERS=push(OPERS,a[k]); // записываем в стек очередную операцию

}

k++; // переход к следующему символу выходной строки

}

while(OPERS!=NULL) // после анализа всего выражения

outstr[p++]=pop(&OPERS); // переписываем операции из стека

outstr[p]='\0'; // в выходную строку

return outstr;

}

st *cl::push(st *head,char a) // функция записи символа в стек и возврата

{ st *PTR; // указателя на вершину стека

if(!(PTR=new st))

{ cout << "\n недостаточно памяти для элемента стека"; exit(-1);}

PTR->c=a; // инициализация элемента стека

PTR->next=head;

return PTR; // PTR – вершина стека

}

char cl::pop(st **head) // функция удаления символа с вершины стека

{ st *PTR; // возвращает символ (с вершины стека) и коррек-

// тирует указатель на вершину стека

char a;

if(!(*head)) return '\0'; // если стек пуст, то возвращается ‘\0'

PTR=*head; // адрес вершины стека

a=PTR->c; // считывается содержимое с вершины стека

*head=PTR->next; // изменяем адрес вершины стека (nex==PTR->next)

delete PTR;

return a;

}

int cl::PRIOR(char a) // функция возвращает приоритет операции

{ switch(a)

{ case '*':case '/':return 3;

case '-':case '+':return 2;

case '(':return 1;

} return 0;

}

void main()

{ char a[80]; // исходная строка

cl cls;

cout << "\nВведите выражение (в конце символ '='): ";

cin >> a;

cout << cls.ANALIZ(a) << endl;

}

В результате работы программы получим:

Введите выражение (в конце символ '=') : (a+b)-c*d=

ab+cd*-

Виртуальные функции

Прежде чем коснуться самого применения виртуальных функций необходимо рассмотреть такие понятия как раннее и позднее связывание.

Во время раннего связывания вызывающий и вызываемый методы связываются при первом удобном случае, обычно при компиляции.

При позднем связывании вызываемого и вызывающего методов они не могут быть связаны во время компиляции. Поэтому реализован специальный механизм, который определяет, как будет происходить связывание вызываемого и вызывающего методов, когда вызов будет сделан фактически.

Очевидно, что скорость и эффективность при раннем связывании выше, чем при использовании позднего связывания. В то же время, позднее связывание обеспечивает некоторую универсальность связывания.

Один из основных принципов объектно-ориентированного программирования предполагает использование идеи «один интерфейс – множество методов реализации». Эта идея заключается также в том, что базовый класс обеспечивает все элементы, которые производные классы могут непосредственно использовать, плюс набор функций, которые производные классы должны реализовать путем их переопределения. Наряду с механизмом перегрузки функций это достигается использованием виртуальных (virtual) функций. Виртуальная функция – это функция, объявленная с ключевым словом virtual в базовом классе и переопределенная в одном или нескольких производных от этого классах. При вызове объекта базового или производных классов динамически (во время выполнения программы) определяется, какую из функций требуется вызвать, основываясь на типе объекта.

Преимущество применения виртуальных методов заключается в том, что при этом используется именно механизм позднего связывания, который допускает обработку объектов, тип которых неизвестен во время компиляции.

Рассмотрим пример использования виртуальной функции.

#include "iostream.h"

#include "iomanip.h"

#include "string.h"

class grup // базовый класс

{ protected:

char *fak; // наименование факультета

long gr; // номер группы

public:

grup(char *FAK,long GR) : gr(GR)

{ if (!(fak=new char[20]))

{ cout<<"ошибка выделения памяти"<<endl;

return;

}

strcpy(fak,FAK);

}

~grup()

{ cout << "деструктор класса grup " << endl;

delete fak;

}

virtual void see(void); // объявление виртуальной функции

};

class stud : public grup // производный класс

{ char *fam; // фамилия

int oc[4]; // массив оценок

public:

stud(char *FAK,long GR,char *FAM,int OC[]): grup(FAK,GR)

{ if (!(fam=new char[20]))

{ cout<<"ошибка выделения памяти"<<endl;

return;

}

strcpy(fam,FAM);

for(int i=0;i<4;oc[i]=OC[i++]);

}

~stud()

{ cout << "деструктор класса stud " << endl;

delete fam;

}

void see(void);

};

void grup::see(void) // описание виртуальной функции

{ cout << fak << gr << endl;}

void stud::see(void) //

{ grup ::see(); // вызов функции базового класса

cout <<setw(10) << fam << " ";

for(int i=0; i<4; cout << oc[i++]<<’ ’);

cout << endl;

}

int main()

{ int OC[]={4,5,5,3};

grup gr1("факультет 1",123456), gr2("факультет 2",345678), *p;

stud st("факультет 2",150502,"Иванов",OC);

p=&gr1; // указатель на объект базового класса

p->see(); // вызов функции базового класса объекта gr1

(&gr2)->see(); // вызов функции базового класса объекта gr2

p=&st; // указатель на объект производного класса

p->see(); // вызов функции производного класса объекта st

return 0;

}

Результат работы программы:

факультет 1 123456

факультет 2 345678

факультет 2",150502

Иванов 4 5 5 3

Применение указателя на объект базового класса позволяет использовать его для всех производных классов. Объявление virtual void see(void) говорит о том, что функция see может быть различной для базового и производных классов. Тип виртуальной функции не может быть переопределен в производных классах. Исключением является случай, когда возвращаемый тип виртуальной функции является указателем или ссылкой на порожденный класс, а виртуальная функция основного класса - указателем или ссылкой на базовый класс. В производных классах функция может иметь список параметров, отличный от параметров виртуальной функции базового класса. В этом случае эта функция будет не виртуальной, а перегруженной. При этом спецификатор virtual игнорируется. Вызов функций должен производиться с учетом списка параметров.

Если функция вызывается с использованием ее полного имени grup::see(), то виртуальный механизм игнорируется. Игнорирование этого может привести к серьезной ошибке:

void stud::see(void)

{ see();

. . . . }

В этом случае инструкция see() приводит к бесконечному рекурсивному вызову функции see().

Рассмотрим еще один пример программы, в которой использованы виртуальные функции.

#include <iostream.h>

class vehicle // класс "транспортное средство"

{ int _vehicle;

public:

// описание виртуальной функции message класса vehicle

virtual void message(void) {cout << "Транспортное средство\n";}

};

class car : public vehicle // класс "легковая машина"

{ int _car;

public:

// описание виртуальной функции message класса car

void message(void) {cout << "Легковая машина\n";}

};

class truck : public vehicle // класс "грузовая машина"

{ int _trunk;

public:

int fun(void) {return _trunk;}

};

class boat : public vehicle // класс "лодка"

{ int _boart;

public:

int func(void) {return _boart;}

// описание виртуальной функции message класса boat

void message(void) {cout << "Лодка\n";}

};

void main()

{ vehicle *p; // описываем p как указатель на

// объект класса vehicle

p = new vehicle; // создаем объект класса vehicle,

// указатель p указывает на этот объект

p->message(); // вызываем метод message объекта vehicle

delete p; // удаляем объект p

p = new car;

p->message();

delete p;

p = new truck;

p->message();

// p->fun(); error fun is not a member of ’ vehicle’

delete p;

p = new boat;

p->message();

delete p;

}

Результат работы программы:

Транспортное средство

Легковая машина

Транспортное средство

Лодка

Классы car, truck и boat, являются производными от базового класса vehicle. В базовом классе vehicle описана виртуальная функция message. В двух из трех классов(car, boat) также описаны свои функции message, а в классе truck нет описания своей функции message.

Если в базовом классе у функции message отсутствует спецификатор virtual, то компилятор связал бы любой вызов метода объекта указателя p с методом message класса vehicle, так как при его описании указано, что переменная p указывает на объект класса vehicle. То есть произвели бы раннее связывание. Результатом работы такой программы был бы вывод четырех строк "Транспортное средство".

При работе с объектами классов car и boat вызываются их собственные методы message, что и подтверждается выводом на экран соответствующих сообщений. У класса truck нет своего метода message, по этой причине производится вызов соответствующего метода базового класса vehicle.

Заметим, что деструктор может быть виртуальным, а конструктор - нет.

Замечание. В чем разница между виртуальными функциями (методами) и переопределением функции?

Что изменилось, если бы функция see() не была бы описана как виртуальная? В этом случае решение о том, какая именно из функций see() должна быть выполнена, будет принято при ее компиляции.

Механизм вызова виртуальных функций можно пояснить следующим образом. При создании нового объекта для него выделяется память. Для виртуальных функций (и только для них) создаются виртуальные таблицы (virtual table, сокращенно vtbl) и указатель на виртуальную таблицу (virtual table pointer, сокращенно vptr). Доступ к виртуальной функции осуществляется через этот указатель и соответствующую таблицу (то есть выполняется косвенный вызов функции). Виртуальная таблица обычно представляет собой массив указателей на функции. Каждый класс, в котором объявляются или наследуются виртуальные функции, имеет свою виртуальную таблицу. Например:

class A

{public:

A();

virtual ~A();

virtual f1();

virtual char f2();

. . .

};

Виртуальная таблица будет иметь примерно такой вид:

  Конструкторы и деструкторы при наследовании - student2.ru хххх реализация A::A
vtbl A Конструкторы и деструкторы при наследовании - student2.ru хххх реализация A::f1
  Конструкторы и деструкторы при наследовании - student2.ru хххх реализация A::f2

Размер виртуальной таблицы класса пропорционален числу объявленных в нем виртуальных функций. Каждый класс имеет свою виртуальную таблицу, размер которой сравнительно мал. Но если классов много и они содержат большое число виртуальных функций, то соответствующие им таблицы могут занимать достаточно много места.

Далее возникает вопрос, где хранятся эти таблицы. Один из подходов заключается в том, что копии виртуальной таблицы помещаются в каждый объектный файл. Затем компоновщик удаляет дубликаты, оставляя одну таблицу в конечном исполняемом файле или библиотеке. Другой, более распространенный подход, состоит в том, что виртуальная таблица создается в объектном файле первой не inline виртуальной функции. Для класса А это объектный файл A::~A(). Если все виртуальные функции объявлены как inline, то копии виртуальной таблицы будут созданы во всех объектных файлах, использующих ее.

В тоже время vtbl, являясь частью механизма виртуальных функций, является бесполезной без указателей vprt. Они устанавливают соответствие между объектом и некоторой vtbl. Каждый объект, класс которого объявляет виртуальную функцию, содержит vprt, добавляемый компилятором к объекту (рис.2).

Конструкторы и деструкторы при наследовании - student2.ru Свойство виртуальности проявляется только тогда, когда обращение к функции идет через указатель или ссылку на объект. Указатель или ссылка могут указывать как на объект базового, так и на объект производного классов. Если в программе имеется сам объект, то уже на стадии компиляции известен его тип и, следовательно, механизм виртуальности не используется. Например:

func(cls obj)

{

obj.vvod(); // вызов компоненты-функции obj::vvod

}

func1(cls &obj)

{

obj.vvod(); // вызов компоненты-функции в соответствии

} // с типом объекта, на который ссылается obj

Виртуальные функции позволяют принимать решение в процессе выполнения.

#include <iostream.h>

#include <iomanip.h>

#include <string.h>

#define N 5

class base // базовый класс

{ public:

virtual char *name(){ return " noname ";}

virtual double area(){ return 0;}

};

class rect : public base // производный класс (прямоугольник)

{ int h,s;

public:

virtual char *name(){ return " прямоугольника ";}

rect(int H,int S): h(H),s(S) {}

double area(){ return h*s;}

};

class circl : public base // производный класс (окружность)

{ int r;

public:

virtual char *name(){ return " круга ";}

circl(int R): r(R){}

double area(){ return 3.14*r*r;}

};

int main()

{ base *p[N],a;

double s_area=0;

rect b(2,3);

circl c(4);

for(int i=0;i<N;i++) p[i]=&a;

p[0]=&b;

p[1]=&c;

for(i=0;i<N;i++)

cout << "плошадь" << p[i]->name() << p[i]->area() << endl;

return 0;

}

Массив указателей p хранит адреса объектов базового и производных классов и необходим для вызова виртуальных функций этих классов. Виртуальная функция базового класса необходима тогда, когда она явно вызывается для базового класса или не определена (не переопределена) для некоторого производного класса.

Если функция была объявлена как виртуальная в некотором классе (базовом классе), то она остается виртуальной независимо от количества уровней в иерархии классов, через которые она прошла.

Class A

{ . . .

public: virtual void fun() {}

};

Class B : public A

{ . . .

public: void fun() {}

};

Class C : public B

{ . . .

public:

. . .// в объявлении класса С отсутствует описание функции fun()

};

main()

{ A a,*p=&a;

B b;

C c;

p->fun(); // вызов версии виртуальной функции fun для класса А

p=&b;

p->fun(); // вызов версии виртуальной функции fun для класса B

p=&c;

p->fun(); // вызов версии виртуальной функции fun для класса B (из А)

}

Если в производном классе виртуальная функция не переопределяется, то используется ее версия из базового класса.

Class A

{ . . .

public: virtual void fun() {}

};

Class B : public A

{ . . .

public: void fun() {}

};

Class C : public B

{ . . .

public: void fun() {}

};

main()

{ A a,*p=&a;

B b;

C c;

p->fun(); // вызов версии виртуальной функции fun для класса А

p=&b;

p->fun(); // вызов версии виртуальной функции fun для класса B

p=&c;

p->fun(); // вызов версии виртуальной функции fun для класса С

}

Основное достоинство данной иерархии состоит в том, что код не нуждается в глобальном изменении, даже при добавлении вычислений площадей новых фигур. Изменения вносятся локально и благодаря полиморфному характеру кода распространяются автоматически.

Рассмотрим механизм вызова виртуальной функции базового и производного классов из компонент-функций этих классов, вызываемых через указатель на базовый класс.

Class A

{ public :

virtual void f()

{ return; }

void fn()

{ f(); } // вызов функции f

};

Class B : public A

{ public:

void f()

{ return; }

void fn()

{ f(); } // вызов функции f

};

main()

{ A a,*pa=&a;

B b,*pb=&b;

pa->fn(); // вызов виртуальной функции f класса А через A::fn()

pa = &b;

pa->fn(); // вызов виртуальной функции f класса В через A::fn()

pb->fn(); // вызов виртуальной функции f класса В через B::fn()

}

В инструкции pa->fn() выполняется вызов функции fn() базового класса А, так как указатель pa – указатель на базовый класс и компилятор выполняет вызов функции базового класса. Далее из функции fn() выполняется вызов вначале виртуальной функции f() класса А, так как указатель pa инициализирован адресом объекта А. Затем, после инициализации paадресом объекта В, выполняется вызов виртуальной функции f() класса В из функции fn() класса А. Далее используя указатель pb, инициализированный так же адресом объекта В, вызывается функция f() класса В через функцию fn() класса В.

В заключение рассмотрим механизм вызова виртуальных функций внутри конструкторов и деструкторов.

#include <iostream>

using namespace std;

class Base {

public:

Base()

{ cout << " конструктор базового класса"<<endl;

f1();

}

~Base()

{ cout << " деструктор базового класса"<<endl;

f2();

}

virtual void f1() { cout << " функция f1 базового класса"<<endl; }

virtual void f2() { cout << " функция f2 базового класса "<<endl; }

void f3(){ cout<<" функция f3 базового класса "<<endl;

f1();

}

};

class Derived : public Base

{ public:

Derived()

{ cout<<"конструктор производного класса "<<endl; }

~Derived()

{ cout<<"деструктор производного класса "<<endl; }

virtual void f1() { cout << " функция f1 производного класса"<<endl; }

virtual void f2() { cout << " функция f2 производного класса"<<endl; }

};

int main()

{ cout << "создание обекта"<<endl;

Derived *pd = new Derived();

cout << "разрушение объекта"<<endl;

delete pd;

}

Результат работы программы:

конструктор базового класса

функция f1 базового класса

конструктор производного класса

функция f3 базового класса

функция f1 производного класса

деструктор производного класса

деструктор базового класса

функция f2 базового класса

При вызове виртуальных функций в конструкторе или деструкторе, компилятор вызывает локальную версию функции, т.е., механизм виртуальных вызовов в конструкторах и деструкторах не работает. Это происходит потому, что vtbl во время вызова конструктора полностью не инициализирована.

Аналогично, в деструкторе вызывается функция базового класса, потому как объект производного класса был уже уничтожен (был вызван его деструктор).

Перечислим основные свойства и правила использования виртуальных функций:

- виртуальный механизм поддерживает полиморфизм на этапе выполнения программы. Это значит, что требуемая версия программы выбирается на этапе выполнения программы, а не компиляции;

- класс, содержащий хотя бы одну виртуальную функцию, называется полиморфным;

- виртуальные функции можно объявить только в классах (class) и структурах (struct);

- виртуальными функциями могут быть только нестатические функции (без спецификатора static), так как характеристика virtual унаследуется. Функция порожденного класса автоматически становится virtual;

- виртуальные функции можно объявить со спецификатором friend для другого класса;

- виртуальными функциями могут быть только неглобальные функции (то есть компоненты класса);

- если виртуальная функция, объявленная в базовом классе со спецификатором virtual переопределена в производном без спецификатора viryual, то она при этом остается виртуальной независимо от уровня наследования. То есть механизм виртуализации функций наследуется;

- для вызова виртуальной функции требуется больше времени, чем для невиртуальной. При этом также требуется дополнительная память для хранения виртуальной таблицы;

- при использовании полного имени при вызове некоторой виртуальной функции (например, grup ::see();), виртуальный механизм не применяется.

Абстрактные классы

Базовый класс иерархии типа обычно содержит ряд виртуальных функций, обеспечивающих динамическую типизацию. Часто в базовом классе эти виртуальные функции фиктивны и имеют пустое тело. Эти функции существуют как некоторая абстракция, конкретное значение им придается в производных классах. Такие функции называются чисто виртуальными функциями, то есть такими, тело которых, как правило, не определено. Общая форма записи чисто виртуальной функции имеет вид:

virtual прототип функции = 0;

Чисто виртуальная функция используется для того, чтобы отложить решение о реализации функции. То, что функция объявлена чисто виртуальной, требует, чтобы эта функция была определена во всех производных классах от класса, содержащего эту функцию. Если класс имеет хотя бы одну чисто виртуальную функцию, то он называется абстрактным. Для абстрактного класса нельзя создать объекты и он используется только как базовый класс для других классов. Если base – абстрактный класс, то для инструкций

base a;

base *p= new base;

компилятор выдаст сообщение об ошибке. В то же время вполне можно использовать инструкции вида

rect b;

base *p=&b;

base &p=b;

Чисто виртуальную функцию, как и просто виртуальную функцию, не обязательно переопределять в производных классах. При этом если в производном классе она не переопределена, то этот класс тоже будет абстрактным, и при попытке создать объект этого класса компилятор выдаст ошибку. Таким образом, забыть переопределить чисто виртуальную функцию невозможно. Абстрактный базовый класс навязывает определенный интерфейс всем производным от него классам. Главное назначение абстрактных классов – в определении интерфейса для некоторой иерархии классов.

Класс можно сделать абстрактным, даже если все его функции определены. Это можно сделать, например, чтобы быть уверенным, что объект этого класса создан не будет. Обычно для этих целей выбирается деструктор.

class base

{ компоненты-данные

public:

virtual ~base() = 0;

компоненты-функции

}

base ::~base()

{реализация деструктора}

Объект класса base создать невозможно, в то же время деструктор его определен и будет вызван при разрушении объектов производных классов.

Для иерархии типа полезно иметь базовый абстрактный класс. Он содержит общие свойства порожденных объектов и используется для объявления указателей, которые могут обращаться к объектам классов, порожденным от базового. Рассмотрим это на примере программы экологического моделирования. В примере мир будет иметь различные формы взаимодействия жизни с использованием абстрактного базового класса living. Его интерфейс унаследован различными формами жизни. Создадим fox (лис) – хищника, rabbit (кролик) – жертву и grass - (траву).

#include "iostream.h"

#include "conio.h"

// моделирование хищник - жертва с использованием

// иерархии классов

const int N=6, // размер квадратной площади (мира)

STATES=4, // количество видов жизни

DRAB=5,DFOX=5, // количество циклов жизни кролика и лиса

CYCLES=10; // общее число циклов моделирования мира

enum state{EMPTY,GRASS,RABBIT,FOX};

class living; // forvard объявление

typedef living *world[N][N]; // world- модель мира

void init(world);

void gener(world);

void update(world,world);

void dele(world);

Class living

{protected:

int row,col; // местоположение в модели

void sums(world w,int sm[]); //

public:

living(int r,int c):row(r),col(c){}

virtual state who() = 0; // идентификация состояний

virtual living *next(world w)=0; // расчет next

virtual void print()=0; // вывод содержимого поля модели

};

void living::sums(world w,int sm[])

{ int i,j;

sm[EMPTY]=sm[GRASS]=sm[RABBIT]=sm[FOX]=0;

int i1=-1,i2=1,j1=-1,j2=1;

if(row==0) i1=0; // координаты внешних клеток модели

if(row==N-1) i2=0;

if(col==0) j1=0;

if(col==N-1) j2=0;

for(i=i1;i<=i2;++i)

for(j=j1;j<=j2;++j)

sm[w[row+i][col+j]->who()]++;

}

В базовом классе living объявлены две чисто виртуальные функции - who() и next() и одна обычная функция sums(). Моделирование имеет правила для решения о том, кто продолжает жить в следующем цикле. Они основаны на соседствующих популяциях в некотором квадрате. Глубина иерархии наследования – один уровень.

// текущий класс - только хищники

Class fox:public living

{ protected:

int age; // используется для принятия решения о смерти лиса

public:

fox(int r,int c,int a=0):living(r,c),age(a){}

state who() {return FOX;} // отложенный метод для foxes

living *next(world w); // отложенный метод для foxes

void print(){cout << " ли ";}

};

// текущий класс - только жертвы

Class rabbit:public living

{ protected:

int age; // используется для принятия решения о смерти кролика

public:

rabbit(int r,int c,int a=0):living(r,c),age(a){}

state who() {return RABBIT;} // отложенный метод для rabbit

living *next(world w); // отложенный метод для rabbit

void print(){cout << " кр ";}

};

// текущий класс - только растения

Class grass:public living

{ public:

grass(int r,int c):living(r,c){}

state who() {return GRASS;} // отложенный метод для grass

living *next(world w); // отложенный метод для grass

void print(){cout << " тр ";}

};

// жизнь отсутствует

Class empty : public living

{ public:

empty(int r,int c):living(r,c){}

state who() {return EMPTY;} // отложенный метод для empty

living *next(world w); // отложенный метод для empty

void print(){cout << " ";}

};

Характеристика поведения каждой формы жизни фиксируется в версии next(). Если в окрестности имеется больше grass, чем rabbit, grass остается, иначе grass будет съедена.

living *grass::next(world w)

{ int sum[STATES];

sums(w,sum);

if(sum[GRASS]>sum[RABBIT]) // кролик ест траву

return (new grass(row,col));

else

return(new empty(row,col));

}

Если возраст rabbit превышает определенное значение DRAB, он умирает либо, если поблизости много лис, он может быть съеден.

living *rabbit::next(world w)

{ int sum[STATES];

sums(w,sum);

if(sum[FOX]>=sum[RABBIT]) // лис ест кролика

return (new empty(row,col));

else if(age>DRAB) // кролик слишком старый

return(new empty(row,col));

else

return(new rabbit(row,col,age+1)); // кролик постарел

}

Fox тоже умирает от старости.

living *fox::next(world w)

{ int sum[STATES];

sums(w,sum);

if(sum[FOX]>5) // слишком много лис

return (new empty(row,col));

else if(age>DFOX) // лис слишком старый

return(new empty(row,col));

else

return(new fox(row,col,age+1)); // лис постарел

}

// заполнение пустой площади

living *empty::next(world w)

{ int sum[STATES];

sums(w,sum);

if(sum[FOX]>1) // первыми добавляются лисы

return (new fox(row,col));

else if(sum[RABBIT]>1) // вторыми добавляются кролики

return (new rabbit(row,col));

else if(sum[GRASS]) // третьими добавляются растения

return (new grass(row,col));

else return (new empty(row,col));// иначе пусто

}

Массив world представляет собой контейнер для жизненных форм. Он должен иметь в собственности объекты living, чтобы распределять новые и удалять старые.

// world полностью пуст

void init(world w)

{ int i,j;

for(i=0;i<N;++i)

for(j=0;j<N;++j)

w[i][j]=new empty(i,j);

}

// генерация исходной модели мира

void gener(world w)

{ int i,j;

for(i=0;i<N;++i)

for(j=0;j<N;++j)

{ if(i%2==0 && j%3==0) w[i][j]=new fox(i,j);

else if(i%3==0 && j%2==0) w[i][j]=new rabbit(i,j);

else if(i%5==0) w[i][j]=new grass(i,j);

else w[i][j]=new empty(i,j);

}

}

// вывод содержимого модели мира на экран

void pr_state(world w)

{ int i,j;

for(i=0;i<N;++i)

{ cout<<endl;

for(j=0;j<N;++j)

w[i][j]->print();

}

cout << endl;

}

// новый world w_new рассчитывается из старого world w_old

void update(world w_new, world w_old)

{ int i,j;

for(i=0;i<N;++i)

for(j=0;j<N;++j)

w_new[i][j]=w_old[i][j]->next(w_old);

}

// очистка мира

void dele(world w)

{ int i,j;

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

for(j=1;j<N-1;++j) delete(w[i][j]);

}

Модель имеет odd и even мир. Их смена является основой для расчета последующего цикла.

int main()

{ world odd,even;

int i;

init(odd);

init(even);

gener(even); // генерация начального мира

cout<<"1 цикл жизни модели"<<endl;

pr_state(even); // вывод сгенерированной модели

for(i=0;i<CYCLES-1;++i) //цикл моделирования

{ getch();

cout<<i+2<<" цикл жизни модели"<<endl;

if(i%2)

{ update(even,odd); // создание even модели из odd модели

pr_state(even); // вывод сгенерированной модели even

dele(odd); // удаление модели odd

}

else

{ update(odd,even); // создание odd модели из even модели

pr_state(odd); // вывод сгенерированной модели odd

dele(even); // удаление модели even

}

}

return 1;

}

Виртуальные деструкторы

Виртуальные деструкторы необходимы при использовании указателей на базовый класс при выделении памяти под динамически создаваемые объекты производных классов. Это обусловлено тем, что если объект уничтожается явно, например, используя операцию delete, то вызывается деструктор только класса, совпадающего с типом указателя на уничтожаемый объект. При этом не учитывается тип объекта, на который указывает данный указатель.

В случае объявления деструктора базового класса виртуальным, все деструкторы производных классов становятся так же виртуальными. При этом если будет выполняться явное уничтожение объекта производного класса для указателя на базовый класс, то в начале вызывается деструктор производного класса, а затем вверх по иерархии до деструктора базового класса.

Рассмотрим отличия в работе программы при использовании виртуальных деструкторов и в случае их отсутствия на примере программы вычисляющей площади некоторых фигур (круг, прямоугольник).

#include "iostream.h"

#include "iomanip.h"

#include "string.h"

class Shape // базовый класс

{protected:

float s; // площадь фигуры

public:

Shape(char *fig) : s(0)

{ cout << "конструктор класса Shape (фигура "<< fig <<')'<< endl;}

virtual ~Shape()

{ cout << "деструктор класса Shape" << endl;}

void virtual print()

{cout<<s<<endl;}

void virtual area()=0;

};

class Circle : public Shape // производный класс Круг

{ int r;

public:

Circle(char *name,int r): Shape(name)

{ cout << "конструктор класса Circle "<<endl;

this->r=r;

}

~Circle()

{ cout << "деструктор класса Circle " << endl;}

void area();

};

class Bar : public Shape // производный класс Прямоугольник

{ int n,m;

public:

Bar(char *name,int n,int m): Shape(name)

{ cout << "конструктор класса Bar "<<endl;

this->n=n;

this->m=m;

}

~Bar()

{ cout << "деструктор класса Bar " << endl;}

void area();

};

void Circle::area()

{ s=r*r*3.14;

cout<<"Площадь круга = ";

this->print();

}

void Bar::area()

{ s=n*m;

cout<<"Площадь прямоугольника = ";

this->print();

}

int main()

{ Shape *fg1,*fg2;

fg1=new Circle("Круг",2);

fg2=new Bar("Прямоугольник",3,4);

fg1->area();

fg2->area();

delete fg1;

delete fg2;

return 1;

}

Результат работы программы:

конструктор класса Shape (фигура Circle)

конструктор класса Circle

конструктор класса Shape (фигура Bar)

конструктор класса Bar

площадь круга =12.56

площадь прямоугольника =12

деструктор класса Circle

деструктор класса Shape

деструктор класса Bar

деструктор класса Shape

В случае если деструктор базового класса являлся бы не виртуальным, то при удалении объектов производных классов осуществлялся бы вызов только деструктора базового класса (т.е. класса для которого объявлен соответствующий указатель).

Если в классе имеются виртуальные функции, то желательно объявлять деструктор этого класса также виртуальным, даже если этого не требуется. Это может предотвратить возможные ошибки.

Множественное наследование

В языке С++ имеется возможность образовывать производный класс от нескольких базовых классов. Общая форма множественного наследования имеет вид:

class имя_произв_класса : имя_базового_кл 1,…,имя_базового_кл N

{ содержимое класса

};

Иерархическая структура, в которой производный класс наследует от несколько базовых классов, называется множественным наследованием. В этом случае производный класс, имея собственные компоненты, имеет доступ к protected- и public-компонентам базовых классов.

Наши рекомендации