Понятие класса и объекта. Инкапсуляция



Мы поможем в написании ваших работ!


Мы поможем в написании ваших работ!



Мы поможем в написании ваших работ!


ЗНАЕТЕ ЛИ ВЫ?

Понятие класса и объекта. Инкапсуляция



Предисловие

В учебном пособии излагаются основы объектно-ориентированной разработки программ на языке С++. Пособие может использоваться при проведении лекций, лабораторных и практических занятий, а также в рамках самостоятельной работы студентов. Использование пособия предполагает, что читатели хорошо знакомы с языком С – предшественником языка С++, имеют представление об объектно-ориентированном проектировании программ и обладают базовыми навыками работы в интегрированных средах разработки приложений.

Пособие предназначено для студентов, обучающихся по специальностям 220100, 220400 и 220500, и может быть полезно аспирантам, магистрантам и специалистам, использующим язык С++ для решения широкого круга прикладных задач.

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

Пособие организовано в виде серии глав, каждая из которых посвящена отдельной концепции объектно-ориентированного программирования на С++. В конце каждой главы дается перечень вопросов для самопроверки, а также список задач. Вопросы делятся на две категории: теоретические и практические, что способствует как закреплению знаний, так и приобретению практических навыков. Желательно, чтобы все практические вопросы и задачи проверялись на ЭВМ.

Главы 1-4, 6-8 написаны И.В.Зотовым, а главы 5 и 9 – Ширабакиной Т.А.

Авторы выражают искреннюю признательность рецензентам профессору Лопину В.Н. и доценту Жмакину А.П. за детальное изучение рукописи и ценные замечания, которые позволили улучшить содержание пособия. Также авторы благодарны сотрудникам редакционно-издательского отдела Курского государственного технического университета за нелегкий труд, связанный с редактированием рукописи.

 


Введение

Язык С++ представляет собой строгое надмножество языка С и является в настоящий момент одним из наиболее распространенных языков программирования высокого уровня. Область его применения весьма широка: от написания несложных программ расчетного характера до разработки компиляторов, СУБД и операционных систем. С++ поддерживает такие традиционные технологии программирования, как структурное и модульное программирование, а также содержит широкий набор конструкций для поддержки объектно-ориентированного программирования.

С++ дает программисту возможность написания гибких и эффективных программ, что является его несомненным достоинством. Однако, С++ весьма сложен для освоения; в особенности, это касается объектно-ориентированной части языка. Указанная сложность обусловлена целым рядом факторов, в частности, большим числом синтаксических конструкций (например, одних только операций в С++ более чем в 2 раза больше, чем в языке Object Pascal), глубокой контекстной зависимостью многих элементов (к примеру, один и тот же символ согласно контексту может обозначать унарную операцию, бинарную операцию или быть разделителем), а также рекурсивным определением языка. Для С++ характерны чрезвычайно сложная система правил автоматического приведения типов, нетривиальный порядок разбора выражений и деклараций.

Язык С++ в настоящее время стандартизован; его полное описание содержится в международном стандарте [8]. Это несомненный плюс для тех, кто использует язык С++. Однако, изучение языка по стандарту доступно лишь профессионалам; для остальных более разумный выход – использование учебных пособий. Несмотря на то, что в последние годы вышло большое число учебников и пособий по С++ на русском языке, потребность в продолжении издания по-прежнему высока. Это обусловлено, с одной стороны, тем, что существующие пособия ориентируются на устаревшие среды программирования, которые не полностью соответствуют стандарту С++ (в частности, Borland C++ 3.x). С другой стороны, в них, как правило, недостаточно глубоко затрагиваются вопросы эффективности. Те же издания, где вопросы эффективного программирования все же находят отражение, страдают недостаточной структурированностью материала.

При формировании данного учебного пособия ставилась цель преодолеть приведенные выше недостатки существующих изданий. В нем рассматривается стандартный С++ в соответствии с [8]; в качестве среды программирования используется относительно новая Borland C++ Builder 5.0. Каждая глава завершается списком вопросов для самопроверки, включающим как традиционные вопросы, связанные с определением конструкций, так и вопросы «на смекалку». В рамках каждой темы обязательно проводится анализ эффективности тех или иных программных решений, приводятся опасные и неоднозначные конструкции, которых следует избегать на практике. В целях усиления практических навыков читателю предлагается для решения несколько задач по каждой теме. Задачи выбраны так, чтобы в них максимально использовался материал соответствующей главы, при этом обучаемому дается «пространство» для проявления творческих способностей.

 


