Лекция 18. Множественное наследование

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

Класс может быть порождён из любого числа базовых классов. Наличие более чем одного непосредственного базового класса называется множественным наследованием.
class A { ... };
class B { ... };
class C { ... };
class D : public A, public B, private C { ... };

Указатель на производный класс можно передать в функции, которые ожидают указатель на один из базовых классов. Реализация этого механизма подразумевает простые методы компиляции для обеспечения того, что функция, ожидающая указатель на один базовый класс, увидит часть производного класса, отличную от той, которую увидит функция, ожидающая указатель на другой базовый класс. Виртуальные функции работают как обычно.
class A { public: void f1(); virtual void g() = 0; }; class B { public: void f2(); virtual void h() = 0; }; class C : public A, public B { public: void g(); // Замещение A::g() void h(); // Замещение B::h() }; void f1(A *p) { p->f1(); } void f2(B *p) { p->f2(); } void main() { C c; f1(&c); f2(&c); A *p = &c; p->g(); // Правильно p->h(); // Ошибка: функция h не является членом класса А dynamic_cast<B *>(p)->h(); // Правильно }

Класс не может быть задан в качестве непосредственного базового класса более одного раза, но он может быть более одного раза косвенным базовым классом.

class A { ... }; class B : public A, public A { ... }; // Ошибка!
class A { ... }; class B : public A { ... }; class C : public A { ... }; class D : public B, public C { ... }; // Всё правильно

Здесь объект класса D будет иметь два подобъекта класса A.

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

A ← B ← D → C → A

Класс не может появляться дважды в списке базовых классов просто потому, что каждая ссылка на него или его члены была бы неоднозначной. Эта проблема не возникает, если класс появляется дважды в качестве косвенного базового класса. Объект класса D имеет два подобъекта класса AB::A и C::A.

Поскольку в объекте D имеется два объекта A, приведение (явно или неявное) между указателем на A и указателем на D неоднозначно и поэтому запрещено.

D *pd = new D; A *pa = pd; pa = (A *)pd; pa = (B *)pd; pa = (С *)pd; // Неоднозначность! // Всё равно неоднозначность! // Приведение к указателю на A в объекте B // Приведение к указателю на A в объекте С

Однако класс может быть одновременно непосредственным и косвенным базовым классом.

class A { ... }; class B : public A { ... }; class C : public A { ... }; class D : public A, public B, public C { ... }; pa = pd; pa = (B *)pd; pa = (С *)pd; // Можно! // Приведение к указателю на A непосредственно в объекте D // Приведение к указателю на A в объекте B // Приведение к указателю на A в объекте С

2. Using-объявления

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

class A { protected: void f(int x); }; // Защищённая функция

class B1 : public A { public: void f(double x); }; class B2 : public A { public: void f(char x); using A::f; };

class C : public B1, public B2 { public: void f(char *s); using A::f; using B1::f; using B2::f; }; // Функция A::f(int) стала открытой
void main() { C c; c.f(1); c.f(2.4); c.f('&'); c.f("abc"); } // Вызов функции A::f(int) // Вызов функции B1::f(double) // Вызов функции B2::f(char) // Вызов функции C::f(char*)

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

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

К описателю базового класса можно добавлять ключевое слово virtual.
class A { ... }; class B : virtual public A { ... }; class C : virtual public A { ... }; class D : public B, public C { ... };

В этом случае класс D содержит только один экземпляр класса А. Графически это выглядит так:

Класс может содержать как виртуальный, так и не виртуальный базовый класс данного типа.
class A { ... }; class B : virtual public A { ... }; class C : virtual public A { ... }; class D : public A { ... }; class E : public B, public C, public D { ... };

Здесь класс E включает два экземпляра класса A: один из класса D, а другой виртуальный класс A, разделяемый классами B и C.

class A { ... }; class B : virtual public A { ... }; class C : virtual public A { ... }; class D : public A, public B, public C { ... }; // Нельзя! Приведение к классу A неоднозначно.

При определении функций класса с виртуальным базовым классом программист, в общем случае, не может знать, будет ли базовый класс использоваться совместно с другими классами. Это может представлять некоторую проблему при реализации алгоритмов, которые требуют, чтобы функция базового класса вызывались ровно один раз. Язык гарантирует, что конструктор виртуального базового класса вызывается только один раз. Конструктор виртуального базового класса вызывается (явно или неявно) из конструктора объекта (конструктора самого «нижнего» производного класса).
class A { private: int n; public: A(int nn) : n(nn) { } }; class B1 : virtual public A { private: int n; public: B1(int a, int nn) : A(a), n(nn) { } }; class B2 : virtual public A { private: int n; public: B2(int a, int nn) : A(a), n(nn) { } }; class C : public B1, public B2 { private: int n; public: // Необходима инициализация единственного объекта виртуального базового класса А. // Первый параметр конструкторов классов B1 и B2 не используется. C(int a, int b1, int b2, int nn) : A(a), B1(0, b1), B2(0, b2), n(nn) { } }; class A { private: int n; public: A(int nn) : n(nn) { } }; class B1 : public A { private: int n; public: B1(int a, int nn) : A(a), n(nn) { } }; class B2 : public A { private: int n; public: B2(int a, int nn) : A(a), n(nn) { } }; class C : public B1, public B2 { private: int n; public: // Объекты базового класса А в объектах классов B1 и В2 инициализируются конструкторами этих классов C(int a1, int a2, int b1, int b2, int nn) : B1(a1, b1), B2(a2, b2), n(nn) { } };

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

Производный класс может заместить виртуальную функцию своего непосредственного или косвенного виртуального базового класса. Два различных класса могут заместить различные виртуальные функции виртуального базового класса. Таким способом несколько производных классов могут внести свой вклад в реализацию интерфейса, представленного в виртуальном базовом классе.
class A { public: virtual ~A() { } virtual void g(); virtual void h(); }; class B1 : virtual public A { public: void g(); }; class B2 : virtual public A { public: void h(); }; class C : public B1, public B2 { }; void main() { C c; A *p = &c;

p->g(); p->h(); } // Вызов функции B1::g // Вызов функции B2::h

Если два класса замещают одну и ту же функцию базового класса, то порождение от них производного класса, не замещающего эту функцию, недопустимо. В этом случае не может быть создана таблица виртуальных функций, потому что вызов этой функции будет неоднозначен.
class A { public: virtual ~A() { } virtual void g(); virtual void h(); }; class B1 : virtual public A { public: void g(); void h(); }; class B2 : virtual public A { public: void g(); void h(); };

class C : public B1, public B2 { public: void g(); }; void main() { C c; A *p = &c; p->g(); p->h(); } // Функция C::g замещает функции B1::g и B2::g // Всё в порядке – вызов функции C::g // Неоднозначность – B1::h или B2::h?

4. Указатели на члены класса

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

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

Язык С++ предлагает средство косвенной ссылки на член класса. Указатель на член класса является значением, идентифицирующим член класса. Можно рассматривать его как позицию члена класса в объекте класса, но, конечно же, компилятор принимает в расчёт различия между данными, виртуальными функциями, невиртуальными функциями и т.д.

Указатель на член класса можно получить при помощи применения оператора взятия адреса & к полностью квалифицированному имени члена класса. Переменная типа «указатель на член класса X» объявляется с использование формы X::*.

Указатель на член класса m используется в комбинации с объектом. Операторы ->* и .* позволяют программисту выразить такую комбинацию. p->*m связывает m с объектом, на который указывает p, а obj.*m связывает m с объектом obj. Результат можно использовать в соответствии с типом m. Невозможно сохранить результат этих операций для его дальнейшего использования.

Естественно, если бы мы знали, какой член класса нужно использовать, мы могли бы сделать это непосредственно, не используя указатели. Так же как и указатели на обычные функции, указатели на члены класса используются, когда возникает необходимость сослаться на объект, имя которого неизвестно. Однако в отличие от указателя на переменную или обычную функцию, указатель на член класса не является просто указателем на область памяти. Он больше соответствует смещению в структуре или индексу в массиве. Сочетание указателя на член класса с объектом или указателем на объект даёт то, что идентифицирует конкретный член конкретного объекта.
class Base { public: virtual void open() = 0; virtual void close() = 0; void print() { std::cout << "Base: print" << std::endl; } virtual ~Base() { } }; class D1 : public Base { public: void open() { std::cout << "D1: open" << std::endl; } void close() { std::cout << "D1: close" << std::endl; } void print() { std::cout << "D1: print" << std::endl; } }; class D2 : public Base { public: void open() { std::cout << "D2: open" << std::endl; } void close() { std::cout << "D2: close" << std::endl; } void print() { std::cout << "D2: print" << std::endl; } };

typedef void (Base::*PF)(); void main() { PF pf1 = &Base::open; PF pf2 = &Base::close; PF pf3 = &Base::print; D1 d; (d.*pf1)(); (d.*pf2)(); (d.*pf3)(); Base *pb = new D2; (pb->*pf1)(); (pb->*pf2)(); (pb->*pf3)(); } // Определяем тип для указателя на функцию-член класса Base // Запоминаем указатели на члены класса Base // (функции open и close являются виртуальными, // а функция print – невиртуальной). // d – объект класса D1 // Вызов функции D1::open() // Вызов функции D1::close() // Вызов функции Base::print() // pb – указатель на объект класса Base (реально объект принадлежит классу D2) // Вызов функции D2::open() // Вызов функции D2::close() // Вызов функции Base::print()

5. Пример

Разработаем класс Storable, который позволяет записывать на диск и считывать с диска объекты производных от него классов. Используем его для создания классов для фигур, информация о которых может быть прочитана с диска и записана на диск.

Файл Shapes.h

#define SHAPES class Shapes { protected: static int count; int color; int left, top, right, bottom; Shapes() { count++; } public: enum {LEFT, UP, RIGHT, DOWN}; virtual ~Shapes() { count--; } static int GetCount() { return count; } int Left() const { return left; } int Top() const { return top; } int Right() const { return right; } int Bottom() const { return bottom; } virtual void Draw() = 0; virtual void Move(int where, const Shapes *shape) = 0; virtual Shapes* NewShape() = 0; virtual Shapes* Clone() = 0; };

Файл Shapes.cpp

#include "Shapes.h" int Shapes::count = 0;

Файл Storable.h

#define STORABLE #include <fstream> using namespace std; class Storable { protected: ifstream is; ofstream os; public: enum {READ, WRITE}; Storable(const char *f1, const char *f2); Storable(const char *f, int mode); virtual ~Storable(); virtual int Read() = 0; virtual int Write() = 0; };

Файл Storable.cpp

#include "Storable.h" Storable::Storable(const char *f1, const char *f2) { is.open(f1, ios_base::in); if (!is.is_open()) throw f1; os.open(f2, ios_base::out); if (!os.is_open()) throw f2; } Storable::Storable(const char *f, int mode) { if (mode == READ) { is.open(f, ios_base::in); if (!is.is_open()) throw f; } else if (mode == WRITE) { os.open(f, ios_base::out); if (!os.is_open()) throw f; } } Storable::~Storable() { if (is.is_open()) is.close(); if (os.is_open()) os.close(); }

Файл Circle.h*

#if !defined(SHAPES) #include "Shapes.h" #endif class Circle : public Shapes { private: int cx, cy, radius; public: Circle(int x = 0, int y = 0, int r = 0, int c = 0); ~Circle() { } void Draw(); void Move(int where, const Shapes *shape); };

Файл Triangle.h*

#if !defined(SHAPES) #include "Shapes.h" #endif class Triangle : public Shapes { private: int x1, y1, x2, y2, x3, y3; public: Triangle(int x1 = 0, int y1 = 0, int x2 = 0, int y2 = 0, int x3 = 0, int y3 = 0, int c = 0); ~Triangle() { } void Draw(); void Move(int where, const Shapes *shape); };

Файл Circle_Storable.h

#include "Circle.h" #if !defined(STORABLE) #include "Storable.h" #endif class Circle_Storable : public Circle, virtual public Storable { public: Circle_Storable(const char *f1, const char *f2, int x = 0, int y = 0, int r = 0, int c = 0) : Storable(f1, f2), Circle(x, y, r, c) { } Circle_Storable(const char *f, int mode, int x = 0, int y = 0, int r = 0, int c = 0) : Storable(f, mode), Circle(x, y, r, c) { } ~Circle_Storable() { } int Read(); int Write(); };

Файл Circle_Storable.cpp

#include "Circle_Storable.h" int Circle_Storable::Read() { if (!is.is_open()) return 0; is >> cx >> cy >> radius >> color; left = cx - radius; top = cy - radius; right = cx + radius; bottom = cy + radius; return 1; } int Circle_Storable::Write() { if (!os.is_open()) return 0; os << cx << " " << cy << " " << radius << " " << color; return 1; }

Файл Triangle_Storable.h

#include "Triangle.h" #if !defined(STORABLE) #include "Storable.h" #endif class Triangle_Storable : public Triangle, virtual public Storable { public: Triangle_Storable(const char *f1, const char *f2, int x1 = 0, int y1 = 0, int x2 = 0, int y2 = 0, int x3 = 0, int y3 = 0, int c = 0) : Storable(f1, f2), Triangle(x1, y1, x2, y2, x3, y3, c) { } Triangle_Storable(const char *f, int mode, int x1 = 0, int y1 = 0, int x2 = 0, int y2 = 0, int x3 = 0, int y3 = 0, int c = 0) : Storable(f, mode), Triangle(x1, y1, x2, y2, x3, y3, c) { } ~Triangle_Storable() { } int Read(); int Write(); };

Файл Triangle_Storable.cpp

#include "Triangle_Storable.h" int Max(int a, int b, int c); int Min(int a, int b, int c); int Triangle_Storable::Read() { if (!is.is_open()) return 0; is >> x1 >> y1 >> x2 >> y2 >> x3 >> y3 >> color; left = Min(x1, x2, x3); top = Min(y1, y2, y3); right = Max(x1, x2, x3); bottom = Max(y1, y2, y3); return 1; } int Triangle_Storable::Write() { if (!os.is_open()) return 0; os << x1 << " " << y1 << " " << x2 << " " << y2 << " " << x3 << " " << y3 << " " << color; return 1; }

Файл main.cpp

#include "Circle_Storable.h" #include "Triangle_Storable.h" void main() { Shapes* shapes[10]; try { shapes[0] = new Circle_Storable("c1.txt", Storable::WRITE, 100, 100, 30, 50); shapes[1] = new Triangle_Storable("t1.txt", Storable::WRITE, 0, 0, 20, 0, 0, 20, 90); shapes[2] = new Circle_Storable("c2.txt", Storable::WRITE, 200, 200, 50, 20); } catch(const char *s) { printf("Impossible to open file '%s'\n", s); return; } for(int i = 0; i < Shapes::GetCount(); i++) shapes[i]->Draw(); for(int i = 1; i < Shapes::GetCount(); i++) shapes[i]->Move(Shapes::LEFT, shapes[i - 1]); for(int i = 0; i < Shapes::GetCount(); i++) shapes[i]->Draw(); // В массиве сохранены указатели на класс Shapes. Реально объекты принадлежат к классу Circle_Storable или Triangle_Storable, // которые являются классами, производными как от класса Shapes, так и от класса Storable. Мы не знаем к какому именно классу // (Circle_Storable или Triangle_Storable) принадлежит объект, но можем преобразовать указатель к указателю на класс Storable, // который, так же как и класс Shapes, является базовым для обоих классов. Однако это возможно сделать только во время работы // программы, поэтому используется оператор преобразования типа dynamic_cast, который проверяет принадлежность объекта к классу, // производному от классов Shapes и Storable, и выполняет преобразование. Если бы реально объект не принадлежал к классу, // производному от этих классов, преобразование указателя на класс Shapes в указатель на класс Storable было бы невозможно, // и оператор dynamic_cast вернул бы значение 0. for(int i = 0; i < Shapes::GetCount(); i++) dynamic_cast<Storable*>(shapes[i])->Write(); for(int i = 0, n = Shapes::GetCount(); i < n; i++) delete shapes[i]; }