650 |
Часть IV # Расширенное использование СФ-^ |
Приведение указателей
Правила неявных преобразований (слабый контроль за типами для значений) применяются только к значениям, а не к ссылкам или к указателям (строгий конт роль за типами для значений). Однако явные преобразования могут использовать ся для параметров любого характера. Можно ли передать указатель целого типа для указателя Base? Нет, в соответствии с правилами строгого контроля типов, слелуюш,ая строка ошибочна:
a.getOther(&x); / / синтаксическая ошибка
Однако всегда можно указать компилятору, что он не должен принимать этот код. Для этого используется явное приведение для правильного типа.
a.getOther((Base*)&x); / / не проблема, преобразование ОК!!
В этом вызове функции создается и инициализируется указатель Base. Он обозна чает ячейку памяти, содержашую х. Внутри getOtherO сообш,ения класса Base направляются в область, занимаемую х. Поскольку методы Base не знают о структуре данных х, они легко могут ее повредить. Вся операция в целом вооб- и;е не имеет смысла, но в C + + она законна. Если утверждается, что так нужно, компилятор не будет спорить с вами.
Это же справедливо в отношении преобразования указателей (или ссылок) для любого типа указателей или ссылок. Неявные преобразования разных типов не допускаются. Например, ошибкой является следуюи;ее:
a.getOther(&a); / / ошибка: отсутствует преобразование из Other* в Base*
Метод getOtherO ожидает указатель типа Base. Вместо этого он получает объект типа Other. В соответствии с принципом строгого контроля типов компи лятор помечает эту строку как синтаксическую ошибку — неявное приведение указателей (или ссылок) разного типа не допускается. Однако компилятор разре шает вызов функции с явным оператором выбора.
a.getOther((Base*)&a); |
/ / не проблема, явное преобразование - ОК |
При этом указатель на объект класса Base создается и инициализируется как указатель на объект а в Other. Данный указатель передается методу getOtherO как фактический параметр. Внутри метода getOther() этот указатель использует ся для передачи объекту Other сообш^ений, принадлежащих классу Base. Компи лятор не может пометить их как ошибочные. Выполнение программы приводит к аварийному завершению или дает неверные результаты. Эта программа бес смысленна, но в C + + она допустима.
Операторы преобразования
Операторы преобразования используются в C + + как обычные приведения типов. Когда они применяются к объектам типов, определенных пользователем, они обычно возвраш,ают значение одного из компонентов объекта. Например, приведение к int, примененное к объекту типа Other, может вернуть значение элемента данных х. Использование этого оператора исключает синтаксические ошибки при применении объекта типа Other, в котором предполагается целый тип (или другой числовой тип).
b.set(a); |
/ / т о ж е , что и Ь. set(int(a)); |
Это пример клиентской программы, которая поддерживается за счет добавления соответствуюидих сервисов для серверного класса Other. Способы реализации данного вида сервиса будут изложены в главе 16 "Расширенное использование перегрузки операций". Однако решение использовать операторы преобразова ния — еще один удар по системе строгого контроля типов в языке C+ + .
Глава 15 • Виртуальные функции и использование наследования |
651 |
Если эта программа была написана, чтобы выполнить преобразование из Other в int, прекрасно. (Лучше было бы использовать явное приведение типов.) Если по ошибке объект а использовался вместо целого, компилятор не сообш^ит вам об этом. Заш.ита строгого контроля типов удалена, а ошибка обнаруживается во время рабочего тестирования и отладки.
С+Н- является языком со слабым контролем числовых типов. Можно свободно выполнять преобразование из одного числового типа в другой и явное приведение типов не требуется. Будьте осторожны, чтобы не сделать ошибку.
В С + + ведется строгий контроль типов, определенных программистом. Язык не обеспечивает приведение типов для числовых типов и типов, определенных пользователем, или разных типов, заданных пользователем. Ошибка помечается как синтаксическая, и ее можно скорректировать до выполнения программы.
Конструкторы и операторы преобразований ослабляют систему строгого конт роля типов в C-I-+ для типов, определенных пользователем. Они допускают явные и неявные преобразования цифровых типов и типов, определенных программи стом, но сделанная при этом ошибка не помечается как синтаксическая.
В той степени, в которой это касается указателей (или ссылок), C + + обеспе чивает смесь сильного и слабого контроля типов. Указатели не обозначают объек ты с типами, отличаюш.имися от их собственного типа. Однако они могут свободно преобразовываться в указатели любого другого типа. Следует использовать явное приведение типов (в отличие от числовых значений неявные приведения не допус каются, даже для указателей на числовые типы). Важно, чтобы память, на кото рую указывают указатели (или ссылки), использовалась после этого приведения правильно.
Преобразование классов, связанных наследованием
Использование наследования вводит дополнительные возможности для приме нения объекта одного типа, когда предполагается объект другого типа. Классы, связанные обш,едоступным наследованием, не являются полностью несовмести мыми, поскольку объект производного класса содержит все операции и элементы набора данных, которые имеются у объекта базового класса. Вы можете назначить объект одного класса для объекта другого типа (возможно, используя явное при ведение типов). Вы можете передать объект одного класса как параметр, когда ожидается параметр другого класса.
Правила C + + ддя преобразования классов, связанных наследованием, не очень сложны. Кажется, однако, что они выполняются вопреки широко распространен ной интуиции программирования. Если это так, то постарайтесь направить ин туицию в нужном направлении.
Помните: когда производный класс открыто наследуется из базового класса, C + + поддерживает явные стандартные преобразования из производных объектов в открытые базовые классы. Кроме того, допускаются преобразования из базового объекта в производный класс, но для них потребуется явное приведение. Это пра вило применяется к объектам классов, ссылкам на объекты классов и указателям на объекты.
Чтобы это правило стало понятным, рассмотрим несколько примеров и пред ставим диаграммы, иллюстрируюш,ие преобразования из одного типа в другой.
Важными являются концепции безопасных и опасных преобразований.
Безопасные и опасные преобразования
Рассмотрим фрагменты программы, в которых используются числовые пере менные и которые показывают обработку переменных разных типов.
i nt b = 10; double d;
d = b; |
// из "меньшего" в "больший" тип: безопасная пересылка |
652Часть IV* Расш.,:..,
Вэтом примере небольшой объем данных (4 байта на машине автора) пересы лается в большую часть данных (8 байт на машине автора). Какие бы значения не содержала целая переменная, она может быть сохранена в переменной с плаваюш^ей точкой двойной точности. При пересылке она не теряет ни точность, ни значение. Именно поэтому это преобразование считается безопасным, и ком пилятор C + + не выдает никаких предупреждений для программы такого типа.
Рассмотрим движение данных в обратном направлении.
i nt b; double d = 3.14;
b = d; / / из "большего" в "меньший" тип : опасная пересылка
В данном случае значение в 8 байт с плаваюидей точкой двойной д/шны может не поместиться в меньшей области, выделяемой для переменной целого типа. Дробная часть будет потеряна. Если значение двойной длины находится вне до пустимого диапазона для целых чисел, то также будет потеряно и само значение. Поэтому такое преобразование является опасным, и для подобных программ компиляторы C++ могут выдавать предупреждения.
Однако C++ не считает такое присваивание незаконным. Прежде всего, не все опасные операции являются неверными. Значение двойной длины может быть не большим, поэтому его можно легко сохранить в значении целого типа. В рассмат риваемый момент у значения двойной длины может отсутствовать дробная часть.
Только программист может оценить язык C + + . Если он знает, что делает (какое значение преобразуется и что случится с ним в результате пересылки), и доволен полученными результатами, прекрасно. Если нет, то C++ не будет выступать в роли "старшего брата".
Обсудим преобразование переменных разных классов. В отличие от предыду- ш,ей части, в которой обсуждалось преобразование объектов несвязанных клас сов, предположим, что классы связаны наследованием.
Рассмотрим классы Base (содержит один целый элемент данных с размером
в 4 байта) и Derived |
(включает два элемента данных целого типа с размером |
в 8 байт) |
|
|
class Base { |
// класс Base |
protected: |
|
// защищенные данные |
int x; |
|
public: |
|
// используется в Derived |
Base(int |
a) |
{ x = a; |
} |
// наследуется |
void set |
(int a) |
( X = a; |
} |
|
int show 0 |
const |
{ return x; |
} |
} ; |
|
class Derived : public Base { int y;
public:
Derived (int a, int b) : Base(a), y(b)
{ }
void access (int &a, int &b) const
{a = Base::x; b = y; }
};
//наследуется
//вдополнение к х
//список инициализации
//пустое тело
//дополнительная возможность
//извлечение данных объекта
Применим этулогику "соответствия размерам" к передаче данных между пере
менными двух классов.
Base Ь(30); Derived |
d(20,40) |
d = b; |
// из "меньшего" в "больший" тип: соответствует |
Глава 15 • Виртуальные функции и использование наследования |
653 1 |
Подобно предыдущему примеру с числовыми размерами, передадим "меньшее" значение (4 байта) большему значению (8 байт). У объекта назначения места для осуществления перемещения с избытком, причем данные не будут потеряны. Те перь переместим данные в противоположном направлении.
|
|
Base b(30); |
Derived с1(20,40); |
|
|
|
|
|
b = d; |
|
|
/ / из "большего" в "меньший" тип: не помещается |
|
А) Копирование числовых переменных |
|
В данном случае перемещается большее зна |
|
БЕЗОПАСНО |
НЕ БЕЗОПАСНО |
чение Derived в переменную меньшей д/шны |
|
Base. Памяти, выделенной для переменной Base, |
|
|
п |
п |
|
|
|
|
недостаточно для вмещения всех элементов |
|
|
|
данных значения Derived. Большое значение не |
|
|
|
|
|
|
помещается в меньшую область. На рис. 15.1 |
|
double d; |
intb; |
intb; |
double d; |
показано перемещение данных из меньшего зна |
|
чения в большее, и оно помечается как безопас |
|
8 байт |
4 байта |
4 байта |
8 байт |
ное. Также представлено перемещение данных |
|
|
|
|
|
|
из большего значения в меньшее, которое поме |
|
|
|
|
|
|
чено как опасное. |
|
|
|
В) Копирование переменных объекта |
|
Эта логика хорошо работала для числовых |
|
|
переменных, но не применима для объектов |
|
БЕЗОПАСНО |
НЕ БЕЗОПАСНО |
классов, связанных наследованием. Необходимо |
|
|
п |
п |
|
как можно быстрее развить интуицию. Реальным |
|
|
|
вопросом для объектов класса является не нали |
|
|
|
чие достаточного пространства, а доступность |
|
|
|
|
|
|
данных для согласованного состояния объекта. |
|
Derived d; |
Base b; |
Base b; |
Derived d; |
При перемещении данных от объекта Derived |
|
в объект Base объект Derived обладает достаточ |
|
8 байт |
4 байта |
4 байта |
8 байт |
|
ными данными для заполнения объекта Base. |
|
Рис. 1 5 . 1 . Пересылка |
данных |
между |
|
|
|
|
|
|
|
значениями |
различных |
|
|
|
|
|
|
|
размеров: |
неверный |
вариант |
А) Копирование числовых переменных |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
БЕЗОПАСНО |
^чНЕ БЕЗОПАСНО^ |
|
Дополнительные данные будут отброшены, по |
|
п |
п--—^л |
|
скольку они совершенно не нужны объекту Base. |
|
|
Объект Base всегда будет в согласованном со |
|
|
стоянии. Это безопасно. |
|
|
|
|
|
|
|
|
Когда данные перемеш^аются от объекта Base |
double d; |
int b; |
irjVb; |
doublp d; |
|
к объекту Derived, объект Base располагает до |
|
8 байт |
4 байта |
4'байта |
8 байт |
|
статочными данными для размеидения части Base |
|
|
|
|
|
|
объекта Derived. Данные для установки части |
|
|
|
|
|
Derived объекта Derived неоткуда взять, и это |
|
|
|
|
|
проблема. Объект Derived переходит в несогла |
В) Копирование переменных объекта |
|
|
сованное состояние. |
|
|
|
|
|
|
|
|
БЕЗОПАСНО |
^sHE БЕЗОПАСНО'' |
|
Это опасно, и С+Н- выдает синтаксическую |
|
п |
|
|
|
|
ошибку. На рис. 15.2 показано, что для числовых |
|
Y<!-''l-i |
|
значений важно сохранить значения и точность, |
|
|
а для значений класса данные должны быть до |
|
|
|
|
|
ступными для установки всех сохраняемых зна |
|
|
|
|
|
чений объекта назначения. |
|
|
Base b; |
Derived d; |
Deriyed d; |
Ва'Ц b; |
|
Поможет ли в этом случае явное приведение |
4 байта |
8 байт |
Э'байт |
4 бай^а |
|
типов? Прежде всего, язык С+-+- предоставляет |
Рис. 15.2. Пересылка данных между |
|
средства для указания компилятору, что вам из |
|
значениями |
различных |
размеров: |
|
вестно о своих действиях. |
|
|
|
правильный |
вариант |
|
Base Ь(30); Derived |
d(20,40); |
d = (Derived)b; |
/ / данные взять неоткуда |
654 |
Часть IV • Расширенное использование C+-I- |
Это все еще синтаксическая ошибка, потому что объект Base не может предо ставить отсутствующие данные. Возможно, вам захочется использовать нули для установки элементов данных класса Derived, которые не могут быть инициализи рованы из объекта Base (здесь это элемент данных у). Однако это невозможно выполнить по умолчанию: компилятору не известно, допустимы ли нули. Чтобы уведомить об этом компилятор, можно перегрузить оператор присваивания класса Derived. Передайте ему параметр Base, скопируйте поля параметра и установите остальные поля в любое значение.
void Derived::operator |
= (const |
Base &b) |
/ / |
параметр Base |
{ Base: : x = b.show(); |
у = 0; |
} |
/ / |
компромисс |
Другие примеры операторов присваивания классов можно найти в главе 11 Там операторы присваивания включают параметр либо того же класса, что и класс назначения присваивания, либо тип одного из компонентов класса. Здесь пред ставлен оператор присваивания, параметр которого относится к классу Base. Это замечательно, поскольку перегруженный оператор присваивания представляет со бой просто функцию C-f-H-, и можно спроектировать функции C + + с помощью параметров любого типа. Объект базового класса является одним из компонентов объекта производного класса.
Почему тело оператора присваивания настолько сложно? Почему необходимо относиться к классу Base очень внимательно? Почему используется операция яв ного задания объекта, а также функция show()? Прежде всего, элемент данных х защищен в классе Base, не правда ли? Возникает много вопросов. Главный из них: можно ли упростить оператор присваивания и записать его следующим образом?
void Derived::operate г = (const Base &b) |
/ / |
параметр Base parameter |
{ X = b.x; у = 0; } |
/ / |
отлично!! |
Ответ на этот вопрос будет дан позже в этой главе.
Второй вопрос: в программе имеются две строки, которые пытаются скопиро вать объект класса Base в объект класса Derived. Какую строку поддерживает этот оператор назначения?
Ь; |
/ / |
то |
же, |
что |
и d.operators (b); |
?? |
(Derived)b; |
/ / |
то |
же, |
что |
и d.operator=(b); |
?? |
Оператор назначения поддерживает первую строку. Вторая строка не вызывает оператор присваивания, поэтому это делается в первой строке. Уже говорилось, что всегда следует помнить о различии между присваиванием и инициализацией, поскольку в языке C + + это разные вещи. Для поддержки второй строки тре буется функция-член, которая будет вызываться при компиляции оператора приведения. Что такое приведение типов? Приведение типов означает вызов конструктора. Конструктор принадлежит к классу приведения типов. Следователь но, для поддержки второй строки программы необходимо записать следующее:
Derived::Derived(const |
??? &b) |
/ / какой тип параметра? |
{ / * what do I do here? |
*/ } |
|
Теперь имя конструктора известно. Тип данного параметра предполад^ается ис пользовать для инициализации полей объекта класса Derived. Согласно строке программы параметр должен иметь тип Base.
Derived::Derived(const |
Base &b) |
/ / параметр Base |
{ / * what do I do here? |
*/ } |
|
Обратите внимание, что часто в обсуждении используются слова "строка программы, которую требуется поддерживать" в различных видах. Причина за ключается в следующем: внешний вид серверных классов определяется исходя
|
ш-.^ш'^ша^^^ш^! |
Глава 15 • Виртуальные функции и использование наследования |
655 |
из принятого решения о том, что требуется поддержать в клиентской программе. Вам надо скопировать параметр в часть Ва^е объекта класса Derived и как-то изменить часть Derived объекта, что сохранит объект в согласованном состоянии. Подобно оператору присваивания, можно установить элемент данных Derived в ноль.
Derived::Derived(const |
Base &b) |
/ / |
параметр Base |
{ Base::x = b.x; у = 0; |
} |
/ / |
Это неправильно! |
Это неверно. Здесь не был учтен совет, что надо постоянно помнить о процессе построения объекта. Предполагалось, что конструктор вызывается при построе нии объекта, а не после того.
При построении объекта его компонентам выделяется область памяти. Конст руктор для каждого компонента вызывается до того, как следующему компоненту выделяется память. Сначала для объекта класса Derived размещается компонент Base. Следовательно, вызывается конструктор Base. Какой конструктор? Посколь ку части Base не передавались никакие данные, это конструктор по умолчанию. Определим класс Base и проверим, содержит ли он конструктор по умолчанию. Нет, он содержит конструктор преобразования без установленного по умолчанию значения параметра. Значит, попытка вызвать спроектированный конструктор приведет к вызову пропущенного конструктора Base, в результате чего появится синтаксическая ошибка. Помните об этих моментах.
Вам необходимо сделать доступным определяемый по умолчанию конструктор Base. Можно добавить конструктор к классу Base или значение параметра по умолчанию к существующему конструктору преобразования Base. Кроме того, рекомендуем воспользоваться списком функции инициализации в проектируемом конструкторе Derived, а не предоставлять конструктор по умолчанию классу Base. Используем список инициализации.
Derived::Derived(const Base &b) |
/ / |
параметр Base |
: Base(b.x) |
/ / |
передача элемента данных? |
{ у = 0; } |
/ / |
как вам это нравится? |
Но это также неверно. Элемент данных х в классе Base не является общедоступ ным, следовательно, к нему нет доступа из-за пределов области действия класса. Некоторые программисты доказывают, что код конструктора, который соответст вует оператору Derived::scope, находится в классе Derived, а у класса Derived есть доступ к незакрытым элементам данных в Base. Это действительно так. Но объект-параметр b отличается от цели сообщения, и методы класса Derived не могут осуществлять доступ к его не общедоступным данным. Оператор присваи вания класса Derived может осуществлять доступ только к своей внутренней организации, определенной в Base. Именно поэтому должна использоваться функция-член Base.
Derived::Derived(const Base &b) |
/ / |
параметр Base |
: Base(b.show()) |
/ / |
передача |
возвращаемого значения |
{ у = 0; } |
/ / |
неужели |
некрасиво? |
Это правильно, но, вероятно, слишком совершенно. Вызывается функция-член show() в Base, возвращаемое значение передается в конструктор преобразова ния Base. Проще вызвать конструктор копирования Base. Он всегда доступен. Поскольку класс Base не осуществляет динамическое управление своей памятью, то не требуется для себя писать специальный конструктор копирования.
Derived::Derived(const Base &b): Base(b), y(0) |
/ / |
копирование |
{ } |
/ / |
это действительно красиво |
656 |
Часть IV » Расширенное использование С+н- |
Мы рассматриваем здесь копирование объекта Derived в объект Base и копи рование объекта Base в объект Derived (операция является ошибкой, если только она не поддерживается конструктором копирования или оператором присваива ния). История применяется как к использованию объектов в операторах присваи вания, так и к передаче объектов функции по значению.
При изложении этой истории проводилась аналогия с целостностью данных. Было сказано, что копирование объекта Derived в объект Base безопасно, по скольку объект Derived содержит все данные Base (плюс дополнительные), а полу ченный в результате объект Base будет находиться в согласованном состоянии. Отмечалось, что копирование объекта Base в объект Derived не является безопас ным, поскольку объект Base представляет собой небольшой объект, у которого отсутствуют данные, необходимые для инициализации крупных объектов Derived. Следовательно, подобная операция может оставить объект Derived в несогласо ванном состоянии.
Использование объекта одного типа там, где ожидается объект другого типа, будет безопасным, только если используемый объект может выполнять все опе рации, запрашиваемые от ожидаемого объекта. Этот подход будет представлен при обсуждении использования указателей (или ссылок) одного типа, когда ожи дается указатель (или ссылка) другого типа.
Преобразование указателей и ссылок в объекты
Поговорим об использовании указателей и ссылок на объекты Derived и Base. Обсудим только указатели, но все сказанное о них также применяется и к ссыл кам. Будет рассматриваться иерархия только двух классов. Base и Derived, но все это применимо и к другим иерархиям классов, в которых базовый класс содержит другие производные классы, а производные классы используются как база для других классов.
В первую очередь динамически создается объект Base. С помош^ью указателей Base и Derived предпринимается попытка вызова его методов. Затем динамически создается объект класса Derived и делается попытка вызова его методов с помош,ью указателей Derived и Base. После этого результаты обобндаются для пере дачи параметров функциям по указателю и по ссылке.
Пусть объект класса Base помещается в динамически распределяемую область памяти. Используйте указатель Base, который обозначает этот объект для доступа ко всем методам Base (например, show()), но не к методам любого другого класса. Методы класса Derived (например, accessO) также недоступны указателю Base просто потому, что они не принадлежат к классу Base.
При обработке этих сообш,ений компилятор идентифицирует имя указателя (рЬ), который указывает на неименованный целевой объект, использует объявление указателя для создания класса указателя (Base), переходит к спецификациям клас са и осуш^ествляет поиск по спецификациям класса по имени метода. Если имя обнаруживается (в данном случае show()), то генерируется код объекта. Если имя метода с соответствуюш^ей сигнатурой не находится (в данном случае accessO), это синтаксическая ошибка.
i nt |
X, |
у; |
Base *pb = new Base(60); |
/ / |
базовый объект |
cout |
« |
" |
X = " « pb->show() « endl; |
/ / |
это OK |
pb->access(x,y); |
/ / |
это невозможно в любом случае |
Затем попытаемся установить указатель класса Derived на такой же объект класса Base. Алгоритм разрешения имени функции, описанный ранее, расширяет ся, когда указатель (или переменная) принадлежит классу Derived. Если метод с именем сообш,ения обнаруживается в классе Derived, замечательно. В против ном случае компилятор переходит к описанию класса Base. Это происходит только тогда, когда метод с соответствующей сигнатурой не находится в описании класса Base, при этом компилятор помечает вызов функции как синтаксическую ошибку.
pd = pb; // синтаксическая ошибка pd = (Derived*) pb; //OK
Глава 15 • Виртуальные функции и использование наследования |
657 |
Следовательно, указатель класса Derived будет в состоянии использовать все элементы определения класса Derived. "Любой элемент определения класса" означает элементы, определенные в классе Derived (например, accessO), и на следованные элементы (например, show()). При попытке установить указатель Derived на объект Base компилятор останавливается, даже не переходя к следую щим операторам.
Derived |
*pd = pb; |
/ / |
указывает на объект Вазе object: не ОК |
cout « |
" X = " « pd->show() « |
endl; |
/ / это было бы ОК |
pd->access(x,y); |
/ / |
но этого следует избегать |
Преобразование из объекта Base в объект Derived небезопасно. Следовательно, преобразование из указателя (или ссылки) Base в указатель (или ссылку) Derived не является безопасным. Все они создают синтаксические ощибки.
Однако при формальном объяснении трудно понять, что происходит. Необхо димо развивать соответствующую интуицию. К сожалению, программирование на традиционных языках не развивает интуицию в отношении преобразования классов, подстановки объектов и разрешению и запрещению вызовов функций. Попробуйте развить свою интуицию в отношении преобразования указателей и ссылок, для этого используйте графические представления и аналоги, основан ные на размере объектов и их способности выполнять работу для клиентской программы.
На рис. 15.3 указатель Derived показан большим прямоугольником в отличие от указателя Base, посколь ку указатель Derived может предоставить больше эле ментов для определения класса, чем Base. Здесь важна именно сгюсобность объектов отвечать на сообщения
|
|
и выполнять работу для клиентской программы. |
|
|
Указатель Base может осуществлять доступ только |
Доступ |
к объекту Base |
к элементам определения классов класса Base, а указа |
через указатель класса |
тель Derived — к элементам определения классов как |
Derived: |
небезопасно |
класса Base, так и класса Derived. Динамически вы |
|
|
деленный объект класса Derived представлен в виде |
|
большого прямоугольника не потому, что он занимает больший объем памяти, |
|
а потому, что он содержит больше возможностей. Даже если он не имеет дополни |
|
тельные элементы данных, он всегда содержит дополнительные функции-члены. |
|
Объект класса Base показан в виде присоединенного прямоугольника, обозна |
|
ченного пунктирной линией, для указания возможностей, имеющихся в объекте |
|
класса Derived, но отсутствующих в объекте класса Base. Буквы В и D внутри |
|
прямоугольников объектов обозначают часть Base и часть Derived объекта соот |
|
ветственно. |
|
|
В результате копирования содержимого указателя рЬ в указатель pd они оба |
|
указывают на один и тот же объект Base (см. рис. 15.3). Здесь важно, что указа |
|
тель, который указывает на объект, может вызвать возможности объекта в соот |
|
ветствии с типом указателя, а не с типом объекта. |
|
Предположим, что объекты базового класса являются слабыми, неспособными |
|
объектами, которые почти ничего не могут. А производные объекты окажутся |
|
большими, сильными, мощными объектами, способными выполнить все. |
|
Думайте об указателях базового класса как о тонких, слабых указателях, кото |
рые могут извлекать только возможности, определенные в классе Base. Например, |
указатель Base может извлечь метод show(), определенный в классе Base, но не метод accessO, определенный в классе Derived.
Наконец, указатели типа Derived должны представляться большими, сильными, дальнозоркими указателями, которые могут извлекать множество возможностей (как showO из Base, так и accessO из Derived).
Когда устанавливается мощный указатель класса Derived для указания на сла бый объект класса Base, он может извлекать намного больше, чем объект может предоставить. Из приведенных выше двух строк, расположенных за назначением указателя pd = pb, первая строка (вызов show()) правильная, а вторая (вызов accessO) — нет, в Base отсутствует access(). Именно поэтому C + + объявляет преобразование pd = pb синтаксической ошибкой, чтобы воспрепятствовать таким вызовам, как pd->access(x, у). Сам по себе вызов является синтаксически пра вильным (pd принадлежит классу Derived), но семантически бессмысленным — объект Base не может ответить на это сообщение.
Конечно, компилятор может видеть то, что уже знаете вы. Он понимает, что указатель pd класса Derived указывает на объект Base, следовательно, вызов pd->show() должен быть доступен, а вызов pd->access(x. у) — не разрешен. Но в этом случае проектировщик компилятора должен уметь делать очень много. В языке C + + часто отдается предпочтение проектировщику компилятора. От компилятора C++ не требуется выполнения анализа потока данных. Вы же должны изучить правила преобразования.
Итак, преобразование pd = pb не является безопасным и помечается как син таксическая ошибка. Но если у вас были только самые лучшие намерения? Что, если предполагалось только вызвать функции Base (например, show()), а не функ ции Derived (например, accessO)? Должен существовать механизм для указания компилятору того, что неизвестно ему, но знакомо вам, а именно, что будут ис пользоваться только возможности класса Base. Такой механизм действительно существует (см. главу 6). Он называется приведение типов.
Используя приведение типов, от компилятора запрашивается выполнение бе зопасного преобразования. При желании "расправиться без суда" надо вызвать pd->show(), а не pd->access(x, у).
Derived *pd = (Derived*)pb; cout « " x = " « pd->show()«endl; / / pd->acces6(x,y);
/ / |
Компилятор, |
мне нужно именно это |
/ / |
сделаем то, |
что безопасно |
/ / |
об этом не стоит даже думать! |
Обратите внимание, что имя приведения типов включает не просто имя класса, но и указатель. Было бы неправильно опустить нотацию указателя. Также не до пускается использование функциональной записи. Рекомендуем ее использовать, только когда имя типа является идентификатором, а Derived* не представляет собой идентификатор (вспомните, что звездочка в идентификаторах C + + не до пускается).
Derived |
*pd |
= (Derived)pb; |
/ / |
невозможно |
преобразовать указатель в объект |
Derived |
*pd |
= Derived*(pb); |
/ / |
недопустимое имя для функционального |
|
|
|
/ / |
приведения |
типов |
Если требуется использовать функциональную запись, применяйте typedef для составления имени типа, например DerivedPtr.
typedef |
Derived* DerivedPtr; |
/ / |
имя нового типа: идентификатор |
Derived |
*pd = DerivedPtr(pb); |
/ / |
идентификатор: OK для такого приведения |
Создадим объект класса Derived в динамически распределяемой области памя ти. Используя указатель класса Derived (большого, мощного, дальнего действия), можно вызвать возможности, унаследованные из класса Base, так и определенные в классе Derived. И это нормально, поскольку объект класса Derived (большой и мощный) располагает всеми этими возможностями.
Derived *pd = new Derived(50,80); |
/ / |
объект Derived может все |
cout « " x = " « |
pd->show() « endl; |
/ / |
OK для |
вызова |
базового метода |
pd->access(x,y) |
; |
/ / |
OK для |
вызова |
производного метода |
Глава 15 • Виртуальные функции и использование наследования |
659 |
Скопируем содержание указателя |
Derived в указатель Base (тонкий, слабый |
и ближнего действия). Указатель Base может осуществлять доступ только к воз |
можностям объекта Base, на который он ссылается. Попытка осуществить доступ |
к возможностям класса Derived через этот указатель бесполезна. Здесь эти воз |
можности отсутствуют, и компилятор сгенерирует синтаксическую ошибку. |
Base *pb = pd; |
/ / |
указатель на тот же объект |
cout « " x="«pb->show()«endl; |
/ / |
Метод Base здесь есть |
/ / pb->access(x,y); |
/ / |
ошибка: отсутствует |
в классе Base |
pd |
|
|
|
|
|
На рис. 15.4 показано, что из этого получается. Снова указатель |
I I I |
^1 |
в |
! |
D |
I |
Derived обозначается в виде большого прямоугольника в отличие |
|
^ |
|
|
|
|
от указателя Base. Прямоугольник для обозначения динамически |
|
|
|
„ |
^ |
|
выделенного объекта класса Derived больше прямоугольника ддя |
, ^ |
_. |
|
|
указателя Base. Буквы В и D внутри прямоугольников объектов |
1 рЬ = pd; |
|
// не проблема |
^ ^ |
-^ |
J г |
г |
J |
pb = (Base*)pd; |
|
//OK |
|
|
обозначают часть Base и часть Derived объекта соответственно. |
|
|
|
|
|
|
Как представлено на рис. 15.4, в результате копирования содер- |
гис . |
1Э.4. |
|
|
|
|
жимого указателя |
pd в указатель рЬ они оба указывают на один |
че-рез указатель |
|
класса Base: |
|
« ^от же объект Derived (большой, сильный, мощный, способный |
безопасно |
|
|
|
|
выполнить все, что может и объект Base, плюс намного больше). |
|
|
|
|
|
|
Однако этот указатель рЬ класса Base является тонким, слабым |
|
|
|
и близоруким, он способен извлекать только возможности Base объекта, но не |
|
|
|
возможности |
Derived. |
|
|
|
|
|
|
|
|
Когда определяется, что слабый указатель класса Base ссылается на большой |
|
|
|
объект класса Derived, этот слабый указатель не может повредить. Он может вы |
|
|
|
звать только возможности Base (например, show()), а они всегда присутствуют |
|
|
|
в мощном объекте Derived. Именно поэтому язык C + + |
воспринимает преобра |
|
|
|
зование рЬ = pd как безопасное. Также он воспринимает копирование большого |
|
|
|
объекта класса Derived в маленький объект класса Base. Это преобразование не |
|
|
|
может привести к пересылке безымянному объекту сообщения, на которое объект |
|
|
|
не может ответить. |
|
|
|
|
|
|
|
|
Если быть точным и указать сопровождающему программисту то, что было |
|
|
|
известно |
на |
момент написания |
программы (указатель |
Derived преобразуется |
|
|
|
в указатель Base), можно использовать явное приведение типов. Поскольку это |
|
|
|
преобразование безопасное, использование приведения является необязательным |
|
|
|
условием. |
|
|
|
|
|
|
|
|
|
Base *pb = (Base*)pd; |
/ / |
явное приведение: предупреждает остальных |
|
|
|
cout « " x="«pb->show()«endl; |
/ / |
Метод Base здесь есть |
|
|
|
/ / |
pb->access(x,y); |
/ / |
ошибка: отсутствует |
в классе Base |
Это преобразование является безопасным. Компилятор в данном фрагменте программы помечает вызов метода access(x,y) класса Derived как синтаксиче скую ошибку. Компилятору известно, что указатель рЬ принадлежит классу Base, а в классе Base отсутствует какой-либо метод access(). Поскольку компилятор C + + не выполняет анализ потока данных, у него есть право не знать то, что известно вам. Компилятор не знает, что указатель рЬ обозначает вполне развитый объект Derived, который способен ответить на сообщение accessO, как и любой другой объект Derived. Разработчик компилятора C + + отдыхает, а нам остается найти метод, чтобы сообщить компилятору то, что известно нам.
Вам известно, как указать контроллеру, что этот небольшой указатель обозна чает большой объект. Чтобы сообщить компилятору то, что известно вам, следует воспользоваться приведением. Необходимо выполнить приведение этого слабого указателя Base (который может извлечь возможности класса Derived) к мощному указателю Derived, так что методы Derived станут досягаемыми.
Base *pb = (Base*)pd; |
/ / |
явное приведение: предупреждает остальных |
cout « " x="«pb->show()«endl; |
/ / |
Метод Base здесь есть |
(Derived*)pb->access(x,y) ; |
//ошибка: приоритет операторов |