Лекция 16. Наследование

1. Введение

Классы используются для моделирования концепций реального и программного мира. Однако ни одна концепция не существует в изоляции. Она сосуществует с родственными концепциями и именно этой связи она обязана своей мощью. Понятие производного класса и связанные с ним механизмы языка предназначены для выражения иерархических отношений, т.е. для отражения общности классов. Например, концепции круга и треугольника связаны тем, что и тот, и другой являются фигурами. Концепция фигуры является общей для них. Поэтому мы должны явно определить, что классы Circle и Triangle имеют общий базовый класс Shape. Представление понятий «круг» и «треугольник» в программе без введения понятия «фигура» означало бы потерю чего-то существенного.

2. Базовые и производные классы

Рассмотрим приведённый во введении пример. И круг, и треугольник являются фигурами, однако если не использовать специальных средств для явного указания этого факта, компилятор сам не сможет сделать подобный вывод. Соответственно, мы не сможем, например, поместить указатели на объекты классов Circle и Triangle в один список. Такое отношение между классами называют наследованием.

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

Объявление производного класса имеет следующий синтаксис:
class <имя производного класса> : <спецификатор доступа> <имя базового класса> [, <спецификатор доступа> <имя базового класса> ]    { ... };

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

Shape ← Circle

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

Производный класс может сам, в свою очередь, служить базовым классом.

Если члены базового класса не переопределены в производном классе, они обозначаются и трактуются так же, как и члены производного класса. Говорят, что члены базового класса наследуются производным классом. Операция разрешения области видимости :: может употребляться для явной ссылки на член базового класса. Это обеспечивает доступ к имени, которое переопределено в производном классе.

Возвращаясь к рассматриваемому примеру, мы видим, что круг является фигурой. Поэтому Circle* можно использовать как Shape*. Однако фигура не обязательно является кругом, поэтому Shape* нельзя использовать как Circle*. В общем случае, указатель на производный класс может быть неявно преобразован к указателю на однозначно доступный базовый класс. Ссылка на производный класс может быть неявно преобразована к ссылке на однозначно доступный базовый класс.

class Base { public: int a, b; }; class Derived : public Base { public: int b, c; }; Derived d;
d.a = 1; d.Base::b = 2; d.b = 3; d.c = 4; Base *bp = &d; // Инициализация a, унаследованного из класса Base // Инициализация b из класса Base // Инициализация b, объявленного в классе Derived // Преобразуем указатель на класс Derived в указатель на класс Base

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

class A { public: void f(); }; class B : public A { }; class C : public B { public: void f(); void ff(); }; // Класс B является непосредственным базовым классом для класса C, // а класс A – косвенным базовым классом для класса C

В записи <имя класса>::<имя>   имя класса может быть именем косвенного базового класса, это имя класса определяет класс, в котором начинается поиск имени.

void C::ff() { f(); A::f(); B::f(); } // Вызов функции f() из класса C // Вызов функции f() из класса A // Снова вызов функции f() из класса A, т.к. в классе В функция f() не определена

2.1. Спецификаторы доступа для базовых классов

Спецификатор доступа может быть:

Приватные члены класса недоступны даже в производном классе, если только он не объявлен как дружественный.

По умолчанию подразумевается спецификатор доступа private.

class Base { private: int a; protected: int b; public: int c; };
class Derived1 : public Base { ... }; // a недоступен // b – защищённый член класса Derived1 // c – публичный член класса Derived1
class Derived2 : protected Base { ... }; // a недоступен // b и c – защищённые члены класса Derived2
class Derived3 : private Base { ... }; // a недоступен // b и c – приватные члены класса Derived3

2.2. Объявления доступа

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

class Base { public: int n; ... }; class Derived : private Base { public:
Base::n; ... }; // По умолчанию n был бы приватным членом класса Derived

Приватные члены базового класса в любом случае остаются недоступными. Доступ к защищённым и публичным членам класса может быть скорректирован в любую сторону.

Объявление доступа для имени совместно используемой функции устанавливает доступ всем функция с этим именем в базовом классе.

class Base { public: void f(); void f(int n); }; class Derived : private Base { public:
Base::f; }; // Обе функции Base::f() и Base::f(int) будут публичными

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

class Base { public: void f(); }; class Derived : private Base { public: void f(int n);
Base::f; } // Ошибка!

2.3. Конструкторы и деструкторы

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

class Base { private: int a; protected: int b; public: Base(int aa, int bb) : a(aa), b(bb) { } }; class Derived : public Base { private: int c; public: Derived(int aa, int bb, int cc) :
Base(aa, bb), c(cc) { } }; // Инициализация базового класса // Инициализация членов производного класса

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

class Derived : public Base { private: int c; public: Derived(int aa, int bb, int cc) :
a(aa), b(bb), c(cc) { } }; // Ошибка – a и b не объявлены в классе Derived

Объекты класса создаются снизу вверх: сначала базовый класс, потом производный. Они уничтожаются в противоположном порядке. Подобъекты базовых классов конструируются в порядке их объявления в классе и уничтожаются в обратном порядке.

2.4. Доступ к защищённым членам класса

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

class Base { protected: intn; }; class Derived1 : public Base { ... }; class Derived2 : public Base { void member_function(Base *pb, Derived1 *p1); friend void friend_function(Base *pb, Derived1 *p1, Derived2 *p2); };
void Derived2::member_function(Base *pb, Derived1 *p1) { pb->n = 0; p1->n = 0; n = 0; } // Запрещено! // Запрещено! // Доступ через объект класса Derived2
void friend_function(Base *pb, Derived1 *p1, Derived2 *p2) { pb->n = 0; p1->n = 0; p2->n = 0; } // Запрещено! // Запрещено! // Доступ по указателю на объект класса Derived2

