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

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
138
Добавлен:
02.05.2014
Размер:
5.67 Mб
Скачать

С++ для начинающих

942

требуется отложенное выделение памяти для объекта, то следует выбрать композицию по ссылке (с помощью указателя).

18.3.2. Открытие отдельных членов

Когда мы применили закрытое наследование класса PeekbackStack от IntArray, то все защищенные и открытые члены IntArray стали закрытыми членами PeekbackStack. Было бы полезно, если бы пользователи PeekbackStack могли узнать размер стека с помощью такой инструкции:

is.size();

Разработчик способен оградить некоторые члены базового класса от эффектов неоткрытого наследования. Вот как, к примеру, открывается функция-член size() класса

class PeekbackStack : private IntArray { public:

//сохранить открытый уровень доступа using IntArray::size;

//...

IntArray:

};

Еще одна причина для открытия отдельных членов заключается в том, что иногда

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

template <class Type>

class PeekbackStack : private IntArray { public:

using intArray::size; // ...

protected:

using intArray::size; using intArray::ia; // ...

_size класса IntArray:

};

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

На практике множественное наследование очень часто применяется для того, чтобы унаследовать открытый интерфейс одного класса и закрытую реализацию другого. Например, в библиотеку классов Booch Components включена следующая реализация растущей очереди Queue (см. также статью Майкла Вило (Michaeel Vilot) и Грейди Буча

(Grady Booch) в [LIPPMAN96b]):

С++ для начинающих

943

 

 

template < class item, class container >

 

 

 

 

class Unbounded_Queue:

// реализация

 

 

 

private Simple_List< item >,

 

 

 

public Queue< item >

// интерфейс

 

 

{

}

 

 

 

 

 

 

 

 

18.3.3. Защищенное наследование

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

//увы: при этом не поддерживается дальнейшее наследование

//PeekbackStack: все члены IntArray теперь закрыты

Stack, то закрытое наследование

class Stack : private IntArray { ... }

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

class PeekbackStack : public Stack { ... };

класс Stack должен наследовать IntArray защищенно:

class Stack : protected IntArray { ... };

18.3.4. Композиция объектов

Есть две формы композиции объектов:

композиция по значению, когда членом одного класса объявляется сам объект другого класса. Мы показывали это в исправленной реализации PeekbackStack;

композиция по ссылке, когда членом одного класса является указатель или ссылка на объект другого класса.

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

Предположим, что мы решили с помощью композиции представить класс Endangered.

Надо ли определить его объект непосредственно внутри ZooAnimal или сослаться на него с помощью указателя или ссылки? Сначала выясним, все ли объекты ZooAnimal обладают этой характеристикой, а если нет, то может ли она изменяться с течением времени (допустимо ли добавлять или удалять эту характеристику).

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

С++ для начинающих

944

эффективности включение больших объектов не оптимально, особенно когда они часто копируются. В таких случаях композиция по ссылке позволит обойтись без ненужных копирований, если применять при этом подсчет ссылок и технику, называемую копированием при записи. Увеличение эффективности, правда, достигается за счет усложнения управления объектом. Обсуждение этой техники не вошло в наш вводный курс; тем, кому это интересно, рекомендуем прочитать книгу [KOENIG97], главы 6 и 7.)

Если же оказывается, что только некоторые объекты класса ZooAnimal обладают указанной характеристикой, то лучшим вариантом будет композиция по ссылке (скажем, в примере с зоопарком не имеет смысла включать в процветающие виды большой объект, описывающий виды вымирающие).

Поскольку объекта Endangered может и не существовать, то представлять его надо указателем, а не ссылкой. (Предполагается, что нулевой указатель не адресует объект. Ссылка же всегда должна именовать определенный объект. В разделе 3.6 это различие объяснялось более подробно.)

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

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

class ZooAnimal { public:

// ...

const Endangered* Endangered() const; void addEndangered( Endangered* ); void removeEndangered();

// ...

protected:

Endangered *_endangered; // ...

перестать грозить панде.

};

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

class DisplayManager { ... };

class DisplayUNIX : public DisplayManager { ... };

определить иерархию классов DisplayManager:

class DisplayPC : public DisplayManager { ... };

Наш класс ZooAnimal не является разновидностью класса DisplayManager, но содержит экземпляр последнего посредством композиции, а не наследования. Возникает вопрос: использовать композицию по значению или по ссылке?

Композиция по значению не может представить объект DisplayManager, с помощью которого можно будет адресовать либо объект DisplayUNIX, либо объект DisplayPC.

С++ для начинающих

945

Только ссылка или указатель на объект DisplayManager позволят нам полиморфно манипулировать его подтипами. Иначе говоря, объектно-ориентированное программирование поддерживается только композицией по ссылке (подробнее см. [LIPPMAN96a].)

