Читайте также:
|
|
Множественное наследование предполагает, что производный класс порождается из двух или более базовых классов. Концепция множественного наследования довольно противоречивая и дискуссионная тема. Потенциально, возможности ее велики. Множественное наследование позволяет построить более адекватную модель прикладной области и выразить средствами языка программирования сложную структуру непосредственно. С другой стороны, множественное наследование, часто, может создать проблемы в программах. Из-за того, что имя метода или поля может по-разному проявлять себя в разных базовых классах, а различные базовые классы могут быть сами порождены от общего предка (и много других проблем), то корректно написать большую программу с использованием сложной иерархии множественного наследования весьма и весьма трудно. Поэтому, применять множественное наследование нужно с большим вниманием и только тогда, когда это абсолютно необходимо.
Следующий формальный пример демонстрирует синтаксис множественного наследования:
Пример 15-3.
#include <iostream.h>
class A
{
int a;
public:
A(int i): a(i) { }
virtual void print() {cout << a << endl;}
int get_a() {return a;}
};
class B
{
int b;
public:
B(int j): b(j) { }
void print() {cout << b << endl;}
int get_b() {return b;}
};
class C: public A, public B
{
int c;
public:
C(int i, int j, int k): A(i), B(j), c(k) { }
void print() {A::print(); B::print();}
// используем print() с операцией разрешения обл. действия ::
void get_ab() {cout << get_a() << " " << get_b() << endl;}
// используем get_a() и get_b() без операции ::
};
int main()
{
C x(5, 8, 10);
A* ap = &x;
B* bp = &x;
ap -> print(); // используем C::print();
bp -> print(); // используем B::print();
// bp -> A::print(); // как будто x наследовано только из B,
// доступ A::print() - невозможен;
x.A::print(); // используем A::print();
x.get_ab();
return 0;
}
Класс C – наследник обоих классов A и B. Поэтому, объект класса C имеет данные-члены a, b, и c, или более точно, объект класса C строится “на вершине” (является надстройкой) копии объекта класса A с полем a, и копии объекта класса B с полем b, добавляя свое (дополнительное) поле c.
Проблема возникает, если как класс A, так и B - порождены от общего базового класса R:
class R
{
int r;
public:
// методы класса R
};
class A: public R
{
int a;
public:
// методы класса A
};
class B: public R
{
int b;
public:
// методы класса B
};
class C: public A, public B
{
int c;
public:
// методы класса C
};
В этом случае объект класса есть надстройка над объектом класса A и объекта класса B, но оба последних содержат объект класса R. В соответствии с “идеологией”, применяемой при одиночном наследовании, мы должны бы создать объект класса R, чтобы сконструировать объект класса A, затем создать, возможно, другой объект класса R для конструирования объекта класса B, и, наконец, сконструировать объект класса C как надстройку объектов класса A и B. Эти два объекта класса R не взаимодействуют друг с другом. Такое дублирование (объекта класса R) является верным, но во многих случаях не необходимым, а даже бессмысленным. Например, пусть, с одной стороны, нашему приложению действительно нужно использовать один и тот же объект для построения объекта класса A и объекта класса B. Но, с другой стороны, в этом случае объект класса C не может быть использован для представления объектов класса R, поскольку он содержит два объекта класса R. О такого рода проблемах при множественном наследовании говорят как о возникновении неоднозначностей.
C++ обеспечивает способ, позволяющий иметь только один объект класса R при построении объекта класса A и объекта класса B. Речь идет о, так называемом, виртуальном наследовании: необходимо объявить класс R как виртуальный базовый класс. (Это не совсем удачно, но мы используем слова “ абстрактный базовый класс” для классов, содержащих хотя бы одну чисто виртуальную ф-ию, и слова “ виртуальный базовый класс” – для классов, которые могут не содержать виртуальных функций вообще.)
Если A и B виртуально наследуются из класса R, то тогда объект любого класса, наследованного из обоих классов A и B, будет содержать только одну копию объекта класса R. Ответственность за конструирование этой общей копии лежит на производном классе. Теперь, если класс C порожден из A и B, то конструктор класса C должен создать, сначала, объект класса R, а затем сконструировать объект класса A и объект класса B, который должен будет содержать эту общую копию объекта класса R (не дублируя). Если пользоваться терминологией объектов и подобъектов, то в цепи наследований каждый невиртуальный базовый класс дает в результате отдельный (свой) подобъект, а каждый виртуальный базовый класс – единственный подобъект, независимо от того, сколько раз класс присутствует в графе (иерархии) наследования.
Пример 15-4.
class R
{
int r;
public:
R (int x = 0): r(x) { } // конструктор в R
// другие методы класса R
};
class A: public virtual R
{
int a;
public:
A (int x, int y): R(x), a(y) { } // конструктор в A
// другие методы класса A
};
class B: public virtual R
{
int b;
public:
B(int x, int z): R(x), b(z) { } // конструктор в B
// другие методы класса B
};
class C: public A, public B
{
int c;
public:
C(int x, int y, int z, int w): R(x), A(x, y), B(x, z), c(w) { }
// конструктор в C, который конструирует сначала объект класса R
// другие методы класса C
};
Могут быть, тем не менее, проблемы и с вызовами методов. Предположим, что класс R имеет функцию f(), класс A имеет функцию, называющуюся также f(), которая обращается к f() из R, делая что-нибудь для ее R-базового объекта, а сама также делает что-нибудь для своего собственного класса A, при этом и класс B тоже имеет функцию с именем f(), которая вызывает f() из R, делая что-то для R-объекта, а также и для своего класса B. Теперь, пусть некая функция f() опять определяется в классе C. Эта функция вызывает A::f() и B::f(), чтобы сделать нечто для ее базовых объектов классов A и B, соответственно, и, возможно, делает что-нибудь еще для самого класса C. И теперь, когда вызывается C::f(), то дважды вызывается R::f() для одного и того же объекта класса R. В большинстве случаев, происходящее -это не то, что мы хотели сделать. Вот идиома (пример использования идиомы: “ собаку съел ” в C++ – неразложимое выражение, смысл котор. не совпадает со значениями слов; в данном контексте – словесная формула), называемая идиомой неопределенности функций при множественном наследовании, которая решает эту проблему:
“В классе A используй функцию fA(), чтобы сделать работу, которую f() приучена делать для самого класса A, и позволь функции f() вызвать R::f() и A::fA(), чтобы завершить ее работу. Сделай то же самое в классе B.” См. Пример:
Пример 15-5, “Разделяй и властвуй”
class R
{
//...
public:
void f();
//...
};
class A: virtual public R
{
//...
protected:
void fA();
//...
public:
void f() {fA(); R::f();}
//...
};
class B: virtual public R
{
//...
protected:
void fB();
//...
public:
void f() {fB(); R::f();}
//...
};
class C: public A, public B
{
//...
protected:
fC(); // заботится только о классе C
//...
public:
void f()
{
R::f();
A::fA();
B::fB();
fC();
}
//...
};
Функция f() в классе R также может быть объявлена как виртуальная. Тогда посредством указателя или ссылки на класс R, при использовании для производного класса, можно обратиться к подходящей функции f() в соответствии с реальным типом объекта.
Иерархия наследования, использующая виртуальные базовые классы и идиому неопределенности функций при множественном наследовании, называется канонической формой наследования. Если при разработке программы допускается применение в будущем множественного наследования, то эта каноническая форма должна быть принята во внимание. В предыдущем примере класс C на текущей стадии разработки может быть еще не определен. Если ожидается, что подобный класс C будет в будущем, то функции A::f() и B::f() должны быть определены в соответствии с канонической формой наследования.
Во многих ситуациях множественное наследование используется для определения производного класса так, что для одного базового класса наследование открытое, а от другого – закрытое. Тогда открытый базовый класс предоставляет интерфейс производного класса, а закрытый базовый класс – реализацию (т.е., класс, чьи методы реализуют требуемую функциональность объектов производного класса).
Пример 15-6.
#include <iostream.h>
#include "Vector.cpp"
// Шаблон класса Vector смотри в Лекции No 12 о конструкторах и деструкторах…
// абстрактный базовый класс стека (в виде шаблона класса)
template <class T>
class Stack
{
public:
virtual bool empty() = 0;
virtual bool full() = 0;
virtual Stack& push(T& new_member) = 0;
virtual Stack& pop() = 0;
virtual T& top() = 0;
};
// конкретный класс стека, использующий реализацию класса Vector
template <class T>
class VecStack: public Stack<T>, private Vector<T*>
{
public:
bool empty()
{ return get_size() == 0; }
bool full()
{ return false; }
Stack<T>& push(T& new_member)
{
add(&new_member);
return *this;
}
Stack<T>& pop()
{
if (!empty())
{ remove(); }
return *this;
}
T& top()
{ return *(*this)[get_size() - 1]; }
};
int main()
{
VecStack<int> s;
int one = 1, two = 2, three = 3;
s.push(one).push(two).push(three);
cout << s.top() << endl;
s.pop().pop();
cout << s.top() << endl;
s.pop();
if (!s.empty())
{ cout << "Error.\n"; }
else
{ cout << "Empty.\n"; }
return 0;
}
// Дальше… - можно пропустить.
Имитация динамического множественного наследования (необязательный пример)
Динамическое множественное наследование означает, что класс C может быть производным от класса A ИЛИ от класса B. Решение о том, как будет получен производный класс принимается во время выполнения программы. C++, в действительности, непосредственно не поддерживает такое динамическое множественное наследование. Однако, если как класс A, так и класс B порождены из общего базового класса R, то существует способ имитировать поведение класса, динамически наследуемого из A или B.
Хитрость состоит в том, чтобы добавить в класс C член, который является указателем на класс R. Этот указатель может быть связан либо с объектом класса A, либо с объектом класса B во время выполнения кода. Когда к некоторой виртуальной функции из класса R выполяется обращение через этот указатель, то объект класса C может вести себя так, как будто он наследован от класса A или класса B (напомним, что поведение объектов класса определяется его методами), сами же объекты классов A или B могут быть созданы “по ходу дела”, т.е. динамически, в методе класса C.
Пример 15-7
#include <iostream.h>
class R
{
protected:
int r;
public:
R(int x = 0): r(x) { }
virtual void print() {cout << "class R print" << endl;}
};
class A: public R
{
protected:
int a;
public:
A(int x = 0, int y = 0): R(x), a(y) { }
void print() {cout << "class A print" << endl;}
};
class B: public R
{
protected:
int b;
public:
B(int x = 0, int z = 0): R(x), b(z) { }
void print() {cout << "class B print" << endl;}
};
enum from {A_type, B_type};
class C
{
int c;
R * rptr;
public:
C(): rptr(NULL) { } // конструктор по умолчанию
C& C_ctor(int x, int w, int z, from t) // метод - 'именованный конструктор'
{
if (t == A_type)
{
A * aptr = new A(x, w);
rptr = aptr;
}
if (t == B_type)
{
B * bptr = new B(x, w);
rptr = bptr;
}
c = z;
return *this;
}
void print() {rptr -> print();} //вызов через указатель R * rptr
~C() {delete rptr;}
};
int main()
{
C c;
int n;
cout << "Enter an integer.\n";
cin >> n;
if (n % 2)
{
// c ведет себя как производный от A
c.C_ctor(1, 2, 3, A_type);
c.print();
}
else
{
// c ведет себя как производный от B
c.C_ctor(1, 2, 3, B_type);
c.print();
}
return 0;
}
Дата добавления: 2015-11-16; просмотров: 55 | Нарушение авторских прав
<== предыдущая страница | | | следующая страница ==> |
Абстрактные базовые классы и чисто виртуальные функции | | | Виртуальные функции. |