Объектно-ориентированное программирование - Учебное пособие (А.А. Хусаинов)

4.5. множественное наследование

 

Производный класс может иметь любое число базовых классов. Использование двух или более классов называется множественным наследованием.

При инициализации объекта производного класса сначала вызываются конструкторы базовых классов, в порядке их перечисления в объявлении производного класса, а потом – конструктор производного класса.

Пример. Пусть INTEGER – класс, объектами которого являются целые числа, POSITIVE – класс положительных целых чисел. Определим рациональную дробь как объект производного класса от этих двух классов. Пара, представляющая рациональную дробь, состоит из взаимно простых целых чисел.

#include <iostream.h>   //библиотека потокового ввода-вывода

#include <process.h>    //библиотека с прототипом функции exit

#include <conio.h>        //библиотека консольного ввода-вывода

class INTEGER             //класс целых чисел

{

public:

   long NUM;                              //информационное поле

   INTEGER (long Val): NUM(Val) {} //конструктор

};

class POSITIVE            //класс положительных чисел

{

public:

   unsigned long Den;     // информационное поле

   POSITIVE(unsigned long d) : Den(d) //конструктор

   {

               if(d==0) {cout << "Ошибка"; exit(1);}//ноль недопустим

   }

};

class RATIONAL : public INTEGER, public POSITIVE

//класс дроби

{

   //дружественная функция вывода дроби в некоторый поток

   friend ostream &operator<<(ostream& stream, RATIONAL& o);

public:

   RATIONAL(long v, unsigned long u=1): INTEGER(v), POSITIVE(u)

   //конструктор

   {

   long w;

   if (v==0) {u=1; return;}

   if(v<0) {w = -v;}

   else

               {

               w=v;

               }

   //поскольку числитель и знаменатель должны быть

   //взаимно простыми числами то следует найти наибольший

   //общий делитель для числителя и знаменателя

   while (w!=u)

   {

               if(w>u) w=w-u;

               if(u>w) u=u-w;

   }

   //и следует сократить дробь

   NUM = NUM/w;

   Den = Den/w;

   }

};

ostream& operator<<(ostream& stream, RATIONAL& o)

{

   stream<<o.NUM<<"/"<<o.Den;

        return stream;

}

 

main()

{

   RATIONAL r1(10, 20), r2(-15, 10);

   clrscr();

   cout<<"Первая дробь (числитель равен 10, знаменатель равен 20): ";

   cout<<r1<<" ";

   cout<<"Вторая дробь (числитель равен -15,знаменатель равен 10): ";

   cout<<r2<<" ";

   getch();

}

 

Результаты работы программы

 

Первая дробь (числитель равен 10, знаменатель равен 20): 1/2

Вторая дробь (числитель равен -15,знаменатель равен 10): -3/2

 

В данном примере при инициализации объекта – рационального числа – сначала будет вызван конструктор INTEGER, затем – конструктор класса POSITIVE, затем – конструктор класса RATIONAL.

Доступ к членам базовых классов, имеющих одинаковые имена, осуществляется через имена базовых классов, которым они принадлежат, при помощи операции разрешения доступа. Например:

 

Class A

{

public: void f();

};

class B

{

public: void f();

};

class C : public A, public B {};

 

main()

{

   C c;

   c.f();

 // ошибка – неизвестно, какая из функций вызывается A::f() или B::f()

 

   c.A::f(); // правильный вызов

}

 

На рис. 4.1 приведена иерархическая структура, иллюстрирующая множественное наследование из приведённого выше примера.

Неоднозначность, возникающая при вызове функций, может быть преодолена с помощью переопределения функции в производном классе, например:

 

Class C : public A, public B

{

public: void f() { A::f();}

}

int main()

{

   C c;

   c.f();    // правильный вызов функции A::f()

   c.B::f(); // правильный вызов функции B::f()

}

 

Базовые классы с одинаковым именем не могут присутствовать в определении производного класса. Например, если попытаться определить вектор, как пару точек

 

Class Point {int x,y;}

Class Vector : public Point, public Point {} // ошибка

 

то компилятор выведет сообщение об ошибке, поскольку для объекта

 

Vector v;

 

неясно, как начальную точку вектора отличить от конечной.

 