1. Объектная модель С++. Классы и объекты

Понятие класса и объекта. Инкапсуляция

Основа объектной модели в языке С++ – понятие класса. Класс описывает реальное или абстрактное понятие, являясь его программной моделью. Он включает описание набора атрибутов и действий, свойственных целому множеству объектов или явлений. В С++ класс обладает правами типа данных подобно структуре, т.е. от него можно определять переменные и константы, его можно использовать в списке параметров функций и т.п. Но класс – это нечто гораздо большее, чем просто структура. Структура – лишь «пассивная» коллекция данных. Класс, в отличие от нее, обладает определенной «функциональностью».

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

Class CPoint

{

Public: // интерфейс класса

CPoint(int x, int y);

int GetX();

int GetY();

void Show();

void Hide();

bool IsVisible();

void MoveTo(int x, int y);

Class CDate // дата текущего года

{

public:

CDate(int d = 1, int m = 1); // конструктор

int difference(const CDate & date); // разность дат

private:

int __day, __month; // день и месяц

};

CPoint point1(100,70); // локальный объект

CPoint * ppoint2 = new CPoint(200,45); // динамический объект

CString(); // конструктор по умолчанию

CString (const CString &); // конструктор копирования

CString (const char *); // общий конструктор

CString (size_t, char); // еще один общий конструктор – перегрузка

...

};

Ниже даны варианты формирования объектов приведенного класса.

CString S1; // вызов конструктора по умолчанию

CString S2(“строковая константа”); // вызов общего конструктора

Пример

CMatrix::~CMatrix() {

for(size_t i = 0; i < __nrows; i++) delete[] __thematrix[i];

delete[] __thematrix;

}


Seq.input(0); // нулевое значение

Пример

...

Указатель this

Всякая нестатическая компонентная функция при вызове получает указатель на тот объект, который является инициатором ее вызова. Этот указатель в С++ носит предопределенное имя this и передается в функцию первым неявным аргументом. Благодаря указателю this компонентные функции получают доступ к данным объекта и другим его компонентам. Любое обращение к компоненту в виде member_id внутри компонентной функции фактически означает this–>member_id (для упрощения записи this–> просто опускается).

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

const class_id * const this = &this_object_id,

где this_object_id – имя текущего объекта.

Если вызываемая функция имеет модификатор volatile или const volatile, то определение указателя this будет таким:

volatile class_id * const this = &this_object_id // для volatile

const volatile class_id * const this = &this_object_id // для const volatile

Указатель this доступен только в нестатических компонентных функциях; статическим функциям он не передается, поскольку они работают на уровне класса, а не отдельных объектов.

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

