| Попробуйте часть кода до слова 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.x
15.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; } 
                             