Для классов, порожденных от производных классов с общей базой, по умолчанию существует два экземпляра объекта общей базы. Это позволяет наследовать базовый класс любое количество раз, например, определение:

 

Class Point {public: int x, y;}

Class Point2 : public Point {};

Class Vector : public Point, public Point2 {};

 

будет верным и будет задавать вектор как пару точек. Теперь обращение к начальной и конечной точкам вектора будет производиться с помощью оператора разрешения области видимости. Для данного случая иерархическая структура классов приведена на рис. 4.2.

 

4.6. Виртуальные классы

 

Базовый класс называется виртуальным, если его поля не дублируются при неоднократном наследовании. Виртуальный базовый класс объявляется при наследовании при определении производного класса следующим образом:

 

сlass имя_производного_класса:

virtual public имя_виртуального_базового_класса

{

тело производного класса;

}

 

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

 

Ниже для иллюстрации понятия виртуального класса приведём текст программы, в которой определена иерархия классов, отраженная на рис. 4.3. Класс квадрата дважды наследует координаты четырёх углов.

#include <graphics.h>

#include <math.h>

#include <conio.h>

class four                  // четырехугольник

{

protected:

   float x[4], y[4];          // координаты вершин

public:

   four(){}

   four(float *ix, float *iy) // конструктор

   {

               int i;

               for(i=0; i<4; i++)

               {

                           x[i] = ix[i]; y[i] = iy[i];

               }

   }

   ~four() {delete x; delete y;}   //деструктор

   void show();                    //вывод четырёхугольника на экран

};

 

class rect: public virtual four               // прямоугольник

{

protected:

   int xleft, xright, ytop, ybottom;

public:

   rect(int x1, int y1, int x2, int y2):

   xleft(x1), ytop(y1), xright(x2), ybottom(y2) // конструктор

   {

               x[0] = x1; y[0] = y1;

               x[1] = x1; y[1] = y2;

               x[2] = x2; y[2] = y2;

               x[3] = x2; y[3] = y1;

   }

};

//класс ромба

class romb: public virtual four

{

protected:

   float xc, yc, alpha, a, b;

public:

   romb(float x1, float y, float ugol, float d1, float d2)

   {

               xc = x1; yc = y; alpha = ugol; a = d1; b = d2;

               x[0] = xc + (a/2)*cos(alpha);

               x[0] = yc + (a/2)*sin(alpha);

               x[1] = xc - (b/2)*sin(alpha);

               x[1] = yc + (b/2)*cos(alpha);

               x[2] = xc - (a/2)*cos(alpha);

               x[2] = yc - (a/2)*sin(alpha);

               x[3] = xc + (b/2)*sin(alpha);

               x[3] = yc - (b/2)*cos(alpha);

   }

};

// стороны квадрата параллельны осям координат

class square : public rect, public romb

{

   int xcenter, ycenter; // центр квадрата

   int size;             // сторона квадрата

public:

   square(int x0, int y0, int s): xcenter(x0+s/2), ycenter(y0+s/2),

   size(s), rect(x0 - s/2, y0 - s/2, x0 + s/2, y0 + s/2),

   romb(x0, y0, 3.14159/4, s, s) {show();}

};

void four :: show()     // вывод четырехугольника

{

   int i;

   moveto (x[3], y[3]);

   for(i=0; i<4; i++)

   lineto(x[i], y[i]);

};

 

void main()

{

   int gd = DETECT, gm;

   initgraph (&gd, &gm,"c:\prog\bc31\bgi");

   square q(320, 240, 100);.

   getch();            //ожидание нажатия любой клавиши

}

 

В результате работы программы на экран будет выведен квадрат, сторона которого равна 100, а центр квадрата совпадает с центром экрана.

 

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

Во время инициализации объекта некоторого класса А конструкторы классов, наследованных виртуально, активизируются перед конструкторами всех остальных базовых классов этого класса. Происходит это следующим образом:

если в списке инициализации конструктора класса А используется инициализатор базового класса, наследованного виртуально, то активизируется конструктор этого базового класса;

в противном случае конструктор виртуального базового класса инициализируется без параметров.

Деструкторы активизируются в обратном порядке.

