Программирование >>  Поддержка объектно-ориентированного программирования 

1 ... 113 114 115 [ 116 ] 117 118 119 120


Отметим, что в действительности трудность возникла лишь потому, что у обеих функций draw() одинаковый тип параметров. Если бы типы параметров различались, то обычные правила разрешения неоднозначности при перегрузке гарантировали бы, что трудностей не возникнет, несмотря на наличие различных функций с одним именем.

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

Функции-переходники CCowboy::draw() и WWindow draw являются виртуальными, и простая оптимизация с помощью подстановки невозможна. Однако, есть возможность, что транслятор распознает такие функции и удалит их из цепочки вызовов.

Интерфейсные функции служат для приспособления интерфейса к запросам пользователя. Благодаря им в интерфейсе собираются операции, разбросанные по всей программе. Обратимся к классу vector из $$1 .4. Для таких векторов, как и для массивов, индекс отсчитывается от нуля. Если пользователь хочет работать с диапазоном индексов, отличным от диапазона 0..size-1, нужно сделать соответствующие приспособления, например, такие:

void f()

vector v(10); диапазон [0:9] как будто v в диапазоне [1:10]: for (int i = 1; i<=10; {

v[i-1] = ... не забыть пересчитать индекс

...

Лучшее решение дает класс vec c произвольными границами индекса:

class vec : public vector {

int lb;

public:

vec(int low, int high) : vector(high-low+1) { lb=low; } int& operator[](int i)

{ return vector::operator[](i-lb); } int low() { return lb; } int high() { return lb+size() - 1; }

Класс vec можно использовать без дополнительных операций, необходимых в первом примере:

void g()

vec v( 1 , 1 0); диапазон [ 1 : 1 0] for (int i = 1; i<=10; {

v[i] = ...



Очевидно, вариант с классом vec нагляднее и безопаснее.

Интерфейсные классы имеют и другие важные области применения, например, интерфейс между программами на С++ и программами на другом языке ($$1 2.1 .4) или интерфейс с особыми библиотеками С++.

13.9 Управляющие классы

Концепция абстрактного класса дает эффективное средство для разделения интерфейса и его реализации. Мы применяли эту концепцию и получали постоянную связь между интерфейсом, заданным абстрактным типом, и реализацией, представленной конкретным типом. Так, невозможно переключить абстрактный итератор с одного класса-источника на другой, например, если исчерпано множество (класс set), невозможно перейти на потоки.

Далее, пока мы работаем с объектами абстрактного типа с помощью указателей или ссылок, теряются все преимущества виртуальных функций. Программа пользователя начинает зависеть от конкретных классов реализации. Действительно, не зная размера объекта, даже при абстрактном типе нельзя разместить объект в стеке, передать как параметр по значению или разместить как статический. Если работа с объектами организована через указатели или ссылки, то задача распределения памяти перекладывается на пользователя ($$1 3.1 0).

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

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

управляющая часть содержательная часть

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

Теперь вернемся к абстрактному типу set из $$1 3.3. Как можно определить управляющий класс для этого типа, и какие это даст плюсы и минусы? Для данного класса set можно определить управляющий класс просто перегрузкой операции ->:

class set handle { set* rep; public:

set* operator->() { return rep; } set handler(set* pp) : rep(pp) { }

Это не слишком влияет на работу с множествами, просто передаются объекты типа set handle вместо объектов типа set& или set*, например:

void my(set handle s)

for (T* p = s->first(); p; p = s->next())

...



void user()

set handle sl(new slist set);

set handle v(new vector set v(100));

my(sl);

your(v);

my(v);

your(sl);

Если классы set и set handle разрабатывались совместно,легко реализовать подсчет числа создаваемых множеств:

class set {

friend class set handle; protected:

int handle count; public:

virtual void insert(T*) = 0; virtual void remove(T*) = 0; virtual int is member(T*) = 0; virtual T* first() = 0; virtual T* next() = 0; set() : handle count(0) { }

Чтобы подсчитать число объектов данного типа set, в управляющем классе нужно увеличивать или уменьшать значение счетчика set handle:

class set handle { set* rep; public:

set* operator->() { return rep; } set handle(set* pp)

: rep(pp) { pp->handle count++; } set handle(const set handle& r)

: rep(r.rep) { rep->handle count++; } set handle& operator=(const set handle& r)

rep->handle count++;

if (--rep->handle count == 0) delete rep; rep = r.rep; return *this;

~set handle()

{ if (--rep->handle count == 0) delete rep; }

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

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

void your(set handle s)

for (T* p = s->first(); p; p = s->next())



1 ... 113 114 115 [ 116 ] 117 118 119 120

© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки.
Яндекс.Метрика