Бьерн Страуструп. Язык программирования С++ Второе дополненное издание



Pdf көрінісі
бет127/256
Дата11.07.2022
өлшемі2,87 Mb.
#37591
1   ...   123   124   125   126   127   128   129   130   ...   256
Байланысты:
Бьерн Страуструп. Язык программирования С . М Бином, 2011

6.5.3 Виртуальные базовые классы 
В предыдущих разделах множественное наследование рассматривалось как существенный фактор, 
позволяющий за счет слияния классов безболезненно интегрировать независимо создававшиеся 
программы. Это самое основное применение множественного наследования, и, к счастью (но не 
случайно), это самый простой и надежный способ его применения. 
Иногда применение множественного наследования предполагает достаточно тесную связь между 
классами, которые рассматриваются как "братские" базовые классы. Такие классы-братья обычно 
должны проектироваться совместно. В большинстве случаев для этого не требуется особый стиль 
программирования, существенно отличающийся от того, который мы только что рассматривали. Просто 
на производный класс возлагается некоторая дополнительная работа. Обычно она сводится к 
переопределению одной или нескольких виртуальных функций (см. $$13.2 и $$8.7). В некоторых 
случаях классы-братья должны иметь общую информацию. Поскольку С++ - язык со строгим контролем 
типов, общность информации возможна только при явном указании того, что является общим в этих 
классах. Способом такого указания может служить виртуальный базовый класс. 
Виртуальный базовый класс можно использовать для представления "головного" класса, который может 
конкретизироваться разными способами: 
class window { 
// головная информация 
virtual void draw(); 
}; 
Для простоты рассмотрим только один вид общей информации из класса window - функцию draw(). 
Можно определять разные более развитые классы, представляющие окна (window). В каждом 
определяется своя (более развитая) функция рисования (draw): 
class window_w_border : public virtual window { 
// класс "окно с рамкой" 
// определения, связанные с рамкой 
void 
draw(); 
}; 


Бьерн Страуструп.
Язык программирования С++ 
 
168 
class window_w_menu : public virtual window { 
// класс "окно с меню" 
// определения, связанные с меню 
void 
draw(); 
}; 
Теперь хотелось бы определить окно с рамкой и меню: 
class window_w_border_and_menu 
: public virtual window, 
public 
window_w_border, 
public window_w_menu { 
// класс "окно с рамкой и меню" 
void 
draw(); 
}; 
Каждый производный класс добавляет новые свойства окна. Чтобы воспользоваться комбинацией всех 
этих свойств, мы должны гарантировать, что один и тот же объект класса window используется для 
представления вхождений базового класса window в эти производные классы. Именно это обеспечивает 
описание window во всех производных классах как виртуального базового класса. 
Можно следующим образом изобразить состав объекта класса window_w_border_and_menu: 
Чтобы увидеть разницу между обычным и виртуальным наследованием, сравните этот рисунок с 
рисунком из $$6.5, показывающим состав объекта класса satellite. В графе наследования каждый 
базовый класс с данным именем, который был указан как виртуальный, будет представлен 
единственным объектом этого класса. Напротив, каждый базовый класс, который при описании 
наследования не был указан как виртуальный, будет представлен своим собственным объектом. 
Теперь надо написать все эти функции draw(). Это не слишком трудно, но для неосторожного 
программиста здесь есть ловушка. Сначала пойдем самым простым путем, который как раз к ней и 
ведет: 
void window_w_border::draw() 

window::draw(); 
// 
рисуем рамку 

void window_w_menu::draw() 

window::draw(); 
// 
рисуем меню 

Пока все хорошо. Все это очевидно, и мы следуем образцу определения таких функций при условии 
единственного наследования ($$6.2.1), который работал прекрасно. Однако, в производном классе 
следующего уровня появляется ловушка: 
void window_w_border_and_menu::draw() // 
ловушка! 

window_w_border::draw(); 
window_w_menu::draw(); 
// теперь операции, относящиеся только 
// к окну с рамкой и меню 

На первый взгляд все вполне нормально. Как обычно, сначала выполняются все операции, 
необходимые для базовых классов, а затем те, которые относятся собственно к производным классам. 
Но в результате функция window::draw() будет вызываться дважды! Для большинства графических 
программ это не просто излишний вызов, а порча картинки на экране. Обычно вторая выдача на экран 
затирает первую. 
Чтобы избежать ловушки, надо действовать не так поспешно. Мы отделим действия, выполняемые 


Бьерн Страуструп.
Язык программирования С++ 
 
