
- •1. Дополнения к с
- •1.1. Комментарии
- •1.2. Ключевые слова
- •1.3. Константы
- •1.4. Блочные объявления
- •1.5. Ссылки
- •1.6. Новая роль имён перечислений, структур и объединений
- •1.7. Распределение памяти
- •1.8. Встраиваемые функции
- •1.9. Перегрузка функций
- •1.10. Задание для параметров функции значений по умолчанию
- •1.11. Дополнительные операции для доступа к данным
- •1.12. Предопределённые потоки ввода-вывода
- •2.1. Инкапсуляция
- •2.2. Разграничение доступа (скрытие данных и методов)
- •2.3. Друзья классов
- •2.4. Конструкторы и деструкторы
- •2.5. Конструктор по умолчанию
- •2.6. Конструктор копирования
- •2.7. Несколько слов о деструкторах
- •2.8. Перегрузка операций
- •3.1. Наследование
- •3.2. Виртуальные функции –полиморфизм
- •3.3. Шаблоны
3.1. Наследование
Ключевым моментом объектно-ориентированного программирования, нашедшим свое воплощение в С++, является возможность создания иерархической структуры классов, которая базируется на наследовании – некоторый класс может передавать свои компоненты другому классу. При этом первый класс называется базовым, а второй – производным классом (или подклассом). Имя базового класса указывается непосредственно после имени производного, до открывающей фигурной скобки:
class BaseClass
{
// Компоненты базового класса
};
class SuibClass: public BaseClass
{
// Компоненты производного класса
};
В данном случае производный класс SubClass наследует все компоненты базового, а также содержит любые, определенные непосредственно в нем самом, компоненты.
Обратите внимание на ключевое слово public перед именем базового класса, которое определяет тип наследования – в данном случае общедоступный. Можно также задавать частный (private) тип наследования. Разные типы влияют на доступность компонентов базового класса в производном. Чтобы разобраться в этом вопросе, рассмотрим пример.
Пример 3.1
class BaseClass
{
public:
// Общедоступные компоненты базового класса
int nPub1Base;
void funcBase( );
protected:
// Защищённые компоненты базового класса
int nProtBase;
private:
// Частные компоненты базового класса
int nPrivBase;
};
// class SubClass: public BaseClass //public-наследование
class SubClass: private BaseClass //private-наследование
{
public:
// Общедоступные компоненты производного класса
int nPublSub;
void funcSub( );
protected:
// Защищённые компоненты производного класса
int nProtSub;
private:
// Частные компоненты производного класса
int nPrivSub;
};
void main (int argc, char *argv[])
{
BaseClass clBase;
SubClass clSub;
clBase,nPublBase = 7;
clBase.funcBase( );
clSub.nPublBase = 7; //ОШИБКА! -недоступно
clSub.funcBase( ); //ОШИБКА! -недоступно
clSub.nPublSub = 77;
clSub.funcSub( );
}
void BaseClass :: funcBase( )
{
//Имеется доступ к компонентам своего класса BaseClass
nPublBase = 1;
nProtBase = 2;
nPrivBase = 3;
}
void SubClass :: funcSub( )
{
//Имеется доступ только к public и protected компонентам базового класса, которые для
//производного класса являются private компонентами, доступными только из его методов.
nPublBase = 1;
funcBase( );
nProtBase = 2;
nPrivBase = 3; //ОШИБКА! - недоступно
//Имеется доступ к любым компонентам своего класса
nPublSub = 4;
nProtSub = 5;
nPrivSub = 6;
}
Что интересного в этом примере? Прежде всего обратите внимание, что компонент базового класса, наследуемый как public, сохраняет тот же тип доступа.
Несколько иначе дело обстоит при типе наследования private. Как вы, конечно же, помните, частный компонент класса доступен только другим компонентам и друзьям класса. Поэтому вполне логично, что такие частные компоненты недоступны в производном классе. Ведь в противном случае программист имел бы возможность получить доступ к таким компонентам, реализовав в производном классе необходимые методы, а это противоречит самой идеологии инкапсуляции.
Однако часто необходимо из производного класса обеспечить доступ к некоторым компонентам базового. Чтобы не пришлось объявлять такие компоненты общедоступными (public), т. е. полностью «раскрывать» их, в языке С++ и введена специальная категория доступности – защищенная (protected). Компоненты, которые имеют такой уровень доступа, заблокированы для всех частей программы, за исключением компонентов самого класса и производного от него.
Осталось только привести сводку правил наследования доступа (табл. 3.1).
Таблица 3.1. Правила наследования доступа
Тип наследования доступа |
Доступность в базовом классе |
Доступность компонентов базового класса в производном |
public |
public |
public |
protected |
protected | |
private |
private | |
private |
public |
private |
protected |
private | |
private |
недоступны |
По умолчанию установлен тип наследования доступа private.
Чтобы закончить с вопросом правил наследования доступа, осталось сказать, что в производном классе можно изменять доступность отдельных компонентов базового класса. Например, если в объявление производногокласса BaseClass из предыдущего примера внести следующее дополнение:
class SubClass: BaseClass // private-наследованиепоумолчанию
{
public:
// Общедоступные компоненты производного класса
BaseClass::nPublBase;; // Конкретно объявляем как public
int nPublSub;
void funcSub( );
. . .
};
то из любого места программы становится возможен доступ к этому компоненту базового класса:
clSub.nPublBase = 7;
Производный класс сам может служить базовым, т. е. от него можно образовывать новые классы. В этом случае полученный класс включит в себя элементы всех его базовых классов. От одного базового класса можно образовывать несколько производных. Таким образом, возможна древовидная структура иерархии классов (рис. 3.1).
Схема размещения в памяти объекта простого производного класса представлена на рис. 3.2.
Во всех рассмотренных примерах производный класс был образован от одною базового класса – так называемое единичное наследование. Однако механизм наследования в С++ более развит и включает наряду с единичным имножественное наследование, при котором производный класс образуется сразу от нескольких базовых, что позволяет создавать достаточно сложные иерархии классов (рис. 3.3).
Пример 3.2
class Base1
{
// Компоненты первого базового класса
};
class Base2
{
// Компоненты второго базового класса
};
class Derived : public Base1, private Base2
{
// Компоненты производного класса
};
Рис. 3.1. Древовидная иерархия классов
Рис. 3.2. Схема размещения в памяти объекта простого производного класса
Рис. 3.3. Сетевая иерархия классов
В объявлении класса может быть записано любое количество базовых классов, перечисленных через запятую. Порядок, в котором перечисляются базовые классы, произвольный. Он влияет только на последовательность вызова конструкторов, которая является следующей:
Сначала вызываются конструкторы всех виртуальных базовых классов; если их несколько, то конструкторы вызываются в порядке их наследования (виртуальные классы мы рассмотрим ниже).
Затем конструкторы не виртуальных базовых классов в порядке их наследования.
И, наконец, конструкторы всех компонентных классов.
Как обычно, последовательность вызова деструкторов является обратной относительно последовательности вызовов конструкторов.
Поскольку множественное наследование является более сложной конструкцией по сравнению с единичным, то вполне очевидно, что при этом возникают и некоторые проблемы.
В случае множественного наследования вполне вероятно, что один и тот же класс может унаследовать несколько объектов с одним и тем же именем:
class Base1
{
int nObj;
// Остальные компоненты первого базового класса
};
class Base2
{
int nObj;
// Остальные компоненты второго базового класса
};
class Derived: public Base1, public Base2
{
// Компоненты производного базового класса
} clDer;
В данном случае мы имеем два объекта clDer.nObj с одним и тем же именем, а значит, обращаться к ним просто по имени нельзя. В этом случае выход заключается в использовании оператора разрешения контекста и полных имен объектов:
clDer.BaseClass1::nObj++;
clDer.BaseClass1::nObj++;
Такое же решение проблемы необходимо использовать в случае наследования через промежуточный класс:
Пример 3.3
class BaseClass
{
int nBaseObj;
};
class FirstBaseClass: public BaseClass
{
int nFirstObj;
};
class SecondBaseClass: public BaseClass
{
int nSecondObj;
};
class DerivedClass: public FirstBaseClass, public SecondBaseClass
{
long nDerivedObj;
// Остальные компоненты производного класса
} clDer;
. . .
clDer.FirstBaseClass::nObj++;
clDer.SecondBaseClass::nObj++;
При использовании наследования через промежуточный класс возникает другая проблема – при приведении типа указателя одного класса к другому может возникнуть неоднозначность:
DerivedClass clDerived;
DerivedClass *ptrDer = &clDerived;
BaseClass *ptrBase;
ptrBase = (Base *) ptrDer; // ОШИБКА! – неоднозначность приведения типа
Проблема решается выполнением явного приведения типа к классу, для которого неоднозначностей не возникает
ptrBase = (Base*) (FirstBase*)ptrDer; // Теперь–правильно
Размещение объектов производных классов в памяти в случае простого множественного наследования (пример 3.2) представлено на рис. 3.4.
Рис. 3.4. Размещение объекта в памяти при простом множественном наследовании
Обратите внимание, что если мы передадим Derived* функции, ожидающей Base1, то всё будет в порядке – неоднозначностей не возникает. Однако при вызове функции, ожидающей поступления Base2, возникнет проблема, и необходимо выполнять явное приведение типа.
Посмотрим, что же получается в случае наследования через промежуточный класс, описанный в примере 3.3. Проблема, которая возникает в этом случае, наглядно продемонстрирована на рис. 3.5 – наличие дублирования компонентов «самого» базового класса BaseClass.
Рис. 3.5. Схема размещения объекта при наследовании через промежуточный класс
Некоторые программисты могут и не посчитать это проблемой и сознательно использовать обе (или больше) копии. Однако в большинстве случаев такое положение все-таки нежелательно. Чтобы в таких случаях разрешить наследование только одной копии BaseClass, в язык С++ включено ключевое слово virtual, с помощью которого реализуется так называемое виртуальное наследование:
class BaseClass
{
int nBaseObj;
};
class FirstBaseClass: virtual public BaseClass
{
int nFirstObj;
};
class SecondBaseClass: virtual public BaseClass
{
int nSecondObj;
};
class DerivedClass: public FirstBaseClass, public SecondBaseClass
{
long nDerivedObj;
// Остальные компоненты производного класса
} clDer;
Такое дополнение (ключевым словом virtual) изменяет схему размещения объекта производного класса DerivedClass, как это показано на рис. 3.6.
Теперь
существует только одна копия BaseClass, и
нет никакой неоднозначности и
избыточности.
Рис. 3.6. Схема размещения объекта при виртуальном множественном наследовании