Попробуйте часть кода до слова main заменить на #include "stdafx.h" //1 #include <iostream> //2 using namespace std; //3 |
Классы как средство создания больших программных комплексов
До сих пор мы знакомились с возможностями классов как средства создания и обработки новых типов данных. Наряду с этим важнейшим достижением языка C++ имеется другая, не менее важная заслуга классов, – они позволяют строить развивающиеся иерархические структуры программных комплексов. И главным механизмом здесь является наследование – возможность порождать новые классы на базе уже имеющихся с передачей порожденным классам наследства в виде данных и членов-функций родительского класса (или классов, если прямых родителей несколько). Порожденные классы имеют возможность расширять набор данных, полученных по наследству, модифицировать родительские методы и функции, создавать новые данные и новые функции их обработки. Возможность сохранять ранее созданное программное хозяйство, модифицируя его в соответствии с новыми задачами, позволяет с меньшими затратами и с большей надежностью вести разработки больших программных систем. Дополнительный выигрыш в производительности процесса разработки программного обеспечения можно получить за счет использования библиотек классов и шаблонов, активно создаваемых в настоящее время.
15.1. Базовый и производный классы
Когда говорят о классе D, порожденном из класса B, то принято называть родительский класс базовым, а вновь созданный класс – производным. Механизм наследования ( inheritance ) предусматривает две возможности. В первом случае, который называют простым наследованием, родительский класс один. Во втором случае родителей два или больше, и соответствующий процесс именуют термином множественное наследование. В первую очередь мы познакомимся с механизмом простого наследования.
15.1.1.Простое наследование
Итак, как формально выглядит процедура объявления производного класса D и что он получает в наследство от своего родителя – класса B?
class D: [virtual][public|private|protected] B {тело производного класса};
Служебное слово virtual (виртуальный) используется для предотвращения коллизий в случае сложного множественного наследования (по этому поводу см. раздел 15.2). Кроме уровней доступа public ( общедоступный ) и private ( личный ) в классах, создаваемых на базе структур ( struct ) и настоящих классов ( class ), используется еще один уровень защиты – protected ( защищенный ). Защищенными данными класса могут пользоваться функции и методы самого класса, производных классов и дружественные функции. При создании производного класса D может быть упомянут один из этих уровней доступа, что повлияет на изменение уровня доступа к унаследованным данным и функциям. По этому поводу в стандарте C++ существует целая таблица:
Уровень доступа в B | Уровень доступа при объявлении D | Уровень доступа в D | |
---|---|---|---|
D=struc | D=class | ||
public | опущен | public | private |
protected | опущен | public | private |
private | опущен | нет доступа | нет доступа |
public | public | public | public |
protected | public | protected | protected |
private | public | нет доступа | нет доступа |
public | protected | protected | protected |
protected | protected | protected | protected |
private | protected | нет доступа | нет доступа |
public | private | private | private |
protected | private | private | private |
private | private | нет доступа | нет доступа |
В современной практике программирования действует общепринятое правило – родителями и потомками должны быть только настоящие классы. Поэтому о существовании третьей колонки в табл. 15.1 можно сразу забыть.
Чаще всего производный класс конструируют по следующей схеме, которая носит название открытого наследования:
class D: public B {тело производного класса};
Это означает, что общедоступные данные и функции из родительского класса остаются общедоступными и в порожденном классе, защищенные данные и функции из родительского класса остаются защищенными и в порожденном классе, а к приватным компонентам родителя потомок прямого доступа не имеет. И добраться до них он может на равных правах с другими программами только через соответствующий метод, если таковой у родителя был предусмотрен.
Однако приведенное выше утверждение не распространяется на конструкторы и деструкторы. Они не наследуются, но к ним можно обратиться с добавлением принадлежности классу B.
В приведенном ниже примере имеет место открытое наследование производного класса D от своего родителя. Поле данных x родительского класса для потомка закрыто, но методы setb и showb сохраняют в классе D уровень доступа public. Поэтому с объектом типа D к этим методам обращаться можно:
#include <iostream.h> #include <conio.h> class B { int b; public: void setb(int n){b=n;} void showb(){cout<<"in B b="<<b<<endl;} }; class D: public B { int d; public: void setd(int n){d=n;} void showd(){cout<<"in D d="<<d<<endl;} }; void main() { D qq; //объявление объекта порожденного класса qq.setb(1); //доступ к члену базового класса qq.x qq.showb(); //доступ к члену базового класса qq.setd(2); //доступ к члену производного класса qq.y qq.showd(); //доступ к члену производного класса qq.showb(); //доступ к члену базового класса getch(); } //=== Результат работы === in B b=1 //qq.x in D d=2 //qq.y in B b=1 //qq.x
Обратите внимание на то, что после обращения к методу setd значение поля qq.x не изменилось.
А теперь модифицируем уровень доступа при объявлении производного класса:
class D: private B { int d; public: void setbd(int n,int m) { setb(n); //для класса D функция стала private, но она доступна d=m; } void showbd() { showb(); // для класса D функция стала private, но она доступна cout<<"in D d="<<d<<endl;} }; void main() { D qq; //объявление объекта порожденного класса qq.setbd(1,2); qq.showbd(); getch(); }
Результат работы программы прежний, но в доступе к методам класса B помог производный класс. В последнем примере можно заменить в объявлении класса D уровень доступа на protected – функции setb и showb получат в классе D статус protected, но они по-прежнему будут доступны, и результат работы программы будет прежним.
15.1.2. Вызов конструкторов и деструкторов при наследовании
Последовательность вызова конструкторов и деструкторов легче проследить на следующем примере. В базовом классе B содержится единственный закрытый член данных x, предусмотрены три конструктора (по умолчанию, инициализации и копирования), функция опроса значения закрытого поля и деструктор. Каждый из них выводит свое условное обозначение при вызове. В производном классе D, который наследует поле x в режиме private, содержится и собственное закрытое поле y. В его составе такие же три конструктора, функция опроса значения закрытого поля и деструктор.
Головная программа сначала создает четыре объекта w1, w2, w3 и w4 типа B, а затем четыре объекта q1, q2, q3 и q4 типа D. После создания каждого объекта фиксируется содержимое соответствующих полей и цепочка вызываемых конструкторов. Перед окончанием программы фиксируется цепочка вызовов деструкторов.
#include <iostream.h> #include <conio.h> class B { int x; public: B(){x=0; cout<<"Def_B "<<endl;} B(int n){x=n; cout<<"Init_B "<<endl;} B(const B &y){x=y.x; cout<<"Copy_B "<<endl;} int get_x(){return x;} ~B(){cout<<"Destr_B"<<endl;} }; class D : public B { int y; public: D(){y=0; cout<<"Def_D "<<endl;} D(int n){y=n; cout<<"Init_D "<<endl;} D(const D &z){y=z.y; cout<<"Copy_D "<<endl;} int get_y(){return y;} ~D(){cout<<"Destr_D"<<endl;} }; void main() { B w1; cout<<"w1.x="<<w1.get_x()<<endl; B w2(2); cout<<"w2.x="<<w2.get_x()<<endl; B w3(w2); cout<<"w3.x="<<w3.get_x()<<endl; B w4=w1; cout<<"w4.x="<<w4.get_x()<<endl; D q1; cout<<"q1.x="<<q1.get_x()<<' '<<"q1.y="<<q1.get_y()<<endl; D q2(2); cout<<"q2.x="<<q2.get_x()<<' '<<"q2.y="<<q2.get_y()<<endl; D q3(q2); cout<<"q3.x="<<q3.get_x()<<' '<<"q3.y="<<q3.get_y()<<endl; D q4=q1; cout<<"q4.x="<<q4.get_x()<<' '<<"q4.y="<<q4.get_y()<<endl; } //=== Результаты работы === Def_B //конструктор B по умолчанию для создания w1.x w1.x=0 //значение созданного объекта Init_B //конструктор B инициализации для создания w2.x w2.x=2 //значение созданного объекта Copy_B //конструктор B копирования для создания w3.x w3.x=2 //значение созданного объекта Copy_B //конструктор B копирования для создания w4.x w4.x=0 //значение созданного объекта Def_B //неявный вызов конструктора B для создания q1.x Def_D //конструктор D по умолчанию для создания q1.y q1.x=0 q1.y=0 //значения созданных объектов Def_B //неявный вызов конструктора B для создания q2.x Init_D //конструктор D инициализации для создания q2.y q2.x=0 q2.y=2 //значения созданных объектов Def_B //неявный вызов конструктора B для создания q3.x Copy_D //конструктор D копирования для создания w3.y q3.x=0 q3.y=2 //значения созданных объектов Def_B //неявный вызов конструктора B для создания q4.x Copy_D //конструктор D копирования для создания w4.y q4.x=0 q4.y=0 //значения созданных объектов Destr_D //деструктор D для уничтожения w4.y Destr_B //деструктор B для уничтожения w4.x Destr_D //деструктор D для уничтожения w3.y Destr_B //деструктор B для уничтожения w3.x Destr_D //деструктор D для уничтожения w2.y Destr_B //деструктор B для уничтожения w2.x Destr_D //деструктор D для уничтожения w1.y Destr_B //деструктор B для уничтожения w1.x Destr_B //деструктор B для уничтожения q4.x Destr_B //деструктор B для уничтожения q3.x Destr_B //деструктор B для уничтожения q2.x Destr_B //деструктор B для уничтожения q1.x15.1.
Обратите внимание на то, что в этом примере создание объектов производного класса начинается с автоматического вызова конструктора базового класса по умолчанию. Кроме того, объекты уничтожаются в порядке, обратном последовательности их создания – самый первый объект разрушается последним.
Однако возможна ситуация, когда ни программист, ни система не включили в базовый класс конструктор по умолчанию. Это происходит в тех случаях, когда программист написал только конструкторы с параметрами. В такой ситуации конструкторы производного класса должны сами позаботиться об инициализации объектов родительского класса. Сделать это можно разными способами – явно вызвать конструктор базового класса либо в своем списке инициализации, либо в теле конструктора. Для защищенных ( protected ) полей базового класса можно воспользоваться указателем this. В приводимом ниже примере демонстрируются эти возможности. В качестве базового класса выступает класс Point2D, моделирующий точку на плоскости:
class Point2D { int x,y; //закрытые данные класса Point2D public: Point2D(int xx,int yy):x(xx),y(yy){} //конструктор инициализации Point2D(const Point2D &P):x(P.x),y(P.y){} //конструктор копирования int get_x(){return x;} int get_y(){return y;} };
Порожденный класс Point3D моделирует точку в трехмерном пространстве:
class Point3D: public Point2D { int z; //новая координата в классе Point3D public: Point3D(int xx,int yy,int zz):Point2D(xx,yy),z(zz){} int get_z(){return z;} //новый метод в классе Point3D };
А теперь протестируем оба класса на следующей программе:
#include <iostream.h> #include <conio.h> void main() { Point2D P2(1,2); Point3D P3(3,4,5); cout<<"P3.x="<<P3.get_x()<<" P3.y="<<P3.get_y()<<" P3.z=" <<P3.get_z()<<endl; cout<<"P2.x="<<P2.get_x()<<" P2.y="<<P2.get_y()<<endl; P2=P3; cout<<"P2.x="<<P2.get_x()<<" P2.y="<<P2.get_y()<<endl; getch(); } //=== Результат работы === P3.x=3 P3.y=4 P3.z=5 P2.x=1 P2.y=2 P2.x=3 P2.y=4
Производному классу по наследству достались приватные данные – координаты ( x,y ) родительского объекта и общедоступные методы доступа к этим координатам. Поэтому в головной программе мы можем пользоваться этими методами как по отношению к объектам типа Point2D, так и по отношению к объектам типа Point3D. Немного странным кажется оператор присваивания двухмерному объекту P2 значения трехмерного объекта P3. Но происходит вполне естественная операция – те поля, которые являются общими у этих двух объектов, переносятся, а "лишнее" поле P3.z отсекается. Обратная операция P3=P2 была бы ошибочной, т.к. компилятор не "знает", чем следует заполнить поле P3.z.
Если бы поля ( x,y ) в базовом классе были объявлены как защищенные ( protected ), то их инициализацию в конструкторе производного класса можно было бы выполнить и так:
Point3D(int xx,int yy,int zz):z(zz) { this->x=xx; this->y=yy; }