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



Pdf көрінісі
бет249/256
Дата11.07.2022
өлшемі2,87 Mb.
#37591
1   ...   245   246   247   248   249   250   251   252   ...   256
13.8 Интерфейсные классы 
Про один из самых важных видов классов обычно забывают - это "скромные" интерфейсные классы. 
Такой класс не выполняет какой-то большой работы, ведь иначе, его не называли бы интерфейсным. 
Задача интерфейсном класса приспособить некоторую полезную функцию к определенному контексту. 
Достоинство интерфейсных классов в том, что они позволяют совместно использовать полезную 
функцию, не загоняя ее в жесткие рамки. Действительно, невозможно рассчитывать, что функция 
сможет сама по себе одинаково хорошо удовлетворить самые разные запросы.
Интерфейсный класс в чистом виде даже не требует генерации кода. Вспомним описание шаблона типа 
Splist из $$8.3.2: 
template 
class Splist : private Slist
public: 
void insert(T* p) { Slist::insert(p); } 
void append(T* p) { Slist::append(p); } 
T* get() { return (T*) Slist::get(); } 
}; 
Класс Splist преобразует список ненадежных обобщенных указателей типа void* в более удобное 
семейство надежных классов, представляющих списки. Чтобы применение интерфейсных классов не 
было слишком накладно, нужно использовать функции-подстановки. В примерах, подобных 
приведенному, где задача функций-подстановок только подогнать тип, накладные расходы в памяти и 
скорости выполнения программы не возникают. 
Естественно, можно считать интерфейсным абстрактный базовый класс, который представляет 


Бьерн Страуструп.
Язык программирования С++ 
 
356 
абстрактный тип, реализуемый конкретными типами ($$13.3), также как и управляющие классы из 
раздела 13.9. Но здесь мы рассматриваем классы, у которых нет иных назначений - только задача 
адаптации интерфейса. 
Рассмотрим задачу слияния двух иерархий классов с помощью множественного наследования. Как 
быть в случае коллизии имен, т.е. ситуации, когда в двух классах используются виртуальные функции с 
одним именем, производящие совершенно разные операции? Пусть есть видеоигра под названием 
"Дикий запад", в которой диалог с пользователем организуется с помощью окна общего вида (класс 
Window): 
class Window { 
// 
... 
virtual void draw(); 
}; 
class Cowboy { 
// 
... 
virtual void draw(); 
}; 
class CowboyWindow : public Cowboy, public Window { 
// 
... 
}; 
В этой игре класс CowboyWindow представляет движение ковбоя на экране и управляет 
взаимодействием игрока с ковбоем. Очевидно, появится много полезных функций, определенных в 
классе Window и Cowboy, поэтому предпочтительнее использовать множественное наследование, чем 
описывать Window или Cowboy как члены. Хотелось бы передавать этим функциям в качестве 
параметра объект типа CowboyWindow, не требуя от программиста указания каких-то спецификаций 
объекта. Здесь как раз и возникает вопрос, какую функции выбрать для CowboyWindow: Cowboy::draw() 
или Window::draw(). 
В классе CowboyWindow может быть только одна функция с именем draw(), но поскольку полезная 
функция работает с объектами Cowboy или Window и ничего не знает о CowboyWindow, в классе 
CowboyWindow должны подавляться (переопределяться) и функция Cowboy::draw(), и функция 
Window_draw(). Подавлять обе функции с помощью одной - draw() неправильно, поскольку, хотя 
используется одно имя, все же все функции draw() различны и не могут переопределяться одной. 
Наконец, желательно, чтобы в классе CowboyWindow наследуемые функции Cowboy::draw() и 
Window::draw() имели различные однозначно заданные имена. 
Для решения этой задачи нужно ввести дополнительные классы для Cowboy и Window. Вводится два 
новых имени для функций draw() и гарантируется, что их вызов в классах Cowboy и Window приведет к 
вызову функций с новыми именами: 
class CCowboy : public Cowboy { 
virtual int cow_draw(int) = 0; 
void draw() { cow_draw(i); } // 
переопределение Cowboy::draw 
}; 
class WWindow : public Window { 
virtual int win_draw() = 0; 
void draw() { win_draw(); } // 
переопределение Window::draw 
}; 
Теперь с помощью интерфейсных классов CCowboy и WWindow можно определить класс 
CowboyWindow и сделать требуемые переопределения функций cow_draw() и win_draw: 
class CowboyWindow : public CCowboy, public WWindow { 
// 
... 
void 
cow_draw(); 
void 
win_draw(); 
}; 


Бьерн Страуструп.
Язык программирования С++ 
 
357 
Отметим, что в действительности трудность возникла лишь потому, что у обеих функций 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; i++) { 
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,10); // диапазон [1:10] 
for (int i = 1; i<=10; i++) { 
v[i] 

... 

// 
... 



Бьерн Страуструп.
Язык программирования С++ 
 
358 
Очевидно, вариант с классом vec нагляднее и безопаснее. 
Интерфейсные классы имеют и другие важные области применения, например, интерфейс между 
программами на С++ и программами на другом языке ($$12.1.4) или интерфейс с особыми 
библиотеками С++. 


Достарыңызбен бөлісу:
1   ...   245   246   247   248   249   250   251   252   ...   256




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

    Басты бет