
- •Объектно-ориентированное программирование
- •Глава 1. Объектно-ориентированный подход.
- •Глава 4. Статические компоненты классов
- •Глава 5. Друзья класса
- •Глава 7. Перегрузка стандартных операторов
- •Глава 8. Классы ресурсоемких объектов.
- •Глава 11. Динамическая идентификация типа (rtti). Операторы приведения типа.
- •Глава 12. Обработка исключительных ситуаций
- •Глава 16. Библиотека ввода-вывода.
- •Глава 17. Контейнеры, итераторы, алгоритмы
Глава 11. Динамическая идентификация типа (rtti). Операторы приведения типа.
п.11.1. Оператор typeid
Механизм RTTI(Run-TimeTypeIdentification) позволяет во время выполнения программы определить фактический (динамический) тип объекта, доступ к которому осуществляется через указатель или ссылку. Для этого используется ключевое словоtypeid. Внешне применение оператораtypeidвыглядит как вызов функции с одним параметром:
typeid(выражение);
Оператор typeidвозвращает ссылку на константу типаtype_info, которая описывает тип выражения, переданного в качестве параметра. В классеtype_infoопределены операторы сравнения(operator == (),operator != ()) и методname(), который возвращает строку в стиле Си, содержащую полное имя типа. Все остальные компоненты класса (в том числе конструкторы и оператор присваивания) закрыты. Поэтому невозможно создать переменную типаtype_info и сохранить в ней результатtypeid. Операторtypeidприменим как к встроенным типам данных, так и к пользовательским классам. Классtype_info определен в заголовочном файлеtypeinfo.
Пример:
#define PRINT(x) cout << “Тип “ #x “: “ << typeid(x).name() << endl
#include <typeinfo>
#include <iostream>
using std::cout;
using std::endl;
class MyClass
{
};
struct MyStruct
{
};
int main()
{
int iVal;
float fVal;
char str[80];
MyClass mc;
MeStruct ms;
PRINT(iVal);
PRINT(fVal);
PRINT(str);
PRINT(mc);
PRINT(ms);
cout << “Типы ms и mc”;
if(typeid(ms) == typeid(mc))
cout << “ совпадают”;
else
cout << “ не совпадают”;
cout << endl;
return 0;
}
Не во всех компиляторах печатаются слова «class» и «struct».
Для указателя или ссылки на объект не полиморфного класса оператор typeidвыдаст информацию о его статическом (заявленном) типе.
Пример:
class Mammal
{
};
class Dog: public Mammal
{
};
class Cat: public Mammal
{
};
int main()
{
Mammal Cheburashka;
Cat Matroskin;
Dog Sharik;
Mammal *p;
p = &Cheburashka;
cout << typeid(*p).name() << endl; // class Mammal
p = &Matroskin;
cout << typeid(*p).name() << endl; // class Mammal
p = &Sharik;
cout << typeid(*p).name() << endl; // class Mammal
cout << typeid(p).name() << endl; // -> class *Mammal
return 0;
}
Для указателя или ссылки на объект полиморфного класса оператор typeidвыдаст информацию о его динамическом (реальном типе).
Пример:
class Mammal
{
public:
virtual ~Mammal()
{
}
};
class Dog: public Mammal
{
};
class Cat: public Mammal
{
};
int main()
{
Mammal Cheburashka;
Cat Matroskin;
Dog Sharik;
Mammal *p;
p = &Cheburashka;
cout << typeid(*p).name() << endl; // class Mammal
p = &Matroskin;
cout << typeid(*p).name() << endl; // class Cat
p = &Sharik;
cout << typeid(*p).name() << endl; // class Dog
cout << typeid(p).name() << endl;
return 0;
}
Оператор typeidнельзя применять к нулевым или бестиповым указателям. Если так сделать, то программа упадет.
Пример:
class Mammal
{
public:
virtual ~Mammal()
{
}
};
class Dog: public Mammal
{
};
class Cat: public Mammal
{
};
Mammal * Create()
{
if(rand()%2 == 1)
return new Dog;
else
return new Cat;
}
int main()
{
Mammal *p;
int cats = 0;
int dogs = 0;
for(int i = 0; i < 100; ++i)
{
p = Create();
if(typeid(*p) == typeid(Cat))
++cats;
else
++dogs;
delete p;
}
cout << “Создано“ << dogs << “псов” << endl;
cout << “Создано “ << cats << “ котов” << endl;
return 0;
}
п.11.2. Приведение типа в С++
В С++ существует 5 конструкций для инициирования действий по приведению типа:
классический оператор приведения в стиле Си ((тип)переменная;)
оператор dynamic_cast
оператор static_cast
оператор const_cast
оператор reinterpret_cast
Последние 4 называются операторами приведения в стиле С++. Все они имеют единый синтаксис:
оператор <желаемый_тип>(выражение)
п.11.2.1. Оператор static_cast
Выполняет обычное не полиморфное приведение типа. Если речь идет о типах, не связанных иерархией, вызывается перегруженный оператор приведения.
Пример:
class Base
{
};
class Derived: public Base
{
};
int main()
{
double dVal = 5.3;
int iVal;
Base b;
Derived d;
Base *pb = &b;
Base *pd = &d;
iVal = static_cast<int>(dVal);
dVal = static_cast<double>(b); // !
// Предполагается вызов Base::operator int(), а такого нет, следовательно,
// ошибка времени компиляции
Derived *perror = static_cast<Derived *>(pb); // 1) !
Derived *pok = static_cast<Derived *>(pd); // 2)
return 0;
}
Произойдет ошибка: при попытке обратиться к компоненту, появившемуся в производном классе произойдет ошибка, поскольку указатель *perrorреально смотрит на объект базового класса, в котором данного компонента просто нет.
Хотя *pdявляется указателем на объект базового класса, реально он настроен на объект производного класса. Такое преобразование допустимо.
В первом случае нужно:
if(typeid(*pb) == typeid(Derived))
Derived *perror = static_cast<Derived *>(pb);
else
Derived *perror = NULL;
Следует отметить, что оба класса являются не полиморфными, следовательно, perror = NULLвсегда.
п.11.2.2. Оператор dynamic_cast. Понижающее приведение типа
Увиденное нами в только что рассмотренном примере – попытка понижающего приведения типа, то есть преобразование указателя или ссылки на объект базового класса к указателю или ссылке на объект производного.
Понижающее приведение типа не всегда
выполнимо и не всегда безопасно.
Понижающее приведение типа применимо
только к указателям и ссылкам и допустимо,
только если рассматриваемый указатель
или ссылка на объект базового класса
на самом деле «смотрит» на объект
производного.
Понижающее приведение типа реализуется с помощью оператора dynamic_cast. Этот оператор производит полиморфное приведение типа с проверкой корректности такого приведения. Операторdynamic_cast успешно выполняет заданное приведение типа, только если указатель или ссылка, подлежащие приведению, настроены на объект желаемого типа. Или на объект типа производного от желаемого. В противном случае, операторdynamic_cast, применяемый к указателям, возвращает нулевой указатель. Если оператор применяется к ссылкам, то происходит ошибка времени выполнения (в некоторых компиляторах возвращается нулевое значение типаint). Если речь идет не о полиморфном преобразовании или преобразовании не связанных типов, операторdynamic_cast ведет себя какstatic_cast.
Пример:
pd = dynamic_cast<Derived *>(pb);
Эквивалентная запись:
if(typeid(*pb) == typeid(Derived))
pd = static_cast<Derived *>(pb);
else
pd = NULL;
Пример (если использовать dynamic_cast вместо static_cast):
void func(Base *pb)
{
Derived *pd1 = dynamic_cast<Derived *>(pb); // (1)
Derived *pd2 = static_cast<Derived *>(pb); // (2)
pd1->setx(6); // !
pd2->setX(6); // !
}
class Derived: public Base
{
int x;
public:
void setX(int newX)
{
x = newX;
}
};
Структура класса Baseв данном примере роли не играет. МетодsetX()не является перекрытием какого-либо из классаBase. КлассыBaseиDerivedявляются полиморфными!
Пусть где-нибудь создается объект класса Derived, который передается на вход функцииfunc().
Derived d;
func(&d); // Derived * -> Base *
В момент вызова func()происходит повышающее приведение типа указателя на объект производного класса. У наблюдателя, который находится внутриfunc() есть указатель*pb на объект базового класса. Внутриfunc() возникла необходимость обратиться кsetX() производного класса. В данном примере необходимо преобразование*pb типу «указатель на объект производного класса». Сделать можно двумя способами:
С помощью dynamic_cast
С помощьюstatic_cast
В данном случае разницы нет, функция отработает корректно.
Рассмотрим другую ситуацию: Пусть у класса Basеесть второй потомок, не содержащий методаsetX()и поляx. Создадим объект второго производного класса, который также передадим на вход нашей функцииfunc().
Base
NULL
Derived
setX()
Derived2
Derived2 d;
func (&d);
Произойдет такое же повышающее приведение типа. При таком вызове вариант с оператором dynamic_cast (1) выдастNULL, и программа упадет при попытке разыменовать нулевой указатель.
В строчке (2) произошло бы преобразование типа, причем корректно (с точка зрения машины). Однако в последней строчке поведение программы не предсказуемо, так как объект, переданный в func()не имеет ни методаsetX(), ни изменяемого им поляx.
Вывод: Были рассмотрены 2 случая:
Поведение func()при передаче ей объекта классаDerived. Результат – функция работает корректно, оба способа понижающего приведения типа работают корректно.
Поведение func()при передаче ей объекта классаDerived2(представитель параллельной иерархии классов-потомков классаBase). Результат:
программа успешно скомпилируется;
понижающее приведение типа с помощью dynamic_cast завершится ошибкой (вариантNULL), которую можно отследить далее в теле функции, и программа упадет в методеsetX()(this->x = ...гдеthis– нулевой указатель);
понижающее приведение типа с помощью static_cast произойдет успешно (так как с формальной точки зрения оно допустимо), но это преобразование приведет к непредсказуемым последствиям в последней строчке функции. Отследить эту логическую ошибку в тексте программы уже невозможно.
Понижающее приведение типа следует
выполнять только с помощью оператора
dynamic_cast
с последующей
проверкой успеха.
п.11.2.3. Оператор const_cast
Позволяет управлять модификаторами constиvolatile. При использовании данного оператора желаемый тип объекта должен в точности совпадать с заявленным, за исключением модификаторовconstиvolatile.
Пример:
const int -> int // +
const int -> double // -
Снять константность можно только с
помощью оператора static_cast.
п.11.2.4. Оператор reinterpret_cast
Выполняет замену типа ячейки памяти: меняется способ интерпретации содержимого ячейки. Позволяет выполнять любые преобразования типов, в том числе запрещенные..
Оператор reinterpret_castявляется самым опасным в С++, поэтому
использовать его надо крайне аккуратно.
Оператор reinterpret_cast хорошо использовать для приведения указателей.
Пример(разработка оболочки, инкапсулирующей файловые операции):
#include <cstdio>
class File
{
FILE *fd;
File(const File &); // Запрет на копирование и присваивание
File & operator = (const File &);
public:
File();
File(const char *name, const char *mode);
~File();
bool open(const char *name, const char *mode);
void close();
void write(const char *data, unsigned size);
void read(char *data, unsigned size);
void print(int val);
void print(double val);
void print(const char *str);
void scan(int &val);
void scan(double &val);
void scan(char *str);
};
File::File()
{
fd = NULL;
}
File::File(const char *name, const char *mode): fd(NULL)
{
open(name, mode);
}
File::~File()
{
close();
}
boold File::open(const char *name, const char *mode)
{
close();
fd = fopen(name, mode);
return fd != NULL;
}
void File::close()
{
if(fd != NULL)
{
fclose(fd);
fd = NULL;
}
}
void File::write(const char *data, unsigned size)
{
if(fd != NULL)
fwrite(fd, 1, size, data);
}
void File::read(char *data, unsigned size)
{
if(fd != NULL)
fread(fd, 1, size, data);
}
void File::print(int val)
{
if(fd != NULL)
fprintf(fd, “%d”, val);
}
void File::print(double val)
{
if(fd != NULL)
fprintf(fd, “%f”, val);
}
void File::print(const char *str)
{
if(fd != NULL)
fprintf(fd, “%s”, str);
}
void File::scan(int &val)
{
if(fd != NULL)
fscanf(fd, “%d”, &val);
}
void File::scan(double &val)
{
if(fd != NULL)
fscanf(fd, “%f”, &val);
}
void File::scan(char *str)
{
if(fd != NULL)
fscanf(fd, “%s”, val);
}
Если в данную обертку добавить проверку переданных данных, то она обеспечит важное преимущество перед прямым использованием функций библиотеки:
автоматическое закрытие файла при выходе из блока;
перед выполнением операций чтения, записи осуществляется проверка ее целесообразности;
Рекомендуется добавить метод проверки готовности файла к работе.
Замечание: При использовании методовread() иwrite() целесообразно использовать операторreinterpret_cast:
f.write(reinterpret_cast<char *>(&v), sizeof(v));
где v– произвольная переменная.