Главная | Обратная связь | Поможем написать вашу работу!
МегаЛекции

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




Инструкция 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();

...

};

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

  хххх реализация A::A
vtbl A хххх реализация A::f1
  хххх реализация A::f2

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

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

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

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

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 << " ли ";}

};

 

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

Поделиться:





Воспользуйтесь поиском по сайту:



©2015 - 2024 megalektsii.ru Все авторские права принадлежат авторам лекционных материалов. Обратная связь с нами...