Закрытое наследование - средство реализации отношений содержит.

План занятия 22

 

Множественное наследование

 

Если у производного класса имеется несколько базовых клас­сов, то говорят о множественном наследовании. Множественное наследование позволяет сочетать в одном производном классе свойства и поведение нескольких классов.

Следующий пример демонстрирует множественное наследование.

 

Пример 1

 

#include <iostream>

#include <windows.h>

 

using namespace std;

 

class Coord

{

  int x, y;

public:

  Coord(int _x, int _y){ x = _x; y = _y; }

  int GetX() { return x; }

  int GetY() { return y; }

  void SetX(int _x){ x = _x; }

  void SetY(int _y){ y = _y; }

};

 

class SaveMsg

{

  char Message[80];

public:

  SaveMsg(char* msg){ SetMsg(msg); }

  void SetMsg(char* msg) { strcpy(Message, msg); }

  void ShowMsg() { cout << Message; }

  void gotoxy(short x, short y);

};

 

void SaveMsg :: gotoxy(short x, short y)

{

  HANDLE StdOut = GetStdHandle(STD_OUTPUT_HANDLE);

  COORD coord = { x, y };

  SetConsoleCursorPosition(StdOut, coord);

}

 

class PrintMsg : public Coord, public SaveMsg

{

public:

  PrintMsg(int _x, int _y, char* msg) : Coord(_x, _y), SaveMsg(msg){}

  void Show();

};

 

void

PrintMsg::Show()

{

  gotoxy(GetX(), GetY());

  ShowMsg();

}

 

int main()

{

  setlocale(0, "");

  PrintMsg * ptr;

  ptr = new PrintMsg(1, 1, "Множественное");

  ptr->Show();

  ptr->SetX(10);

  ptr->SetY(10);

  ptr->SetMsg("наследование");

  ptr->Show();

  delete ptr;

  cout << endl;

  return 0;

}

 

В этом примере класс Coord отвечает за хранение и установ­ку значений координат на экране, класс SaveMsg хранит и уста­навливает сообщение, а класс PrintMsg, являющийся произ­водным от них, выводит это сообщение на экран в заданных ко­ординатах. Этот пример демонстрирует также, как осуществля­ется передача параметров конструкторам базовых классов. При множественном наследовании, как и при простом, конструкторы базовых классов вызываются компилятором до вызова конструк­тора производного класса. Единственная возможность передать им аргументы — использовать список инициализации элементов. Причем порядок объявления базовых классов при наследовании определяет и порядок вызова их конструкторов, которому должен соответствовать порядок следования конструкторов базовых классов в списке инициализации. В данном случае класс PrintMsg содержит такое объявление о наследовании:

class PrintMsg: public Coord, public SaveMsg

 

Этому объявлению соответствует следующий конструктор:

 

PrintMsg(int _x, int _y, char* msg): Coord(_x, _y), SaveMsg(msg){}

 

В основной программе этому конструктору передаются аргу­менты при создании объекта класса PrintMsg с помощью опе­ратора new:

 

ptr = new PrintMsg(10, 5, "Множественное ");

 

Выполнение функции-члена Show() приводит к выводу на экран в указанных координатах сообщения, заданного третьим аргументом.

Следующие за этим вызовы функций-членов SetX (), SetY () и SetMsg () приводят к установке новых значений ко­ординат и нового сообщения, которое выводится повторным вы­зовом функции Show ().

 

Пример 1

Рассмотрим еще один пример, который демонстрирует поря­док вызова конструкторов и деструкторов базовых классов.

 

#include <iostream.h>

 

class Basel

{

public:

Basel(){cout «"Мы в конструкторе Basel\n";}

~Basel(){cout «"Мы в деструкторе Basel\n";}

};

 

class Base2

{

public:

Base2() {cout «"Мы в конструкторе Base2\n";}

~Base2(){cout «"Мы в деструкторе Base2\n";}

};

 

class Derived: public Basel, public Base2

{

public:

Derived()

{

cout « "Мы в конструкторе Derived \n";

}

~Derived()

{

cout « "Мы в деструкторе Derived \n";

}

};

 

main()

{

Derived ob;.

return 0;

}

 

Эта программа выводит на экран следующее:

 

Мы в конструкторе Basel

Мы в конструкторе Base2

