Отправляет email-рассылки с помощью сервиса Sendsay

C++ для всех

  Все выпуски  

C++ для всех


Информационный Канал Subscribe.Ru

C++ для всех. Выпуск 5

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

До этого момента в примерах иерархии классов каждый производный класс наследовался от одного базового, такое наследование называется одиночным. Сейчас мы рассмотрим случай, когда производный класс имеет более одного базового класса,- подобный механизм наследования называется множественным наследованием.


 class A
 {
  int value;
 public:
  A(int v):value(v){}
  void setValue(int v){ value=v; }
 };

 class B
 {
  double dvalue;
 public:
  B(double v):dvalue(v){}
  double getValue()const{ return dvalue; }
 };
 
 class C : public A,public B
 {
 public:
  C(int v,double dv):A(v),B(dv){}
 };

 int main()
 {
  C c(10,20.0);
  c.setValue(30);
  c.getValue();

  return 0;
 }

Как результат множественного наследования производный класс получает в наследство все данные и функции-члены базовых классов (естественно при этом соблюдается механизм сокрытия данных). Для каждого базового класса можно задавать любой механизм объявления доступа к методам:


 class A{...}
 class B{...}
 class C{...}
 
 class D: public A,protected B,private A
 {
  ...
 }

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


 class A
 {
 public:
  void show(){ print(); }
  virtual void print()const{ printf("A::print()\n"); }
 };

 class B
 {
 public:
  void show(){ print(); }
  virtual void print()const{ printf("B::print()\n"); }
 };
 
 class C : public A,public B
 {
 public:
  virtual void print()const{ printf("C::print()\n"); }
 };

 int main()
 {
  C c;
  c.A::show();
  c.B::show();
  
  return 0;
 }

В результате на экране будет напечатано:


 C::print() 
 C::print() 

Теперь посмотрите как в функции main() вызывалась функция show() - вызов производился через оператор доступа :: применительно к классу, функцию которого требовалось вызвать. Дело в том, что в обеих базовых классах совпали имена функций, поэтому вызов функции show() непосредственно от производного класса приведет к ошибке компиляции в связи с неоднозначностью:


 ...
 int main()
 {
  C c;
  c.show(); //ошибка компиляции - неоднозначность при вызове функции
  
  return 0;
 }

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


 class A
 {
 public:
  void show(){ print(); }
  virtual void print()const{ printf("A::print()\n"); }
 };

 class B
 {
 public:
  void show(){ print(); }
  virtual void print()const{ printf("B::print()\n"); }
 };
 
 class C : public A,public B
 {
 public:
  virtual void print()const{ printf("C::print()\n"); }
  void a_show(){ A::show(); }
  void b_show(){ B::show(); }
  void show()
  { 
   A::show();
   B::show();
  }
 };

 int main()
 {
  C c;
  c.a_show();
  c.b_show();
  c.show();
  
  return 0;
 }

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


 class A
 {
 public:
  void calc(int,double){}
 };

 class B
 {
 public:
  void calc(double){}
 };
 
 class C : public A,public B
 {
 };
 
 int main()
 {
  C c;
  c.calc(10.0); //ошибка компиляции - неоднозначность

  return 0;
 }

Решение этой проблемы есть - необходимые функции просто выводят в общую область видимости с помощью ключевого слова using. Не совсем удобно, но по другому никак:


 class A
 {
 public:
  void calc(int,double){}
 };

 class B
 {
 public:
  void calc(double){}
 };
 
 class C : public A,public B
 {
 public:
  using A::calc;
  using B::calc;
 };
 
 int main()
 {
  C c;
  c.calc(10.0);  //работает
  c.calc(10,20.0);        //работает

  return 0;
 }

Механизм перекрытия при множественном наследовании работает как обычно, т.е. после определения невиртуальной функции в производном классе Вы перекрываете все функции с аналогичным именем в базовых классах (даже если отличаются параметры) и доступ к этим функциям непосредственно от объекта производного класса будет невозможен:


 class A
 {
 public:
  void calc(int,double){}
 };

 class B
 {
 public:
  void show_value(){}

 };
 
 class C : public A,public B
 {
 public:
  void calc(char){}
 };
 
 int main()
 {
  C c;
  c.calc('1');  //работает
  c.calc(10,20.0); //ошибка - не знаю такой функции

  return 0;
 }

Чтобы сделать эти функции видимыми, их нужно объявить в производном классе с помощью ключевого слова using.

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


        class A
        {
  int val;
 public:
  A():val(0){}
  void increment(){ ++val; }
  void print(){ printf("%d\n",val); }
 };

 class B : public A
 {
 }; 
 
 class C : public A
 {
 };

 class D : public B,public C
 {
 };
 
 int main()
 {
  D d;
  printf("size: %d\n",sizeof(d));
  
  d.increment(); //ошибка компиляции - неоднозначность
  d.B::increment();
  d.B::print();
  d.C::print();

  return 0;
 }

На экране увидим следующее:


 size: 8
 1
 0

Т.е. видно, что существует два независимых набора данных класса A, доступ к которым производится через классы иерархии.

Графически иерархию классов можно изобразить так:


 A   A
 |   |
 B   C
  \ / 
   D

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


        class A
        {
  int val;
 public:
  A():val(0){}
  virtual ~A(){}  

  void increment(){ ++val; }
  void print(){ printf("%d\n",val); }
 };

 class B : public virtual A
 {
 }; 
 
 class C : public virtual A
 {
 };

 class D : public B,public C
 {
 };
 
 int main()
 {
  D d;
  
  d.B::increment();
  d.B::print();
  d.C::print();

  return 0;
 }

На экране увидим:


 1
 1

Т.е. теперь мы оперируем с одним набором данных повторяющегося базового класса А. Графически иерархию можно изобразить так:


   A
  / \ 
 B   C
  \ / 
   D

В дополнение к вышесказанному следует добавить, что конструктор и деструктор базового виртуального класса при виртуальном наследовании будет вызван только один раз. От порядка представления базовых классов в списке наследования зависит порядок вызова конструкторов и соответственно деструкторов базовых классов. Одно и то же имя класса не может быть использовано в списке базовых классов более одного раза.

Теперь поговорим о том, когда удобно применять множественное наследование.

  • Объединение интерфейсов. Удобно при сложной иерархии отделять функциональные обособленные части в отдельные классы-интерфейсы, например абстрактные классы, состоящие только из чисто виртуальных функций. Далее конструируется производный класс, который с помощью множественного наследования включает в себя поведение нескольких базовых классов-интерфейсов. Один абстрактный класс, к примеру, отвечает за интерфейс, другой абстрактный - за реализацию работы с данными, - объединили в третий и получили полноценный законченный класс.
  • Обобщение свойств. В принципе, аналогичен предыдущему способу, только происходит объединение методов классов, т.е. отнаследовали от двух классов - получили третий, который включает в себя всю реализацию из прежних двух, упор именно на законченный реализованный интерфейс здесь не делается.

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


На сегодня все. Пожелания, предложения, вопросы прошу на iqsoft@cg.ukrtel.net

В следующем выпуске: Шаблонные классы.


(с) Юрий Гордиенко



http://subscribe.ru/
E-mail: ask@subscribe.ru
Отписаться
Убрать рекламу

В избранное