
- •6Vpj7-h3cxh-hbtpt-x4t74-3yvy7
- •Оглавление
- •Предисловие
- •Введение
- •1.1. Понятие класса и объекта. Инкапсуляция
- •1.2. Определение классов. Компоненты. Доступность
- •Class_key /*class_id*/ { /*members_list*/ };
- •Value_type class_id::function_id(parameters) {statements}
- •CPoint point1(100,70); // локальный объект
- •Static cPoint point3(50,120); // статический объект
- •Class_id(parameters) /*:initializer_list*/ {/*statements*/}
- •CString(const char *);
- •Delete[] __thematrix;
- •1.4. Обращение к компонентам объектов
- •1.5. Статические и нестатические компоненты классов
- •1.7. Указатель this
- •В опросы для самопроверки
- •2. Механизм наследования. Полиморфизм
- •2.1. Формы наследования. Базовые и производные классы
- •Class_key class_id: inheritance_specifier base_class_id {member_list};
- •2.3. Абстрактные классы
- •2.4. Множественное наследование и виртуальные классы
- •2.5. Преобразование динамических типов. Динамическая идентификация типов
- •Catch ( std::bad_cast & ) { // обработка исключения
- •Return 0;
- •Вопросы для самопроверки
- •3. Дружественные функции и классы
- •3.1. Дружественные функции
- •3.2. Дружественные классы
- •Вопросы для самопроверки
- •4. Механизм вложения
- •4.1. Вложенные классы
- •4.2. Локальные классы
- •Вопросы для самопроверки
- •5. Объектная модель и шаблоны
- •5.1. Определение, описание и инстанцирование шаблонов
- •::Function_id(function_parameter_list) { statements }
- •5.2. Параметры и аргументы шаблонов
- •Class identifier typename identifier
- •// Key, Data – параметры-типы (типы ключа и данных отображения)
- •// Container – контейнер, где содержится информация отображения class сMap {
- •Class MyTemplate
- •Int array[10]; struct Structure { int m; static int sm; } str;
- •5.3. Шаблоны компонентных функций
- •Value_type function_template_id(function_parameter_list) { statements }
- •::Function_template_id(function_parameter_list) { statements }
- •5.4. Специализация шаблонов
- •Вопросы для самопроверки
- •6. Перегрузка операций
- •Value_type operator @ (parameter_list);
- •Value_type operator @ (parameter_list) { statements }
- •Return fail();
- •6.3. Перегрузка бинарных операций
- •Value_type operator @ (parameter); // компонентная функция
- •Value_type operator @ (parameter, parameter); // глобальная функция friend value_type operator @ (parameter, parameter); // дружественная функция
- •Return *this;
- •Return *this;
- •/* Присваиваем собственные данные класса d */
- •6.4. Перегрузка операций управления памятью
- •Typedef void (*new_handler) ();
- •Extern new_handler set_new_handler( new_handler new_p );
- •Void operator delete(void * memory) {
- •... // Специальная обработка пользователя ::operator delete(memory); // освободить память
- •Вопросы для самопроверки
- •7. Механизм исключений
- •Throw expression
- •7.3. Специальные средства поддержки механизма исключений
- •Unexpected_function set_unexpected(unexpected_function func_name);
- •Typedef void (* unexpected_function) ();
- •Extern char * __throwExceptionName; extern char * __throwFileName; extern unsigned __throwLineNumber;
- •Вопросы для самопроверки
- •8. Подсчет ссылок
- •8.1. Назначение механизма подсчета ссылок
- •8.2. Контекстно-независимая модель счетчика ссылок
- •8.4. Внедрение подсчета ссылок в существующий класс
- •Вопросы для самопроверки
- •9. Стандартная библиотека шаблонов (stl)
- •9.1. Назначение и архитектура stl
- •9.2. Последовательные контейнеры
- •Class vector {
- •// Определение итераторов
- •Sort(first,last); // сортировка вектора в диапазоне итераторов
- •Ifstream ifile ("example.In"); ofstream ofile ("example.Out");
- •OutputIterator copy(
- •InputIterator first, InputIterator last, OutputIterator result );
- •// Заполнение списка
- •Operator- (int)
- •Operator- (random access iterator) operator[] (int)
- •InputIterator find(InputIterator first, InputIterator last, const t & value);
- •InputIterator find(InputIterator first, InputIterator last, const t & value)
- •Return first;
- •OutputIterator copy (InputIterator first, InputIterator last, OutputIterator result)
- •Return result;
- •OutputIterator transform (InputIterator first, InputIterator last, OutputIterator result, UnaryOperation op)
- •Return result;
- •Void sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp)
- •__Quick_sort_loop(first, last, comp); __final_insertion_sort(first, last, comp);
- •T accumulate(InputIterator first, InputIterator last, t init, Function f);
- •V.Push_back(2); V.Push_back(5);
- •9.5. Функторы
- •T operator()(const t & X) const { return -X; }
- •9.7. Адаптеры
- •S1.Push(1); s1.Push(5);
- •// Записать в вектор числа 1 2 3 4
- •// Сортировать по неубыванию
- •// Записать в вектор числа 4 6 10 3 13 2
- •Вопросы для самопроверки
- •Заключение
- •Библиографический Список
- •6Vpj7-h3cxh-hbtpt-x4t74-3yvy7
2.4. Множественное наследование и виртуальные классы
При множественном наследовании производный класс может иметь сразу несколько непосредственных базовых классов. Необходимость в таких классах возникает тогда, когда их свойства сочетаются сразу в нескольких других классах. Примером может быть класс CStudentReader, инкапсулирующий свойства и поведение студентов-читателей городской библиотеки. Очевидно, что каждый объект этого класса входит как в число студентов вообще (базовый класс CStudent), так и в число всех читателей библиотеки, некоторые из которых не являются студентами (базовый класс CReader). Еще один пример. Стандартная библиотека С++ включает шаблонный класс basic_iostream, который описывает двунаправленные потоки ввода-вывода. Он является производным сразу от двух шаблонов: basic_istream и basic_ostream. Первый отражает свойства входных потоков, а второй – свойства выходных потоков. Ясно, что basic_iostream – разновидность как basic_istream, так и basic_ostream.
Для того чтобы определить класс, производный от нескольких классов, в формате его определения (в списке наследования) необходимо явно перечислить имена всех базовых классов. При этом к каждому из них может применяться своя схема наследования: закрытое, защищенное или открытое. Один и тот же класс не может появляться в списке наследования более одного раза. Порядок перечисления базовых классов не играет роли.
Ниже для примера дан эскиз определения классов CStudent, CReader и CStudentReader:
class CStudent // первый базовый класс – «студент»
{
public:
CStudent( /* параметры конструктора */ )
{ /* тело конструктора */ }
...
void Study(short term); /* учеба студента, семестр term */
bool PassExam(const string & subject); /* экзамен, предмет subject */
// другие компоненты
...
};
class CReader // второй базовый класс – «читатель»
{
public:
CReader( /* параметры конструктора */ )
{ /* тело конструктора */ }
...
bool OrderBook(const string & title); /* заказ книги title */
// другие компоненты
};
class CReaderStudent: public CStudent, public CReader
// «студент-читатель»
{
public:
CReaderStudent( /* параметры конструктора */ ):
CStudent( /* ... */ ), CReader( /* ... */ )
{ /* тело конструктора */ }
// другие компоненты
...
};
Г
рафически
иерархию из нашего примера можно
представить так, как показано на рисунке
1, где S
– класс
CStudent,
R
– класс CReader,
а D
– производный класс CStudentReader;
стрелки показывают отношение «наследует
от».
Объект всякого класса, входящего в классовую иерархию, содержит в себе экземпляры каждого базового класса (как прямого, так и непрямого). Например, объект класса CStudentReader будет включать как часть объект класса CStudent и объект класса CReader. Графически это можно представить следующей диаграммой(рис. 2).
Рис. 2
Особенностью множественного наследования в С++ является то, что любой класс может использоваться как косвенный базовый класс многократно; возможна также ситуация, когда класс одновременно является и прямым, и косвенным базовым классом. В этом случае объект производного класса будет включать сразу несколько объектов одного и того же базового класса15. Эта ситуация обусловливает одну из главных проблем множественного наследования – потенциальную неоднозначность. Она заключается в возможном конфликте имен компонент, унаследованных от базовых классов. Например, в приведенных выше классах CStudent и CReader может быть определена функция GetName(), возвращающая имя студента / имя читателя. Эта функция будет унаследована классом CStudentReader фактически дважды: первый раз из класса CStudent, второй раз из класса CReader. В результате ее не получится вызвать для объекта класса CStudentReader без дополнительной квалификации:
CStudentReader * pSR = &StudentReader;
StudentReader –> GetName(); // неоднозначность
StudentReader –> CStudent::GetName(); // нормально
StudentReader –> CReader::GetName(); // тоже нормально
Вызов с квалификацией выглядит менее читаемым и, что более важно, его применение создает ряд неудобств. В частности, если функция GetName виртуальная, а класс CStudentReader переопределяет ее, вызов с квалификацией сделает функцию невиртуальной.
К
онфликт
имен компонент независимых классов
происходит не так часто, поэтому
описанная проблема, возможно, и не так
существенна. Однако, когда один и тот
же класс выступает как базовый для
другого класса несколько раз, эта
проблема возникнет с вероятностью
100%. Классический случай – иерархия,
представленная на рисунке 3, где класс
A
будет базовым для класса D
два раза: один раз по цепочке наследования
DBA,
другой раз – по цепочке DCA.
Е
сли
использовать обычную схему невиртуального
наследования, то каждый объект класса
D
будет содержать в себе объект класса
A
дважды и будет дважды наследовать все
его компоненты. Фактически получится
иерархия, показанная на рисунке 4.
А возможная схема размещения данных объекта класса D в памяти будет подобна приведенной на рисунке 5.
Рис. 5
Если в классе A есть компонент X, то в классе D появятся унаследованные компоненты A::B::D::X и A::C::D::X. При обращении к таким компонентам без их полной квалификации неизбежна ошибка компиляции. Дублирование унаследованных данных, кроме того, вызывает дополнительный расход памяти на объект класса D.
Устранить описанную проблему можно путем введения виртуального наследования. При таком типе наследования базовый класс, от которого возможно многократное косвенное наследование, объявляется виртуальным для всех непосредственных производных классов. Для того чтобы сделать базовый класс виртуальным, к его имени в списке наследования нужно добавить ключевое слово virtual на пример:
class A { /* ... */ };
class B: virtual public A { /* ... */ };
class C: virtual public A { /* ... */ };
class D: public B, public C { /* ... */ };
где классы B и C виртуально наследуют от класса A, за счет чего их наследник – класс D – косвенно наследует от класса A лишь один раз. В результате классовая иерархия приобретает первоначальный вид ромба (см. рис.3), без дублирования базового класса.
Размещение данных объекта класса D при виртуальном наследовании может быть другим (рис.6).
Рис. 6
Данные объекта класса A теперь присутствуют в единственном экземпляре, однако добавляются дополнительные указатели на них от данных объектов классов B и C.
Виртуальное наследование решает проблему потенциальной неоднозначности, однако его применение тоже не лишено трудностей. Первая проблема – вызов конструкторов виртуальных базовых классов. Конструктор любого класса, входящего в иерархию с виртуальным наследованием, должен явно вызывать конструкторы всех виртуальных базовых классов, где бы в иерархии этот класс не находился. Таким образом, разработчик очередного класса иерархии должен знать: какие аргументы требуют конструкторы виртуальных базовых классов. При этом отношение «быть виртуальной базой» передается при наследовании, т.е. все виртуальные базовые классы базового класса автоматически становятся таковыми и для производного класса. Еще одна проблема – доминирование виртуальных функций. Если виртуальный базовый класс содержит виртуальную функцию, которая переопределяется в его непосредственных производных классах, то ниже в иерархии вызов этой функции может быть неоднозначным.
Ниже дан пример, иллюстрирующий две названные выше проблемы виртуального наследования.
Пример
class A // виртуальный базовый класс
{
public:
A(int ia): __ia(ia) {}
virtual void f(); // функция будет переопределяться
...
private:
int __ia;
};
class B: virtual public A
{
public:
B(int ia, int ib): A(ia), __ib(ib) {}
virtual void f(); // переопределение
...
private:
int __ib;
};
class C: virtual public A
{
public:
C(int ia, int ic): A(ia), __ic(ic) {}
// virtual void f(); // переопределение
...
private:
int __ic;
};
class D: public B, public C
{
public:
D(int ia, int ib, int ic): // необходим параметр ia для класса A
A(ia), B(ia,ib), C(ia,ic)
// необходим вызов конструктора класса A
// в B(ia,ib) и C(ia,ic) аргумент ia фиктивный
{}
...
};
class E: public D // новый класс разработчика
{
public:
E(int ia, int ib, int ic): // необходим параметр ia для класса A
A(ia), D(ia,ib,ic) // необходим вызов конструктора класса A
// в вызове D(ia,ib,ic) аргумент ia фиктивный
{}
...
};
Как видно из определений классов D и E, их конструкторы явно вызывают конструктор виртуального базового класса A, а также конструкторы непосредственных базовых классов. Причем порядок записи вызовов конструкторов не играет роли – все равно первыми будут вызваны конструкторы виртуальных базовых классов. В этом состоит проявление первой проблемы. Вторая проблема иллюстрируется функцией f. Она введена в классе A и переопределяется в классах B и C (в C заголовок функции взят в комментарий). В таком виде иерархия будет успешно скомпилирована. Но если убрать комментарий с функции C::f, наследование этой функции в классах D, E становится неоднозначным. Это и есть проявление проблемы доминирования – неясно какой вариант переопределения виртуальной функции использовать далее в иерархии B::f или C::f.
С++ позволяет сочетать виртуальное наследование с невиртуальным. Примером является иерархия классов, изображенная на рисунке 7. В ней класс A является обычным базовым классом, класс Q – виртуальным базовым классом, а класс V – одновременно и обычным, и виртуальным (смешанным) базовым классом. Объект класса D будет включать 2 копии объекта класса A, одну копию объекта класса Q и по одной копии объекта виртуального и невиртуального класса V. Ниже дано возможное определение классов данной иерархии.
Рис. 7
class A
{
public:
A(int ia): __ia(ia) {}
private:
int __ia;
};
class Q
{
public:
Q(int iq): __iq(iq) {}
private:
int __iq;
};
class V
{
public:
V(): __iv(0) {}
V(int iv): __iv(iv) {}
private:
int __iv;
};
class B: virtual public Q, virtual public V, public A
{
public:
B(int iq, int iv, int ia, int ib): Q(iq), V(iv), A(ia), __ib(ib) {}
private:
int __ib;
};
class C: virtual public Q, virtual public V, public A
{
public:
C(int iq, int iv, int ia, int ic): Q(iq), V(iv), A(ia), __ic(ic) {}
private:
int __ic;
};
class D: public B, public C, public V
{
public:
D(int ia, int ib, int ic, int iq, int iv):
V(iv), Q(iq), B(iq,iv,ia,ib), C(iq,iv,ia,ic)
// вызов V(iv) инициализирует объект виртуальной базы V
{}
};
Интересная особенность характерна для конструктора класса D. Так как для класса D класс V является базовым дважды, необходимо дважды вызывать конструктор V::V(int). Но это запрещено синтаксисом С++. Выход из этой ситуации таков. Конструктор V::V(int) инициализирует объект виртуального базового класса V, а объект невиртуального базового класса V инициализируется конструктором по умолчанию V::V().