Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Штерн В. - Основы C++. Методы программной инженерии - 2003

.pdf
Скачиваний:
238
Добавлен:
13.08.2013
Размер:
28.32 Mб
Скачать

660

'.:г;.; \! # Расширенное исоользовоние С-*--*"

1. Derived pointer, object, and derived method X = 50 у = 80

2.Derived pointer, derived object, base method x=50

3.Base pointer, derived object, base method x=50

4. Converted pointer, derived object and method X = 50 у = 80

5.Base pointer, base object, base method x=60

6.Converted pointer, base object, derived method x=60 y=-33686019

Рис. 15-5. Вывод программы из листинга 15.1

Это хорошо, однако недостаточно. Операция выбора со стрелкой имеет более высокий приоритет, чем операция приведения. В результате компилятор попытается пре­ образовать в производный указатель то, что возвраидает метод access(). Все перепутывается. Поэтому необходи­ мо использовать еще одну пару скобок.

((Derived*)pb)->access(x,y); / / это работает!

Это работает, но выглядит устрашающе. Все сложные операции переносятся в программу, поскольку компиля­ тор не может распознать, что указатель рЬ ссылается на объект Derived.

Объединим все эти компоненты. В листинге 15.1 пред­ ставлены классы Base и Derived, причем клиентская программа обрабатывает их объекты. Вывод программы показан на рис. 15.5.

Листинг 15.1. Использование указателей для доступа к объектам базового и производного классов

#include <iostream> using namespace std;

class

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; } } ;

//производный класс

//пустое тело конструктора

//добавлено в производный класс

int mainO

 

 

 

{ int X, у;

// неименованный производный объект

Derived

*pd = new Derived(50,80);

cout «

" 1. Derived pointer, object, andderived method\n";

 

pd->access(x,y);

// нет проблем: типы соответствуют

cout « " X = " « X « " у = " « у «endl «end1;

// х = 50 у = 80

cout «

" 2. Derived pointer, derived object, base method\n";

cout «

" X = " « pd->show() « endl «

endl;

// x = 50

Base *pb = pd;

// указатель на этот же объект

cout «

" 3. Base pointer, derived object, base method\n";

// x = 50

cout «

" X = " « pb->show() « endl «

endl;

// pb->access(x,y) ;

// ошибка: нет доступа к производному методу

cout «

" 4. Converted pointer, derived object and method\n";

((Derived*)pb)->access(x,y) ;

//знаем, это здесь

cout «

" X = " « X « " y= " « у «endl «endl;

// x = 50 у = 80

pb = new Base(60) ;

// неименованный базовый объект

Глава 15 • Виртуальные функции и использование наследования

I 661 I

cout « " 5. Base pointer, base object, base method\n";

 

cout «

X = " « pb->show() « endl «endl;

// x = 60

 

cout «

" 6. Converted pointer, base object, derived niethod\n";

 

((Derived*)pb)->access(x,y) ;

//передается на свой собственный риск

cout « " X =" « X « " у = " « у «endl «endl;

//старье! !

 

delete pd; delete pb;

//необходима аккуратность

 

return О ;

 

 

Вначале клиентская программа создает объект класса Derived и использует указатель класса Derived для доступа к методу access() производного класса. Это тривиально. Компилятор находит метод в определении класса, к которому принад­ лежит указатель, и вызывает его. При первом выводе печатается х = 50, у = 80.

Затем клиентская программа вызывает метод show() класса Base и использует тот же указатель класса Derived. Это также тривиально. Компилятор не находит определение метода в описании класса Derived, переходит к определению класса Base, находит метод и формирует вызов (и выводит на печать х = 50). Преобразо­ вание типов не используется. Неименованный объект, на который ссылался указа­ тель Derived, имеет тип Derived и может выполнить все, что требуется от объекта либо класса Base, либо Derived (второй вывод).

Потом клиентская программа устанавливает указатель Base на объект Derived. Эти указатели разного типа. Обычно неявные преобразования указателей разного типа не допускаются. Поскольку данные указатели являются указателями связан­ ного типа, это правило становится менее строгим для безопасных преобразований.

Base *pb = pd;

/ / разные типы: безопасно для связанных типов

Это безопасно, поскольку указатель класса Derived выполняет все, что может класс Base. Некоторые программисты все еще верят, что явное приведение типов полезно, поскольку оно предоставляет программистам, осуществляющим сопро­ вождение, информацию о преобразовании.

Base *pb = (Base*) pd; / / связанные типы: приведение необязательно

В листинге 15.1 клиентская программа не использует это преобразование. За­ тем клиентская программа применяет указатель Base для вызова метода show() класса Base. Поскольку неименованный объект, на который указывает Base, имеет тип Derived, не возникает проблем с отправкой сообщений базового класса к этому объекту. (На печать выводится х = 50, третий вывод.)

Далее клиентская программа использует указатель Base для вызова метода access(). Совершенно не важно, что указатель ссылается на объект класса Derived, который может выполнять задание. Компилятор не проверяет объект, а указатель осуществляет поиск определения класса, к которому принадлежит указатель (Base), не находит согласования с методом и объявляет вызов синтаксической ошибкой. Он превращен в комментарии.

Потом клиентская программа указывает компилятору, что этот базовый указа­ тель ссылается на объект Derived. Клиентская программа выполняет это посред­ ством преобразования указателя Base в указатель Derived. Это преобразование небезопасно и должно выполняться явно. Преобразованный указатель относится к классу Derived, и у компилятора не возникает проблем при вызове метода accessO.

((Derived*)pb)->access(x,y) ; //известно, это здесь

Поскольку объект, на который ссылается данный преобразованный указатель, является объектом класса Derived, вызов метода выполняется правильно и выво­ дит на печать х = 50, у = 80, четвертый вывод.

I 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) ;

