Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdfI 662 |
Часть IV • Расширенное использование С-^-^ |
Клиентская программа создает объект Base и использует указатель Base для вызова метода show() класса Base. Это тривиально. Компилятор осуществляет поиск определения класса, к которому принадлежит указатель, а не класса, к ко торому относится объект, выполняет согласование сообщения и метода и вызы вает его. (На печать выводится х - 60, пятый вывод.)
Наконец, клиентская программа выполняет приведение указателя Base в ука затель Derived. Это преобразование не является безопасным, поэтому требуется явное приведение типов, чтобы указать компилятору то, что ему не известно. А именно: программисту известно, что он делает. Приведение указателя выполня ется с использованием всех соответствующих скобок и вызывается метод access () класса Derived.
((Derived*)pb)->access(x,y); |
/ / передача на свой собственный риск |
Указание компилятору, что нам известно о происходящем, означает, что мето ды класса Derived не будут вызываться с помощью этого указателя, поскольку объект, на который он ссылается, может выполнять только работу класса Base.
Вэтом случае компилятор не пытается предугадывать действия программиста
ивычислять, на какой вид объекта реально указывает Base. Об этом можно со жалеть, поскольку метод access() класса Derived, вызываемый в объекте Base, выводит непонятно что (шестой вывод).
Рассмотрим три комментария. Все, что было здесь сказано об указателях, также справедливо и в отношении ссылок C-h-f. (Единственное отличие состоит
втом, что ссылку невозможно изменить так, чтобы она указывала на другой объект.) Ссылка базового типа может указывать на объект производного класса без выполнения приведения и вызывает только методы, определенные в базовом классе, но не в производном классе. Ссылка производного класса не может указывать на объект базового класса без выполнения явного приведения типов. С использованием приведения она может указывать на базовый класс и вызы вать любой метод. Программист должен показать, что метод производного класса не вызывается с использованием ссылки производного класса, которая указывает на объект базового класса.
Внимание Указатели (и ссылки) базового класса могут указывать на объект производного класса без явного приведения. Они не вызовут каких-либо повреждений, потому что могут осуществлять доступ только к базовой части производного объекта. Явное приведение типов является необязательным. Указатели (и ссылки) производного класса не должны указывать на объект базового класса, потому что они должны запрашивать от объекта отклик на сообш^ения производного класса. При необходимости используйте явное приведение.
Все сказанное здесь об указателях и ссылках выполняется только для связан ных классов. Если классы не связаны наследованием, их указатели (и ссылки) не могут указывать на объекты других классов без явного наследования. Неявные преобразования не допускаются, потому что классы не имеют обш,их операций. Единственная причина, по которой допускается, чтобы базовый указатель ссылал ся на производный объект, заключается в том, что базовый и производный классы имеют обш,ие операции — операции, определенные в базовом классе. Базовый указатель может вызвать эти операции.
О с т о р о ж н о ! Указатели (и ссылки) конкретного класса не должны указывать на объекты классов, не связанных с ним наследованием. Это синтаксическая ошибка. При необходимости используйте явное приведение. Язык C++ это допускает.
Глава 15 • Виртуальные функции и использование наследования |
663 |
Неявные приведения классов, связанных наследованием, допускаются в слу чае, если режим наследования является общедоступным. Если наследование за крытое или защищенное, то все преобразования требуют явного приведения тигюв. Подобные действия связаны с тем, что в закрытом или защищенном режиме на следования общедоступные операции базового класса становятся в производном классе закрытыми или защищенными. Отсутствуют гарантии, что базовые и про изводные классы имеют какие-либо общие операции. Следовательно, базовому указателю должно быть запрещено указывать на объект производного класса. Операции производного класса не доступны базовому указателю (главное свой ство классов C+ + ), а операции базового класса — производному объекту (свой ства закрытого и защищенного наследования). Рекомендуем использовать только открытый режим порождения (derivation).
С о в е т у е м Осуществляйте порождение производных классов, используя только открытый вывод. Это позволит нацеливать указатели (или ссылки) одного класса на объект другого класса в одной иерархии наследования. В закрытом и защищенном наследовании базовые и производные объекты не содержат общедоступных операций (или данных).
Преобразование аргументов указателя и ссылки
Поговорим о преобразовании аргументов при вызове функций. У нас есть класс Other, который реализует функции-члены с параметрами в виде указателя и ссылки из классов Base и Derived.
Перегруженные методы setB() ожидают параметры класса Base. Они устанав ливают значение Other элемента набора данных в значение элемента набора дан ных в параметре Base. Перегруженные методы setD() ожидают параметры класса Derived. Они устанавливают элемент данных Other в значение дополнительного элемента данных в параметре Derived объекта. Метод get() возвращает внутрен нее состояние объекта Other.
class |
Other { |
/ / другой класс |
|
int |
z ; |
|
|
public: |
|
// передача no ссылке |
|
void setB(const Base &b) |
|||
{ z = b.showO; } |
// передача no указателю |
||
void setB(const Base *b) |
|||
{ z = b->show(); } |
// передача noссылке |
||
void setD(const Derived &d) |
|||
{ int a; d.access(a.z); } |
// передача no указателю |
||
void setD(const Derived *d) |
|||
{ int a; d->access(a,z); } |
|
||
int |
get() |
const |
/ / селектор |
{ return |
z; } } ; |
|
Этого достаточно, чтобы продемонстрировать главные вопросы. В следующем фрагменте программы каждая функция получает аргумент типа, определенного интерфейсом функции. Такой способ для использования функции является наибо лее естественным. Функция ожидает аргумент определенного типа. Имейте в виду, что внутри параметра функции аргумент принимает сообщения, которые принад лежат соответствующему типу. В функциях setB() параметр отправляет сообще ние showO класса Base, в функциях setD() — сообщение accessO класса Derived.
Base b(30); |
Derived d(50,80); |
/ / |
связанные объекты |
||
Other a l , |
a2; |
/ / |
несвязанные объекты |
||
al.setB(b); |
a2.setD(d); |
/ / |
точное |
согласование |
|
al.setB(&b); |
a2.setD(&d) ; |
/ / |
точное |
согласование |
|
Глава 15 • Виртуальные функции и использование наследования |
665 |
||
&d |
|
в указатель (ссылку) Base безопасно. С объектом аргу |
||
ш |
a1.setB(&d) |
мента Derived внутри функции, ожидающей объект Base, |
||
ничего плохого не случится. |
|
|||
|
|
|||
|
void setB(const Base* b) |
al.setB(&d) |
// безопасное преобразование |
|
|
{z = b->show();} |
|
|
|
На рис. 15.6 показан вызов данной функции. Когда Рис. 15.6. в памяти выделяется область для параметра указате ля Ь, он инициализируется в содержание фактического аргумента (толстая стрелка). Фактический аргумент яв ляется неименованным указателем на объект d класса
Derived. Этот безымянный указатель обозначен как &d. В результате оба указате ля ссылаются на тот же объект класса Derived (тонкие стрелки). Когда функция выполняется, она отправляет сообщения своему параметру Ь. Поскольку это пара метр класса Base, он может извлечь сообщения только из части Base объекта (пунктирная линия под объектом). Указатель параметра не может получить сооб щения из части Derived объекта, но эти сообщения не вызываются внутри функ ции, потому что его параметр принадлежит классу Base, а не классу Derived.
Когда у функции имеется параметр-указатель (или ссылка) Derived, не следует вызывать эту функцию с указателем (или ссылкой) Base как фактическим аргумен том. В теле такой функции от объекта, указанного параметром указателем, можно потребовать выполнения того, что может сделать только мощный объект Derived, но не слабый объект Base.
a2.setD(&b) / / синтаксическая ошибка
Это преобразование не является безопасным и поме
чается как синтаксическая ошибка. На рис. 15.7 показан |
|
ia1.setD{&b) |
|
вызов функции. Когда выделяется пространство для па |
|
||
|
|
||
раметра d, он инициализируется содержанием реального |
void setD(cx)nst Derived* d) |
||
аргумента, неименованного указателя на объект b клас |
|||
{int a; d->access(a,z);} |
|||
са Base. Когда выполняется функция, она отправляет |
|||
Рис. 15.7- Преобразование |
|||
сообщения своему параметру d. Параметр относится |
|||
к классу Derived, поэтому он может извлекать сообще |
из указат,еля |
Base |
|
в указат^ель |
Derived |
||
ния как из Base, так и из Derived частей объекта (пунк |
при передаче |
параметра |
тирная линия).
Но этот небольшой объект не содержит какой-либо части Derived. Слабый объект Base не знает, как на них отвечать. Когда во время выполнения несущест вующей части объекта направляется сообщение, результат не определяется. Про грамма может завершиться аварийно или выдать неверные результаты.
Предположим, что метод Derived: :setD() написан иначе и отправляет своему параметру только сообщения класса Base.
void setD(const |
Derived *d) |
/ / |
передача no указателю |
{ z = d->show(); |
} |
/ / |
только сервисы Base |
Фактически отправление объекта Base этой функции является безопасным, но компилятор этого не знает. Можно применить явное приведение, чтобы сообщить компилятору то, что нам известно.
a2.setD((Derived*)&b) ; |
/ / явное преобразование |
Это обычный способ сообщения компилятору, что вам известно о своих действи ях. Когда данное приведение встречается впервые, оно кажется сложным. Однако оно просто указывает, что этот объект Base будет безопасен в рамках функции setD(), Итак, этот небольшой объект Base может претендовать на то, что он яв ляется выросшим объектом Derived, потому что внутри этой функции он в любом случае будет выполнять только работу Base. Но обязательно следует убедиться в правильности ваших действий!
668 Часть IV • Расширенное использование С^^^
точное согласование и дают вывод a1 = 30 и а2 = 80. Вызов setB() с указателем Derived в качестве фактического аргумента не создает никаких проблем. Преоб разование из Derived в Base является безопасным в отличие от вызова setD() с указателем Base в качестве аргумента. Это преобразование небезопасное и по мечается как синтаксическая ошибка.
Чтобы убедить компилятор принять вызов, используется явное приведение типа указателя из Base в Derived. Компилятор успокаивается, но объект Derived он не создает. Решить такую проблему мог бы конструктор с параметром-указате лем (подобным использованному для параметра ссылки). Но эти конструкторы не являются обш,епринятыми, и поэтому в примере использован старый фрагмент, чтобы еш,е раз показать небезопасность приведения из Base в Derived.
Внимание всегда безопасно передавать указатель (или ссылку) производного класса как аргумент для функции с параметром-указателем (или ссылкой) базового класса (приведение является необязательным). Передача базового указателя (или ссылки) как аргумента для функции с параметром-указателем (или ссылкой) производного класса является синтаксической ошибкой. Принуждение компилятора принять вызов этой функции, используя приведение базового указателя (или ссылки)
к производному классу, может быть опасным процессом.
Мы подробно рассмотрели вопросы преобразования объектов (указателей либо ссылок) разных классов. Для несвязанных классов эта тема не имеет какого-либо практического значения. Редко случается, что вы можете использовать некоторый тип, когда ожидается другой несвязанный тип. Преобразования несвязанных ти пов сбивают с толку и опасны.
Совершенно другое дело для классов, связанных наследованием. Использова ние одного типа вместо ожидаемого другого типа из обш,ей иерархии наследования популярно при программировании на СН-+ (как, впрочем, и в любом объектноориентированном программировании). Важно понимать этот метод, его ограниче ния и последствия.
Преобразования типов, связанных наследованием, также вызывают путаницу и могут быть опасными. Они не подчиняются обычной интуиции программирова ния в отношении преобразований числовых типов. Главным критерием такого преобразования является не наличие достаточного пространства у результата пре образования для размеш,ения источника, а как раз наоборот. Главное — является ли преобразование безопасным и будет ли от его результата зависеть выполнение чего-то такого, что оно не может сделать.
Преобразование из производного типа в базовый является безопасным. Пре образование из базового типа в производный — опасно.
Рассмотрим виртуальные функции C+ + .
Виртуальные функции
В C + + каждый объект вычислений характеризуется свойствами, которые определяют тип объекта. Объект обозначается его именем (идентификатором) и типом, связанным с этим идентификатором. Вы встречались с подобной связью, когда определяли типы объекта в объявлениях. Это справедливо как для перемен ных, так и для функций программ.
При объявлении или определении переменной исходная программа должна выполнить связывание и определить тип объекта вычислений. Эта связь между именем объекта и его типом не может быть разорвана во время выполнения. Программа может определять другие объекты вычислений, используя этот же идентификатор и тип. Но другие объекты будут другими объектами вычислений, даже если они используют одно и то же имя.
Глава 15 • Виртуальные функции и использование наследования |
669 |
Это же справедливо и для функций программ. Объявление функции (ее прото типа) или определение функции (ее тела) включает идентификатор имени функ ции. Имя функции связывается с объектной программой, сгенерированной для этой функции. Подобная связь не может быть разорвана или изменена во время выполнения программы. Программа может определить некоторые другие функции, используя это же имя функции. Функции могут быть в одном и том же классе (с разными сигнатурами) или в другом классе или области видимости (с той же или с иной сигнатурой). Но это будут другие функции. Просто у них одинаковое имя функции.
Перегрузка имени функции C-f-f- не разрывает связь языка С с уникальными именами функции. Для человека, когда имя функции повторно используется в том же классе или в другой области видимости, это то же самое имя. Для компилятора все эти функции будут иметь разные имена. Имя, известное компилятору, являет ся конкатенацией имени класса, к которому принадлежит функция, идентификато ра функции, типа результата и типов параметров.
Когда компилятор видит определение функции, он создает видоизмененные имена. В них к идентификатору функции добавляются имя класса, тип результата и типы параметров. В результате для компилятора каждое имя функции действи тельно уникально. Этот метод называется искажением имен.
Например, функция draw() может быть определена в нескольких классах и с различными сигнатурами. Каждая функция представляет собой отдельный объект вычисления.
class |
Circle |
{ |
|
int |
radius; |
|
|
public: |
|
|
|
void drawO |
; |
// остальная часть класса Circle |
|
. . . . } |
; |
||
class Square { |
|
||
int side; |
|
|
|
public: |
|
|
|
void drawO; |
|
||
. . . . } ; |
// остальная часть класса Square |
||
class Rectangle { |
|
||
int sidel, side2; |
|
||
public: |
|
|
|
void drawO; |
|
||
. . . . } ; |
/ / |
остальная часть класса Rectangle |
Связь между именем и объектом вычислений устанавливается во время ком пиляции, когда компилятор обрабатывает определение или объявление объекта вычислений. Рассмотрим, например, слелуюш,ий фрагмент клиентской программы, определяющий объекты Circle, Square и Rectangle и рисующий их на экране.
Circle с; Square s; Rectangle г; |
/ / |
имя/тип связываются |
c.drawO; s.drawO; r.drawO; |
/ / |
имя/функция объединены в пару |
Компилятор и лицо, осуществляющее сопровождение, знают, что объект с имеет тип Circle, объект s — тип Square, а объект г — тип Rectangle. Компиля тор и сопровождающее лицо уверены в том, что первый вызов draw() указывает на -Circle: :draw(), второй вызов draw() относится к Square: :draw(), а третий вызов drawO — к Rectangle: :draw().
В языках, подобных C+-I-, нежелательно изменять эти связи на этапе компи ляции во время выполнения программы. Тип объекта в программировании описы вает фиксированные свойства объекта. Это еще одно проявление строгого контроля типов.