1. Требования
Предполагается: знание Delphi на уровне использования DLL, а также написания
собственных; знание С++ на уровне написания простейшего приложения в среде
MS VC++.
Желательно: общее понимание соглашений о вызове функций; общее
представление о способах передачи параметров и возврата значения.
Используемые инструменты: Borland Delphi 6, MS VC++ 6.0
2. Обоснование
Необходимость использования чужого кода в своей программе возникает регулярно.
Вставка готовых удачных решений позволяет не изобретать велосипед заново.
В хороших случаях чужой код написан на том же языке, что и свой, либо решение
оформлено в виде DLL или компонента. Однако, бывают случаи похуже. Например,
приобретается PCI-плата расширения с программным обеспечением для нее, а это ПО
оказывается файлами исходного кода на С или С++, в то время как проект уже
начат на Delphi, и, кроме того, в команде разработчиков С++ знают плохо.
3. Варианты решения
В принципе, можно весь проект писать на С++. Если такая возможность есть –
не исключено, что это лучший выход. Но пользовательский интерфейс в Delphi
разрабатывается быстрее, чем в MS VC++ (не только мое мнение, но хорошую цитату
не нашел), кроме того, в группе могут плохо знать
С++. И если даже С++ знают хорошо, но проект уже начат на Delphi, переписывать
готовое – значит, тратить неоплачиваемое время.
Можно переписать код С++ на Delphi. Для этого требуется время, и, возможно,
немалое, а главное – знание С++ на уровне существенно выше начального
(«читаю со словарем»). При этом, многие языковые конструкции С++ не
имеют прямых аналогов в Delphi, и их перенос чреват появлением ошибок, в том
числе, совершенно дурацких, и потому трудноотлавливаемых. В частности,
прекрасный пример из обсуждения статьи «ЯП, ОПП и т.д. и т.п. в свете
безопасности программирования»:
for(;P('\n'),R-;P('|'))
for(e=C;e-;P('_'+(*u++/8)%2))P('| '+(*u/4)%2);
Можно попробовать засечь время и перевести это на Pascal. Станет примерно
понятно, сколько времени уйдет на перевод класса, где подобные конструкции не
единичны.
Можно воспользоваться интерфейсами и технологией СОМ (пожалуй, точнее –
моделью СОМ и технологией ActiveX). Но – вот цитата из [1], глава
«Модель многокомпонентных объектов»:
«И еще одно замечание: не думайте, что путь будет легким. Крейг Брокшмидт
говорил, что перед тем, как он начал понимать эти концепции, у него был «
шесть месяцев туман в голове.» Минимальная предпосылка –
исчерпывающее знание языка С++.» Конец цитаты. И, хотя «модель СОМ
предоставляет унифицированный, открытый, объектно-ориентированный протокол
связи между программами» (цитата оттуда же), она требует такой
квалификации от программиста, которая редко встречается в среде
непрофессионалов.
Можно реализовать код, который необходимо использовать, в виде DLL. Один из
существенных плюсов DLL – неважно, на каком языке она написана, если
соблюдены некоторые условия, такие, как соглашение о вызове и совместимость
типов.
С учетом того, что в группе разработчиков в основном о С++ поверхностные
представления, а СОМ – незнакомая аббревиатура, и, при этом, срок сдачи
проекта – традиционно – вчера, ничего лучше варианта с DLL у нас
придумать не получилось.
4. Немного теории
Передать, точнее, экспортировать несколько функций из DLL – не проблема.
Приводим типы, соглашения о вызовах, заполняем список экспортируемых функций –
и всё (в основном). Об этом написано немало, например, в [2], в параграфе
«Использование DLL, разработанных в С++».
Экспортировать класс несколько сложнее. Даже если и DLL, и основная
программа написаны на Delphi, возникают проблемы с распределением памяти,
которые решаются использованием модуля ShаreMem, первым в списке uses как
проекта, так и DLL [2, 3]. Причем, этот модуль можно, в принципе, заменить
самодельным менеджером памяти [там же, 3]. Но как использовать ShаreMem, если
DLL написана на другом языке, или написать собственный менеджер для двух
языков? Наверное, можно и так, но, напоминаю, срок сдачи проекта – вчера.
Если есть и другие возражения, часто время – определяющий фактор.
Можно создавать экземпляр класса при загрузке DLL, ликвидировать при выгрузке
(используя события DLL_PROCESS_ATTACH/DETACH), а для методов класса (функций-членов, раз уж класс на С++)
написать однострочные функции-обертки, не
являющиеся членами, а просто вызывающие соответствующие функции-члены.
Некрасиво, и много лишней работы. Попробуем все же экспортировать класс.
В [2], сказано: «Библиотеки DLL не могут экспортировать классы
и объекты, если только вы не используете специализированную технологию
Microsoft под названием СОМ или какую-либо другую усовершенствованную
технологию». Впрочем, там же есть замечание: «На самом деле объекты
могут быть переданы из DLL в вызывающую программу в случае, если эти объекты
спроектированы для использования в качестве интерфейсов или чистых абстрактных
классов». Кроме этого замечания, в [2] об экспорте объектов почти всё, но
уже хорошо, что есть шанс «сделать это по-быстрому».
И, наконец, в [4] находим параграф «Экспорт объектов из DLL». Там
сказано: «К объекту и его методам можно получить доступ, даже если этот
объект содержится внутри DLL. Но к определению такого объекта внутри DLL и его
использованию предъявляются определенные требования. Иллюстрируемый здесь
подход применяется в весьма специфических ситуациях, и, как правило, такого же
эффекта можно достичь путем применения пакетов или интерфейсов». Наша
ситуация вполне специфическая; пакеты здесь неприменимы, так как они все же для
использования с Delphi, про использование интерфейсов и СОМ уже сказано, а
использовать интерфейсы без СОМ вне Delphi, судя по [2], нельзя.
И, пожалуй, самое важное из [
4]:
«На экспорт объектов из DLL накладываются следующие ограничения:
- Вызывающее приложение может использовать лишь те методы объекта, которые были объявлены как виртуальные.
- Экземпляры объектов должны создаваться внутри DLL.
- Экспортируемый объект должен быть определен как в DLL, так и в вызывающем приложении с помощью методов, определенных в том же порядке.
- Из объекта, содержащегося вутри DLL, нельзя создать объект-потомок.
Здесь перечислены лишь основные ограничения, но возможны и некоторые другие.»
Далее там рассказывается о работе с DLL, написанной в Delphi, но полученной
информации достаточно для работы с DLL, создаваемой в MS VC++.
Мастер MS VC++ позволяет создать обычную (regular) DLL и DLL-расширение
(extension). Обычная DLL может экспортировать только С-функции и не способна
экспортировать С++-классы, функции-члены или переопределенные функции [1].
Стало быть, надо использовать DLL-расширение. Мастер создаст заготовку, затем в
каталог проекта надо будет скопировать два файла – заголовочный и файл
кода (*.h и *.cpp), содержащие класс, с экземпляром которого предстоит
поработать. Затем подключить их к проекту DLL и немного доработать в
соответствии с указанными ограничениями.
Во-первых, все экспортируемые открытые методы (ну, или функции-члены, как
хотите) необходимо объявить с директивой __stdcall, по понятным причинам.
Во-вторых, их также необходимо объявить виртуальными. Это делается для того,
чтобы точки входа оказались записанными в таблицу виртуальных функций (VMT),
через которую и будет осуществлятся экспорт-импорт.
В-третьих, класс требуется объявить с макроопределенной директивой
AFX_EXT_CLASS или AFX_CLASS_EXPORT, это синонимы.
Сделанные изменения не влияют на работоспособность класса в ехе-проекте, даже
директива экспортируемого класса.
Далее в файл .срр проекта DLL нужно добавить функции создания и ликвидации
объекта. Пример в [4] обходится без функции ликвидации, видимо, потому, что
в приведенном там примере и DLL и импортирующее приложение написаны на Delphi,
так что можно освободить память методом Free, который есть у всех наследников
TObject и отсутствует у объектов С++, не имеющих общего класса-предка.
Функция создания объекта должна просто вызывать конструктор, передать ему
полученные от приложения параметры и вернуть указатель на созданный объект.
Функция ликвидации принимает указатель на объект и вызывает деструктор.
И обязятельно вписать эти функции в список экспортируемых.
И всё! Пятнадцать минут работы, при минимальном знании С++. Остальное в Delphi.
В импортирующей программе необходимо объявить класс, содержащий виртуальные
открытые функции в том же порядке. Также необходимо объявить сложные структуры
данных, используемые в DLL и передаваемые через ее границу в любом направлении.
Имеются в виду структуры С++, они же записи Паскаля. И, конечно же, нужно
объявить импортируемые функции создания и уничтожения класса. Теперь для
создания экземпляра класса вызывается соответствующая функция DLL, когда объект
перестает быть нужным – снова вызывается соответствующая функция DLL, а
методы вызываются традиционно – «Объект.Метод(Параметры)». При
этом обзывать методы в Delphi можно как угодно, важен лишь порядок их
следования и списки параметров.
Если в С++ функция-член возвращает значение, в Delphi соответствующий метод
должен быть тоже функцией. Если же функция-член возвращает void, в Delphi
соответствующий метод – процедура.
Если в С++ параметр передается по значению, то и в Delphi тоже, если же по
ссылке (то есть как указатель), то в Delphi такой параметр должен быть объявлен
с ключевым словом var.
Чуть подробнее о параметрах и их типах. Практически везде, где говорится о DLL,
упоминается, что, если хотите обеспечить совместимость DLL с программами на
других языках, необходимо обеспечить совместимость типов. То есть, стремиться
использовать стандартные типы ОС Windоws. Такие типы, как string или file
вообще не совместимы с С++, с TDateTime можно поэкспериментировать, вообще-то,
он соответствует стандарту, принятому в OLE-автоматизации ([3]). Опять же, [3]
заявляет о соответствии типов single и double Delphi с float и double в С++
соответственно. Хотя в [5] есть такой совет со ссылкой на News Group:
«Если вы создаете DLL не с помощью Delphi, то избегайте чисел с плавающей
точкой в возвращаемом значении. Вместо этого используйте var-параметр
(указатель или ссылочный параметр в С++) Причина кроется в том, что Borland и
Microsoft применяют различные способы возврата чисел с плавающей точкой.
Borland С++ и Delphi могут использовать один и тот же метод».
Тип С++ | Байтов | Тип Delphi |
int | ?(4) | integer |
unsigned int | ?(4) | cardinal |
char, __int8 | 1 | shortint |
short, __int16 | 2 | smallint |
long, __int32 (int) | 4 | longint (integer) |
__int64 | 8 | int64 |
unsigned char | 1 | byte |
unsigned short | 2 | word |
unsigned long | 4 | longword |
float | 4 | single |
double | 8 | double |
char * | | PChar |
Таблица соответствия типов Delphi и С++
5. Немного практики
Для примера будет использован несложный и бесполезный класс на С++,
состряпанный на ходу.
В MS VC++ создадим проект, используя MFC AppWizard(exe), без использования
представления «Документ-вид», на основе диалога, и обзовем
его «example_exe».
Добавим два новых файла – example.cpp и example.h.
Файл
example.h:
//*****************************************************************************
// традиционный финт ушами во избежание
// повторного включения файла .h
#if !defined(EXAMPLE__INCLUDED)
#define EXAMPLE__INCLUDED
// введем парочку структур для демонстрации работы с ними
typedef struct
{
int n;
int i;
short j;
char k;
}struct_1;
typedef struct
{
int n2;
short a[3];
}struct_2;
// Класс-пример. Ничего полезного, просто демонстрация.
class CExample
{
private:
int Field;
CString Name;
void Show(CString str);
public:
// конструктор и деструктор, как полагается
CExample(int F, CString N);
~CExample();
// просто сообщение
void Message(CString str, int Digit);
// «процедура» и «функция»
void Proc(int * Digit);
int Func(int Number);
// работа с закрытым полем
void SetF(int F);
int GetF();
// работа со структурами
struct_2 * Struct1to2(struct_1 s1);
};
#endif //if !defined(EXAMPLE__INCLUDED)
//*****************************************************************************
В классе есть пара закрытых полей, закрытая функция-член, набор открытых
функций. Конструктор принимает два параметра. Строковый параметр будем
интерпретировать, как имя объекта. Функция Message нужна для отображения на
экране хоть каких-то сообщений, демонстрирующих, что что-то происходит.
Proc имитирует процедуру, то есть, не возвращает значения, зато изменяет
что-то в программе, в нашем случае, переданный параметр. Func и есть функция,
то есть, ничего не изменяет, зато вычисляет некоторое значение и возвращает
его. Плюс здесь же установщик и считыватель закрытого поля, а также простенькая
демонстрация работы со структурами.
Файл
example.срр:
//*****************************************************************************
#include "stdafx.h"
#include "Example.h"
// конструктор инициализирует два закрытых поля
// и выдает сообщение об успешном создании
// при помощи закрытой функции
CExample::CExample(int F, CString N)
{
this->Field = F;
this->Name = N;
this->Show(N + " Created successfully");
}
// деструктор только сообщает о самоликвидации
CExample::~CExample()
{
this->Show("Deleted successfully");
}
// закрытая функция, по сути – оболочка MessageBox'а
// заголовком бокса будет имя класса
void CExample::Show(CString str)
{
::MessageBox(NULL, str, this->Name, MB_OK);
}
// открытая функция, выводит строку и число в десятичном виде.
void CExample::Message(CString str, int Digit)
{
str.Format(str + " %d", Digit);
this->Show(str);
}
// "процедура" не возвращает значение, зато изменяет параметр
void CExample::Proc(int * Digit)
{
*Digit *= 2;
}
// "функция" не изменяет параметр, зато возвращает значение
int CExample::Func(int Number)
{
int Result;
Result = Number * 2;
return Result;
}
// банально присваиваем значение параметра закрытому полю.
void CExample::SetF(int F)
{
this->Field = F;
}
// еще банальнее...
int CExample::GetF()
{
return this->Field;
}
// присваиваем значения полей одной структуры полям другой
struct_2 * CExample::Struct1to2(struct_1 s1)
{
struct_2 * s2 = new struct_2;
s2->n2 = s1.n * 2;
s2->a[0] = s1.i;
s2->a[1] = s1.j;
s2->a[2] = s1.k;
return s2;
}
//*****************************************************************************
Для примера более, чем достаточно. Теперь надо посмотреть, как это работает.
В файле Example_exeDlg.h в описании класса CExample_exeDlg где-нибудь в секции
public надо вписать
CExample * ex;
то есть, объявить переменную-член, указатель на наш учебно-тренировочный класс,
и в конструкторе Example_exeDlg вписать
ex = NULL;
Можно
ex сделать и не членом, в принципе, и инициализировать при объявлении.
И, конечно, не забыть наверху этого же файла вклеить заголовочный файл класса:
#include "Example.h"
На диалоговую форму накидаем кнопок и создадим их обработчики:
void CExample_exeDlg::OnBtCreate()
{
if (ex == NULL)
ex = new CExample(7, "Example");
}
Если объект еще не создан – создаем и инициализируем пару закрытых полей.
void CExample_exeDlg::OnBtDestroy()
{
delete ex;
ex = NULL;
}
Освобождаем память и устанавливаем указатель в «пусто»
void CExample_exeDlg::OnBtMessage()
{
ex->Message("Any digit - ", 3);
}
Демонстрационное сообщение.
void CExample_exeDlg::OnBtProc()
{
int k = 5;
ex->Message("before K = ", k);
ex->Proc(&k);
ex->Message("after K = ", k);
}
Показываем в последовательных сообщениях, какое значение переменная имела до
выполнения процедуры, и какое стала иметь после.
void CExample_exeDlg::OnBtFunc()
{
int k = 5, l;
ex->Message("before K = ", k);
l = ex->Func(k);
ex->Message("after K = ", k);
ex->Message("Result of Func = ", l);
}
Примерно то же самое – значение до выполнения, значение после выполнения и
результат выполнения.
void CExample_exeDlg::OnBtGet()
{
ex->Message("", ex->GetF());
}
void CExample_exeDlg::OnBtSet()
{
ex->SetF(ex->GetF() + 1);
}
Эти две – без комментариев. Должно быть так все понятно...
Функцию для работы со структурами в этом проекте не буду трогать, не интересно,
тут весь фокус, как их передать через границу DLL.
Кроме того, не будем возиться с полями ввода, а передадим параметры
непосредственно в коде. Наглядность это уменьшает ненамного, а работы меньше.
Еще момент – ID кнопок по-умолчанию поменял с BUTTON1 на BT_CREATE и так далее,
для наглядности.
Всё! На форме только кнопки, вывод информации через MessageBox. Можно проверить
работу.
Сделаем DLL для этого класса. В MS VC++ создадим проект, используя
MFC AppWizard(dll), назовем «example_dll». В каталог этого проекта копируем
готовые example.cpp и example.h, добавим их к проекту. Будем изменять, в
соответствии с выясненными правилами. Начнем с объявления класса:
// Можно использовать AFX_EXT_CLASS, это синонимы.
class AFX_CLASS_EXPORT CExample
Затем из
void
Message(CString str, int Digit);
делаем
virtual void __stdcall
Message(CString str, int Digit);
и так со всеми открытыми методами, кроме конструктора и деструктора. И на этом
бы всё, да CString – несовместимый, опасный тип. Меняем объявление:
virtual void __stdcall Message
(char * str, int Digit);
и определение:
void CExample::Message
(char* str, int Digit)
{
//добавляем CString:
CString s = str;
//и немного изменяем работу со строкой:
//str.Format(str + " %d", Digit);
s.Format(s + " %d", Digit);
//this->Show(str);
this->Show(s);
}
то есть, приходим к совместимому типу «указатель на нуль-терминальную строку»,
но, чтобы не потерять функциональность класса CString, объявляем локальную
переменную этого класса и используем ее. Осталось еще полторы детали.
Первая деталь – в файле example_dll.cpp в конце пишем:
// вставляем функцию инициализации..
CExample * __stdcall
InitExample(int F, char * N)
{
CExample * ex;
// транслируем конструктору принятые параметры
ex = new CExample(F, N);
// и возвращаем указатель на созданный объект
return ex;
}
// ..и ликвидации
void __stdcall
DelExample(CExample * ex)
{
delete ex;
}
И половина детали – в файле EXAMPLE_DLL.def в конце дописываем пару строчек, так, чтобы
получилось:
;*****************************************************************************
; EXAMPLE_DLL.def : Declares the module parameters for the DLL.
LIBRARY "EXAMPLE_DLL"
DESCRIPTION 'EXAMPLE_DLL Windows Dynamic Link Library'
EXPORTS
; Explicit exports can go here
InitExample
DelExample
;*****************************************************************************
После компиляции DLL готова. Подготовим проект в Delphi, чтобы
продемонстрировать ее работу. Создадим проект «Example_Delphi», и в модуле
главной формы, перед объявлением класса формы, впишем четыре типа. Два - структуры
struct1 и 2:
TRec1 = record
n : integer;
i : integer;
j : smallint;
k : shortint;
end;
TRec2 = record
n2 : integer;
a : array[0..2] of smallint;
end;
Третий – указатель на вторую структуру:
PRec2 = ^TRec2;
А четвертый – наш класс, с которым будем работать:
TExample = class
public
procedure Mess_(str : PChar; Digit : integer);
virtual; stdcall; abstract;
procedure Proc(var Digit : integer);
virtual; stdcall; abstract;
function Func(Number : integer): integer;
virtual; stdcall; abstract;
procedure SetF(F : integer);
virtual; stdcall; abstract;
function GetF(): integer;
virtual; stdcall; abstract;
function Struct1to2(rec1 : TRec1): PRec2;
virtual; stdcall; abstract;
end;
Директивы virtual и stdcall в пояснениях не нуждаются. О них сказано выше.
А зачем abstract? Очень
просто. Без нее компилятор будет ругаться на неправильное упреждающее
объявление функции, ведь описания ее у нас нет, описание – в DLL. Директивы
должны идти именно в этом порядке. Иначе компилятору не нравится.
Обратите внимание на первый метод. Остальные названы так же, как и в С++, но
слово Message в Delphi зарезервированное, и использовать его не по назначению
не стоит. Хорошо, назовем иначе, важно, что она стоит на первом месте среди
виртуальных функций, как и в С++, значит, ее найдут по номеру в VMT. Имя роли не играет.
Еще надо добавить объявление экспортируемых функций создания/ликвидации, в
конце секции
interface:
function InitExample(F: integer; N : PChar) : TExample; stdcall;
external
'..\Example_DLL\debug\Example_DLL.dll';
procedure DelExample(ex : TExample); stdcall;
external
'..\Example_DLL\debug\Example_DLL.dll';
Здесь предполагается, что DLL лежит там, где и появилась после компиляции, а
директории «Example_dll» и «Example_Delphi»
имеют общую родительскую. Больше нигде ее искать не будут.
Если же указать только имя, приложение будет искать библиотеку в своей папке,
папках WINDOWS, SYSTEM32 и прописанных в переменной окружения PATH. Впрочем,
это азбука.
Всё, класс можно использовать. Давайте опять наделаем кнопок, а вывод в поле
Memo, благо, в Delphi с ним работать быстрее и проще, чем в MS VС++.
Вот обработчики кнопок:
procedure TForm1.Button1Click(Sender: TObject);
begin
if not Assigned(Self.ex) then
Self.ex := InitExample(10, 'Ex_Delphi');
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
DelExample(Self.ex);
Self.ex := nil;
end;
procedure TForm1.Button3Click(Sender: TObject);
begin
Self.ex.Mess_(PChar('Некоторая цифра – '), 5);
end;
procedure TForm1.Button4Click(Sender: TObject);
var
j : integer;
begin
j := 15;
Self.Memo1.Lines.Add('j До – ' + IntToStr(j));
Self.ex.Proc(j);
Self.Memo1.Lines.Add('j После – ' + IntToStr(j));
end;
procedure TForm1.Button5Click(Sender: TObject);
var
j : integer;
begin
j := 20;
Self.Memo1.Lines.Add('j До – ' + IntToStr(j));
Self.Memo1.Lines.Add('Результат – ' + IntToStr(Self.ex.Func(j)));
Self.Memo1.Lines.Add('j После – ' + IntToStr(j));
end;
procedure TForm1.Button6Click(Sender: TObject);
begin
Self.Memo1.Lines.Add(IntToStr(Self.ex.GetF));
end;
procedure TForm1.Button7Click(Sender: TObject);
begin
Self.ex.SetF(Self.ex.GetF + 1);
end;
То же самое, что и в С++, и работает так же. Что и требовалось. И добавим
кнопку для функции, которая принимает и возвращает структуры. Вот ее
обработчик:
procedure TForm1.Button8Click(Sender: TObject);
var
s1 : TRec1;
s2 : PRec2;
begin
// здесь компилятор будет ругаться, но в данном
// случае это не важно. Просто посмотрим, что
// до инициализации s2 - это всякая чушь...
Self.Memo1.Lines.Add('s2 до:' + #9 +
IntToStr(s2.n2) + #9 +
IntToStr(s2.a[0]) + #9 +
IntToStr(s2.a[1]) + #9 +
IntToStr(s2.a[2]) );
// инициализация s1
s1.n := 10;
s1.i := 1;
s1.j := 2;
s1.k := 3;
// если функция возвращает указатель на запись (структуру) -
// надо подготовить указатель. Это вам не класс.
// s2 - типа PRec2, а не TRec2
s2 := Self.ex.Struct1to2(s1);
// ... а потом - то, что мы требовали.
Self.Memo1.Lines.Add('s2 после:' + #9 +
IntToStr(s2.n2) + #9 +
IntToStr(s2.a[0]) + #9 +
IntToStr(s2.a[1]) + #9 +
IntToStr(s2.a[2]) );
end;
Что делает функция – понятно, тут другая тонкость. Конструктор возвращает (в
коде на С++) указатель на класс, а мы присваиваем возвращаемое значение
переменной, которая, вроде бы, не указатель. Struct1to2 тоже возвращает
указатель – и его надо подготовить. Это объясняется в [3]:
«Объект – это динамический экземпляр класса. Объект всегда создается
динамически, в «куче», поэтому ссылка на объект фактически является указателем
(но при этом не требует обычного оператора разыменования «^»). Когда вы
присваиваете переменной ссылку на объект, Delphi копирует только указатель, а
не весь объект. Используемый объект должен быть освобожден явно.»
А в С++ структура отличается от класса несколько меньше, и работа с ними почти
одинакова.
И еще пара тонкостей. Если в DLL добавить еще виртуальную функцию-член,
обязательно в конце, после имеющихся, такая DLL будет совместима со
старой программой, где в абстрактном классе эта функция не объявлена. И если
изменить имеющуюся функцию, добавив в конец параметров параметр по-умолчанию,
такая DLL будет совместима со старой программой, где в абстрактном классе эта
функция не имеет такого параметра.
Разумеется, можно вынести описание абстрактного класса, объявление
экспортируемых функций, используемых типов и тому подобное в отдельный модуль.
Возможно, это лучше, чем запихивать всё в один файл. По крайней мере, я так и
делаю. Но это уже детали, касающиеся стиля, а не функциональности.
6. Заключение
При таком подходе нельзя обращаться к полям данных напрямую. Хотя, это не
проблема, можно использовать функции-считыватели и установщики. Нельзя
использовать закрытые функции. А зачем их использовать? Для использования есть
открытые. Те, кто знакомы с моделью СОМ, могут сказать, что это извращенный
вариант нормальной технологии. Но для создания полноценного СОМ-сервера нужно
несколько больше знаний. Показанный способ позволяет использовать открытые
методы класса, не вдаваясь в подробности реализации класса и не затрачивая
много времени. Это дает возможность быстро получить работоспособный вариант
программы, и уже потом доводить ее до ума. А то и просто получить
удовлетворительный результат.
7. Используемая литература
К материалу прилагаются файлы: