Если класс A является базовым классом для B, то B наследует атрибуты
A. т.е. B содержит A плюс еще что-то. С учетом этого становится очевидно,
что хорошо, когда класс B может наследовать из двух базовых классов A1 и
A2. Это называется множественным наследованием.
Приведем некий типичный пример множественного наследования. Пусть есть
два библиотечных класса displayed и task. Первый представляет задачи,
информация о которых может выдаваться на экран с помощью некоторого
монитора, а второй - задачи, выполняемые под управлением некоторого
диспетчера. Программист может создавать собственные классы, например,
такие:
class my_displayed_task: public displayed, public task
{
// текст пользователя
};
class my_task: public task {
// эта задача не изображается
// на экране, т.к. не содержит класс displayed
// текст пользователя
};
class my_displayed: public displayed
{
// а это не задача
// т.к. не содержит класс task
// текст пользователя
};
Если наследоваться может только один класс, то пользователю доступны
только два из трех приведенных классов. В результате либо получается
дублирование частей программы, либо теряется гибкость, а, как правило,
происходит и то, и другое. Приведенный пример проходит в С++ безо всяких
дополнительных расходов времени и памяти по сравнению с программами, в
которых наследуется не более одного класса. Статический контроль типов от
этого тоже не страдает.
Все неоднозначности выявляются на стадии трансляции:
class task
{
public:
void trace ();
// ...
};
class displayed
{
public:
void trace ();
// ...
};
class my_displayed_task:public displayed, public task
{
// в этом классе trace () не определяется
};
void g ( my_displayed_task * p )
{
p -> trace (); // ошибка: неоднозначность
}
В этом примере видны отличия С++ от объектно-ориентированных диалектов
языка Лисп, в которых есть множественное наследование. В этих диалектах
неоднозначность разрешается так: или считается существенным порядок
описания, или считаются идентичными объекты с одним и тем же именем в
разных базовых классах, или используются комбинированные способы, когда
совпадение объектов доля базовых классов сочетается с более сложным
способом для производных классов. В С++ неоднозначность, как правило,
разрешается введением еще одной функции:
class my_displayed_task:public displayed, public task
{
// ...
public:
void trace ()
{
// текст пользователя
displayed::trace (); // вызов trace () из displayed
task::trace (); // вызов trace () из task
}
// ...
};
void g ( my_displayed_task * p )
{
p -> trace (); // теперь нормально
}
1.5.4 Инкапсуляция
Пусть члену класса (неважно функции-члену или члену, представляющему
данные) требуется защита от "несанкционированного доступа". Как разумно
ограничить множество функций, которым такой член будет доступен? Очевидный
ответ для языков, поддерживающих объектно-ориентированное
программирование, таков: доступ имеют все операции, которые определены для
этого объекта, иными словами, все функции-члены. Например:
class window
{
// ...
protected:
Rectangle inside;
// ...
};
class dumb_terminal : public window
{
// ...
public:
void prompt ();
// ...
};
Здесь в базовом классе window член inside типа Rectangle описывается
как защищенный (protected), но функции-члены производных классов,
например, dumb_terminal::prompt(), могут обратиться к нему и выяснить, с
какого вида окном они работают. Для всех других функций член
window::inside недоступен.
В таком подходе сочетается высокая степень защищенности
(действительно, вряд ли вы "случайно" определите производный класс) с
гибкостью, необходимой для программ, которые создают классы и используют
их иерархию (действительно, "для себя" всегда можно в производных классах
предусмотреть доступ к защищенным членам).
Неочевидное следствие из этого: нельзя составить полный и
окончательный список всех функций, которым будет доступен защищенный член,
поскольку всегда можно добавить еще одну, определив ее как функцию-член в
новом производном классе. Для метода абстракции данных такой подход часто
бывает мало приемлемым. Если язык ориентируется на метод абстракции
данных, то очевидное для него решение - это требование указывать в
описании класса список всех функций, которым нужен доступ к члену. В С++
для этой цели используется описание частных (private) членов. Оно
использовалось и в приводившихся описаниях классов complex и shape.
Важность инкапсуляции, т.е. заключения членов в защитную оболочку,
резко возрастает с ростом размеров программы и увеличивающимся разбросом
областей приложения. В $$6.6 более подробно обсуждаются возможности языка
по инкапсуляции.
1.6 Пределы совершенства
Язык С++ проектировался как "лучший С", поддерживающий абстракцию
данных и объектно-ориентированное программирование. При этом он должен
быть пригодным для большинства основных задач системного программирования.
Основная трудность для языка, который создавался в расчете на методы
упрятывания данных, абстракции данных и объектно-ориентированного
программирования, в том, что для того, чтобы быть языком общего
назначения, он должен:
- идти на традиционных машинах;
- сосуществовать с традиционными операционными системами и языками;
- соперничать с традиционными языками программирования в эффективности
выполнения программы;
- быть пригодным во всех основных областях приложения.
Это значит, что должны быть возможности для эффективных числовых
операций (арифметика с плавающей точкой без особых накладных расходов,
иначе пользователь предпочтет Фортран) и средства такого доступа к памяти,
который позволит писать на этом языке драйверы устройств. Кроме того, надо
уметь писать вызовы функций в достаточно непривычной записи, принятой для
обращений в традиционных операционных системах. Наконец, должна быть
возможность из языка, поддерживающего объектно-ориентированное
программирование, вызывать функции, написанные на других языках, а из
других языков вызывать функцию на этом языке, поддерживающем
объектно-ориентированное программирование.
Далее, нельзя рассчитывать на широкое использование искомого языка
программирования как языка общего назначения, если реализация его целиком
полагается на возможности, которые отсутствуют в машинах с традиционной
архитектурой.
Если не вводить в язык возможности низкого уровня, то придется для
основных задач большинства областей приложения использовать некоторые
языки низкого уровня, например С или ассемблер. Но С++ проектировался с
расчетом, что в нем можно сделать все, что допустимо на С, причем без
увеличения времени выполнения. Вообще, С++ проектировался, исходя из
принципа, что не должно возникать никаких дополнительных затрат времени и
памяти, если только этого явно не пожелает сам программист.
Язык проектировался в расчете на современные методы трансляции,
которые обеспечивают проверку согласованности программы, ее эффективность
и компактность представления. Основным средством борьбы со сложностью
программ видится, прежде всего, строгий контроль типов и инкапсуляция.
Особенно это касается больших программ, создаваемых многими людьми.
Пользователь может не являться одним из создателей таких программ, и может
вообще не быть программистом. Поскольку никакую настоящую программу
нельзя написать без поддержки библиотек, создаваемых другими
программистами, последнее замечание можно отнести практически ко всем
программам.
С++ проектировался для поддержки того принципа, что всякая программа
есть модель некоторых существующих в реальности понятий, а класс является
конкретным представлением понятия, взятого из области приложения ($$12.2).
Поэтому классы пронизывают всю программу на С++, и налагаются жесткие
требования на гибкость понятия класса, компактность объектов класса и
эффективность их использования. Если работать с классами будет неудобно
или слишком накладно, то они просто не будут использоваться, и программы
выродятся в программы на "лучшем С". Значит пользователь не сумеет
насладиться теми возможностями, ради которых, собственно, и создавался
язык.
Достарыңызбен бөлісу: |