Наследственная иерархия

Наследование

Наследование – это такое отношение между классами, когда один класс повторяет структуру и поведение другого класса (одиночное наследование) или других классов (множественное наследование).

Пример. Рассмотрим определение типа Shape (фигура) для использования в графической системе. Предположим, у нас есть два класса:

class Point {... }; //Точка

class Color {... }; //Цвет

Мы можем определить Shape следующим образом:

еnum Kind { circle, triangle, square }; // перечисление: окружность,

// треугольник, квадрат

class Shape {

Kind k, //поле типа (какая фигура?)

Point center, // центр фигуры

Color col, //цвет фигуры

public:

void move(Point to); // переместить

void draw(); // нарисовать

void rotate(int); // повернуть

Point isCenter();// возвращает значение центра фигуры

};

«Поле типа» k необходимо, чтобы такие операции, как draw и rotate, могли определить, с каким видом фигуры они имеют дело. Функцию draw() можно определить следующим образом:

void Shape:: draw() {

switch(k) {

case circle:... break; // нарисовать окружность

case triangle:... break; // нарисовать треугольник

case square:... // нарисовать квадрат

}

}

Таким образом, функции должны «знать» обо всех воз­можных видах фигур. Поэтому код любой такой функции растет с добавлением новой фигуры в систему. Если мы определили новую фигуру, каждую операцию над фигурой нужно просмотреть и, вероятно, модифицировать. У нас есть возможность добавить новую фигуру к системе, только если мы имеем исходные тексты каждой функции. Так как добавление новой фигуры связано с внесением изменений в код каждой важной операции над фигурами, оно требует большого мастерства и потенциально влечет появление ошибок в коде, управляющем другими (старыми) фигурами.

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

Сначала мы описываем класс, который определяет общие свойства всех фигур:

class Shape {

Point center;

Color col;

...

public:

void move(Point to) { center =to;... draw (); }

virtual void draw() = 0;

virtual void rotate(int angle)=0;

...

};

Описание virtual означает, что функция является виртуальной – замещается в классе, производном от данного.

Функция, интерфейс вызова которой мо­жет быть определен, а реализация – нет, объявляется чисто виртуальной, для чего используется синтаксис «= 0». Например, ре­ализации функций draw и rotate могут быть определены только для конкретных фигур, поэтому эти функции вообще не реализованы в классе Shape.

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

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

Для определения конкретной фигуры мы должны сказать, что она является фигу­рой, и указать особые свойства (в том числе определить чисто виртуальные функции):

class Circle: public Shape {

int radius;

public:

Circle(Point cntr, Color cl, int rds) {...};

void draw(){...};

void rotate(int) {};//функция ничего не делает

};

Классы Circle и Shape называются подклассом (потомком) и суперклассом (надклассом, родительским классом) соответственно. В С++ подкласс, как правило, называют производным классом, а суперкласс – базовым классом.

Класс Circle создан для того, чтобы оперировать с его объектами. Классы, для которых создаются экземпляры, называ­ют конкретными. С другой стороны, в классе Shape описаны чисто виртуальные функции, не имеющие реализации по определению, и создание экземпляров данного класса запрещено языком С++. Классы, экземпляры которых не созда­ются, называются абстрактными. Ожидается, что подклассы абстрактных классов доопределят их до жизнеспособной абстракции, наполняя класс содержанием. Классы, у которых нет потомков, называют листовыми.

Самый общий класс в иерархии классов называется корневым. В большинстве приложений корневых классов бывает несколько, и они отражают наиболее общие абстракции предметной области.

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

Для этой цели описание класса разделяется на три части:

– открытую (public), видимую всем клиентам;

– защищенную (protected), видимую самому классу, его подклассам и друзь­ям (friend);

– закрытую (private), видимую только самому классу и его друзьям.

Таким образом, язык C++ позволяет достаточно гибко найти компромисс между наследованием и инкапсуляцией.

Наследование подразумевает, что подклассы повторяют и могут дополнять структуры их супер­классов. В нашем примере класс Circle содержит эле­менты структуры суперкласса Shape и более специализированный элемен­т – радиус.

Поведение суперклассов также наследуется. Например, с помощью операции move класса Shape можно переместить экземпляр класса Circle. При этом в производном классе допускается добавление новых и переопределение существующих мето­дов.

Например, опишем класс круг – SolidCircle – закрашенная окружность:

class SolidCircle: public Circle {

protected: Color fillcol; // цвет заполнения

public: SolidCircle(Point cntr, Color cl, int rds, Color fcl):

Circle(Point cntr, Color cl, int rds), fillcol (fcl) {...};

void draw(){ Circle:: draw ();... };

};

Функция draw сначала вызывает аналогичную функцию родительского класса, которая рисует границу круга, а затем сама заполняет ее цветом.

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

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

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

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

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


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: