Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Объектно-ориентированное программирование.PDF
Скачиваний:
208
Добавлен:
01.05.2014
Размер:
3.64 Mб
Скачать

converted to PDF by BoJIoc

класса. Далее при определении конкретного объекта параметр-тип связывается с реальным типом данных. Например, связный список может быть описан в языке C++ следующим образом:

template class List

{

public:

void add(T);

T firstElement(); // поля данных

T value;

List * nextElement;

};

В этом примере идентификатор T используется как обозначение типа. Каждый экземпляр класса List содержит значение типа T и указатель на следующий элемент списка. Функция-член add добавляет новый элемент в список. Первый элемент в списке возвращается функцией firstElement.

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

List aList;

List bList;

Функции (включая функции-члены класса) также могут иметь описания-шаблоны. Ниже приводится описание функции, которая находит количество элементов в списке независимо от его типа.

template int length (List & aList)

{

if (aList == 0) return 0;

return 1 + length(aList.nextElement);

}

В С++ функции-шаблоны интенсивно используются в стандартной библиотеке шаблонов, которая описывается в главе 16.

14.9. Полиморфизм в различных языках

В этом разделе обсуждаются механизмы реализации полиморфизма в различных языках программирования.

14.9.1. Полиморфизм в C++

Полиморфизм часто является источником затруднений для изучающих C++. Поэтому остановимся на этом вопросе подробнее.

Полиморфные переменные

Как мы отметили в главе 10, в языке C++ истинные полиморфные переменные возникают только при использовании указателей или ссылок. Когда настоящей переменной (то есть не указателю и не ссылке) присваивается значение типа подкласса, то динамический класс значения вынужденно приводится так, чтобы совпадать со статическим типом переменной.

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

class One

converted to PDF by BoJIoc

{

public:

virtual int value()

{

return 1;

}

};

class Two : public One

{

public:

virtual int value()

{

return 2;

}

};

Класс One описывает виртуальный метод, который возвращает значение 1. Этот метод переопределяется в классе Two на метод, возвращающий значение 2.

Определяются следующие функции:

void directAssign (One x)

{

printf("by assignment value is %d\n", x.value());

}

void byPointer (One * x)

{

printf("by pointer value is %d\n", x->value());

}

void byReference (One & x)

{

printf("by reference value is %d\n", x.value());

}

Эти функции используют в качестве аргумента значение класса One, которое передается соответственно по значению, через указатель и через ссылку. При выполнении этих функций с аргументом класса Two для первой функции параметр преобразуется к классу One, и в результате будет напечатано значение 1. Две другие функции допускают полиморфный аргумент. В обоих случаях переданное значение сохранит свой динамический тип данных, и напечатано будет значение 2.

Виртуальное и невиртуальное переопределение

Приводящий в замешательство аспект переопределения методов в языке C++ — это разница между переопределением виртуального и невиртуального методов. Как мы отмечали в главе 11, ключевое слово virtual не является необходимым для того, чтобы происходило переопределение. Однако семантический смысл сильно меняется в зависимости от того, используется это слово или нет. Если удалить ключевое слово virtual из описания метода в классе One в предыдущем примере (даже если его сохранить в классе Two), то результат «1» будет напечатан для всех трех функций.

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

converted to PDF by BoJIoc

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

virtual void display (char *, int);

Подкласс пытается переопределить метод: virtual void display (char *, short);

Поскольку списки аргументов различаются, то второе определение не распознается как переопределение. Это приводит к тому, что виртуальное переопределение рассматривается как обычное (невиртуальное) определение метода. Поэтому, например, при вызове в форме родительского типа будет выбираться первый метод, а не второй. Такие ошибки чрезвычайно трудноуловимы, поскольку обе формы записи допустимы и надеяться на диагностику компилятора не приходится.

Возникает естественный вопрос: почему разрешены обе формы записи и почему не всякое переопределение виртуально? Имеются по крайней мере два правдоподобных объяснения. Первое состоит в том, что за редкими исключениями невиртуальная форма переопределения это именно то, что хочет программист, и обязательная виртуальность была бы только помехой. Второе и более убедительное соображение относится к эффективности. Виртуальное переопределение всегда более затратно с точки зрения выполнения программы, чем невиртуальное. Принцип, проходящий красной нитью через весь C++, состоит в том, что если какое-то свойство языка не востребовано или же используется только для конкретной проблемы, то программист не должен расплачиваться (на этапе выполнения) за его существование. Соответственно, если виртуальное наследование не требуется или же используется лишь в специальном случае, никаких дополнительных накладных расходов быть не должно. Только в тех случаях, когда программист в явном виде утверждает, что ему требуется виртуальная функция, подключается дополнительная надстройка.

Параметрическая перегрузка

Язык C++ позволяет нескольким функциям иметь одно имя внутри любого контекста до тех пор, пока списки аргументов функций различаются в достаточной степени, чтобы компилятор недвусмысленно определял, какую именно функцию намереваются вызвать. Такая ситуация, как правило, возникает при использовании нескольких конструкторов для одного и того же класса, каждый из которых имеет свой набор аргументов. Однако таким образом могут описываться любые функции, методы или операции.

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

Отложенные методы в C++

В языке C++ отложенный метод (который здесь называется чисто виртуальным методом) должен быть описан в явном виде с ключевым словом virtual. Тело отложенного метода не определяется, вместо этого функции «присваивается» значение 0:

class Shape

{

public:

...

virtual void draw() = 0;

converted to PDF by BoJIoc

...

};

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

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

Обобщенные функции и шаблоны

Обобщенные функции и классы в языке C++ реализуются с помощью ключевого слова template (шаблон). Пример класса-шаблона был приведен выше. Классы-шаблоны и функции-шаблоны интенсивно используются стандартной библиотекой шаблонов языка C++, которую мы обсуждаем в главе 16.

14.9.2. Полиморфизм в Java

Язык Java поддерживает как иерархию подклассов (с ключевым словом extends), так и иерархию подтипов (с ключевым словом interfaces). Переменные могут быть объявлены или через класс, или через интерфейс. Все переменные являются полиморфными.

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

Отложенные методы реализуются в языке Java через ключевое слово abstract. Методы, описанные как abstract, не имеют тела; они оканчиваются точкой с запятой. Абстрактные методы должны переопределяться в подклассах. Класс, который включает в себя абстрактный метод, должен в свою очередь быть описан как абстрактный. Не разрешается создавать экземпляры абстрактных классов.

abstract class shape

{

//ниже должно переопределяться public abstract draw();

//...

}

class triangle extends shape

{

public draw()

{

//нарисовать треугольник

}

//...

}

Интересным свойством языка Java является модификатор final, который в некотором смысле противоположен ключевому слову abstract. Класс или метод, описанный как final, не может порождать подклассы или переопределяться.

14.9.3. Полиморфизм в Object Pascal

Полиморфные переменные

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