Преобразования типов разделяют не только по механизму работы, но и по направлению. О направлении преобразования говорят при наследовании. Возможно направление от родителей к потомкам (нисходящее) и от потомков к родителям (восходящее). Нисходящее потому что как бы устремлено вниз по иерархии наследования, а восходящее потому что устремлено вверх по иерархии наследования.
Указатели, тип которых соответствует базовому классу, можно направлять на объекты любых наследников того базового класса.
//Листинг #1.1 Указатель базового класса можно перенаправлять на объекты-наследники
#include <iostream>
usingstd::cout;
usingstd::cin;
classA{
public:
voidshow(){
cout<<"class A\n";
}
};
classB:publicA{
public:
voidshow(){
cout<<"class B\n";
}
};
intmain(){
Aa;
Bb;
A*ptr;
ptr=&a;//направили указатель ptr на объект a (тип указателя A, тип объекта а тоже А)
ptr=&b;//направили указатель ptr на объект b (тип указателя A, тип объекта b класс B)
cin.get();
}
Несмотря на то, что мы направили указатель в объект наследника, С++ не позволит нам сейчас использовать функции объекта наследника.
Компилятор C++ позволяет обращаться к элементам, входящим только в производный класс, из указателя базового класса, ссылающегося на производный класс, лишь при условии явного приведения указателя базового класса к типу производного класса;
Чтобы увидеть возникновение проблемной ситуации, для начала нужно понять, что попытка свести указатель к объекту-наследнику скорее всего происходила для того, чтобы использовать функциональность объекта-наследника, а потом посмотреть код, где очевидно такого поведения не происходит:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//Листинг #1.2 Указатель базового класса можно перенаправлять на объекты-наследники
#include <iostream>
usingstd::cout;
usingstd::cin;
classA{
public:
voidshow(){
cout<<"class A\n";
}
};
classB:publicA{
public:
voidshow(){
cout<<"class B\n";
}
};
intmain(){
Aa;
Bb;
A*ptr;
ptr=&a;//направили указатель ptr на объект a (тип указателя A, тип объекта а тоже А)
ptr->show();//Ожидаем вызов функции из класса А, видим, что она срабатывает
ptr=&b;//направили указатель ptr на объект b (тип указателя A, тип объекта b класс B)
ptr->show();//Ожидаем вызов функции из класса B, видим, что срабатывает не она, а функция из класса А
cin.get();
}
После компиляции листинга #1.2 и запуска программы мы наблюдаем, что в обоих случаях сработал класс A, что для некоторых неопытных может быть сюрпризом. Так работают компиляторы С++, что если принудительно не преобразовать тип в такой ситуации, то будет срабатывать не функционал наследника, а функционал основного класса. Исправим ситуацию с помощью явного преобразования типов:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//Листинг #2.1 Явное преобразование типов может поспособствовать вызову функции объекта-наследника
#include <iostream>
usingstd::cout;
usingstd::cin;
classA{
public:
voidshow(){
cout<<"class A\n";
}
};
classB:publicA{
public:
voidshow(){
cout<<"class B\n";
}
};
intmain(){
Aa;
Bb;
A*ptr;
ptr=&a;
ptr->show();
static_cast<B*>(&a) ->show();//У объекта тип A, преобразуем его к типу B и используем функцию из B
cin.get();
}
Синтаксис может казаться даже немного странным, но так оно и должно быть. Во-первых, преобразование указателя, относящегося к родительскому классу в направлении к наследнику очень опасное. Сами по себе преобразования опасное дело, а такое, от родителя к насленику, крайне опасное. Но кроме того что оно опасно, если попробовать сделать неменого иначе, то можно ненароком произвести обратное преобразование до момента нужности использования приведённого объекта.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//Листинг #2.2 Смесь явного преобразования и неявного, может путать
#include <iostream>
usingstd::cout;
usingstd::cin;
classA{
public:
voidshow(){
cout<<"class A\n";
}
};
classB:publicA{
public:
voidshow(){
cout<<"class B\n";
}
};
intmain(){
Aa;
Bb;
A*ptr;
ptr=&a;
ptr->show();
ptr=static_cast<B*>(&a);//Сначала мы явно преобразуем тип A* в тип B*, а в момент присваивания происходит неявное преобразование от B* к A*
ptr->show();//Из-за того, что в конечном итоге ptr указатель A*, срабатывает функция класса A
cin.get();
}
Возможно, нисходящее приведение типов вам не понадобится, но могут быть ситуации, где такое помогает. Один из простых примеров применения нисходящего преобразования типов может выглядеть приблизительно вот так:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
//Листинг #3 Применяем нисходящее преобразование для вызова функций из объектов-наследников, используя указатель на базовый класс
#include <iostream>
usingstd::cout;
usingstd::cin;
classA;
classB;
classC;
enum{
class_A,class_B,class_C//Это перечисление для удобства, помогает читаемости
};
classA{};
classB:publicA{
public:
enum{Type=class_B};//Используем перечисление, описанное для удобства: обозначаем, что текущий класс — это класс B
voidshow(){
cout<<"class B\n";//Отображаем, что работает функция класса B
}
};
classC:publicA{
public:
enum{Type=class_C};//Используем перечисление, описанное для удобства: обозначаем, что текущий класс — это класс C
voidshow(){
cout<<"class C\n";//Отображаем, что работает функция класса C
}
};
voidfoo(A& object, int type) { //В функцию отдаём два аргумента:
//первый — объект, тип которого преобразуется к типу БазовыйКласс, т. е. к А
//второй — число, помогающее опознать пришедший в функцию класс (тип) объекта
/*Делаем выбор на основании типа, пришедшего в функцию*/
switch (type)
{
case B::Type: //Если пришёл объект класса B, то
static_cast<B*>(&object)->show();//явно преобразуем тип объекта к типу B, чтобы вызвать функцию из класса B
break;
caseC::Type://Если пришёл объект класса C, то
static_cast<C*>(&object)->show();//явно преобразуем тип объекта к типу C, чтобы вызвать функцию из класса C
break;
}
}
intmain(){
Bb;
foo(b,B::Type);//Аргументом уходит объект b, тип которого B, вторым аргументом уходит перечисление, описанное внутри класса B
Cc;
foo(c,C::Type);//Аргументом уходит объект c, тип которого C, вторым аргументом уходит перечисление, описанное внутри класса C
cin.get();
}
Перечисление, которое использовано для удобства, не обязательно, но если его не использовать, то нужно будет обозначать внутри перечислений, описанных внутри классов, не условные обозначения, а цифры, соответственно и в switch нужно будет использовать цифры. Поскольку использование цифр не даёт определённой ясности и при добавлении новых классов или изменении порядка классов может произойти сбой логики: цифра, должная обозначать B вдруг начнёт обозначать C и т. п. — условные обозначения могут помочь избежать подобной проблемы в зародыше. С условными обозначениями сразу понятно, что к чему относится. Поэтому одно enum описано для того, чтобы эти понятные условные обозначения были, а вторые enum используются внутри классов в качестве констант-признаков класса. По константнам-признакам класса определется, какой класс задействован. Внутри функции main() сначала происходит вызов функции, в которую подаётся объект класса B и константа-признак класса B, признак нужен для того, чтобы внутри функции было понятно, объект какого класса пришёл в функцию. Поскольку параметр функции обозначен как объект класса A, то при входе аргумента в функцию происходит неявное преобразование типа вошедшего в функцию объекта от своего изначального типа, типа B, к типу "Объект класса A". Преобразования в направлении от наследника к родителю называют восходящим преобразованием. У нас B наследован от A, преобразование в направлении от B к A восходящее. Восходящие преобразования всегда безопасны. На момент входа в функцию и выхода в тело функции мы имеем объект, тип которого временно повышен до типа родительского класса. Но уже внутри тела функции мы производим обратное преобразование путём использования указателя и сводя тип к типу наследника. Обратное преобразование необходимо, чтобы вызвать метод, соответствующий непосредственно ушедшего в функцию foo(…) объекта. Когда мы проводим преобразование в направлении от типа основного класса к типу наследника, то мы спускаемя вниз по иерархии, т. е. проводим нисходящее преобразование.
На этот момент вы уже должны понимать, что такое восходящее преобразование типов и что такое нисходящее. Чтобы не путаться, можете иерархию представлять в виде базовый класс как начальник, а потомки подчинённые. Любой подчинённый, в свою очередь, может быть начальником своих подчинённых. Если преобразование на повышение должности, то преобразование восходящее, если преобразование на понижение, то нисхоодящее.
Повышающее преобразование всегда безопасно, и даже может происходить неявно.
Понижающее преобразование потенциально опасно, и чтобы его произвести, необходимо выполнять его явным образом.
Одна из опасностей понижающего преобразования связана с тем, что посредством указателя на базовый класс можно зазывать несуществующее, а это повлечёт за собой непрогнозируемое поведение программы. Мы в примерах вызывали существующие для объектов функции show(), но ради этого мы даже отдавали в функцию вспомогательный аргумент, чтобы определить правильную принадлежность к типу.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//Листинг #4 Нисходящее преобразование потенциально опасно Возможно свести программу к непрогнозируемому поведению
#include <iostream>
usingstd::cout;
usingstd::cin;
classA{};
classB:publicA{
inty;
public:
voidshow(){
cout<<"class B\n";//Отображаем, что работает функция класса B
}
};
intmain(){
Aa;
A& ptr = a;
static_cast<B&>(ptr).show();
}
В листинге #4 указатель ptr изначально завязан на объект родительского класса. Сводя указатель понижающим преобразованием на класс потомка, мы в итоге зазываем функцию, которую внутри класса родителя не описывали. Функцию вызывать так можно, но использование такой функции влечёт за собой непрогнозируемое поведение программы. На самом деле программа листинга #4 может даже правильно быть выполнена, но это только счастливая случайность. В действительности на момент выполненного преобразования мы получаем временный указатель выбранного типа, т. е. временный указатель, завязанный на объект-наследника. Указатель есть, а вот наследника нет. И используем этот указатель в качестве рычага, запускающего функционал несуществующего наследника. Коли наследника нет, то и функционала нет. Функционал не существует, потому что преобразованием создан только указатель, но не объект класса B. И это проблема. Чуть ранее мы использовали уже собранные в памяти объекты классов. Поскольку они были собраны до того, как использовались их функции-методы, то там проблем не было. А в последнем показанном случае происходит попытка использовать объект до момента его сборки в памяти, что, разумеется, плохо сказывается на программе в конечном итоге.
Чтобы не путаться в направлениях (восходящеее или нисходящее), сначала смотрите на тип приводимого объекта, а потом на назначаемый тип тому объекту. И помните, что восходящее преобразование безопасно и даже может происходить неявно (что на самом деле может иногда немного мешать), а понижающее преобразование потенциоально опасно и чтобы такое произвести, нужно насильно понижать тип путём явного приведения типов.
Добавить комментарий