169 
базовым классом, от действий, выполняемых из базового класса. Для этого в каждом классе введем 
функцию _draw(), которая выполняет нужные только для него действия, а функция draw() будет 
выполнять те же действия плюс действия, нужные для каждого базового класса. Для класса window 
изменения сводятся к введению излишней функции: 
class window { 
// головная информация 
void 
_draw(); 
void 
draw(); 
}; 
Для производных классов эффект тот же: 
class window_w_border : public virtual window { 
// класс "окно с рамкой" 
// определения, связанные с рамкой 
void 
_draw(); 
void 
draw(); 
}; 
void window_w_border::draw() 

window::_draw(); 
_draw(); // 
рисует рамку 
}; 
Только для производного класса следующего уровня проявляется отличие функции, которое и 
позволяет обойти ловушку с повторным вызовом window::draw(), поскольку теперь вызывается 
window::_draw() и только один раз: 
class window_w_border_and_menu 
: public virtual window, 
public 
window_w_border, 
public window_w_menu { 
void 
_draw(); 
void 
draw(); 
}; 
void window_w_border_and_menu::draw() 

window::_draw(); 
window_w_border::_draw(); 
window_w_menu::_draw(); 
_draw();
// теперь операции, относящиеся только 
// к окну с рамкой и меню 

Не обязательно иметь обе функции window::draw() и window::_draw(), но наличие их позволяет избежать 
различных простых описок. 
В этом примере класс window служит хранилищем общей для window_w_border и window_w_menu 
информации и определяет интерфейс для общения этих двух классов. Если используется 
единственное наследование, то общность информации в дереве классов достигается тем, что эта 
информация передвигается к корню дерева до тех пор, пока она не станет доступна всем 
заинтересованным в ней узловым классам. В результате легко возникает неприятный эффект: корень 
дерева или близкие к нему классы используются как пространство глобальных имен для всех классов 
дерева, а иерархия классов вырождается в множество несвязанных объектов. 
Существенно, чтобы в каждом из классов-братьев переопределялись функции, определенные в общем 
виртуальном базовом классе. Таким образом каждый из братьев может получить свой вариант 
операций, отличный от других. Пусть в классе window есть общая функция ввода get_input(): 
class window { 


Бьерн Страуструп.
Язык программирования С++ 
 
170 
// головная информация 
virtual void draw(); 
virtual void get_input(); 
}; 
В одном из производных классов можно использовать эту функцию, не задумываясь о том, где она 
определена: 
class window_w_banner : public virtual window { 
// класс "окно с заголовком" 
void 
draw(); 
void 
update_banner_text(); 
}; 
void window_w_banner::update_banner_text() 

// 
... 
get_input(); 
// изменить текст заголовка 

В другом производном классе функцию get_input() можно определять, не задумываясь о том, кто ее 
будет использовать: 
class window_w_menu : public virtual window { 
// класс "окно с меню" 
// определения, связанные с меню 
void 
draw(); 
void get_input(); // 
переопределяет window::get_input() 
}; 
Все эти определения собираются вместе в производном классе следующего уровня: 
class window_w_banner_and_menu 
: public virtual window, 
public 
window_w_banner, 
public 
window_w_menu 

void 
draw(); 
}; 
Контроль неоднозначности позволяет убедиться, что в классах-братьях определены разные функции: 
class window_w_input : public virtual window { 
// 
... 
void 
draw(); 
void get_input(); // 
переопределяет window::get_input 
}; 
class window_w_input_and_menu 
: public virtual window, 
public 
window_w_input, 
public 
window_w_menu 
{ // ошибка: оба класса window_w_input и 
// window_w_menu переопределяют функцию 
// window::get_input 
void 
draw(); 
}; 
Транслятор обнаруживает подобную ошибку, а устранить неоднозначность можно обычным способом: 
ввести в классы window_w_input и window_w_menu функцию, переопределяющую "функцию-
нарушителя", и каким-то образом устранить неоднозначность: 
class window_w_input_and_menu 


Бьерн Страуструп.
Язык программирования С++ 
 
171 
: public virtual window, 
public 
window_w_input, 
public 
window_w_menu 

void 
draw(); 
void 
get_input(); 
}; 
В этом классе window_w_input_and_menu::get_input() будет переопределять все функции get_input(). 
Подробно механизм разрешения неоднозначности описан в $$R.10.1.1. 


Достарыңызбен бөлісу:
1   ...   123   124   125   126   127   128   129   130   ...   256




©emirsaba.org 2024
әкімшілігінің қараңыз

    Басты бет