Мы в конструкторе Derived

Мы в деструкторе Derived

Мы в деструкторе Base2

Мы в деструкторе Basel

 

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

Пример 2

Как мы ранее уже отмечали, не все члены класса наследуются. Для удобства мы перечислим здесь не наследуемые члены класса:

• конструкторы;

• конструкторы копирования;

• деструкторы;

• операторы присвоения, определенные программистом;

• друзья класса.

Открытое, защищенное и закрытое наследование

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

На рис. показана сводка доступности элементов базового класса в производном классе для всех типов наследования. В первой колонке указаны спецификаторы доступа в базовом классе.

При наследовании открытого базового класса открытые элементы базового класса становятся открытыми, а защищенные — защищенными элементами базового класса. Закрытые элементы базового класса никогда не доступны из производного класса непосредственно, но к ним можно обращаться посредством вызовов открытых или защищенных функций - элементов базового класса.

При наследовании защищенного базового класса открытые и защищенные элементы базового класса становятся защищенными элементами производного класса. При наследовании закрытого базового класса открытые и защищенные элементы базовый класс становятся закрытыми элементами производного класса (т.е. функции становятся сервисными функциями). Закрытое и защищенное наследование нельзя считать отношениями «является».

 

 

 

Спецификатор наследуемого доступа Доступ в базовом классе Доступ в производ­ном классе
  public   Public Protected Private Public Protected Недоступны
  protected   Public Protected Private Protected Protected Недоступны
  private   Public Protected Private Private Private Недоступны

 

Закрытое наследование

Закрытое наследование — это наследование реализации (в отличие от наследова­ния интерфейса): класс реализован посредством класса. Оно прин­ципиально отличается от открытого: принцип подстановки не соблюдается. Это означает, что нельзя присвоить (во всяком случае, без явного преобразования типа) объект- производного класса базовому. Поэтому закрытое наследование хо­рошо применять в тех случаях, когда требуется иметь функциональность базово­го класса, но не нужны ни копирование, ни присваивание.

При закрытом наследовании все элементы класса-наследника становятся при­ватными и недоступными программе-клиенту.

 

class Base

{

public:

void method1();

void method2();

};

 

class Derive: private Base // наследуемые методы недоступны клиенту

{

    ……

}

 

Программа, использующая класс Derive, не может вызвать ни method1(), ни method2(). В наследнике нужно заново реализовать нужные методы, вызвав в методах на­следника методы родителя.

 

class Derive: private Base // наследуемые методы недоступны клиенту

{

public:

void method1()

{

 Base :: methodl();

}

void method2()

{

Base :: method2();

}

};

Префикс при вызове задавать обязательно, иначе возникает рекурсивное обра­щение к методу-наследнику. Можно открыть методы базового класса с помощью using-объявления, которое имеет следующий синтаксис:

 

using <имя базового класса>::<имя в базовом классе>

 

В наследуемом классе это делается так:

 

class Derive: private Base // наследуемые методы недоступны клиенту

{

 public:

using Base::method1();

};

 

Таким образом, закрытое наследование позволяет ограничить предоставляемую производным классам функциональность.

 

Закрытое наследование - средство реализации отношений содержит.

 

При использовании закрытого наследования открытые (public) и защищенные (protected) члены базового класса становятся закрытыми членами производного класса — т.е. методы базового класса не переходят в открытый интерфейс производного объекта. Однако они могут использоваться внутри функций-членов производного класса.

Рассмотрим тему интерфейсов подробнее. При открытом наследовании открытые методы базового класса становятся открытыми методами производного класса. То есть производный класс наследует интерфейс базового класса. Это соответствует отношению является. А при закрытом наследовании открытые методы базового класса становятся закрытыми методами производного класса. То есть производный класс не наследует интерфейс базового класса. Отсутствие наследования для включаемых объектов означает отношение содержит.

При закрытом наследовании класс наследует реализацию. Включение добавляет в класс именованный объект-член, а закрытое наследование добавляет в класс неименованный унаследованный объект. Значит, закрытое наследование обеспечивает те же свойства, что и включение — реализацию, но не интерфейс. Поэтому его также можно использовать для реализации отношения содержит.

 

