До этого момента в примерах иерархии классов каждый производный класс наследовался от
одного базового, такое наследование называется одиночным. Сейчас мы рассмотрим случай,
когда производный класс имеет более одного базового класса,- подобный механизм наследования
называется множественным наследованием.
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
В дополнение к вышесказанному следует добавить, что конструктор и деструктор базового виртуального
класса при виртуальном наследовании будет вызван только один раз. От порядка представления базовых
классов в списке наследования зависит порядок вызова конструкторов и соответственно деструкторов
базовых классов. Одно и то же имя класса не может быть использовано в списке базовых классов более
одного раза.
Теперь поговорим о том, когда удобно применять множественное наследование.
Объединение интерфейсов. Удобно при сложной иерархии отделять функциональные обособленные
части в отдельные классы-интерфейсы, например абстрактные классы, состоящие только из чисто
виртуальных функций. Далее конструируется производный класс, который с помощью множественного
наследования включает в себя поведение нескольких базовых классов-интерфейсов. Один абстрактный
класс, к примеру, отвечает за интерфейс, другой абстрактный - за реализацию работы с данными, -
объединили в третий и получили полноценный законченный класс.
Обобщение свойств. В принципе, аналогичен предыдущему способу, только происходит объединение
методов классов, т.е. отнаследовали от двух классов - получили третий, который включает в
себя всю реализацию из прежних двух, упор именно на законченный реализованный интерфейс здесь
не делается.
На этом пожалуй и все о множественном наследовании. Единственное, что хочется добавить, так это
рекомендацию сильно не увлекаться, т.к. серьезные классы при множественном наследовании имеют
тенденцию сильно разрастаться, также не следует использовать данный механизм в случаях, когда можно
обойтись простым одиночным наследованием или включением объекта класса в качестве члена класса.