Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Роджерсон Д. - Основы COM - 2000.pdf
Скачиваний:
412
Добавлен:
13.08.2013
Размер:
2.4 Mб
Скачать

25

public:

virtual void Fy1() = 0; virtual void Fy2() = 0;

};

 

class CA : public IX, public IY

// Компонент

{

 

public:

 

// Реализация абстрактного базового класса IX virtual void Fx1() { cout << “Fx1” << endl; } virtual void Fx2() { cout << “Fx2” << endl; }

// Реализация абстрактного базового класса IY virtual void Fy1() { cout << “Fy1” << endl; } virtual void Fy2() { cout << “Fy2” << endl; }

};

IX и IY — это чисто абстрактные базовые классы, которые используются для реализации интерфейсов. Чисто абстрактный базовый класс (pure abstract base class) — это базовый класс, который содержит только чисто виртуальные функции (pure virtual functions). Чисто виртуальная функция — это виртуальная функция, «помеченная =0 — знаком спецификатора чистоты (pure specifier). Чисто виртуальные функции не реализуются в классах, в которых объявлены. Как видно из приведенного выше примера, функции IX::Fx1, IX::Fx2, IY::Fy1 и IY::Fy2 только декларируются. Реализуются же они в производном классе. В приведенном фрагменте кода компонент CA наследует два чисто абстрактных базовых класса — IX и IY — и реализует их чисто виртуальные функции.

Для того, чтобы реализовать функции-члены IX и IY, CA использует множественное наследование. Последнее означает, что класс является производным более чем от одного базового класса. Класс С++ чаще всего использует единичное наследование, т.е. имеет только один базовый класс. Далее в этой главе мы более подробно поговорим о множественных интерфейсах и множественном наследовании.

Абстрактный базовый класс напоминает канцелярский бланк, а производный класс заполняет этот бланк. Абстрактный базовый класс определяет функции, которые будет предоставлять производный класс, а производные классы реализуют их. Открытое (public) наследование от чисто абстрактного базового класса называется наследованием интерфейса (interface inheritance), так как производный класс наследует лишь описания функций. Абстрактный базовый класс не содержит никакой реализации, которую можно было бы унаследовать.

В этой книге все интерфейсы будут реализованы при помощи чисто абстрактных базовых классов. Поскольку СОМ не зависит от языка программирования, имеется двоичный стандарт того, что в этой модели считается интерфейсом. В последнем разделе данной главы — «Что за интерфейсом» — представлена эта структура. По счастью, многие компиляторы С++ автоматически генерируют в памяти верную структуру, если использовать чисто абстрактные базовые классы.

IX и IY не совсем интерфейсы в смысле СОМ. Чтобы стать настоящими интерфейсами, IX и IY должны наследовать специальный интерфейс IUnknown. Однако IUnknown — это предмет следующей главы, поэтому я не буду обсуждать его сейчас. До конца данной главы мы будем считать, что IX и IY — это интерфейсы СОМ.

Соглашения о кодировании

В своих программах я использую некоторые соглашения, чтобы отличать интерфейсы от других классов. Все имена интерфейсов начинаются с буквы «I». Так, «IX» следует читать «интерфейс X». Имена классов имеют префикс «C», и «CA» читается как «класс A».

Другое соглашение состоит в том, что вместо определения интерфейса как класса я использую следующее определение из заголовочного файла OBJBASE.H Microsoft Win32 Software Development Kit (SDK):

#define interface struct

Определение использует ключевое слово struct, а не class, поскольку члены структуры автоматически объявляются имеющими общий доступ, так что не требуется ключевое слово public. Это меньше загромождает код. Ниже повторно приводятся примеры интерфейсов, записанные теперь в рамках новых соглашений.

#include <objbase.h> interface IX

{

virtual void __stdcall Fx1() = 0; virtual void __stdcall Fx2() = 0;

};

26

interface IY

{

virtual void __stdcall Fy1() = 0; virtual void __stdcall Fy2() = 0;

}

Чтобы показать интерфейс на картинке, я использую прямоугольник с «разъемом для подключения» на одной из сторон. Пример дан на рис. 2-2.

