Наследование - это механизм получения нового класса на основе уже существующего. Существующий класс может быть дополнен или изменен для создания нового класса.
Существующие классы называются базовыми, а новые – производными. Производный класс наследует описание базового класса; затем он может быть изменен добавлением новых членов, изменением существующих функций- членов и изменением прав доступа. Таким образом, наследование позволяет повторно использовать уже разработанный код, что повышает производительность программиста и уменьшает вероятность ошибок. С помощью наследования может быть создана иерархия классов, которые совместно используют код и интерфейсы.
Наследуемые компоненты не перемещаются в производный класс, а остаются в базовых классах. Сообщение, обработку которого не могут выполнить методы производного класса, автоматически передается в базовый класс. Если для обработки сообщения нужны данные, отсутствующие в производном классе, то их пытаются отыскать автоматически и незаметно для программиста в базовом классе.
При наследовании некоторые имена методов и данных базового класса могут быть по-новому определены в производном классе. В этом случае соответствующие компоненты базового класса становятся недоступными из производного класса. Для доступа к ним используется операция указания области видимости '::'.
В иерархии производный объект наследует разрешенные для наследования компоненты всех базовых объектов (public, protected).
Допускается множественное наследование – возможность для некоторого класса наследовать компоненты нескольких никак не связанных между собой базовых классов. В иерархии классов соглашение относительно доступности компонентов класса следующие:
private – Член класса может использоваться только функциями-членами данного класса и функциями-"друзьями" своего класса. В производном классе он недоступен.
protected – То же, что и private, но дополнительно член класса с данным атрибутом доступа может использоваться функциями-членами и функциями-"друзьями" классов, производных от данного.
public – Член класса может использоваться любой функцией, которая является членом данного или производного класса, а также к public -членам возможен доступ извне через имя объекта.
Следует иметь в виду, что объявление friend не является атрибутом доступа и не наследуется.
Синтаксис определение производного класса:
class имя_класса : список_базовых_классов {список_компонентов_класса};
В производном классе унаследованные компоненты получают статус доступа private, если новый класс определен с помощью ключевого слова class, и статус public, если с помощью struct.
Например,
а) class S : X, Y, Z {...};
S – производный класс;
X, Y, Z – базовые классы.
Здесь все унаследованные компоненты классов X, Y, Z в классе S получают статус доступа private.
б) struct S : X, Y, Z {...};
S – производный класс;
X, Y, Z – базовые классы.
Здесь все унаследованные компоненты классов X, Y, Z в классе S получают статус доступа public.
Явно изменить умалчиваемый статус доступа при наследовании можно с помощью атрибутов доступа – private, protected и public, которые указываются непосредственно перед именами базовых классов. Как изменяются при этом атрибуты доступа в производном классе показано в следующей таблице
атрибут, указанный при наследовании |
атрибут в базовом классе |
атрибут, полученный в производном классе |
public |
public protected private |
public protected недоступен |
protected |
public protected private |
protected protected недоступен |
private |
public protected private |
private private недоступен |
Пример
class B { protected: int t; public: char u; private: int x; }; struct S : B {}; // наследуемые члены t, uимеют атрибут доступа public class E : B {}; // t, u имеют атрибут доступа private class M : protected B {}; // t, u – protected class D : public B {}; // t – protected, u – public class P : private B {}; // t, u – private
Таким образом, можно только сузить область доступа, но не расширить.
Таким образом, внутри производного класса существует четыре уровня, для которых определяется атрибут доступа:
Рассмотрим как при этом регулируется доступ к членам класса извне класса и внутри класса.
Доступ извне.
Доступными являются лишь элементы с атрибутом public.
Собственные члены класса.
Доступ регулируется только атрибутом доступа, указанным при описании класса.
Наследуемые члены класса.
Доступ определяется атрибутами доступа базового класса, ограничивается атрибутом доступа при наследовании и изменяется явным указанием атрибута доступа в производном классе.
Пример.
class Basis { public: int a; protected: int b, c; }; class Derived : public Basis { public: Basis::c; }; int main (void) { Basis ob; Derived od; ob.a; // правильно ob.b; // ошибка od.c; // правильно od.b; // ошибка return 0; }
Доступ изнутри.
Собственные члены класса.
private и protected члены класса могут быть использованы только функциями-членами данного класса.
Наследуемые члены класса.
private-члены класса могут использоваться только собственными функциями- членами базового класса, но не функциями членами производного класса.
protected или public члены класса доступны для всех функций-членов. Подразделение на public, protected и private относится при этом к описаниям, приведенным в базовом классе, независимо от формы наследования.
Пример
class Basis { public: void f1(int i) { a = i; b = i; } int b; private: int a; }; class Derived : private Basis { public: void f2(int i) { a = i; // ошибка b = i; // правильно } };
Поскольку конструкторы не наследуются, при создании производного класса наследуемые им данные-члены должны инициализироваться конструктором базового класса. Конструктор базового класса вызывается автоматически и выполняется до конструктора производного класса. Если наследуется несколько базовых классов, то их конструкторы выполняются в той последовательности, в которой перечислены базовые классы в определении производного класса. Конструктор производного класса вызывается по окончании работы конструкторов базовых классов. Параметры конструктора базового класса указываются в определении конструктора производного класса. Таким образом происходит передача аргументов от конструктора производного класса конструктору базового класса.
Пример
class Basis { public: Basis(int x, int y) { a = x; b = y; } private: int a, b; }; class Inherit : public Basis { public: Inherit(int x, int y, int s) : Basis (x, y) { sum = s; } private: int sum; };
Запомните, что конструктор базового класса вызывается автоматически и мы указываем его в определении конструктора производного класса только для передачи ему аргументов.
Объекты класса конструируются снизу вверх: сначала базовый, потом компоненты-объекты (если они имеются), а потом сам производный класс. Т.о. объект производного класса содержит в качестве подобъекта объект базового класса.
Уничтожаются объекты в обратном порядке: сначала производный, потом его компоненты-объекты, а потом базовый объект.
Как мы знаем, объект уничтожается при завершении программы или при выходе из области действия определения объектов и эти действия выполняет деструктор. Статус деструктора по умолчанию public. Деструкторы не наследуется, поэтому даже при отсутствии в производном классе деструктора, он не передается из базового, а формируется компилятором как умалчиваемый. Классы, входящие в иерархию, должны иметь в своем распоряжении виртуальные деструкторы. Деструкторы могут переопределяться, но не перегружаться.
В любом классе могут быть в качестве компонентов определены другие классы. В этих классах могут быть свои деструкторы, которые при уничтожении объекта охватывающего (внешнего) класса выполняются после деструктора охватывающего класса. Деструкторы базовых классов выполняются в порядке, обратном перечислению классов в определении производного класса. Таким образом, порядок уничтожения объекта противоположен по отношению к порядку его конструирования.
Пример.
// Определение класса базового класса ТОЧКА и производного класса ПЯТНО. class Point // Определение класса ТОЧКА { public: Point(int x1 = 0, int y1 = 0); int &getx(void); int &gety(void); void show(void); void move(int x1 = 0, int y1 = 0); protected: int x, y; private: void hide(void); }; class Spot : public Point // Определение класса ПЯТНО { public: Spot(int, int, int); void show(void); void hide(void); void move(int, int); void change(int); // изменить размер protected: int r; // радиус int vis; // признак видимости int tag; // признак сохранения видимого образа объекта в памяти spot *pspot; // указатель на область памяти для видимого образа }; // Определение функций - членов класса ТОЧКА Point::Point(int x1, int y1) { x = x1; y = y1; } int &Point::getx(void) { return x; } int &point::gety(void) { return y; } void Point::move(int x1, int y1) { hide(); x = x1; y = y1; show(); } // Определение функций - членов класса ПЯТНО Spot::Spot(int x1, int y1, int r1) : Point(x1, y1) { int size; // размер памяти для хранения изображения vis = 0; tag = 0; r = r1; pspot = (spot *) new char[size]; } Spot::~Spot() { hide(); tag = 0; delete pspot; } void Spot::show(void) { if (!tag) { // нарисовать и // запомнить изображение tag = 1; } else { // отобразить запомненное изображение } vis = 1; } void Spot::hide() { if (!vis) return; // стереть vis = 0; } // Создаются два объекта, показываются, // затем один перемещается, а другой // изменяет размеры int main(void) { Spot A(200, 50, 20); Spot B(500, 200, 30); A.show(); getch(); B.show(); getch(); A.move(50, 60); getch(); B.change(3); getch(); return 0; }
В этом примере в объекте Spot точка создается как безымянный объект класса Point.
К механизму виртуальных функций обращаются в тех случаях, когда в каждом производном классе требуется свой вариант некоторой компонентной функции. Классы включающие такие функции называются полиморфными и играют особую роль в ООП.
Рассмотрим как ведут себя при наследовании не виртуальные компонентные функции с одинаковыми именами, типами и сигнатурами параметров.
Пример.
class Base { public: void print(void) { cout << "\nbase"; } } class Dir : public Base { public: void print(void) { cout << "\ndir"; } }; int main(void) { Base B, *bp = &B; Dir D, *dp = &D; Base *p = &D; bp–>print(); // base dp–>print(); // dir p–>print(); // base return 0; }
В последнем случае вызывается функция print базового класса, хотя указатель p настроен на объект производного класса. Дело в том, что выбор нужной функции выполняется при компиляции программы и определяется типом указателя, а не его значением. Такой режим называется ранним или статическим связыванием.
Большую гибкость обеспечивает позднее (отложенное) или динамическое связывание, которое предоставляется механизмом виртуальных функций. Любая нестатическая функция базового класса может быть сделана виртуальной, для чего используется ключевое слово virtual.
Пример.
class Base { public: virtual void print(void) { cout << "\nbase"; } ... }; // и так далее – см. предыдущий пример.
В этом случае будет напечатано
base dir dir
Т.о. интерпретация каждого вызова виртуальной функции через указатель на базовый класс зависит от значения этого указателя, т.е. от типа объекта, для которого выполняется вызов.
Виртуальные функции - это функции, объявленные в базовом классе и переопределенные в производных классах. Иерархия классов, которая определена открытым наследованием, создает родственный набор пользовательских типов, на все объекты которых может указывать указатель базового класса. Выбор того, какую виртуальную функцию вызвать, будет зависеть от типа объекта, на который фактически (в момент выполнения программы) направлен указатель, а не от типа указателя.
Виртуальными могут быть только нестатические функции-члены.
Виртуальность наследуется. После того как функция определена как виртуальная, ее повторное определение в производном классе (с тем же самым прототипом) создает в этом классе новую виртуальную функцию, причем спецификатор virtual может не использоваться.
Конструкторы не могут быть виртуальными, в отличие от деструкторов. Практически каждый класс, имеющий виртуальную функцию, должен иметь виртуальный деструктор.
Если в производном классе ввести функцию с тем же именем и типом, но с другой сигнатурой параметров, то эта функция производного класса не будет виртуальной.
Виртуальная функция может быть дружественной в другом классе.
Механизм виртуального вызова может быть подавлен с помощью явного использования полного квалифицированного имени.
Абстрактным называется класс, в котором есть хотя бы одна чистая (пустая) виртуальная функция.
Чистой виртуальной называется компонентная функция, которая имеет следующее определение:
virtual тип имя_функции(список_формальных_параметров)= 0;
Чистая виртуальная функция ничего не делает и недоступна для вызовов. Ее назначение – служить основой для подменяющих ее функций в производных классах. Абстрактный класс может использоваться только в качестве базового для производных классов.
Механизм абстрактных классов разработан для представления общих понятий, которые в дальнейшем предполагается конкретизировать. При этом построение иерархии классов выполняется по следующей схеме. Во главе иерархии стоит абстрактный базовый класс. Он используется для наследования интерфейса. Производные классы будут конкретизировать и реализовать этот интерфейс. В абстрактном классе объявлены чистые виртуальные функции, которые по сути есть абстрактные методы.
Пример.
class Base { public: Base(); // конструктор по умолчанию Base(const Base &); // конструктор копирования virtual ~Base(); // виртуальный деструктор virtual void Show(void) = 0; // чистая виртуальная функция // другие чистые виртуальные функции protected: // защищенные члены класса private: // часто остается пустым, иначе будет мешать будущим разработкам }; class Derived : virtual public Base { public: Derived(); // конструктор по умолчанию Derived(const Derived &); // конструктор копирования Derived(параметры); // конструктор с параметрами virtual ~Derived(); // виртуальный деструктор void Show(void); // переопределенная виртуальная функция // другие переопределенные виртуальные функции Derived &operator =(const Derived &); // перегруженная операция присваивания // другие перегруженные операции protected: // используется вместо private, если ожидается наследование private: // используется для деталей реализации };
По сравнению с обычными классами абстрактные классы пользуются "ограниченными правам". А именно:
Объект абстрактного класса не может быть формальным параметром функции, однако формальным параметром может быть указатель на абстрактный класс. В этом случае появляется возможность передавать в вызываемую функцию в качестве фактического параметра значение указателя на производный объект, заменяя им указатель на абстрактный базовый класс. Таким образом мы получаем полиморфные объекты.
Пример.
Сформируем односвязный список, содержащий объекты разных классов, производных от одного абстрактного класса.
#include <iostream.h> #include <string.h> // Абстрактный класс class Person { public: Person() { strcpy(name, "NONAME"); age = 0; next = 0; }; Person(char *name, int age) { strcpy (this->name, name); this->age = age; next = 0; } virtual ~Person() {} virtual void Show(void) = 0; virtual void Input(void) = 0; protected: char name[20]; // имя int age; // возраст Person *next; //указатель на следующий объект в списке friend class List; // для того, чтобы в классе List было доступно поле next }; // Производный класс - СТУДЕНТ class Student : public Person { public: Student() { grade = 0; } Student(char *name, int age, float grade) : Person(name, age) { this->grade = grade; } void Show(void) { cout << "name=" << name << "age=" << age << "grade=" << grade << endl; } void Input(void) { cout << "name="; cin >> name; cout << "age="; cin >> age; cout << "grade="; cin >> grade; } protected: float grade; // рейтинг }; // Производный класс - Преподаватель class Teacher : public Person { public: Teacher() { work = 0; } Teacher(char *name, int age, int work) : Person(name, age) { this->work = work; } void Show(void) { cout << "name=" << name << "age=" << age << "work=" << work << endl; } void Input(void) { cout << "name="; cin >> name; cout << "age="; cin >> age; cout << "work="; cin >> work; } protected: int work; // стаж }; // Класс СПИСОК class List { public: List() { begin = 0; } ~List(); void Insert(Person *); void Show(void); private: Person *begin; }; List::~List() { Person *r; while (begin) { r = begin; begin = begin->next; delete r; } } void List::Insert(Person *p) { Person *r; r = begin; begin = p; p->next = r; } void List::Show(void) { Person *r; r = begin; while (r) { r->Show(); r = r->next; } } int main(void) { List list; Student *ps; Teacher *pt; ps = new Student("Иванов", 21, 50.5); list.Insert(ps); pt = new Teacher("Котов", 34, 10); list.Insert(pt); ps = new Student; ps->Input(); list.Insert(ps); pt = new Teacher; pt->Input(); list.Insert(pt); list.Show(); return 0; }Предыдущая Оглавление Следующая