Нижегородский государственный университет им. Н.И.Лобачевского
Опубликован: 25.11.2008 | Доступ: свободный | Студентов: 9592 / 1296 | Оценка: 4.06 / 3.66 | Длительность: 21:16:00
Лекция 16:

Классы как средство создания больших программных комплексов

< Лекция 15 || Лекция 16: 123 || Лекция 17 >
Аннотация: В данной лекции рассматриваются классы, как средство создания больших программных комплексов. Приводятся практические примеры использования классов

До сих пор мы знакомились с возможностями классов как средства создания и обработки новых типов данных. Наряду с этим важнейшим достижением языка 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++ существует целая таблица:

Таблица 15.1.
Уровень доступа в 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; }
< Лекция 15 || Лекция 16: 123 || Лекция 17 >
Alexey Ku
Alexey Ku

Попробуйте часть кода до слова main заменить на 

#include "stdafx.h" //1

#include <iostream> //2
#include <conio.h>

using namespace std; //3

Александр Талеев
Александр Талеев

#include <iostream.h>
#include <conio.h>
int main(void)
{
int a,b,max;
cout << "a=5";
cin >> a;
cout <<"b=3";
cin >> b;
if(a>b) max=a;
else max=b;
cout <<" max="<<max;
getch();
return 0;
}

при запуске в visual express выдает ошибки 

Ошибка    1    error C1083: Не удается открыть файл включение: iostream.h: No such file or directory    c:\users\саня\documents\visual studio 2012\projects\проект3\проект3\исходный код.cpp    1    1    Проект3

    2    IntelliSense: не удается открыть источник файл "iostream.h"    c:\Users\Саня\Documents\Visual Studio 2012\Projects\Проект3\Проект3\Исходный код.cpp    1    1    Проект3

    3    IntelliSense: идентификатор "cout" не определен    c:\Users\Саня\Documents\Visual Studio 2012\Projects\Проект3\Проект3\Исходный код.cpp    6    1    Проект3

    4    IntelliSense: идентификатор "cin" не определен    c:\Users\Саня\Documents\Visual Studio 2012\Projects\Проект3\Проект3\Исходный код.cpp    7    1    Проект3

при создании файла я выбрал пустой проект. Может нужно было выбрать консольное приложение?