class CListElement { // определение класса

public:

CListElement(int data) { __data=data; } // конструктор

Реализация класса

void CListElement::AddElement() {

if ( __phead ) // список не пуст?

__phead –> __pnext = this; // этот элемент перед головой

this –> __pnext = NULL; // впереди пока нет элементов

__phead = this; // этот элемент - голова

}

...

Изложенный в данной главе материал позволяет понять базовые принципы организации автономных классов, определение объектов таких классов и доступ к их компонентам. Однако, для практики большой интерес представляют не только классы, но и отношения между ними, а также способы формирования новых классов на основе существующих. Одним из таких способов является наследование, которому посвящена глава 2.

 


Вопросы для самопроверки

1. Охарактеризуйте понятие класса. Чем класс отличается от структуры?

2. Какие компоненты могут входить в класс?

3. Почему определение класса обычно выносится в заголовочный файл, а реализация – в отдельный cpp-файл?

4. Для чего необходимо предварительное объявление класса? Каким образом оно осуществляется? Приведите общий формат и пример.

5. В классе CMatrix, описывающем прямоугольные матрицы, имеется компонент static const size_t __max_size. Как правильно проинициализировать этот компонент?

6. В классе CNamedPointer, описывающем именованные указатели, есть компонент const string&__name, содержащий имя указателя. Выделите недостатки такого способа задания компонента для хранения имени.

7. Как правильно инициализировать константный указатель на константу, входящий в число компонент класса? Приведите пример.

8. Имеется оператор (pseq->*mbr_func_ptr)(6); Выведите из него возможный вариант определения имени mbr_func_ptr, учитывая, что pseq – указатель на объект класса CSequence.

9. Пусть требуется определить класс поименованных векторов и пусть всем объектам этого класса, формируемым конструктором по умолчанию, надо давать умалчиваемое значение имени __default_name. Как можно определить компонент __default_name? Приведите эскиз класса векторов, исключив несущественные компоненты.

10. Вызов функции CSequence::AlreadyPrinted() встречается в следующем контексте: if ( !seq.AlreadyPrinted() ) seq.print(); Восстановите возможный прототип этой функции, полагая, что она не имеет значений параметров по умолчанию.

11. Почему в public-секцию класса нежелательно вводить компонентные данные? Поясните ответ примером.

12. Имеется класс CSomeClass. Правильно ли определен заголовок конструктора копирования этого класса: CSomeClass(CSomeClass cloned_object)? Обоснуйте ответ.

13. Какие действия происходят при выполнении оператора CBitSequence * pseq = new CBitSequence(8); если CBitSequence – имя некоторого класса?

14. Опишите алгоритм выполнения оператора delete[] p; где p – указатель на массив объектов некоторого класса.

15. Какие новые свойства придает конструктору ключевое слово explicit? Приведите пример.

16. Почему ключевое слово explicit не требуется использовать в конструкторах по умолчанию и копирующих конструкторах?

17. Имеет ли смысл введение ключевого слова explicit в конструктор с заголовком CSomeClass(size_t m, size_t n = 0) ? Обоснуйте ответ.

18. Когда имеет смысл определение класса с ключом union? Дайте пример.

19. В каком случае функция с модификатором const может изменить компонентные данные объекта?

20. В каких случаях следует объявлять компонентные функции класса константными? Обоснуйте ответ.

21. Почему статические компонентные функции не могут иметь модификаторов const, volatile и const volatile? Обоснуйте ответ.

22. Какой вид принимает определение указателя this в зависимости от спецификаторов компонентной функции const, volatile и const volatile?

 

Задачи

 

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

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

3. Написать определение и реализацию класса, представляющего сотрудника некоторой организации. Класс должен содержать компоненты для хранения фамилии и инициалов сотрудника, его табельного номера, даты рождения, занимаемой должности, номера отдела, оклада. Также в классе должны быть функции, имитирующие ключевые аспекты работы с данными о сотруднике.

4. Разработать класс для представления виртуальной записной книжки. Включить в класс функции для открытия и закрытия записной книжки, перелистывания страниц книжки, записи текста, вырывания страниц. Показать примеры использования класса в проекте.

 


CEmployee employee(

“Ivan Petrov”, CEmployee::male, “salesman”,

“accounting”, 2300, “Moscow ...”);

CFish Perch(3); // окунь

Пример

...

CAnimal * pAnimal = &A;

pAnimal –> Move(); // напечатает «Животное неизвестного вида движется»

pAnimal = &Perch;

pAnimal –> Move(); // напечатает «Рыба плывет»

pAnimal = &Sparrow;

pAnimal –> Move(); // напечатает «Птица летит»

Здесь статический тип объекта *pAnimal есть CAnimal, но динамический тип изменяется от присваивания к присваиванию. Соответственно, вызывается та функция, которая отвечает текущему динамическому типу указателя.

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

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

Основа в работе с виртуальными функциями – таблица виртуальных функций (называемае сокращенно vtable). Такая таблица есть у каждого полиморфного класса. В ней записаны указатели на все виртуальные функции класса, включая и унаследованные им виртуальные функции. Каждая функция в таблице имеет индекс, по которому ее можно легко идентифицировать. Объект всякого полиморфного класса имеет доступ к таблице vtable. Доступ организуется за счет добавления к объекту дополнительного указателя на таблицу виртуальных функций класса (его называют vptr). Добавление осуществляется компилятором автоматически, причем расположение указателя vptr относительно данных объекта зависит от реализации и стандартом языка С++ не оговаривается.

В терминах С++ вызов виртуальной функции через vtable можно записать следующим оператором:

(*pObject –> vptr[i])(pObject);

где pObject – указатель (this) на объект, вызывающий функцию, а i – индекс виртуальной функции в таблице vtable. При вызове сначала происходит обращение к указателю vptr, по которому определяется начало таблицы vtable. Далее, с использованием индекса i, выбирается указатель на нужную виртуальную функцию. После этого указатель разыменовывается и выполняется вызов по нему функции. Отметим, что в данном примере вызывается функция без аргументов. Если аргументы имеются, то они следуют за указателем pObject в списке аргументов.

При работе с виртуальными функциями и полиморфными классами нужно помнить о некоторых правилах и ограничениях.

Во-первых, функцию достаточно объявить виртуальной только в базовом классе; все функции с аналогичной сигнатурой в производных классах автоматически станут виртуальными, даже если в их заголовок не включать слово virtual.

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

В-третьих, виртуальные функции должны иметь идентичные или ковариантные (соизмеримые) типы возвращаемых значений. Эти типы (в случае неидентичности) будут соизмеримы при выполнении следующих трех условий: а) они являются указателями или ссылками на классы; б) классы по п.«а» совпадают или класс в возвращаемом значении переопределяемой функции (overriden) является однозначным базовым классом для класса переопределяющей функции (overrider), причем первый доступен в производном классе; в) типы имеют одинаковый уровень квалификации модификаторами const, volatile, const volatile или же в переопределяющей функции уровень квалификации ниже. Если одноименные функции базового и производного классов имеют несоизмеримые типы возвращаемых значений (например, void и char), возникает ошибка компиляции.

