Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

2vcTnguyvU

.pdf
Скачиваний:
6
Добавлен:
15.04.2023
Размер:
955.27 Кб
Скачать

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

Многие классы сходны с классом employee тем, что в них можно дать разумное определение виртуальным функциям. Однако, есть и другие классы. Некоторые, например, класс shape, представляют абстрактное понятие (фигура), для которого нельзя создать объекты. Класс shape приобретает смысл только как базовый класс в некотором производном классе. Причиной является то, что невозможно дать осмысленное определение виртуальных функций класса shape:

class shape { // ...

public:

virtual void rotate(int) { error("shape::rotate"); } virtual void draw() { error("shape::draw"); }

//нельзя ни вращать, ни рисовать абстрактную фигуру

//...

};

Создание объекта типа shape (абстрактной фигуры) законная, хотя совершенно бессмысленная операция:

shape s; // бессмыслица: ``фигура вообще''

Она бессмысленна потому, что любая операция с объектом s приведет к ошибке. Лучше виртуальные функции класса shape описать как чисто виртуальные. Сделать виртуальную функцию чисто виртуальной можно, добавив инициализатор = 0:

class shape { // ...

public:

virtual void rotate(int) = 0; // чисто виртуальная функция virtual void draw() = 0; }; // чисто виртуальная функция

Класс, в котором есть чисто виртуальные функции, называется абстрактным. Объекты такого класса создать нельзя:

shape s; // ошибка: переменная абстрактного класса shape Абстрактный класс можно использовать только в качестве базового для другого класса:

class circle : public shape { int radius;

public:

void rotate(int) { } // нормально:

//переопределение shape::rotate void draw(); // нормально:

//переопределение shape::draw

40

circle(point p, int r); };

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

class X { public:

virtual void f() = 0; virtual void g() = 0; };

X b; // ошибка: описание объекта абстрактного класса X class Y : public X {

void f(); // переопределение X::f };

Y b; // ошибка: описание объекта абстрактного класса Y class Z : public Y {

void g(); // переопределение X::g };

Z c; // нормально

Абстрактные классы нужны для задания интерфейса без уточнения каких-либо конкретных деталей реализации. Язык C++ обеспечивает поддержку полиморфизма следующими способами:

путем неявного преобразования указателя или ссылки на производный класс к указателю или ссылке на открытый базовый;

#через механизм виртуальных функций;

с помощью операторов dynamic_cast и typeid

Оператор dynamic_cast – это часть механизма идентификации типов во время выполнения (RTTI). Он позволяет опросить тип объекта, адресованного полиморфным указателем или ссылкой.

Идентификация типов во время выполнения

RTTI позволяет программам, которые манипулируют объектами через указатели или ссылки на базовые классы, получить истинный производный тип адресуемого объекта. Для поддержки RTTI в языке C++ есть два оператора:

• оператор dynamic_cast поддерживает преобразования типов во время выполнения, обеспечивая безопасную навигацию по иерархии классов. Он позволяет трансформировать указатель на базовый класс в указатель на производный от него, а также преобразовать l-значение, ссылающееся на базовый класс, в ссылку на производный, но только в том случае, если это завершится успешно;

41

• оператор typeid позволяет получить фактический производный тип объекта, адресованного указателем или ссылкой.

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

Оператор dynamic_cast

Оператор dynamic_cast можно применять для преобразования указателя, ссылающегося на объект типа класса в указатель на тип класса из той же иерархии. Его также используют для трансформации l-значения объекта типа класса в ссылку на тип класса из той же иерархии. Приведение типов с помощью оператора dynamic_cast, в отличие от других имеющихся в C++ способов, осуществляется во время выполнения программы. Если указатель или l-значение не могут быть преобразованы в целевой тип, то dynamic_cast завершается неудачно. В случае приведения типа указателя признаком неудачи служит возврат нулевого значения. Если же l-значение нельзя трансформировать в ссылочный тип, возбуждается исключение. Например, имеется иерархия классов:

class employee { public:

enum empl_type { M, E }; empl_type type; employee* next;

char* name; short department; virtual int salary(); // ...

};

class manager : public employee { public:

employee* group; short level;

42

int salary();

//...

};

class ingineer: public employee { public:

int salary(); };

void func( employee *pe ) {

//используется pe->salary(); }

Вкомпании есть разные категории служащих. Параметром функции-

члена func () является указатель на объект employee, который может адресовать один из типов manager или ingineer. Поскольку func () обращается к виртуальной функции-члену salary(), то вызывается подходящая замещающая функция, определенная в классе manager или ingineer, в зависимости от того, какой объект адресован указателем. Допустим, класс employee перестал удовлетворять нашим потребностям, и мы хотим его модифицировать, добавив еще одну функцию-член bonus(), используемую совместно с salary() при расчете платежной ведомости. Для этого нужно включить новую функцию-член в классы, составляющие иерархию employee:

class employee { public:

virtual int salary(); };

class manager : public employee {

public:

 

int salary();

};

class ingineer : public employee {

public:

 

int salary();

int bonus(); };

Напомним, что func () принимает в качестве параметра указатель на базовый класс employee. Можно применить оператор dynamic_cast для получения указателя на производный класс ingineer и воспользоваться им для вызова функции-члена bonus(). Оператор dynamic_cast< ingineer *> ( pe ) приводит свой операнд pe к типу ingineer*. Преобразование будет успешным, если pe ссылается на объект типа ingineer, и неудачным в противном случае. Тогда результатом dynamic_cast будет 0.

Таким образом, оператор dynamic_cast осуществляет сразу две операции. Он проверяет, выполнимо ли запрошенное приведение, и если это так, выполняет его. Проверка производится во время работы программы.

43

dynamic_cast безопаснее, чем другие операции приведения типов в C++, поскольку проверяет возможность корректного преобразования.

void func( employee *pe ) {

ingineer *pm = dynamic_cast< ingineer * >( pe ); if ( pm ) { // ingineer ::bonus() }

//если pm не относится к объекту типа ingineer

//то dynamic_cast кончается неудачей и pm получает значение 0 else { // используем функцию-член класса employee }

Оператор dynamic_cast< ingineer * >( pe ) приводит свой операнд pe к типу ingineer*. Преобразование будет успешным, если pe ссылается на объект типа ingineer , и неудачным в противном случае: тогда результатом dynamic_cast будет 0.

Оператор dynamic_cast употребляется для безопасного приведения указателя на базовый класс к указателю на производный. Такую операцию часто называют понижающим приведением (downcasting). Она применяется, когда необходимо воспользоваться особенностями производного класса, отсутствующими в базовом. Манипулирование объектами производного класса с помощью указателей на базовый обычно происходит автоматически, с помощью виртуальных функций. Однако иногда использовать виртуальные функции невозможно. В таких ситуациях dynamic_cast предлагает альтернативное решение, хотя этот механизм в большей степени подвержен ошибкам, чем виртуализация, и должен применяться с осторожностью. Одна из возможных ошибок – это работа с результатом dynamic_cast без предварительной проверки на 0: нулевой указатель нельзя использовать для адресации объекта класса класс в указатель на производный. Ее также можно применять для трансформации l-значения типа базового класса в ссылку на тип производного. Синтаксис такого использования dynamic_cast следующий:

dynamic_cast< Type & >( lval )

где Type& – это целевой тип преобразования, а lval – l-значение типа базового класса. Операнд lval успешно приводится к типу Type& только в том случае, когда lval действительно относится к объекту класса, для которого один из производных имеет тип Type. Поскольку нулевых ссылок не бывает, то проверить успешность выполнения операции путем сравнения результата с нулем невозможно. Если вместо указателей используются ссылки, то условие if (ingineer *pm = dynamic_cast< ingineer * >( pe ) ) нельзя переписать в виде if (ingineer &pm = dynamic_cast< ingineer & >( pe ) ).

Для извещения об ошибке в случае приведения к ссылочному типу оператор dynamic_cast возбуждает исключение. В случае неудачного завершения ссылочного варианта dynamic_cast возбуждается исключение

44

типа bad_cast. Класс bad_cast определен в стандартной библиотеке, для ссылки на него необходимо включить в программу заголовочный файл <typeinfo>.

Оператор typeid

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

cout << typeid( re ).name() << endl;

Операнд re оператора typeid имеет тип employee. Но так как re – это ссылка на тип класса с виртуальными функциями, то typeid говорит, что тип адресуемого объекта – manager (а не employee, на который ссылается re). Программа, использующая такой оператор, должна включать заголовочный файл <typeinfo>.

Применяется typeid в сложных системах разработки данных. В таких системах необходимо знать фактический тип объекта, которым программа манипулирует с помощью указателя или ссылки на базовый класс. Оператор typeid допустимо использовать с выражениями и именами любых типов. Например, его операндами могут быть выражения встроенных типов и константы. Если операнд не принадлежит к типу класса, то typeid просто возвращает его тип:

cout << typeid( 8.16 ).name() << endl; // печатается: double

Если операнд имеет тип класса, в котором нет виртуальных функций, то typeid возвращает тип операнда, а не связанного с ним объекта:

int iobj;

cout << typeid( iobj ).name() << endl; // печатается int class Base { /* нет виртуальных функций */ };

class Derived : public Base { /* нет виртуальных функций */ }; Derived dobj;

Base *pb = &dobj;

cout << typeid( *pb ).name() << endl; // печатается: Base

Операнд typeid имеет тип Base, т.е. тип выражения *pb. Поскольку в классе Base нет виртуальных функций, то результатом typeid будет Base, хотя объект, на который указывает pb, имеет тип Derived.

В класса с виртуальными функциями #include <typeinfo>

employee * pe=new manager; employee & re =*pe;

typeid (* pe ) = = typeid (manager ); // true 45

typeid( *pe ) == typeid( employee ) // false

В этих сравнениях *pe – выражение типа класса, который имеет виртуальные функции, поэтому результатом применения typeid будет тип адресуемого операндом объекта manager.

Лабораторная работа № 7 Тема: наследование и абстрактные классы

Цель: Изучить использование наследования классов Задание

1

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

По умолчанию Конструктор задает базовую точку(0,0) в левом верхнем углу окна. Нулевой образец заполнения обычно предполагает его отсутствие. Методы GetX и GetY возвращают координаты x и y базовой точки.

Метод SetPoint позволяет клиенту изменять базовую точку. Похожие методы GetFill и SetFill обеспечивают доступ к образцу заполнения.

Метод Draw инициализирует графическую систему таким образом, что

фигуры заполняются по образцу fillpat. Методы Area() и Perimeter() являются чисто виртуальными функциями. Затем создайте производные классы Circle и Rectangle. Радиус передается конструктору в момент создания объекта. В классе Rectangle базовой точкой является верхний левый угол объекта. В производных классах метод Draw базового класса подменяется своим собственным виртуальным методом Draw. Также определяются виртуальные методы Area и Perimeter.

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

2.

1.Имеются классы, моделирующие зоологическую иерархию: класс Animals, класс Cat, класс Tiger. Каждый класс содержит символьную строку, которая инициализируется конструктором и предусмотрена для специфической

46

информации об объекте. Каждый класс имеет метод Identify, распечатывающий эту информацию.

class Animal { private:

char animalName[20]; public:

Animal(char nma[]) { strcpy(animalName,nma); } virtual void Identify( ) { cout << "I am a " << animalName << " animal" << endl; } };

class Cat: public Animal { private:

char catName[20]; public:

Cat(char nmc[], char nma[]): Animal(nma) { strcpy(catName,nmc); } virtual void Identify( ) { Animal::Identify( );

cout << "I am a " << catName << " cat" << endl; } };

class Tiger: public Cat { private:

char tigerName[20]; public:

Tiger(char nmt[], char nmc[], char nma[]): Cat(nmc,nma) { strcpy(tigerName,nmt); }

virtual void Identify( ) { Cat::Identify();

cout << "I am a " << tigerName << " tiger" << endl; } }; void Announce1(Animal a) {

cout << "In static Announce1, calling Identify:" << endl; a.Identify(); cout << endl; }

void Announce2(Animal *pa)

{cout<< "In dynamic Announce2, calling Identify:" << endl; pa->Identify(); cout << endl; }

Создайте программу, иллюстрирующую статическое и динамическое связывание двух функций Announce1 и Announce2

Контрольные вопросы

1. Дана иерархия классов, в которой у каждого класса есть конструктор по умолчанию и виртуальный деструктор:

class X { ... }; class A { ... }; class B : public A { ... }; class C : public B { ... };

class D : public X, public C { ... }; 47

Ответьте: какие из данных операторов dynamic_cast завершатся неудачно?

(a)D *pd = new D;

A *pa = dynamic_cast< A* > ( pd ); (b ) A *pa = new C;

C *pc = dynamic_cast< C* > ( pa );

(c) B *pb = new B;

D *pd = dynamic_cast< D* > ( pb );

(d) A *pa = new D;

X*px = dynamic_cast< X* > ( pa );

2.Объясните, когда нужно пользоваться оператором dynamic_cast вместо виртуальной функции? Приведите пример программы.

3. Пользуясь иерархией классов из упражнения №2.1 , перепишите следующий фрагмент так, чтобы в нем использовался ссылочный вариант dynamic_cast для преобразования *pa в тип D&:

if ( D *pd = dynamic_cast< D* >( pa ) ) { // использовать члены D} else { // использовать члены A }

Задание для самостоятельной работы

1.Пусть есть класс class base { public:

virtual void iam() { cout << "base\n"; } };

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

2. Пусть есть класс class char_vec { int sz;

char element [1]; public:

static new_char_vec(int s);

char& operator[] (int i) { return element[i]; } // ...

};

Определите функцию new_char_vec() для отведения непрерывного участка памяти для объектов char_vec так, чтобы элементы можно было индексировать как массив element[]. В каком случае эта функция вызовет серьезные трудности?

48

Глава 8. Классы и динамическая память

Динамические структуры данных используют память, полученную из системы во время исполнения. Оператор new выделяет ресурс памяти из динамической области для использования во время выполнения программы. “Зная“ размер данных, оператор запрашивает у системы необходимое количество памяти для сохранения данных и возвращает указатель на начало выделенного участка. Если память не может быть выделена, оператор возвращает NULL. Например, оператор new принимает тип данных T в качестве параметра и резервирует память для переменной типа T, возвращая адрес памяти.

T * p; // объявление p как указателя на T

p= new T; // p указывает на только что созданный объект типа T

По умолчанию содержимое памяти не имеет начального значения. Если такое значение необходимо, оно должно указываться в качестве параметра при использовании оператора new:

p= new T (value); Например, операция int ptr2;

ptr2= new int(57);

Динамическое создание массива

Преимущества динамического выделения памяти особенно очевидны при запросе целого массива. Предположим, что в некотором приложении размер массива становится известен только во время исполнения приложения. Оператор new может резервировать память для массива, используя запись :

p= new T[n]; // выделение массива n - элементов типа T

Этот оператор предписывает p указывать на первый элемент массива. Массив, созданный таким образом, не может быть инициализирован.

Например, оператор new выделяет память для массива из 20 длинных целых при условии, что имеется достаточно памяти. Если указателю p присваивается значение NULL, оператору new не удалось выделить память и программа завершается.

long * p;

p= new long[20]; if (p==NULL)

{ cerr<<”Ошибка выделения памяти! “ <<endl; exit(1); }

49

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]