/ /

точное

согласование

664

Часть !V # Расшыре

.,:дыован11еС+4'

В дополнение к сообщениям, определенным в классе Derived, функции, ожида­ ющие в качестве параметра ссылку класса Derived, могут отправить в параметре сообщения, которые определены также в классе Base. Это не проблема, поскольку аргумент класса Derived может отвечать на сообщения, наследованные из класса Вазе (наследование в данном случае является общедоступным, а не защищенным или закрытым).

В функциях, которые ожидают параметр класса Вазе (ссылку или указатель), сообщения класса Derived не могут быть отправлены параметру. Откуда это из­ вестно? Поскольку параметр класса Вазе не может ответить на сообщения, опре­ деленные в классе Derived, попытка отправить такое сообщение внутри функции компилироваться не будет.

class

Other {

/ /

другой класс

int

z ;

 

 

public:

/ /

ожидается объект Вазе

void

3etB(const Base &b)

{ int a; d.acce33(a,z);

/ /

ошибка: сообщение Derived

 

 

/ /

остальная часть класса Other

Эти два результата позволяют решить, что случится, если указатель или ссыл­ ка другого класса передается как реальный аргумент функции. Если параметр и тип аргумента не связаны наследованием, ответ простой. Если отсутствует явное приведение типов, то вызов функции является синтаксической ошибкой, незави­ симо от того, передается параметр ссылкой или указателем.

Account асс1(100), асс2(1000);

/ /

несвязанные объекты

Other a1, а2;

 

/ /

несвязанные объекты

al.3etB(acc1);

a2.3etD(acc2);

/ /

синтаксическая

ошибка

al.setB(&acc1);

a2.setD(&acc2);

/ /

синтаксическая

ошибка

Параметры передаются значением. Если класс параметров предусматривает соответствующие конструкторы, возможно неявное приведение. Для параметров ссылок и указателей такой механизм не существует. Единственным способом, который позволяет исключить синтаксические ошибки, является использование явного приведения.

a1.3etB((Base&)acc1) ;

/ /

не синтаксическая

ошибка,

но

бессмысленно

a1.3etB((Ba3e*)&acc1);

/ /

не синтаксическая

ошибка,

но

бессмысленно

Эти вызовы функций не содержат синтаксических ошибок — компилятор думает, что вы уверены в своих действиях. Вам же неизвестно, что происходит. В данных функциях параметр Account объектов собирается отвечать на сообщения Вазе (в данном случае showO). Это является семантической ошибкой: действия про­ граммы не имеют смысла.

Когда формальный параметр и типы фактических аргументов связаны друг с другом наследованием, ситуация еще больше осложняется.

Функцию ожидающую указатель Вазе (или ссылку Вазе), можно вызвать с ука­ зателем (или ссылкой) Derived как фактическим аргументом. Это замечательно, потому что объект Derived может делать все то же самое, что и объект Вазе. Функ­ ция, ожидающая параметр Вазе, запрашивает от своих параметров выполнения в теле функции только обязанностей Вазе. Как уже говорилось, сообщения класса Derived внутри тела этой функции будут недопустимыми для компилятора.

void

Other::3etB(const

Вазе &b)

/ /

передача no ссылке

{ int

a; b.acce33(a,z);

}

/ /

синтаксическая ошибка

Эта ситуация невозможна, потому что C-f+ является языком со строгим конт­ ролем за типами. Следовательно, преобразование из указателя (ссылки) Derived

из Derived в Base
при передаче параметра
Преобразование указателя

 

Глава 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. Но обязательно следует убедиться в правильности ваших действий!

666

Расширенное использована

В листинге 15.2 обобщаются результаты этого обсуждения. Вывод программы представлен на рис. 15.8.

Л и с т и нг 15.2. Передача параметров указателя и ссылки базового и производного классов

#include <iostream>

 

 

 

using

namespace std;

 

 

 

class

Base

 

 

 

 

 

 

protected:

 

 

 

 

 

 

int x;

 

 

 

 

 

 

public:

 

 

 

 

 

 

Base(int

a)

 

 

 

 

 

{ X - a; }

 

 

 

 

 

void set

(int

a)

 

 

 

, ( x = a;

}

 

 

 

 

 

int

show 0

const

 

 

 

{ return

x;

}

} ;

 

 

 

class

Derived

public

Base {

int y;

 

 

 

 

 

 

public:

 

 

int

b)

: Base(a), y(b)

Derived

(int

a

{

}

 

 

 

 

 

Base(b)

Derived(const

Base &b)

 

{ У = 0;

}

 

 

 

 

 

void access

(int &a,

int

&b) const

{ a = Base: :x;

b = y;

}

}

;

//базовый класс

//используется Derived

//наследуется

//наследуется

//производный класс

//пустое тело конструктора

//поддерживает явное приведение типов

//явная инициализация

//добавлено в производный класс

class

Other (

 

// другой класс

int

z

;

 

 

public:

 

 

// передача поссылке

void

setB(const

Base &b)

{ z = b.showO;

}

// передача по указателю

void setB(const Base *b)

{ z = b->show(); }

// передача поссылке

void setD(const Derived &d)

{ int a; d.access(a,z); }

// передача по указателю

void setD(const Derived *d)

{ int a; d->access(a,z); }

// средство доступа

int get() const

 

{return z; }

};

int mainO

 

 

// связанные объекты

{

Base b(30); Derived d(50,80);

 

 

Other al, a2; .

 

 

// несвязанные объекты

 

a1.setB(b); a2.setD(d);

«

а2=" «

// точное согласование

 

cout « " a1=" « a1.get()

a2.get() « endl;

 

a1.setB(d); a2.setD(b);

«

а2=" «

// неявное преобразование

 

cout « " a1=" « a1.get()

a2.get() « endl;

 

a1.setB(&b); a2.setD(&d);

« '

а2=" «

// точное согласование

 

cout « " a1=" «-al.getO

a2.get() « endl;

//

a1.setB(&d);

 

 

// неявное преобразование

a2.setD(&b);

 

 

// синтаксическая ошибка

 

a2.setD((Derived*)&b) ;

« ' а2=" «

// явное преобразование

 

cout « " a1=" « al.getO

a2.get() « endl;

 

return 0 ;

 

 

 

 

}

 

 

 

a1=30 а2=80 a1=50 82=0 a1=30 а2=80 a1=50 а2=701189б
р i c о
^^^^'^^ программы для листинга 15.2

Глава 15 • Виртуальные функции и использование наследования

667 3

В этом примере используются те же классы Base и Derived, что и в предыдущих примерах. В классе Derived имеется дополнительный конструктор.

Класс Other располагает двумя перегруженными функциями setBO, которые ожидают параметр-ссылку и указатель класса Base, и двумя перегруженными функциями setDO, которые ожидают параметр-ссылку и указатель класса Derived, а также методом get(), возвращаюилим значение элемента набора данных z.

Клиентская программа определяет и инициализирует объект Base, объект Derived и два объекта Other. Первая строка вывода дает a1 = 30 и а2 = 80, потому что вызовы функции setBO и setD() используют точное согласование типов фактических аргументов и формальных параметров.

При вызове функции setB() с фактическим аргументом класса Derived не воз­ никает проблем. Ссылка Base может быть инициализирована объектом Derived без выполнения приведения типов, поскольку это преобразование безопасно. Обычно подобный вызов функции должен быть отклонен как синтаксическая ошибка. Вызов можно сделать допустимым для компилятора при использовании явного приведения.

a2.setD((Derived&)b); / / синтаксис приведения типов ссылок

Данное приведение ничего хорошего не дает, потому что оно просто успокаивает компилятор. Ссылка Derived в setD() все еш.е указывает на небольшой объект Base (см. рис. 15.7). Вызов функцией Derived: :access() в теле setD() осуществ­ ляет доступ к памяти, которая не относится к параметру объекта Ь.

Чтобы этот вызов имел смысл, параметр указателя Derived в setD() должен ссылаться на объект Derived, а не на Base. Объект Derived должен инициализиро­ ваться значениями, которые содержатся в полях фактического аргумента Base. Однако в объекте Derived имеются поля, отсутствующие в объекте Base. Они должны быть установлены в некоторые приемлемые значения, например в О, или в любое другое, отвечающее требованиям приложения.

Что должна обеспечить функция для класса Derived, чтобы гарантировать надлежащую инициализацию полей объекта? Надо использовать конструктор. Его имя зависит от количества и типа параметров. Поскольку этот конструктор ини­ циализирует поля объекта Derived, используя данные объекта Base, ему требуется только один параметр — типа Base. Следовательно, это должен быть конструктор преобразования.

Как можно видеть, имя хорошо подходит конструктору, который преобразует значение класса Base в значение класса Derived.

Derived::Derived(const Base &b) : Base(b)

/ /

копирование конструктора

{ у = 0; }

/ /

явная инициализация

Конструктор преобразования может вызываться явно и создавать временную переменную класса Derived, которая инициализируется конструктором, указыва­ ется параметром ссылкой внутри setD() и удаляется после вызова setD().

а2.setD((Derived)b); / / явный вызов конструктора

Подобную методику можно использовать только для входных параметров. Если она применяется для параметра вывода, то изменения внутри функции будут сде­ ланы во временной копии, уничтожаемой после завершения вызова.

Поскольку этот параметр не определен как явный, его можно вызывать неяв­ но, что осуществляется в клиентском вызове в листинге 15.2. Следующая строка вывода дает a1 = 30 и а2 = 0.

Вторая часть клиентской программы работает с параметрами-указателями, а не с параметрами-ссылками. Первые два вызова setB() и setD() используют

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-, нежелательно изменять эти связи на этапе компи­ ляции во время выполнения программы. Тип объекта в программировании описы­ вает фиксированные свойства объекта. Это еще одно проявление строгого контроля типов.

Соседние файлы в предмете Программирование на C++