Компонент

IX

IY

Рис. 2-2 Компонент с двумя интерфейсами

На такой основе мы и будем рассматривать и реализовывать интерфейсы СОМ на С++. Согласитесь, что это не сложнее обычной азбуки (A, B, C++, …).

Законченный пример

Давайте рассмотрим несложную, но законченную реализацию интерфейсов IX и IY. Для реализации компонентов мы используем простую программу на С++ без динамической компоновки. Динамическую компоновку мы добавим в гл. 5, а пока гораздо проще обойтись без нее. В листинге 2-1 класс CA реализует компонент, который поддерживает интерфейсы IX и IY. В качестве клиента в этом примере выступает процедура main.

Копия приведенного в книге кода содержится в файле IFACE.CPP на прилагаемом к книге диске. Чтобы скомпилировать его с помощью Microsoft Visual C++, введите команду

cl iface.cpp

Соглашение о вызове __stdcall (или Pascal)

Возможно, Вы заметили в приведенном выше примере ключевое слово __stdcall. Это расширение языка, специфичное для компилятора Microsoft. (Вряд ли Вы сомневались, что какое-то расширение должно присутствовать.) Любой компилятор, поддерживающий разработку для Win32, поддерживает и это ключевое слово или его синоним. Это верно для компиляторов Borland, Symantec и Watcom. Функция, помеченная как __stdcall, использует соглашение о вызове языка Pascal. Такая функция выбирает параметры из стека перед возвратом в вызывающую процедуру. В соответствии же с обычным соглашением о вызове С/С++ стек очищает вызывающая процедура, а не вызываемая. В большинстве других языков, в том числе в Visual Basic, по умолчанию используется это же стандартное соглашение о вызове. Название «стандартное» применяется потому, что оно используется для всех функций Win32 API, за исключением имеющих переменное число аргументов. Для функций с переменным числом аргументов по-прежнему используется соглашение языка С, или __cdecl. Стандартное соглашение о вызовах применяется в Windows потому, что уменьшает размер кода, а первые версии Windows должны были работать на системах с 640 КБ памяти.*

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

Если Вы предпочитаете слово, которое легче запомнить, используйте pascal. Оно определено в WINDEF.H как

#define pascal __stdcall

Если же Вы полагаете, что наличие в Вашем коде слова pascal сделает Вас жалким Pascal’истом, можете воспользоваться следующим определением из OBJBASE.H:

#define STDMETHODCALLTYPE __stdcall

* В языке Pascal параметры передаются в вызываемую процедуру слева на право (сначала первый, потом второй и т.д.), а в языке С — наоборот, справа налево (сначала последний, потом предпоследний и т.д.). Стандартное соглашение о вызове является компромиссом: порядок передачи параметров взят из С, а порядок очистки стека — из Pascal. Прим. ред.

27

IFACE.CPP

//

//Iface.cpp

//Копиляция: cl Iface.cpp

//

 

#include <iostream.h>

// Определить интерфейс

#include <objbase.h>

void trace(const char* pMsg) { cout << pMsg << endl; }

// Абстрактные интерфейсы interface IX

{

virtual void __stdcall Fx1() = 0; virtual void __stdcall Fx2() = 0;

};

interface IY

{

virtual void __stdcall Fy1() = 0; virtual void __stdcall Fy2() = 0;

};

// Реализация интерфейса class CA : public IX,

public IY

{

public:

// Реализация интерфейса IX

virtual void __stdcall Fx1() { cout << "CA::Fx1" << endl; } virtual void __stdcall Fx2() { cout << "CA::Fx2" << endl; }

// Реализация интерфейса IY

virtual void __stdcall Fy1() { cout << "CA::Fy1" << endl; } virtual void __stdcall Fy2() { cout << "CA::Fy2" << endl; }

};

// Клиент int main()

