Читайте также:
|
|
Лекц.14. Наследование
Содержание. Наследование и виртуальные функции. Наследование классов. Примеры. Права доступа в базовых и производных классах, примеры. Применение наследования. Использование объектов производного класса в качестве объектов базового класса. Явное преобразование типов при наследовании. Информация о типе во время выполнения (RTTI). Конструкторы и деструкторы и перегрузка операции присваивания в производном классе.
Наследование классов
Наследование (inheritance) – это механизм получения нового класса из существующего. Существующий класс может быть дополнен или изменен для создания производного класса. Кроме того, что наследование – это способ повторного использования кода, оно позволяет создавать иерархии родственных типов (классов), которые имеют совместно используемый интерфейс. Класс, от которого порождается производный класс, называется базовым (или родительским) классом.
Производный класс наследует все члены, за исключением конструкторов, деструкторов и перегруженной операции =, из базового класса. Эти унаследованные члены образуют ядро, называемое базовой частью, объекта производного класса. В производном классе могут быть определены дополнительные члены (данные и/или функции-члены). Наследованные члены не рассматриваются по своему статусу как эквивалентные дополнительным членам. Они не перемещаются в производный класс, а остаются в базовом. Закрытые члены базового класса, хотя и наследуются как закрытые в производном классе, не являются доступными для функций-членов, определенных в производном классе. Эти закрытые члены базового класса достижимы из производного только посредством открытых функций-членов базового класса, которые теперь становятся открытыми методами производного класса.
Несколько общих замечаний. В контексте объектно-ориентированного программирования объекты взаимодействуют между собой и с другими частями программы с помощью сообщений. В каждом сообщении объекту передается некоторая информация и в ответ на сообщение объект выполняет некоторое действие, предусмотренное набором функций-компонент того класса, которому объект принадлежит. Таким действием может быть или изменение внутреннего состояния объекта, или передача сообщения другому объекту. Как мы уже упоминали, при использовании наследования, объекты разных классов и сами классы вступают в отношение наследования, при котором формируется иерархия объектов, соответствующая заранее предусмотренной (при проектировании) иерархии классов. Понятно, что должны иметься некоторые правила, которым подчинена коммуникация (диспетчеризация) сообщений в такой иерархии. В частности, сообщения, обработку которых не могут выполнить методы производного класса, автоматически передаются в базовый класс. Если для обработки сообщения нужны данные, отсутствующие в производном классе, то производится попытка отыскать их автоматически и незаметно для программиста в базовом классе. Конечным “судьей” для необработанных сообщений, как правило, будет операционная система.
При наследовании некоторые имена методов и (или) компонентных данных базового класса могут быть по-новому определены в производном классе. В этом случае соответствующие компоненты базового класса становятся недоступными из производного класса. Для доступа из производного класса к компонентам базового, имена которых повторно определены в производном, используется операция “::” разрешения (указания, уточнения) области видимости. Любой производный класс, в свою очередь, может становиться базовым для других классов, формируя тем самым направленный граф иерархии классов и объектов. В иерархии производный объект наследует разрешенные для наследования компоненты всех базовых объектов. Иными словами, у такого объекта имеется возможность доступа к методам и данным всех своих базовых классов. Наследование в иерархии может отображаться и в виде дерева, и виде общего направленного ациклического графа, поскольку при наследовании, кроме простого, допускается и множественное наследование – возможность для некоторого класса наследовать компоненты нескольких никак не связанных между собой базовых классов.
И предварительное замечание о понятии виртуальной функции.
Перегруженные функции-члены класса вызываются в соответствии с их сигнатурой, причем неявно предполагается наличие параметра, соответствующего объекту класса (об этом - чуть ниже). Это позволяет компилятору “знать” и непосредственно выбирать соответствующий член класса (на этапе компиляции). Если же мы хотим выбирать функцию среди функций базовых и производных классов динамически, т.е., во время выполнения программы, то нам нужен какой-то иной механизм. Такой механизм в C++ существует и он называется механизмом виртуальных функций. Ключевое слово virtual – это функциональный спецификатор (модификатор), формально обеспечивающий такой механизм. Комбинация виртуальных функций и наследования представляет собой форму чистого полиморфизма и является наиболее общим и гибким способом конструирования программного обеспечения и, естественно, “краеугольным камнем” объектно-ориентированного программирования.
Виртуальная функция является обычным выполняемым кодом. Семантика ее вызова такая же, как и у других функций. В производном классе она может быть переопределена, и функциональный прототип порожденной (“производной”) функции должен иметь соответствие сигнатуры и типа возврата. Отличие состоит в том, что выбор того, какое определение функции вызвать для виртуальной функции, производится динамически. Типичный случай – когда основной (базовый) класс имеет виртуальные функции, а порожденный (производный) класс имеет свои версии этих функций. Указатель на базовый класс может указывать или на объект базового класса, или на объект производного класса.
Выбранная функция-член зависит от класса, на который имеется указание, но не от типа указателя. При отсутствии члена порожденного типа по умолчанию используется виртуальная функция основного класса. Если функция объявлена как virtual, то это свойство передается всем переопределениям в производных классах. В производном классе нет необходимости использовать функциональный модификатор virtual (т.е., явно указывать).
Резюмируем вышеприведенные, довольно размытые рассуждения, посмотрим на синтаксис и вернемся к основному изложению. Итак, класс может быть наследником пустого или большего множества базовых классов. Класс, имеющий по меньшей мере один базовый класс называется производным. Производный класс наследует все, разрешенные к наследованию, данные-члены и функции-члены всех своих базовых классов и всех базовых классов последних, и т.д.. Непосредственные базовые классы производного класса называются прямыми базовыми классами (прямыми родителями, а производные – прямыми потомками). Базовые классы прямых базовых классов называются косвенными или непрямыми базовыми классами рассматриваемого класса. О полном наборе прямых и косвенных базовых классов иногда говорят как о классах прародителях (или предках).
Класс может быть порожден их любого числа базовых классов. Синтаксически, имена базовых классов следуют (перечисляются) после символа “двоеточие”,:, и отделяются (образуя список) запятыми. Каждому имени класса может быть предпослан спецификатор доступа. Один и тот же класс не может быть присутствовать в этом списке более одного раза в качестве прямого базового класса, но он может появляться более одного раза во всем графе наследования. Например, derived 3 в следующем коде имеет base2 в качестве предка дважды в дереве наследования, один раз как прямой базовый класс и один раз как косвенный базовый класс (через derived 2 ):
class base1 {... }; class derived1: public base1 {... }; class base2 {... }; class derived2: public derived1, public base2 {... }; class derived3: protected derived2, private base2 {... };Определения базовых классов должны предшествовать их использованию в качестве базовых.
Как уже понятно, одиночное наследование записывается с помощью следующей формы:
class имя_класса: [ public | protected | private ] имя_базового_класса
{
объявления членов
};
Замечание. Базовые классы инициализируются (конструкторами) в порядке перечисления, а их члены - в порядке объявления.
Ключевое слово class всегда можно заменить на struct, если предполагается, что все члены открыты. Ключевые слова public, protected и private используются для указания того, насколько члены базового класса будут доступны для производного. Ключевое слово protected введено для того, чтобы сохранить сокрытие данных для членов, которые должны быть доступны из производного класса, но в других случаях действуют как закрытые (private). Это – промежуточный вид доступа между private и public. По умолчанию для классов используется спецификатор (ключ) доступа private, а для структур - public. Повторим, что ключи доступа (терминология Т.Павловской), т.е. те спецификаторы доступа, которые мы указываем при определении производных классов при наследовании, управляют уровнем доступа к унаследованным членам и в этом контексте говорят либо об открытом наследовании, либо о защищенном и закрытом наследовании. Закрытое наследование делает все наследованные члены закрытыми в производном классе, защищенное наследование уменьшает доступность открытых членов базового класса до уровня защищенных в производном. Открытое наследование оставляет уровни доступности в производном классе без изменения. Ниже мы увидим на примерах, как это действует. А в виде таблицы это выглядит так:
Ключ доступа | Спецификатор базового класса | Доступ в производном классе | Комментарий |
private | private protected public | private private private | все private |
protected | private protected public | private protected protected | все protected, кроме private |
public | private protected public | private protected public | не изменяется |
Как видно из таблицы, private элементы базового класса в производном классе недоступны вне зависимости от ключа. Обращение к ним может осуществляться только через методы базового класса. Открытое наследование сохраняет статус доступа всех элементов базового класса.
Производный класс может обращаться к членам, которые он наследует от класса-предка, при условии, что эти члены не являются закрытыми. При поиске имени в области видимости класса, компилятор сначала просматривает сам класс, затем ищет в прямых базовых классах, затем в их прямых базовых классах и т. д.. Заметим, что механика просмотра имен в C++, так же как и поиск подходящей перегруженной функции, достаточно сложна и мы не будем здесь это рассматривать (см. например, стандарт языка).
Продолжим наше основное изложение. В производном классе можно определять функции-члены с тем же самым прототипом, что и функция в базовом классе (см. в замечаниях выше…). Такая вновь определенная (переопределенная) функция подавляет, замещает, аннулирует (overrides) функцию базового класса (для производного). Такое переопределение (оно не имеет отношения к тому “переопределению”, о котором сообщает компилятор, обнаружив дважды описанную переменную…) можно считать специальным случаем перегрузки функций (но не все с этим согласны, например, Айра Пол считает, что замещение ничего общего с перегрузкой не имеет, и, в некотором смысле прав…). Хотя функция, определенная в производном классе и переопределяемая функция в базовом классе, в явном виде, очевидно, могут иметь одну и ту же сигнатуру, в действительности, списки параметров различны. Функция в базовом классе имеет неявный первый параметр, который есть указатель (указатель "this") на базовый класс, тогда как функция в производном классе имеет в качестве неявного первого параметра указатель на производный класс. Когда эта функция вызывается объектом (для объекта) базового класса, или через указатель на базовый класс, то указатель на базовый класс (“this”) передается этой функции. Следовательно, вызывается версия функции базового класса. Если эта функция вызывается объектом производного класса, или посредством указателя на объект производного класса, то будет использована версия функции производного класса. Но поскольку версия функции базового класса также унаследована производным классом, то объект производного класса все-таки имеет возможность обратиться к базовой версии функции, однако используя при этом в явном виде имя базового класса (например, 'Base::') перед именем самой функции.
Следующий пример иллюстрирует эту ситуацию:
Пример 14-1.
#include <iostream.h>
class Base
{
int b_number;
public:
Base();
Base(int i): b_number (i) { }
int get_number() {return b_number;}
void print() {cout << b_number << endl;}
};
class Derived: public Base
{
int d_number;
public:
// конструктор, инициализатор используется для инициализации базовой части объекта // типа Derived.
Derived(int i, int j): Base(i), d_number(j) { };
// новая функция-член, которая переопределяет функцию print() класса Base
void print()
{
cout << get_number() << " ";
// доступ к b_number посредством get_number()
cout << d_number << endl;
}
};
int main()
{
Base a(2);
Derived b(3, 4);
cout << "a is ";
a.print(); // print() в Base
cout << "b is ";
b.print(); // print() в Derived
cout << "base part of b is ";
b.Base::print(); // print() в Base
return 0;
}
В этом примере закрытое поле b_number наследуется классом Derived, но функция print(), определенная в производном классе не имеет доступа к нему. Она должна использовать открытую функцию get_number(), которая унаследована из класса Base и стала открытой функцией-членом класса Derived.
Другой способ позволить методам, определенным в производном классе, иметь доступ к наследуемым членам, которые при этом не видны из остальных частей программы, состоит в использовании спецификатора доступа protected(защищенный). Защищенный член класса A достижим со стороны членов и друзей классов, производных от A, но такой член еще скрыт от остальной части программы.
“Исправим” классы предыдущего примера следующим образом:
Пример 14-2.
#include <iostream.h>
class Base
{
protected:
int b_number;
public:
Base();
Base(int i): b_number (i) { }
void print() {cout << b_number << endl;}
};
class Derived: public Base
{
int d_number;
public:
// конструктор
Derived(int i, int j): Base(i), d_number(j) { };
// новый метод, который переопределяет ф-ию print() в Base,
// ф-ия print() в Base еще доступна через Base::print()
void print()
{
cout << b_number << " "; // доступ к b_number напрямую
cout << d_number << endl;
}
};
В этом примере функция print() в классе Derived может обратиться непосредственно к b_number.
Заметим, что защищенные члены базового класса достижимы в определении производного класса только в качестве унаследованных членов объекта производного класса. Если член-объект базового класса или указатель на объект базового класса определен в определении производного класса, то защищенные члены базового класса не являются доступными посредством этого объекта или указателя. Если, например, в нашем классе Derive была бы определена функция-член такого вида:
int create(Base a) {b_number = a.b_number;},
то тогда в левой части оператора присваивания все правильно (OK), а вот справа – нет. Защищенный член b_number не может быть достигнут посредством использования объекта a класса Base. Формально, это следует из того, что спецификатор protected разрешает прямой доступ к компонентам базового класса только из методов производного класса (ну и, конечно, методов самого базового класса), а в остальном этот спецификатор ведет себя как private, что соответствует тому, что к такому полю нельзя обратиться через имя объекта собственного (т.е., содержащего это поле) класса.
Если вспомнить про табличку трансформации прав доступа при наследовании, то мы увидим, что уровень доступа к членам базового класса не “повышается” в производном классе (“повышение” – это переход от меньшей доступности к большей): закрытый член базового класса всегда остается закрытым членом производного; защищенный член базового класса может быть или защищенным, или закрытым в производном классе; а открытый член базового класса может быть закрытым, защищенным или открытым в производном. Права доступа защищенных и открытых членов базового класса могут быть только “урезаны” (ограничены) в зависимости от использованных ключей доступа при определении производного класса (что, собственно, и отражено в таблице). Для иллюстрации рассмотрим пример (длинный, но полезный):
Пример 14-3. Уровни доступа
#include <iostream.h>
class Base
{
int priv;
protected:
int prot;
int get_priv() {return priv;}
public:
int publ;
Base();
Base(int a, int b, int c): priv(a), prot(b), publ(c) { }
int get_prot() {return prot;}
int get_publ() {return publ;}
};
class Derived1: Base // закрытое наследование
{
public:
Derived1 (int a, int b, int c): Base(a, b, c) { }
int get1_priv() {return get_priv();}
// priv не доступен напрямую
int get1_prot() {return prot;}
int get1_publ() {return publ;}
};
class Leaf1: public Derived1
{
public:
Leaf1(int a, int b, int c): Derived1(a, b, c) { }
void print()
{
cout << "Leaf1 members: " << get1_priv() << " "
// << get_priv() // не доступен
<< get1_prot() << " "
// << get_prot() // не доступен
// << publ // не доступен
<< get1_publ() << endl;
} // данные-члены не доступны. get-функции в классе Base не доступны
};
class Derived2: protected Base // защищенное наследование
{
public:
Derived2 (int a, int b, int c): Base(a, b, c) { }
};
class Leaf2: public Derived2
{
public:
Leaf2(int a, int b, int c): Derived2(a, b, c) { }
void print()
{
cout << "Leaf2 members: " << get_priv() << " "
// << priv // не доступен
<< prot << " "
<< publ << endl;
} // открытые и защищенные поля доступны. get-функции в классе Base доступны.
};
class Derived3: public Base // открытое наследование
{
public:
Derived3 (int a, int b, int c): Base(a, b, c) { }
};
class Leaf3: public Derived3
{
public:
Leaf3(int a, int b, int c): Derived3(a, b, c) { }
void print()
{
cout << "Leaf3 members: " << get_priv() << " "
<< prot << " "
<< publ << endl;
} // открытые и защищенные поля доступны. get-функции в классе Base доступны.
};
int main()
{
Derived1 d1(1, 2, 3);
Derived2 d2(4, 5, 6);
Derived3 d3(7, 8, 9);
// cout << d1.publ; // нет доступа
// cout << d1.get_priv(); // нет доступа
// cout << d2.publ; // нет доступа
// cout << d2.get_priv(); // нет доступа
cout << d3.publ; // OK
cout << d3.get_priv(); // OK
Leaf1 lf1(1, 2, 3);
Leaf2 lf2(4, 5, 6);
Leaf3 lf3(7, 8, 9);
// cout << lf1.publ << endl; // нет доступа
// cout << lf2.publ << endl; // нет доступа
cout << lf3.publ << endl; // OK
return 0;
}
Уровень доступа члена базового класса в (из) производном (-ого), который определяется ключем доступа может быть “вновь переписан”. Например, когда используется закрытое наследование, то защищенный член, скажем, с именем number базового класса A станет закрытым в производном классе B. Но мы можем указать для него новый статус доступа, переобъявив этот член в защищенной секции производного класса следующим образом:
protected:
A::number;
Этот метод может использоваться для восстановления уровня доступности члена базового класса, но не изменения этого статуса. Например, " A::number " не может использоваться для изменения уровня доступа члена number на private или public.
Дата добавления: 2015-11-16; просмотров: 93 | Нарушение авторских прав
<== предыдущая страница | | | следующая страница ==> |
Иерархии исключений | | | Применение наследования |