Конструкторы и деструкторы производных классов
Поскольку конструкторы не наследуются, при создании производного класса наследуемые им данные-члены должны инициализироваться конструктором базового класса. Конструктор базового класса вызывается автоматически и выполняется до конструктора производного класса. Если наследуется несколько базовых классов, то их конструкторы выполняются в той последовательности, в которой перечислены базовые классы в определении производного класса. Конструктор производного класса вызывается по окончании работы конструкторов базовых классов. Параметры конструктора базового класса указываются в определении конструктора производного класса. Таким образом, происходит передача аргументов от конструктора производного класса конструктору базового класса. Например. class Basis { int a,b; public: Basis(int x,int y){a=x;b=y;} }; class Inherit:public Basis {int sum; public: Inherit(int x,int y, int s):Basis(x,y){sum=s;} }; Запомните, что конструктор базового класса вызывается автоматически, и мы указываем его в определении конструктора производного класса только для передачи ему аргументов. Объекты класса конструируются снизу вверх: сначала базовый, потом компоненты-объекты (если они имеются), а потом сам производный класс. Таким образом объект производного класса содержит в качестве подобъекта объект базового класса. Уничтожаются объекты в обратном порядке: сначала производный, потом его компоненты-объекты, а потом базовый объект. Как мы знаем, объект уничтожается при завершении программы или при выходе из области действия определения объектов и эти действия выполняет деструктор. Статус деструктора по умолчанию public. Деструкторы не наследуется, поэтому даже при отсутствии в производном классе деструктора он не передается из базового, а формируется компилятором как умалчиваемый. Классы, входящие в иерархию, должны иметь в своем распоряжении виртуальные деструкторы. Деструкторы могут переопределяться, но не перегружаться.
В любом классе могут быть в качестве компонентов определены другие классы. В этих классах могут быть свои деструкторы, которые при уничтожении объекта охватывающего (внешнего) класса выполняются после деструктора охватывающего класса. Деструкторы базовых классов выполняются в порядке, обратном перечислению классов в определении производного класса. Таким образом, порядок уничтожения объекта противоположен по отношению к порядку его конструирования. Пример 2.2.1.. // Определение класса базового класса ТОЧКА и производного класса ПЯТНО. #include <graphics.h> // используем графику #include <conio.h> class point // Определение класса ТОЧКА { protected: int x,y; public: point(int x1=0,int y1=0); int& getx(void); int& gety(void); void show(void); void move(int x1=0,int y1=0); private: void hide(); }; class spot: public point // Определение класса ПЯТНО {protected: int r; // радиус int vis; // признак видимости int tag;// признак сохранения образа объекта в памяти spot *pspot; // указатель на область памяти для образа public: spot(int,int,int); void show(); void hide(); void move(int,int); void change (float d) // изменить размер }; // Определение функций - членов класса ТОЧКА point:: point (int x1,int y1){x = x1; y = y1;} int& point:: getx (void){return x;} int& point:: gety (void){return y;} void point:: show (void){putpixel(x, y, getcolor());} void point:: hide (void){putpixel(x,y,getbkcolor());} void point:: move (int x1,int y1) { hide(); x = x1; y = y1; show(); } // Определение функций - членов класса ПЯТНО spot:: spot (int x1,int y1,int r1): point(x1,y1) {int size; vis = 0; tag = 0; r = r1; size = imagesize(x1–r,y1–r,x1+r,y1+r); pspot = (spot*)new char[size];} spor:: ~spot (){hide(); tag = 0; delete pspot;} void show () {if(tag = = 0) {circle(x,y,r); floodfill(x,y,getcolor()); getimage(x–r,y–r,x+r,y+r,pspot); tag = 1; } else putimage(x–r,y–r,pspot,XOR_PUT); vis = 1;} void spot:: hide () {if(vis = = 0) return; putimage(x–r,y–r,pspot,XOR_PUT); vis = 0;} void spot:: move (int x1,int y1) {hide(); x = x1; y = y1;
show();} void spot:: change (float d) {float a; int size; hide(); tag = 0; delete pspot; a = d*r; if(a<=0) r = 0; else r = (int)a; size = imagesize(x–r,y–r,x+r,y+r); pspot = (spot*)new char[size]; show();} int& spot::getr(void){return r;} }; // Создаются два объекта, показываются, затем один //перемещается, а другой изменяет размеры void main () { //инициализация графики int dr=DETECT,mod; initgraph(&dr,&mod,“C: \ tc \ bgi”); { spot A(200,50,20); spot B(500,200,30); A.show(); getch(); B.show(); getch(); A.move(50,60); getch(); B.change(3); getch(); } closegraph(); } В этом примере в объекте spot точка создается как безымянный объект класса point. Особенностью функции main в примере является наличие внутреннего блока для работы с объектами spot. Это связано с наличием в классе spot деструктора, при выполнении которого вызывается метод hide(), требующий графического режима. Если построить программу без внутреннего блока, то деструктор будет вызываться при окончании программы, когда графический режим закрыт. Эту проблему можно также решить путем явного вызова деструктора, например: … В.change(3); getch(); А.spot:: ~spot(); getch(); B.spot:: ~spot(); closegraph();
2.3. Виртуальные функции К механизму виртуальных функций обращаются в тех случаях, когда в каждом производном классе требуется свой вариант некоторой компонентной функции. Классы, включающие такие функции называются полиморфными и играют особую роль в ООП. Рассмотрим, как ведут себя при наследовании невиртуальные компонентные функции с одинаковыми именами, типами и сигнатурами параметров. Пример 2.3.1. class base { public: void print(){cout<<“\nbase”;} }; class dir: public base { public: void print(){cout<<“\ndir”;} }; void main () { base B,*bp = &B; dir D,*dp = &D; base *p = &D; bp –>print(); // base dp –>print(); // dir p –>print(); // base } В последнем случае вызывается функция print базового класса, хотя указатель p настроен на объект производного класса. Дело в том, что выбор нужной функции выполняется при компиляции программы и определяется типом указателя, а не его значением. Такой режим называется ранним или статическим связыванием. Промер 2.3.2 //птицы class bird{ //... public: void fly()const{cout<<"fly"<<endl;} //может летать }; //пингвин class penguin:public bird{ //... public: void fly()const{cout<<"nofly"<<endl;} //не летает }; int main() {bird b; penguin p; b.fly(); //летит p.fly(); //не летит return 0; } Выполним эту программу. Все нормально: птицы летают, но пингвин не летает.
Усложним задание. Добавим функцию alarm() – сигнал «тревога», который поступает всем птицам, в том числе и пингвину. Реакция птиц на сигнал «тревога»: убежать, улететь.
//Сигнал тревоги void alarm(const bird& b) { b.fly(); } int main() { bird b; penguin p; b.fly(); //летит p.fly(); //не летит alarm(b); //полетела alarm(p); //как ни странно, и пингвин полетел??? return 0; } Выполним эту программу. Видим, что все птицы, в том числе и пингвин, летят. Почему так происходит?
void person::show() { cout<<name<<“ “<<age<<endl; } void student::show() { cout<<name<<“ “<<age<<“ “<<grade<<endl; } void teacher::show() { cout<<name<<“ “<<age<<“ “<<post<<endl; } void main() { person* p; p=new person(“Иванов”,35); p->show(); // Что будет выведено? p=new student(“Петров”,21,75.8); p->show(); // Что будет выведено? p=new teacher(“Поляков”,51,”Декан”); p->show(); // Что будет выведено? return 0; } Ожидаем следующее void main() { person* p; p=new person(“Иванов”,35); p->show(); // Иванов 25 p=new student(“Петров”,21,75.8); p->show(); // Петров 21 75.5 p=new teacher(“Поляков”,51,”Декан”); p->show(); // Поляков 51 Декан return 0; } А на самом деле выведено void main() { person* p; p=new person(“Иванов”,35); p->show(); // Иванов 25 p=new student(“Петров”,21,75.8); p->show(); // Петров 21 p=new teacher(“Поляков”,51,”Декан”); p->show(); // Поляков 51 return 0; } Почему так происходит? Это происходит потому, что вызывается функция show() базового класса, хотя указатель p настроен на объект производного класса. void alarm(const bird& b) { b.fly(); } person* p; p->show(); Дело в том, что выбор нужной функции выполняется при компиляции программы и определяется типом указателя, а не его значением. Как мы уже знаем, такой режим называется ранним или статическим связыванием. Большую гибкость обеспечивает позднее (отложенное) или динамическое связывание, которое предоставляется механизмом виртуальных функций. Любая нестатическая функция базового класса может быть сделана виртуальной, для чего используется ключевое слово virtual. Пример 2.3.3 class base { public: virtual void print(){cout<<“\nbase”;} ... }; // и так далее – см. предыдущий пример. В этом случае будет напечатано base dir dir
Пример 2.3.4 class person { protected: string name; int age; public: person(string Name=“Noname”,int Age=0): name(Name),age(Age){} virtual void show(); }; class student:public person { protected: double grade; public: student(string Name=“Noname”, int Age=0, double Grade=0.0): person(Name,Age),grade(Grade){} virtual void show(); };
Таким образом, интерпретация каждого вызова виртуальной функции через указатель на базовый класс зависит от значения этого указателя, т.е. от типа объекта, для которого выполняется вызов. Виртуальные функции–это функции, объявленные в базовом классе и переопределенные в производных классах. Иерархия классов, которая определена открытым наследованием, создает родственный набор пользовательских типов, на все объекты которых может указывать указатель базового класса. Выбор того, какую виртуальную функцию вызвать, будет зависеть от типа объекта, на который фактически (в момент выполнения программы) направлен указатель, а не от типа указателя. Виртуальными могут быть только нестатические функции-члены. Виртуальность наследуется. После того, как функция определена как виртуальная, ее повторное определение в производном классе (с тем же самым прототипом) создает в этом классе новую виртуальную функцию, причем спецификатор virtual может не использоваться. Конструкторы не могут быть виртуальными, в отличие от деструкторов. Практически каждый класс, имеющий виртуальную функцию, должен иметь виртуальный деструктор. Если в производном классе ввести функцию с тем же именем и типом, но с другой сигнатурой параметров, то эта функция производного класса не будет виртуальной. Виртуальная функция может быть дружественной в другом классе. Механизм виртуального вызова может быть подавлен с помощью явного использования полного квалифицированного имени. В Visual Studio C++.NET для переопределения виртуальной функции вы можете использовать ключевое слово override class person { public: virtual void show(); }; class student:public person { public: void show()override; };
2.4. Абстрактные классы Абстрактным называется класс, в котором есть хотя бы одна чистая (пустая) виртуальная функция. Чистой виртуальной называется компонентная функция, которая имеет следующее определение: virtual тип имя_функции(список_формальных_параметров) = 0; Чистая виртуальная функция ничего не делает и недоступна для вызовов. Ее назначение – служить основой для подменяющих ее функций в производных классах. Абстрактный класс может использоваться только в качестве базового для производных классов. Механизм абстрактных классов разработан для представления общих понятий, которые в дальнейшем предполагается конкретизировать. При этом построение иерархии классов выполняется по следующей схеме. Во главе иерархии стоит абстрактный базовый класс. Он используется для наследования интерфейса. Производные классы будут конкретизировать и реализовать этот интерфейс. В абстрактном классе объявлены чистые виртуальные функции, которые,по сути, есть абстрактные методы.
Пример 2.4.1. class Base{ public: Base(); // конструктор по умолчанию Base(const Base&); // конструктор копирования virtual ~Base(); // виртуальный деструктор virtual void Show()=0; // чистая виртуальная функция // другие чистые виртуальные функции protected: // защищенные члены класса private:
}; class Derived: virtual public Base{ public: Derived(); // конструктор по умолчанию Derived(const Derived&); // конструктор копирования Derived(параметры); // конструктор с параметрами virtual ~Derived(); // виртуальный деструктор void Show(); // переопределенная виртуальная функция // другие переопределенные виртуальные функции // перегруженная операция присваивания Derived& operator=(const Derived&); // ее смысл будет понятен после прочтения главы 3 // другие перегруженные операции protected: // используется вместо private, если ожидается наследование private: // используется для деталей реализации }; По сравнению с обычными классами абстрактные классы пользуются «ограниченными правами». А именно: * невозможно создать объект абстрактного класса; * абстрактный класс нельзя употреблять для задания типа параметра функции или типа возвращаемого функцией значения; * абстрактный класс нельзя использовать при явном приведении типов; в то же время можно определить указатели и ссылки на абстрактный класс. Объект абстрактного класса не может быть формальным параметром функции, однако формальным параметром может быть указатель на абстрактный класс. В этом случае появляется возможность передавать в вызываемую функцию в качестве фактического параметра значение указателя на производный объект, заменяя им указатель на абстрактный базовый класс. Таким образом мы получаем полиморфные объекты. В Visual Studio C++.NET для определения абстрактных функций и классов вы можете использовать ключевое слово abctract class person abstract { protected: string name; int age; public: person(string Name,int Age): name(Name), age(Age){} virtual void show()const abstract; }; Включение объектов Есть два варианта включения объекта типа X в класс A; 1)Объявить в классе А член типа Х; class A{ X x; //... }; 2)Объявить в классе А член типа X* или X&. class A{ X* p; X& r; }; Предпочтительно включать собственно объект, как в первом случае. Это эффективнее и меньше подвержено ошибкам, так как связь между содержащимся и содержащим объектами описывается правилами конструирования и уничтожения. Например, // Персона class person{ char* name: public: person(char*); //... }; //Школа class school{ person head; //директор public: school(char* name):head(name){} //... }; Второй вариант с указателем можно применять тогда, когда за время жизни «содержащего» объекта нужно изменить указатель на «содержащийся» объект. Например, class school{ person* head; //директор public: school(char* name):head(new person(name)){} ~school{delete head;} person* change(char * newname){ person* temp=head; head=new person(newname); return temp;} //... }; Второй вариант можно использовать, когда требуется задавать «содержащийся» объект в качестве аргумента. Например, class school{ person* head; //директор public: school(person* q):head(q){} //... }; Имея объекты, включающие другие объекты, мы создаем иерархию объектов. Она является альтернативой и дополнением к иерархии классов. А как быть в том случае, когда количество включаемых объектов заранее неизвестно и(или) может изменяться за время жизни «содержащего» объекта. Например, если объект school содержит учеников, то их количество может меняться. Существует два способа решения этой проблемы. Первый состоит в том, что организуется связанный список включенных объектов, а «содержащий» объект имеет член-указатель на начало этого списка. Например, class person { char* name; person* next; ... }; class school { person* head; // указатель на директора школы person* begin; // указатель на начало списка учеников public: shool(char* name):head(new person(name)),begin(NULL){} ~shool(); void add(person* ob); //... }; В этом случае при создании объекта school создается пустой список включенных объектов. Для включения объекта вызывается метод add(), которому в качестве параметра передается указатель на включаемый объект. Деструктор последовательно удаляет все включенные объекты. Объект person содержит поле next, которое позволяет связать объекты в список. Законченная программа, демонстрирующая этот способ включения приведена в упражнении. Второй способ заключается в использовании специального объекта-контейнера. Контейнерный класс предназначен для хранения объектов и представляет собой простые и удобные способы доступа к ним. class school { person* head; container pupil; ... }; Здесь pupil–контейнер, содержащий учеников. Все, что необходимо для добавления, удаления, просмотра и т.д. включенных объектов, должно содержаться в методах класса container. Примером могут служить контейнеры стандартной библиотеки шаблонов(STL) С++. Рассмотрим отношения между наследованием и включением. Включение и наследование Пусть класс В есть производный класс от класса D. class B{...}; class D:public B{...}; Слово public в заголовке класса D говорит об открытом наследовании. Открытое наследование означает, что производный класс D является подтипом класса В, т.е. объект D является и объектом В. Такое наследование является отношением is-a или говорят, что D есть разновидность В. Иногда его называют также интерфейсным наследованием. При открытом наследовании переменная производного класса может рассматривается как переменная типа базового класса. Указатель, тип которого–«указатель на базовый класс», может указывать на объекты, имеющие тип производного класса. Используя наследование, мы строим иерархию классов. Рассмотрим следующую иерархию классов. class person { protected: char* name; int age; public: person(char*,int); virtual void show() const; //... }; class employee:public person{ protected: int work; public: employee)char*,int,int); void show() const; //... }; class teacher:public employee{ protected: int teacher_work; public: teacher(char*,int,int,int); void show() const; //... }; Определим указатели на объекты этих классов. person* pp; teacher* pt; Создадим объекты этих классов. person a(“Петров”,25); employee b(“Королев”,30.10); pt=new teacher(“Тимофеев”,45.23,15); Просмотрим эти объекты. pp=&a; pp->show(); //вызывает person::show для объекта а pp=&b; pp->show(); // вызывает employee::show для объекта b pp=pt; pp->show(); // вызывает teacher::show для объекта *pt Здесь указатель базового класса pp указывает на объекты производных классов employee, teacher, т.е. он совместим по присваиванию с указателями на объекты этих классов. При вызове функции show с помощью указателя pp вызывается функция show того класса, на объект которого фактически указывает pp. Это достигается за счет объявления функции show виртуальной, в результате чего мы имеем позднее связывание. Пусть теперь класс D имеет член класса В. class D{ public: B b; //... }; В свою очередь, класс В имеет член класса С. class В{ public: С с; //... }; Такое включение называют отношением has-a. Используя включение, мы строим иерархию объектов. На практике возникает проблема выбора между наследованием и включением. Рассмотрим классы «Самолет» и «Двигатель». Новичкам часто приходит в голову сделать «Самолет» производным от «Двигатель». Это не верно, поскольку самолет не является двигателем, он имеет двигатель. Один из способов увидеть это–задуматься, может ли самолет иметь несколько двигателей? Поскольку это возможно, нам следует использовать включение, а не наследование. Рассмотрим следующий пример: class B{ public: virtual void f(); void g(); }; class D{ public: B b; void f(); }; void h(D* pd){ B* pb; pb=pd; // #1 Ошибка pb->g(); // #2 вызывается B::g() pd->g(); // #3 Ошибка pd->b.g(); // #4 вызывается B::g() pb->f(); // #5 вызывается B::f() pd->f(); // #6 вызывается D::f() } Почему в строках #1 и #3 ошибки? В строке #1 нет преобразования D* в B*. В строке #3 D не имеет члена g(). В отличие от открытого наследования, не существует неявного преобразования из класса в один из его членов, и класс, содержащий член другого класса, не замещает виртуальных функций того класса. Если для класса D использовать открытое наследование
class D:public B{ public: void f();}; то функция void h(D* pd){ B* pb=pd; pb->g(); // вызывается B::g() pd->g(); // вызывается B::g() pb->f(); // вызывается D::f() pd->f(); // вызывается D::f() } не содержит ошибок. Так как D является производным классом от B, то выполняется неявное преобразование из D в B. Следствием является возросшая зависимость между B и D. Существуют случаи, когда вам нравится наследование, но вы не можете позволить таких преобразований. Например, мы хотим повторно использовать код базового класса, но не предполагаем рассматривать объекты производного класса как экземпляры базового. Все, что мы хотим от наследования–это повторное использование кода. Решением здесь является закрытое наследование. Закрытое наследование не носит характера отношения подтипов или отношения is-a. Мы будем называть его отношением like-a (подобный) или наследованием реализации в противоположность наследованию интерфейса. Закрытое(так же, как и защищенное) наследование не создает иерархии типов. С точки зрения проектирования закрытое наследование равносильно включению, если не считать вопроса с замещением функций. Важное применение такого подхода–открытое наследование из абстрактного класса и одновременно закрытое(или защищенное) наследование от конкретного класса для представления реализации. Пример 2.6.1. Бинарное дерево поиска //Файл tree.h // Обобщенное дерево typedef void* Tp; //тип обобщенного указателя int comp(Tp a,Tp b); class node { //узел private: friend class tree; node* left; node* right; Tp data; int count; node(Tp d,Tp* l,Tp*r):data(d),left(l),right(r),count(1){} friend void print(node* n); }; class tree {//дерево public: tree(){root=0;} void insert(Tp d); Tp find(Tp d) const{return(find(root,d));} void print() const{print(root);} protected: node* root; //корень Tp find(node* r,Tp d) const; void print(node* r) const; }; Узлы двоичного дерева содержат обобщенный указатель на данные data. Он будет соответствовать типу указателя в производном классе. Поле count содержит число повторяющихся вхождений данных. Для конкретного производного класса мы должны написать функцию comp для сравнения значений конкретного производного типа. Функция insert() помещает узлы в дерево. void tree::insert(TP d) {node* temp=root; node* old; if(root==0){root=new node(d,0,0);return;} while(temp!=0){ old=temp; if(comp(temp->data,d)==0){(temp->count)++;return;} if(comp(temp->data,d)>0)temp=temp->left; else temp=temp->right;} it(comp(old->data,d)>0)old->left=new(d,0,0); else old->right=new node(d,0,0); } Функция Tp find(node* r,Tp d) ищет в поддереве с корнем r информацию, представленную d. Tp tree::find(node* r,Tp d)const {if(r==0) return 0; else if(comp(r->data,d)==0)return(r->data); else if (comp(r->data,d)>0)return(find(r->left,d)); else return(find(r->right,d)); } Функция print()–стандартная рекурсия для обхода бинарного дерева void tree::print(node* r) const {if(r!=0){ print(r->left); ::print(r); print(r->right); } В каждом узле применяется внешняя функция::print(). Теперь создадим производный класс, который в качестве членов данных хранит указатели на char. //Файл s_tree.cpp #include “tree.h” #include <string.h> class s_tree:private tree{ public: s_tree(){} void insert(char* d){tree::insert(d);} char* find(char* d) const {return(char*)tree::find(d)); voif print() const{tree::print();} }; В классе s_tree функция insert использует неявное преобразование char* к void*. Функция сравнения comp выглядит следующим образом: int comp(Tp a,Tp b) {return(strcmp((char*)a,(char*)b));} Для вывода значений, хранящихся в узле, используется внешняя функция print(node* n){ cout<<(char*)(n->data)<<endl; cout<<n->cout<<endl; } Здесь для явного приведения типа void* к char* мы используем операцию приведения типа (имя_типа)выражение. Более надежным является использование оператора static_cast< char*>(Tp)
Воспользуйтесь поиском по сайту: ©2015 - 2024 megalektsii.ru Все авторские права принадлежат авторам лекционных материалов. Обратная связь с нами...
|