В-четвертых, если деструктор первичного базового класса виртуален, то деструкторы всех производных от него классов также будут виртуальными, несмотря на то, что деструкторы не наследуются. Виртуализация деструкторов позволяет корректно удалять объекты производных классов; без нее будет всегда удаляться только часть объекта, соответствующая последнему производному классу[10].

Ниже рассмотрены примеры, поясняющие описанные правила работы с виртуальными функциями.

Пример

struct CBase {

virtual void member_function();

virtual const CBase * member_function_2(int);

virtual ~CBase() {} // все деструкторы иерархии будут виртуальными

};

struct CDerived: public CBase {

void member_function(int); // скрывает виртуальную функцию из CBase

CBase * member_function_2(int); // переопределяет функцию из CBase

// тип значения менее квалифицирован

// виртуальный деструктор создается автоматически

};

struct CDerived_2: public CDerived {

void member_function(); // переопределяет функцию из CBase

CDerived_2 * member_function_2(int); // переопределяет функцию CBase

// типы значений соизмеримы

// виртуальный деструктор создается автоматически

};

// неправильная иерархия классов

class BaseClass {

public:

// невиртуальный деструктор

~BaseClass() { /* код деструктора */ };

...

};

class DerivedClass: public BaseClass {

...

};

...

BaseClass * pBaseClass = new DerivedClass;

...

delete pBaseClass;

// неопределенное поведение из-за невиртуального деструктора

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

void PrintObjects(ostream & OutStream, const SomeBaseClass array[], int nItems)

{

for ( int i=0; i<nItems; ++i ) OutStream << array[i];

}

Если в функцию передается массив из nItems константных объектов класса SomeBaseClass, то она корректно выводит их значения в поток[11]:

SomeBaseClass arrayBase[10];

...

Абстрактные классы

Поддержка механизма абстрактных классов в языке С++ преследует две цели. Во-первых, такие классы позволяют моделировать абстрактные понятия. Примером абстрактного класса может служить класс произвольных геометрических фигур CShape. У этого класса есть по меньшей мере один аспект поведения, который нельзя представить иначе как в обобщенной форме, – это изображение фигуры. Нарисовать фигуру, не зная закон ее построения, нельзя. Во-вторых, абстрактные классы могут быть использованы для описания абстрактных наборов функциональных возможностей, называемых интерфейсами. Интерфейс лишь определяет некоторый «срез» функциональности, а производные от этого интерфейса классы дают свои варианты его реализации. Примером может быть интерфейс IDispatch, который унифицирует взаимодействие объектов-клиентов и объектов-серверов в рамках технологии COM[12] корпорации Microsoft. IDispatch только задает состав функций (методов) для взаимодействия, а их реализацию дает производный класс (например, в ATL[13] данный интерфейс по умолчанию реализуется шаблонным классом IDispatchImpl).

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

virtual value_type function_id(parameter_list) = 0;

где value_type, function_id, parameter_list – соответственно тип значения, идентификатор и список параметров функции. Символы = 0 представляют так называемый «пустой» спецификатор (pure-specifier). Именно он сообщает компилятору о том, что функция является чисто виртуальной.

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

Использование механизма абстрактных классов требует выполнения ряда требований и ограничений. Некоторые из них даны ниже.

