C++.Бархатный путь

Указатели на объекты


Рассмотрим простой пример.

#include iostream.h class A { }; class AB: public A { }; class AC: public A { }; void main () { A *pObj; A MyA; pObj = MyA; cout "OK A" endl; AB MyAB; AC MyAC; pObj = MyAB; cout "OK AB" endl; pObj = MyAC; cout "OK AC" endl; }

Это очень простой пример. Пустые классы, простое наследование… Единственно, что важно в объявлении этих классов - спецификаторы доступа в описании баз производных классов. Базовый класс (его будущие члены) должен быть абсолютно доступен в производном классе. Первый оператор функции main() - объявление указателя на объект класса A. Затем следует определение объекта-представителя класса A, следом - настройка указателя на этот объект. Естественно, при этом используется операция взятия адреса. Всё это давно известно и очень просто. Следующие две строки являются определениями пары объектов, которые являются представителями двух разных производных классов…

За объявлениями объектов в программе располагаются строки, которые позволяют настроить указатель на базовый класс на объект производного класса. Для настройки указателя на объект производного класса нам не потребовалось никаких дополнительных преобразований. Здесь важно только одно обстоятельство. Между классами должно существовать отношение наследования. Таким образом, проявляется важное свойство объектно-ориентированного программирования: УКАЗАТЕЛЬ НА БАЗОВЫЙ КЛАСС МОЖЕТ ССЫЛАТЬСЯ НА ОБЪЕКТЫ - ПРОИЗВОДНЫХ КЛАССОВ. Подобное, на первый взгляд, странное обстоятельство имеет своё объяснение.

Рассмотрим схемы объектов MyA, MyAB, MyAC:

MyA::= A

MyAB::= A AB

MyAC::= A AC

Все три объекта имеют общий элемент (объекты производных классов - фрагмент) - представитель базового класса A. Исключительно благодаря этому общему элементу указатель на объект класса A можно настроить на объекты производных классов. Указателю просто присваивается адрес базового фрагмента объекта производного типа. В этом и состоит секрет подобной настройки. Как мы увидим, для указателя pObj, настроенного на объект производного класса, вообще не существует фрагмента объекта, представленного производным классом.


pObj

A AC

Ниже пунктирной линии - пустота. Для того чтобы убедиться в этом, мы усложним структуру класса A, определив в нём функцию Fun1. Конечно же, эта функция ничего не будет делать. Но у неё будет спецификация возвращаемого значения и непустой список параметров. Нам от неё большего и не требуется. Лишь бы сообщала о собственном вызове…

class A { public: int Fun1(int); }; int A::Fun1(int key) { cout " Fun1( " key " ) from A " endl; return 0; }

Аналогичной модификации подвергнем производные классы AB и AC (здесь предполагаются вызовы функций-членов непосредственно из функции main(), а потому надо помнить о спецификаторе public), а затем продолжим опыты.

class AB: public A { public: int Fun1(int key); }; int AB::Fun1(int key) { cout " Fun1( " key " ) from AB " endl; return 0; } class AC: public A { public: int Fun1(int key); int Fun2(int key);// В этом классе мы объявим вторую функцию. }; int AC::Fun1(int key) { cout " Fun1( " key " ) from AC " endl; return 0; } int AC::Fun2(int key) { cout " Fun2( " key " ) from AC " endl; return 0; }

Теперь мы займёмся функцией main(). Первая пара операторов последовательно из объекта запускает функцию-член производного класса, а затем - подобную функцию базового класса. С этой целью используется квалифицированное имя функции-члена.

MyAC.Fun2(2); //Вызвана AC::Fun2()… MyAC.Fun1(2); //Вызвана AC::Fun1()… MyAC.A::Fun1(2); //Вызвана A::Fun1()…

Следующие строки посвящены попытке вызова функций-членов по указателю на объект базового типа. Предполагается, что в данный момент он настроен на объект MyAC.

pObj-Fun1(2); //Вызвана A::Fun1()…

И это всё, что можно способен указатель на объект базового типа, если его настроить на объект производного типа. Ничего нового. Тип указателя на объект - базовый класс. В базовом классе существует единственная функция-член, она известна транслятору, а про структуру производного класса в базовом классе никто ничего не знает. Так что следующие операторы представляют пример того, что не следует делать с указателем на объекты базового класса, даже настроенного на объект производного класса.



//pObj-Fun2(2); //pObj-AC::Fun1(2);

То ли дело указатель на объект производного типа! И опять здесь нет ничего нового и неожиданного. С "нижнего этажа бункера" видны все "этажи"!

AC* pObjAC = MyAC; pObjAC-Fun1(2); pObjAC-Fun2(2); pObjAC-AC::Fun1(2); pObjAC-Fun1(2); pObjAC-A::Fun1(2);

И, разумеется, указатель на объект производного класса не настраивается на объект базового. //pObjAC = MyA;

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


Содержание раздела