Пример. Рассмотрим программу, работающую с иерархией классов, приведенных на рис. 4.4. Эта программа иллюстрирует порядок вызова конструкторов виртуальных классов.

 

Ниже приведён текст программы:

 

#include <stdio.h>        //стандартная библиотека ввода-вывода

//классы реализованы посредством конструкторов и информационных полей

class V

{

public:

   int a,b,c;

   V(): c(3){};

   V(int p): a(p){};

};

class A: virtual public V

{

public:

   A():V(3) {a=1;}

};

class B: virtual public V

{

public:

   B() {b=2;}

};

class C: public A,B

{

public:

//функция вывода

   void out(){printf("a=\%d b=\%d c=\%d ",a,b,c);}

};

 

int main()

{

C ob1;

ob1.out();

return 0;

}

 

При создании объекта ob1 класса С конструктор класса С вызовет конструктор V(), который установит С=3, затем конструкторы А() и В() объектов А и В, не имеющие параметров. Затем конструктор класса А вызовет конструктор виртуального базового класса V(3), который установит а = 3, но в теле конструктора класса А будет произведено присваивание а = 1, в результате чего а станет равен 1. Затем будет вызван конструктор класса В, который установит b = 2.

 

Результаты работы программы

 

a=1  b=2  c=3

 

Пример. Рассмотрим иерархию классов, приведённую на рис. 4.5. Этот пример, как и предыдущий, иллюстрирует порядок вызова конструкторов виртуальных классов, но для более сложного случая.

 

Ниже приведён текст программы.

 

#include <stdio.h>                    //стандартная библиотека ввода-вывода

class V1 //первый класс

{

   friend class D; //дружественный класс

   friend class B; //дружественный класс

   int fix1;

public:

   V1(int val): fix1(val){};          //конструктор

   V1(): fix1(10){};

};

class V2 //второй класс

{

friend class D;   //дружественный класс

int fix2;

public:

   V2(): fix2(20){};         //конструктор

   V2(int p): fix2(p){};

};

//схема наследования

class B: virtual public V1, virtual public V2 {};

class C: virtual public V1, virtual public V2 {};

class D: public B,C {

public:

   D(): V1(30){};

   D(int p): V2(p){};

   //функция вывода

  void out(){printf("fix1=\%d fix2=\%d ",fix1,fix2);}

};

int main()

{

D ob1; D ob2(100);

ob1.out();

ob2.out();

return 0;

}

 

При создании объекта ob1 будут вызваны конструкторы V1(30) и V2(), которые установят fix1 = 30 и fix2 = 20. При создании объекта ob2 будет вызван конструктор V1() без параметра, а затем – конструктор V2(100). Они установят fix1 = 10 и       fix2 = 100.

 

Результаты работы программы

 

fix1=30  fix2=20

fix1=10  fix2=100

 

Пример. Рассмотрим иерархию классов, приведённую на рис. 4.6. Сначала вызываются конструкторы V1,V2,V3, а затем – B,C,D.

 

 

Ниже приведён текст программы, иллюстрирующей вышеприведённую иерархию.

 

#include <stdio.h>        //стандартная библиотека ввода-вывода

//построения иерархии

class V1 //класс V1

{

friend class D;   //дружественный класс

int fix1;

public:

   V1(int val): fix1(val){};

   V1(): fix1(10){};

};

class V2 //класс V2

{

   friend class D; //дружественный класс

   int fix2;

public:

   V2(): fix2(20){};

};

class V3 //класс V3

{

   int fix3;

   friend D;

public:

   V3(): fix3(40){};

   V3(int p): fix3(p){};

};

//схема наследования

class A: virtual public V1 {};

class B: virtual public V1 {};

class C: virtual public V2, virtual public V3 {};

class D: public B,C {

public:

   D(): V1(30){};

   D(int p): V3(p){};

   //функция вывода

void out(){printf("fix1=\%d fix2=\%d fix3=\%d ",fix1,fix2,fix3);}

};

int main()

{

D ob1; D ob2(100);

ob1.out();

ob2.out();

return 0;

}

 

Результаты работы программы

 

fix1=30  fix2=20  fix3=40

fix1=10  fix2=20  fix3=100