Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Введение в С++. Страуструп..doc
Скачиваний:
62
Добавлен:
02.05.2014
Размер:
3.46 Mб
Скачать

1.5.2 Проверка типа

Необходимость контроля типа при обращениях к виртуальным функциям

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

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

Непосредственно в С++ это сделать нельзя. Однако, используя шаблоны типа и

наследование, можно приблизиться к той эффективности и простоте

проектирования и использования библиотек, которые свойственны языкам с

динамическим контролем типов. К таким языкам относится, например, язык

Smalltalk, на котором можно описать "стек чего-угодно". Рассмотрим

определение стека с помощью шаблона типа:

template < class T > class stack

{

T * p;

int sz;

public:

stack ( int );

~stack ();

void push ( T );

T & pop ();

};

Не ослабляя статического контроля типов, можно использовать такой стек

для хранения указателей на объекты типа plane (самолет):

stack < plane * > cs ( 200 );

void f ()

{

cs.push ( new Saab900 ); // Ошибка при трансляции :

// требуется plane*, а передан car*

cs.push ( new Saab37B );

// прекрасно: Saab 37B - на самом

// деле самолет, т.е. типа plane

cs.pop () -> takeoff ();

cs.pop () -> takeoff ();

}

Если статического контроля типов нет, приведенная выше ошибка

обнаружится только при выполнении программы:

// пример динамическое контроля типа

// вместо статического; это не С++

Stack s; // стек может хранить указатели на объекты

// произвольного типа

void f ()

{

s.push ( new Saab900 );

s.push ( new Saab37B );

s.pop () -> takeoff (); // прекрасно: Saab 37B - самолет

cs.pop () -> takeoff (); // динамическая ошибка:

// машина не может взлететь

}

Для способа определения, допустима ли операция над объектом, обычно

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

виртуальных функций в С++.

Рассчитывая на статический контроль типов и вызов виртуальных функций,

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

динамический контроль типов. Класс в С++ задает строго определенный

интерфейс для множества объектов этого и любого производного класса, тогда

как в Smalltalk класс задает только минимально необходимое число операций,

и пользователь вправе применять незаданные в классе операции. Иными

словами, класс в С++ содержит точное описание операций, и пользователю

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

1.5.3 Множественное наследование

Если класс 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 более подробно обсуждаются возможности языка

по инкапсуляции.