Защищенное наследование — это разновидность закрытого наследования. В этом случае при объявлении базового класса указывается ключевое слово protected. При защищенном наследовании открытые и защищенные члены базового класса становятся защищенными членами производного класса. Как и при закрытом наследовании, интерфейс базового класса доступен в производном классе, но не доступен внешнему миру. Главное отличие между защищенным и закрытым наследованием проявляется при порождении от класса, который сам является производным классом. При закрытом наследовании класс третьего поколения не получает доступа к интерфейсу базовых классов. Это происходит потому, что открытые методы базового класса становятся закрытыми в производном классе, и доступ к закрытым членам и методам из следующего уровня наследования невозможен. При защищенном наследовании открытые методы базового класса становятся защищенными членами производного класса и доступны внутри классов следующего уровня наследования.


Виртуальные базовые классы

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

class IndBase

{

private:

int x;

public:

int GetX(){return x;}

void SetX(int _x){x = _x;}

double var;

};

 

class Basel: public IndBase

{

//...

};

 

class Base2: public IndBase

{

//...

};

 

class Derived: public Basel, public Base2

{

//...

};

 

main()

{

Derived ob;

ob.var = 5.0;

ob.SetX(0);

int z = ob. GetX () ;

//ob.Basel::var = 5.0;

//ob.Basel::SetX(0) ;

//int z = ob.Basel::GetX ();

return 0;

}

Пример 3

 

Здесь класс Derived косвенно наследует класс IndBase через свои базовые классы Basel и Base2. Поэтому при ком­пиляции приведенного примера возникнут ошибки, вызванные неоднозначностью обращения к членам класса var и GetX () в строках:

 

ob.var =5.0;

ob.SetX(0);

int z = ob.GetX() ;

 

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

 

ob.Basel::var = 5.0;

ob.Basel::SetX(0);

int z = ob.Basel::GetX();

 

Можно также квалифицировать эти вызовы следующим образом:

 

ob.Base2::var = 5.0;

ob.Base2::SetX(0);

int z = ob.Base2::GetX();

Пример 4

Хотя этот способ и позволяет избежать неоднозначности при вызове, тем не менее класс IndBase будет включен в состав класса Derived дважды, увеличивая его размер. Избежать по­вторного включения косвенного базового класса в производный класс можно, дав указание компилятору использовать виртуаль­ный базовый класс. Это осуществляется с помощью ключевого слова virtual, которое указывается перед спецификатором на­следуемого доступа или после него. Следующий пример является модифицированным вариантом предыдущего, использующим класс IndBase в качестве виртуального базового класса.

class IndBase

{

private:

int x;

public:

int GetX(){return x;}

void SetX(int _x){x = _x;}

double var;

};

 

class Basel: virtual public IndBase

{

//...

};

 

class Base2: virtual public IndBase

{

//...

};

 

class Derived: public Basel, public Base2

{

//...

};

 

main()

{

Derived ob;

ob.var = 5.0;

ob.SetX(0);

int z = ob.GetX() ;

return 0;

}

Пример 5

В этом случае класс Derived содержит один экземпляр клас­са IndBase. и вызовы

 

ob.var = 5.0;

ob.SetX(0);

int z = ob.GetX() ;

 

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

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

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

 

class Basel{};

class Base2{};

class Derived: public Basel, public Base2 {};

 

main()

{

Derived ob;

//...

return 0;

}

 

конструкторы вызываются в следующем порядке:

 

Basel () ;

Base2() ;

Derived () ;

Пример 6

 

Объявление базового класса виртуальным изменяет порядок вызова конструкторов при создании экземпляра производного класса. Конструкторы виртуальных базовых классов вызываются первыми, раньше конструкторов невиртуальных базовых классов.

Если виртуальных базовых классов несколько, их конструкторы вызываются в порядке их объявления в списке наследования. За­тем вызываются конструкторы невиртуальных базовых классов в порядке их объявления в списке наследования и, наконец, вызы­вается конструктор производного класса. Например, в случае объявления

class Basel{};

class Base2{};

class Derived: public Basel, virtual public Base2 { } ;

 

main()

{

Derived ob;

//…;

return 0;

}

конструкторы вызываются в следующем порядке:

Base2() ;

Basel () ;

Derived();

Пример 7

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


Дата добавления: 2018-08-06; просмотров: 956; Мы поможем в написании вашей работы!

Поделиться с друзьями:




Мы поможем в написании ваших работ!