Теперь нужно решить, должен ли член класса ZooAnimal быть ссылкой или указателем на DisplayManager:

член может быть объявлен ссылкой лишь в том случае, если при создании объекта ZooAnimal имеется реальный объект DisplayManager, который не будет изменяться по ходу выполнения программы;

если применяется стратегия отложенного выделения памяти, когда память для объекта DisplayManager выделяется только при попытке вывести объект на дисплей, то объект следует представить указателем, инициализировав его значением 0;

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

переключением мы понимаем предоставление пользователю возможности выбрать один из подтипов DisplayManager в начале или в середине работы программы.

Конечно, маловероятно, что для каждого подобъекта ZooAnimal в нашем приложении будет нужен собственный подтип DisplayManager для отображения. Скорее всего мы ограничимся статическим членом в классе ZooAnimal, указывающим на объект

DisplayManager.

Упражнение 18.6

Объясните, в каких случаях имеет место наследование типа, а в каких наследование

(a) Queue : List

// очередь : список

(b)EncryptedString : String // зашифрованная строка : строка

(c)Gif : FileFormat

(d)

Circle

:

Point

// окружность : точка

(e)

Dqueue

:

Queue, List

 

реализации:

(f) DrawableGeom : Geom, Canvas // рисуемая фигура : фигура, холст

Упражнение 18.7

Замените член IntArray в реализации PeekbackStack (см. раздел 18.3.1) на класс deque из стандартной библиотеки. Напишите небольшую программу для тестирования.

Упражнение 18.8

Сравните композицию по ссылке с композицией по значению, приведите примеры их использования.

18.4. Область видимости класса и наследование

У каждого класса есть собственная область видимости, в которой определены имена членов и вложенные типы (см. разделы 13.9 и 13.10). При наследовании область

видимости производного класса вкладывается в область видимости непосредственного базового. Если имя не удается разрешить в области видимости производного класса, то поиск определения продолжается в области видимости базового.

С++ для начинающих

946

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

class ZooAnimal { public:

ostream &print( ostream& ) const;

// сделаны открытыми только ради демонстрации разных случаев string is_a;

int ival; private:

double dval;

определение класса ZooAnimal:

};

class Bear : public ZooAnimal { public:

ostream &print( ostream& ) const;

// сделаны открытыми только ради демонстрации разных случаев string name;

int ival;

иупрощенное определение производного класса Bear:

};

Bear bear;

Когда мы пишем: bear.is_a;

то имя разрешается следующим образом:

bear это объект класса Bear. Сначала поиск имени is_a ведется в области видимости Bear. Там его нет.

Поскольку класс Bear производный от ZooAnimal, то далее поиск is_a ведется в области видимости последнего. Обнаруживается, что имя принадлежит его члену. Разрешение закончилось успешно.

Хотя к членам базового класса можно обращаться напрямую, как к членам производного, они сохраняют свою принадлежность к базовому классу. Как правило, не имеет значения, в каком именно классе определено имя. Но это становится важным, если в базовом и производном классах есть одноименные члены. Например, когда мы пишем:

bear.ival;

С++ для начинающих

947

ival это член класса Bear, найденный на первом шаге описанного выше процесса разрешения имени.

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

bear.ZooAnimal::ival;

Тем самым мы говорим компилятору, что объявление ival следует искать в области видимости класса ZooAnimal.

Проиллюстрируем использование оператора разрешения области видимости на несколько абсурдном примере (надеемся, вы никогда не напишете чего-либо подобного в реальном

int ival;

int Bear::mumble( int ival )

{

return ival + // обращение к параметру

::ival + // обращение к глобальному объекту

ZooAnimal::ival + Bear::ival;

коде):

}

Неквалифицированное обращение к ival разрешается в пользу формального параметра. (Если бы переменная ival не была определена внутри mumble(), то имел бы место доступ к члену класса Bear. Если бы ival не была определена и в Bear, то подразумевался бы член ZooAnimal. А если бы ival не было и там, то речь шла бы о глобальном объекте.)

Разрешение имени члена класса всегда предшествует выяснению того, является ли обращение к нему корректным. На первый взгляд, это противоречит интуиции.

int dval;

int Bear::mumble( int ival )

{

// ошибка: разрешается в пользу закрытого члена ZooAnimal::dval return ival + dval;

Например, изменим реализацию mumble():

}

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

(a)Определено ли dval в локальной области видимости функции-члена класса Bear? Нет.

(b)Определено ли dval в области видимости Bear? Нет.

С++ для начинающих

948

(c)Определено ли dval в области видимости ZooAnimal? Да. Обращение разрешается в пользу этого имени.