2.5. Использование защищённых членов класса

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

Язык C++ позволяет осуществлять гибкое управление доступом к членам класса за счёт использования трех спецификаторов доступа. Однако защищённые члены класса более подвержены злоупотреблениям, чем приватные. Помещение значительной части данных в общей класс, доступный для всех производных классов, приводит к риску разрушения этих данных. Более того, так же как и открытые, защищённые данные не просто реструктурировать ввиду сложности нахождения всех случаев их использования. Таким образом, защищённые данные приводят к проблемам сопровождения.

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

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

3. Пример

Реализуем пример, использовавшийся в лекции, – базовый класс Shapes и производные от него классы Circle и Triangle.

Файл Shapes.h

#define SHAPES // Страж включения class Shapes { protected: static int count; int color; int iam; // Поле для задания типа объекта int left, top, right, bottom; Shapes() { count++; } // Защищённый конструктор public: enum {CIRCLE, TRIANGLE}; enum {LEFT, UP, RIGHT, DOWN}; ~Shapes() { count--; } static int GetCount() { return count; } // Доступ к защищённым членам базового класса разрешен только через объекты или указатели на объекты производного класса. // Когда в функции void Move(int where, const Shapes *shape) мы будем ссылаться на предыдущую фигуру просто как на фигуру, // мы не сможем получить доступ к этим членам класса. Поэтому необходимы функции, которые возвращают то или иное значение. // Функции, определённые в классе, являются встраиваемыми, поэтому никаких накладных расходов на их вызов не будет. int Left() const { return left; } int Top() const { return top; } int Right() const { return right; } int Bottom() const { return bottom; } int I_am () const { return iam; } };

Файл Shapes.cpp

#include "Shapes.h" int Shapes::count = 0; // Определение статического члена класса

Файл 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); };

Файл Circle.cpp

#include "Circle.h" Circle::Circle(int x, int y, int r, int c) { cx = x; cy = y; radius = r; color = c; left = cx - radius; top = cy - radius; right = cx + radius; bottom = cy + radius; iam = CIRCLE; // Запоминаем, что объект является кругом } void Circle::Draw() { ... } void Circle::Move(int where, const Shapes *shape) { switch (where) { case LEFT: cx = shape->Left() - radius; cy = (shape->Top() + shape->Bottom()) / 2; break; case UP: cx = (shape->Left() + shape->Right()) / 2; cy = shape->Top() - radius; break; case RIGHT: cx = shape->Right() + radius; cy = (shape->Top() + shape->Bottom()) / 2; break; case DOWN: cx = (shape->Left() + shape->Right()) / 2; cy = shape->Bottom() + radius; break; } left = cx - radius; top = cy - radius; right = cx + radius; bottom = cy + radius; }

Файл 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); };

Файл Triangle.cpp

#include "Triangle.h" int Max(int a, int b, int c); int Min(int a, int b, int c); Triangle::Triangle(int x1, int y1, int x2, int y2, int x3, int y3, int c) { this->x1 = x1; this->y1 = y1; this->x2 = x2; this->y2 = y2; this->x3 = x3; this->y3 = y3; color = c; left = Min(x1, x2, x3); top = Min(y1, y2, y3); right = Max(x1, x2, x3); bottom = Max(y1, y2, y3); iam = TRIANGLE; // Запоминаем, что объект является треугольником } void Triangle::Draw() { ... } void Triangle::Move(int where, const Shapes *shape) { int dx, dy; switch (where) { case LEFT: dx = shape->Left() - right; dy = (shape->Top() - top + shape->Bottom() - bottom) / 2; break; case UP: dx = (shape->Left() - left + shape->Right() - right) / 2; dy = shape->Top() - bottom; break; case RIGHT: dx = shape->Right() - left; dy = (shape->Top() - top + shape->Bottom() - bottom) / 2; break; case DOWN: dx = (shape->Left() - left + shape->Right() - right) / 2; dy = shape->Bottom() - top; break; } x1 += dx; y1 += dy; x2 += dx; y2 += dy; x3 += dx; y3 += dy; left = Min(x1, x2, x3); top = Min(y1, y2, y3); right = Max(x1, x2, x3); bottom = Max(y1, y2, y3); }

Файл main.cpp

#include "Circle.h" #include "Triangle.h" void main() { Shapes* shapes[10]; // Т.к. конструктор для класса Shapes является защищённым, // можно объявить массив указателей, но не массив фигур shapes[0] = new Circle(100, 100, 30, 50); shapes[1] = new Triangle(0, 0, 20, 0, 0, 20, 90); shapes[2] = new Circle(200, 200, 50, 20); for(int i = 0; i < Shapes::GetCount(); i++) if (shapes[i]->I_am() == Shapes::CIRCLE) // Проверка типа объекта static_cast<Circle*>(shapes[i])->Draw(); // Необходимо преобразование, чтобы вызвать правильную функцию else static_cast<Triangle*>(shapes[i])->Draw(); for(int i = 1; i < Shapes::GetCount(); i++) if (shapes[i]->I_am() == Shapes::CIRCLE) static_cast<Circle*>(shapes[i])->Move(Shapes::LEFT, shapes[i - 1]); else static_cast<Triangle*>(shapes[i])->Move(Shapes::LEFT, shapes[i - 1]); for(int i = 0; i < Shapes::GetCount(); i++) if (shapes[i]->I_am() == Shapes::CIRCLE) static_cast<Circle*>(shapes[i])->Draw(); else static_cast<Triangle*>(shapes[i])->Draw(); for(int i = 0, n = Shapes::GetCount(); i < n; i++) if (shapes[i]->I_am() == Shapes::CIRCLE) delete static_cast<Circle*>(shapes[i]); else delete static_cast<Triangle*>(shapes[i]); }