1. Если класс является абстрактным, то создать экземпляр этого класса можно лишь в одном случае: когда этот экземпляр есть часть объекта производного класса. Создать автономный объект абстрактного класса нельзя.

2. Чисто виртуальная функция может иметь реализацию. Реализация при этом должна быть во внешнем определении функции. Чисто виртуальный деструктор обязательно должен иметь реализацию, даже если он ничего не делает[14].

3. Абстрактный класс не может специфицировать параметр функции, тип возвращаемого значения функции. Также его нельзя использовать при явном преобразовании типа.

4. Абстрактный класс может быть получен из неабстрактного класса путем введения новой, чисто виртуальной функции. Чисто виртуальная функция может «переопределять» обычную виртуальную функцию.

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

Пример

Пример

Class B: virtual public A

{

public:

B(int ia, int ib): A(ia), __ib(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 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;

};

Пример

#include <typeinfo.h> // необходимо для std::bad_cast

class A {

public:

virtual void fA() {}

// делаем все классы полиморфными

};

class B: public A {

public:

void fB() {}

};

class C: public B {

public:

void fC() {}

};

class D: private B {

public:

void fD() {}

};

// использование классов

A * p = 0;

C c;

D d;

p = dynamic_cast<A*>(&c); // обобщение динамического типа &c

p->fA(); // так можно вызвать только функции класса A

dynamic_cast<B*>(p)->fB(); // уточнение динамического типа p

dynamic_cast<C*>(p)->fC(); // можно вызвать и fB, и fC

// p = dynamic_cast<A*>(&d); // так как D закрыто наследует от B

// p->fA(); // преобразование завершается с ошибкой (p == NULL)

try {

*p = dynamic_cast<A&>(d); // здесь будет исключение

}

catch ( std::bad_cast & ) {

// обработка исключения

}

p->fA(); // это никогда не выполнится

...

В С++, наряду с преобразованием динамических типов имеется возможность выяснения типов выражений непосредственно при выполнении программы (run-time type identification – RTTI). Средства поддержки RTTI описаны в заголовочном файле typeinfo.h. Их основу составляет класс std::type_info, который инкапсулирует информацию о типе объекта и функции для работы с ней. В нем есть, например, функция name, возвращающая строку с именем текущего типа объекта. Имеется также функция before, позволяющая проверить лексикографическую упорядоченность имен двух типов.

Выяснение типа выражения реализуется унарной операцией typeid. Если операция typeid срабатывает без ошибок, то ее результатом является ссылка на константный статический объект класса std::type_info, содержащий информацию о типе операнда (на каждый тип получается уникальный объект type_info, так что сравнение типов можно заменить сравнением адресов этих объектов). Аргументом операции typeid может быть леводопустимое выражение (lvalue), дающее полиморфный класс, а также идентификатор типа; кроме того, аргументом может быть выражение неполиморфного типа. Если аргумент – lvalue полиморфного типа, то typeid определяет текущий динамический тип аргумента. Если же аргумент – идентификатор типа, то результатом является информация о данном типе. Наконец, если аргументом является выражение неполиморфного типа, то typeid определяет его статический тип. Операция typeid может завершиться с ошибкой (например, когда аргумент – разыменованный нулевой указатель); в этом случае выбрасывается исключение класса std::bad_typeid (также определен в файле typeinfo.h).

Приведенный ниже пример показывает различные варианты использования механизма RTTI.

Пример

#include <iostream> // для cout, endl

// иерархия полиморфных классов

class A {

public:

virtual void fA() {}

};

class B: public A {

public:

void fB() {}

};

class C: public B {

public:

void fC() {}

};

class D: public B {

public:

void fD() {}

};

class E: public D {

public:

void fE() {}

};

int main() {

// объекты с различными модификаторами

C c;

const D d;

volatile E e;

A * pA = &c; // модификация динамического типа

std::cout << "typeid(*pA) = " << typeid(*pA).name() << std::endl

<< "typeid(c) = " << typeid(c).name() << std::endl

<< "typeid(pA) = " << typeid(pA).name() << std::endl;

// выведет:

// typeid(*pA) = C

// typeid(c) = C

// typeid(pA) = A *

pA = const_cast<D*>(&d); // модификация динамического типа

std::cout << "typeid(*pA) = " << typeid(*pA).name() << std::endl

<< "typeid(d) = " << typeid(d).name() << std::endl;

// выведет:

// typeid(*pA) = D

// typeid(d) = D

pA = const_cast<E*>(&e);

std::cout << "typeid(*pA) = " << typeid(*pA).name() << std::endl

<< "typeid(E) = " << typeid(E).name() << std::endl;

// выведет:

// typeid(*pA) = E

// typeid(e) = E

if ( typeid(*pA) == typeid(E) ) {

// условие есть true

std::cout << "typeid(*pA) < typeid(C) == "

<< ( typeid(*pA).before( typeid(C) ) ?

"true" : "false" ) << std::endl;

// выведет: typeid(*pA) < typeid(C) == false

std::cout << "typeid(*pA) > typeid(C) == "

<< ( typeid(C).before( typeid(*pA) ) ?

"true" : "false" ) << std::endl;

// выведет: typeid(*pA) > typeid(C) == true

}

return 0;

}

Работа приведенной программы очевидна из комментариев. Единственное, о чем следует сказать отдельно, – обработка модификаторов const, volatile, const volatile. Операция typeid, согласно стандарту, игнорирует любые модификаторы первого уровня. Например, const A и A, const B & и B она будет считать идентичными типами соответственно, а const A * и A * она будет различать (здесь const – модификатор второго уровня).

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

 


Вопросы для самопроверки

 

1. Охарактеризуйте понятие наследования.

2. Какие формы наследования поддерживаются в С++?

3. Дайте общий формат определения производного класса.

4. Опишите структуру объекта производного класса.

5. В каком порядке вызываются конструкторы и деструкторы при создании и удалении объекта производного класса?

6. В каких случаях при построении производного класса целесообразно использование закрытого и защищенного наследования? Дайте обоснование и приведите пример.

7. Дан класс CEmployee, описывающий сотрудника некоторой фирмы, а также классы CChief и COrdinary, представляющие соответственно руководящих и рядовых сотрудников этой фирмы. Как наиболее эффективно объединить эти классы в иерархию? Приведите эскиз.

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

9. Дан класс CPerson, описывающий человека, и класс CEmployee, представляющий сотрудников некоторой организации. Ясно, что класс CEmployee будет содержать многое из того, что есть в CPerson, поэтому разумно связать эти два класса отношением наследования. Какую схему наследования необходимо применить, чтобы эффективно использовать функциональность класса CPerson в классе CEmployee. Привести эскиз классовой иерархии и дать ее обоснование.

10. Даны классы CArmor, CFighter, CBomber, CArtillery, представляющие вражеские цели в программе «виртуальная карта разведчика» (CArmor – танки, CFighter – истребители, CBomber – бомбардировщики, CArtillery – артиллерийские орудия). Как наиболее рационально связать эти классы в иерархию?

11. Что такое полиморфизм?

12. В чем отличие между статическим и виртуальным полиморфизмом? Приведите пример.

13. Какие классы принято называть полиморфными?

14. Приведите концептуальные отличия между виртуальными и невиртуальными компонентными функциями.

15. Почему деструктор базового класса обязательно должен быть виртуальным? Опишите возможные последствия его «невиртуальности» на примере.

16. В каком случае приведенное ниже определение деструктора базового класса X следует считать правильным и безопасным: ~X() { delete[] __data; } ? Поясните ответ.

17. Пусть в некотором классе CSomeClass есть компонентная функция SomeFunc. Класс CSomeClass предполагается использовать в качестве базового в классовой иерархии. По каким признакам можно определить, следует ли делать функцию SomeFunc виртуальной?

18. Класс CMatrix содержит функцию virtual void Input(); Производный от него класс CSpecialMatrix включает функцию virtual void Input(istream &); К чему приводит несоответствие сигнатур функции Input в базовом и производном классах?

19. Требуется спроектировать классы, представляющие прямоугольники, квадраты, эллипсы, окружности и треугольники. Как наиболее рационально построить иерархию из этих классов? Приведите эскизы определения классов.

20. Какие классы называют абстрактными? Приведите пример определения абстрактного класса.

21. Как поступить, если проектируемый класс надо сделать абстрактным, но функций, которые можно было бы объявить чисто виртуальными, нет?

22. При каком условии возможно создание объекта абстрактного класса?

23. Опишите концептуальные отличия между виртуальными и чисто виртуальными функциями.



Последнее изменение этой страницы: 2016-08-14; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 3.233.219.62 (0.059 с.)