После того как имя разрешено, компилятор проверяет, возможен ли доступ к нему. В данном случае нет: dval является закрытым членом, и прямое обращение к нему из mumble() запрещено. Правильное (и, возможно, имевшееся в виду) разрешение требует явного употребления оператора разрешения области видимости:

return ival + ::dval; // правильно

Почему же имя члена разрешается перед проверкой уровня доступа? Чтобы

предотвратить тонкие изменения семантики программы в связи с совершенно независимым, казалось бы, изменением уровня доступа к члену. Рассмотрим, например,

int dval;

int Bear::mumble( int ival )

{

foo( dval ); // ...

такой вызов:

}

Если бы функция foo() была перегруженной, то перемещение члена ZooAnimal::dval

из закрытой секции в защищенную вполне могло бы изменить всю последовательность вызовов внутри mumble(), а разработчик об этом даже и не подозревал бы.

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

вызова члена базового класса необходимо применить оператор разрешения области

ostream& Bear::print( ostream &os) const

{

// вызывается ZooAnimal::print(os) ZooAnimal::print( os );

os << name; return os;

видимости:

}

18.4.1. Область видимости класса при множественном наследовании

Как влияет множественное наследование на алгоритм просмотра области видимости класса? Все непосредственные базовые классы просматриваются одновременно, что может приводить к неоднозначности в случае, когда в нескольких из них есть одноименные члены. Рассмотрим на нескольких примерах, как возникает неоднозначность и какие меры можно предпринять для ее устранения. Предположим, есть следующий набор классов:

С++ для начинающих

949

class Endangered { public:

ostream& print( ostream& ) const; void highlight();

// ...

};

class ZooAnimal { public:

bool onExhibit() const;

//...

private:

bool highlight( int zoo_location );

//...

};

class Bear : public ZooAnimal { public:

ostream& print( ostream& ) const; void dance( dance_type ) const; // ...

};

class Panda : public Bear, public Endangered { public:

void cuddle() const; // ...

Panda объявляется производным от двух классов:

};

Хотя при наследовании функций print() и highlight() из обоих базовых классов Bear и Endangered имеется потенциальная неоднозначность, сообщение об ошибке не выдается до момента явно неоднозначного обращения к любой из этих функций.

В то время как неоднозначность двух унаследованных функций print() очевидна с первого взгляда, наличие конфликта между членами highlight() удивляет (ради этого пример и составлялся): ведь у них разные уровни доступа и разные прототипы. Более того, экземпляр из Endangered это член непосредственного базового класса, а из ZooAnimal член класса, стоящего на две ступеньки выше в иерархии.

Однако все это не имеет значения (впрочем, как мы скоро увидим, может иметь, но в случае виртуального наследования). Bear наследует закрытую функцию-член highlight() из ZooAnimal; лексически она видна, хотя вызывать ее из Bear или Panda запрещено. Значит, Panda наследует два лексически видимых члена с именем highlight,

поэтому любое неквалифицированное обращение к этому имени приводит к ошибке компиляции.

Поиск имени начинается в ближайшей области видимости, объемлющей его вхождение. Например, в коде

С++ для начинающих

950

int main()

{

Panda yin_yang;

yin_yang.dance( Bear::macarena );

}

ближайшей будет область видимости класса Panda, к которому принадлежит yin_yang.

void Panda::mumble()

{

dance( Bear::macarena ); // ...

Если же мы напишем:

}

то ближайшей будет локальная область видимости функции-члена mumble(). Если объявление dance в ней имеется, то разрешение имени на этом благополучно завершится.

Впротивном случае поиск будет продолжен в объемлющих областях видимости.

Вслучае множественного наследования имитируется одновременный просмотр всех поддеревьев наследования в нашем случае это класс Endangered и поддерево Bear/ZooAnimal. Если объявление обнаружено только в поддереве одного из базовых классов, то разрешение имени заканчивается успешно, как, например, при таком вызове

// правильно: Bear::dance()

dance():

yin_yang.dance( Bear::macarena );

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

int main()

{

//ошибка: неоднозначность: одна из

//Bear::print( ostream& ) const

//Endangered::print( ostream& ) const Panda yin_yang;

yin_yang.print( cout );

неквалифицированном обращении к print():

}

На уровне программы в целом для разрешения неоднозначности достаточно явно квалифицировать имя нужной функции-члена с помощью оператора разрешения области видимости:

С++ для начинающих

951

int main()

{

// правильно, но не лучшее решение

Panda yin_yang; yin_yang.Bear::print( cout );

}

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

inline void Panda::highlight() { Endangered::highlight();

}

inline ostream&

Panda::print( ostream &os ) const

{

Bear::print( os ); Endangered::print( os ); return os;

требуемое поведение:

}

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

Упражнение 18.9 Дана следующая иерархия классов: