[Из песочницы] Приведение типов. Наглядное отличие static_cast от dynamic_cast
Доброго времени суток. Очень много статей в интернете о разнице операторов приведения типов, но понимания в данной теме они мне не особо то и не добавили. Пришлось разбираться самому. Хочу поделиться с вами моим опытом на довольно наглядном примере.
Статья рассчитана на тех, кто хочет осознать приведение типов в С++.
Итак, пусть у нас есть такая иерархия наследования:
#include
struct A{
A():a(0), b(0){}
int a;
int b;
};
struct B : A{
B():g(0){}
int g;
};
struct D{
D():f(0){}
float f;
};
struct C : A, D{
C():d(0){}
double d;
};
На картинке изображена иерархия наследования и расположение членов-данных наследников в памяти
Небольшое отступление: почему так важно преобразование типов? Говоря по рабоче-крестьянски, при присваивании объекту типа X объект типа Y, мы должны определить, какое значение будет иметь после присваивания объект типа X.
Начнем с использования static_cast:
int main(){
C* pC = new C;
A* pA = pC;
D* pD = static_cast (pC);
std::cout << pС << " " << pD << " " << pA << std::endl;
return 0;
}
Почему таков эффект при выводе значений указателей (значение указателя это адрес, по которому лежит переменная)? Дело в том, что static_cast производит сдвиг указателя.
Рассмотрим на примере:
D* pD = static_cast (pC);
1. Происходит преобразование типа из C* в D*. Результатом этого есть указатель типа D* (назовем его tempD), который указывает (внимание!) на ту часть в объекте класса C, которая унаследована от класса D. Значение самого pC не меняется!
2. Теперь присваиваем указателю pD значение указателя tempD (всё хорошо, типы одинаковы)
Разумный вопрос:, а зачем собственно нужно сдвигать указатель? Говоря по простому, указатель класса D* руководствуется определением класса D. Если бы не произошло смещения, то меняя значения переменных через указатель D, мы бы меняли переменные объекта класса С, которые не относятся к переменным, унаследованным от класса D (если бы указатель pD имел то же значение, что и pC, то при обращении pD→f в действительности мы бы работали с переменной
а).
Промежуточный итог: static_cast при работе с иерархией классов определяет значения указателей так, чтобы обращение к переменным класса через указатель было корректным.
Поговорим о недостатках static_cast. Вернемся к той же иерархии наследования.
Рассмотрим такой код:
int main(){
C* pC = new C;
A* pA = static_cast(pC);
D* pD = static_cast (pC);
B* pB = static_cast (pA);
std::cout << &(pB->g) << " " << pD << " " << pA << std::endl;
pB->g = 100;
std::cout << pC->a << " " << pC->b << " " << pC->f << std::endl;
return 0;
}
Почему pC→f имеет значение отличное от 0? Рассмотрим код по строчкам:
- В куче выделяется память под указатель типа С.
- Происходит повышающее преобразование. Указатель pA имеет такое же значение как и pC.
- Происходит повышающее преобразование. Указатель pD имеет значение, которое есть АДРЕС переменной f, в объекте класса C, на который указывает указатель pC.
- Происходит понижающее преобразование. Указатель pB имеет то же значение, что и указатель pA.
Где опасность? Дело в том, что в таком варианте исполнения, указатель pB действительно уверовал в то, что объект, на который указывал pA, был объектом типа B. При преобразовании static_cast проверяет, что такая иерархия действительно имеет место быть (т.е. что класс B является наследником класса A), но он не проверяет, что объект, на который указывает указатель pA, действительно является объектом типа B.
Сама опасность:
Теперь если мы хотим сделать запись в переменную g через указатель pB (ведь pB полностью уверен что указывает на объект типа B), мы на самом деле запишем данные в переменную f, унаследованную от класса D. Причем указатель pD будет интерпретировать информацию, записанную в переменную f, как float, что мы и видим при выводе через cout.
Как решить такую проблему?
Для этого следует использовать dynamic_cast, который проверяет не только валидность иерархии классов, но и тот факт, что указатель действительно указывает на объект того типа, к которому мы хотим привести.
Для того, чтобы такая проверка была возможна, следует добавить к классам виртуальность (dynamic_cast использует таблицы виртуальных функций, чтобы делать проверку).
Демонстрация решения проблемы, при той же иерархии классов:
int main(){
C* pC = new C;
A* pA = pC;
if(D* pD = dynamic_cast (pC))
std::cout << " OK " << std::endl;
else
std::cout << " not OK " << std::endl;
if(B* pB = dynamic_cast (pA))
std::cout << " OK " << std::endl;
else
std::cout << " not OK " << std::endl;
return 0;
}
Предлагаю запустить код и убедиться, что операция
B* pB = dynamic_cast (pA)
не получится (потому что pA указывает на объект типа С, что и проверил dynamic_cast и вынес свой вердикт).
Ссылок никаких не привожу, источник — личный опыт.
Всем спасибо!