{

trace("Клиент: Создание экземпляра компонента");

CA* pA = new CA;

// Получить указатель IX IX* pIX = pA;

trace("Клиент: Использование интерфейса IX"); pIX->Fx1();

pIX->Fx2();

// Получить указатель IY IY* pIY = pA;

trace("Клиент: Использование интерфейса IY"); pIY->Fy1();

pIY->Fy2();

trace("Клиент: Удаление компонента"); delete pA;

return 0;

}

Листинг 2-1 Полный пример использования интерфейсов

Результаты работы этой программы таковы:

Клиент: Создание экземпляра компонента

28

Клиент: Использование интерфейса IX CA::Fx1

CA::Fx2

Клиент: Использование интерфейса IY CA::Fy1

CA::Fy2

Клиент: Удаление компонента

Как видно из текста, клиент и компонент взаимодействуют через два интерфейса. Последние реализованы с помощью двух чисто абстрактных базовых классов IX и IY. Компонент реализуется классом CA, который наследует как IX, так и IY. Класс CA реализует функции-члены обоих интерфейсов.

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

!" Интерфейсы СОМ реализуются как чисто абстрактные базовые классы С++ !" Один компонент СОМ может поддерживать несколько интерфейсов

!" Класс С++ может использовать множественное наследование для реализации компонента, поддерживающего несколько интерфейсов.

В этом примере я оставил в инкапсуляции некоторые «прорехи», которые мы заделаем в нескольких последующих главах. Но мне хотелось бы обсудить кое-какие проблемы сразу, поскольку они очевидны из листинга 2-1.

Взаимодействие в обход интерфейсов

Помните, как я говорил, что клиент и компонент взаимодействуют только через интерфейс? Клиент из листинга 2-1 не следует этому правилу. Он взаимодействует с компонентом посредством pA — указателя на класс CA, а не на интерфейс. Это может показаться несущественным, но на самом деле очень важно. Использование указателя на CA требует, чтобы клиент знал, как объявлен (обычно в заголовочном файле) класс CA. Объявление класса содержит множестве деталей реализации. Изменение этих деталей потребует перекомпиляции клиента. Компоненты (как я уже говорил) должны уметь добавлять и удалять интерфейсы без нарушения работы старых клиентов. Это одна из причин, по которым мы настаиваем, что клиент и компонент должны взаимодействовать только через интерфейсы. Вспомните, что интерфейсы основаны на чисто абстрактных базовых классах, с которыми не связана какая-либо реализация.

Конечно, не обязательно изолировать клиент от компонента, если они находятся в одном файле. Однако подобная изоляция необходима, если клиент и компонент подключаются друг к другу динамически, особенно когда у Вас нет исходных текстов. В гл. 3 мы исправим наш пример так, чтобы в нем не использовался указатель на CA. Клиенту более не потребуется знать как объявлен класс CA.

Использование указателя на CA — не единственное место, где клиент из предыдущего примера в обход интерфейса взаимодействует с компонентом. Для управления существованием компонента клиент применяет операторы new и delete. Эти операторы не только не входят ни в один из интерфейсов, но и специфичны для языка С++. В гл. 4 мы рассмотрим, как удалить компонент через интерфейс без помощи специфичного для языка оператора. В гл. 6 и 7 мы рассмотрим гораздо более мощный способ создания компонентов.

Теперь давайте обсудим некоторые более тонкие детали реализации клиента и компонента из предыдущего примера.

Детали реализации

Листинг 2-1 — это стандартная программа на С++. В ней нет ничего необычного, за исключением того, что она стала нашим первым шагом в создании компонента и клиента СОМ. Очень легко спутать требования СОМ к компоненту и конкретный способ реализации. В этом разделе я проясню некоторые места, где часто возникает путаница.

Класс — это не компонент

В листинге 2-1 класс CA реализует один компонент. СОМ не требует, чтобы один класс С++ соответствовал одному компоненту. Вы можете реализовать один компонент при помощи нескольких классов. На самом деле компонент можно реализовать вообще без классов. Классы С++ не используются при реализации компонентов СОМ на С, а потому они не обязательно должны использоваться и на С++. Просто компоненты СОМ гораздо легче реализовать через классы, чем строить вручную.

Соседние файлы в предмете Программирование на C++