Библиотечные классы Borland

Библиотека контейнерного класса

В данном разделе описываются структуры данных BIDS (Borland International Data Structures), которые называются также библиотекой контейнерного класса. Контейнеры - это объекты, реализующие общие структуры данных, предлагающие функции-элементы для доступа к каждому элементу данных и добавления таких элементов. При этом внутренние детали скрыты от пользователей. Контейнеры могут содержать целые и вещественные числа, строки, структуры, классы, типы, определенные пользователями, и любые объекты C++.

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

Библиотеку класса контейнера можно разделить на две категории: фундаментальные структуры данных FDS (Fundamental Data Structures) и абстрактные типы данных ADT (Abstract Data Types)

Контейнеры и файлы заголовков Borland

Контейнер Файл-заголовок
FDS Borland
Двоичное дерево binimp.h
Таблица хеширования hashimp.h
Связанный список listimp.h
Двусвязанный список dlistimp.h
Вектор vectimp.h
ADT Borland
Массив arrays.h
Ассоциация assoc.h
Удаление из очереди dequeue.h
Очередь queues.h
Множество sets.h
Стек stacks.h

Структуры FDS - это контейнеры нижнего уровня, реализующие структуры памяти. Каждая FDS имеет фундаментальные функции добавления и отсоединения. Типы ADT обычно используются в конструкциях обработки данных. Каждый тип ADT имеет соответствующие методы, например, контейнеры стека - функции-элементы Push и Pop.

Каждый тип ADT основан на FDS. Например, TArrayAsVector реализует массив, используя в качестве соответствующей структуры FDS вектор. Контейнеры ADT используют характеристики хранения соответствующих FDS и добавляет специфические методы доступа, которые делают каждый из них уникальным.

Контейнеры могут хранить копии объектов (прямые контейнеры) или указатели на объекты (косвенные контейнеры). Например, следующий контейнер является косвенным массивом, в котором хранятся указатели на значения float: TIArraysAsVector<float> FloatPtrArray(10); где I - имя шаблона, указывающее соответствующий контейнер.

В некоторых типах контейнеров содержимое записывается в отсортированном виде. Отсортированные контейнеры (как косвенные, так и прямые) должны иметь соответствующую операцию <, что позволяет определить порядок элементов. Эти операции предусмотрены для предопределенных типов. Для типов, определенных пользователем, вы должны обеспечить такую операцию. Для прямых объектов тип должен иметь допустимую операцию == - используемый по умолчанию конструктор. Для косвенных контейнеров сортируются объекты, а не указатели, которые содержит контейнер.

Управление памятью

Контейнеры позволяют вам контролировать управление памятью. Например, следующий контейнер позволяет передавать объект управления памятью: TMQueueVector<MyClass, MyMemManage> MyQueue (100);

TMQueueVector воспринимает два типа параметров: объект, который будет содержать очередь (MyClass), и имя класса управления памятью (MyMemManage), который вы хотите использовать. M в имени шаблона означает, что вы должны для реализации данного контейнера задать администратор памяти. Если в именах шаблонов M не указывается, то используется стандартный распределитель памяти TStandardAllocator, найденный в alloctr.h.

TStandardAllocator предусматривает операции new, new[], delete и delete[], которые вызывают соответствующие глобальные функции. Пользовательское распределение памяти должно предусматривать специфическую для класса операцию new. В качестве примера для построения можно использовать содержимое alloctr.h.

Имена контейнеров и комбинации ADT/FDS

Характеристики каждого класса закодированы в имени контейнера. Например, TDequeueAsDoubleList - это прямой контейнер, использующий схему управления памятью и реализующий двусвязанный список.

Сокращение Описание
T Префикс библиотечного класса Borland.
M Контейнер управления памятью, предусматриваемый пользователем.
I Косвенный контейнер.
C Счетный контейнер.
S Отсортированный контейнер.

Библиотеки BIDS не содержат всех возможных комбинаций ADT/FDS. Предусмотренные комбинации указаны в следующей таблице: ADT Sorted Dictiona- FDS Stack Queue Dequeue Bag Set array Array ry Вектор * * * * * * * Список * Двойной * * список Таблица * хеширования Двоичное дерево Для построения собственных реализаций ADT/FDS вы можете использовать шаблоны классов.

Итераторы контейнера

Каждый контейнер имеет соответствующий класс итераторов, которые предназначены для итерации конкретного вида контейнера. Например, итератор класса TArrayAsVector имеет соответствующий итератор TArrayAsVectorIterator, который отвечает за итерацию по всем элементам массива. Итератор контейнера реализуют для контейнера операции пред- и постинкрементации ++, а также функцию-элемент Current, возвращающую текущий объект, и функцию-элемент Restart, перезапускающую итерацию.

Владение объектом

Косвенные контейнеры наследуют из TShouldDelete (файл shddel.h) функцию-элемент OwnsElements. Эта функция позволяет указать, будет ли контейнер при применении функций Detach или Flash по умолчанию удалять объекты.

Использование контейнеров

Использование контейнеров с шаблонами позволяет вам построить приложение на основе стека (например, используя в качестве соответствующих структур вектора), что позволяет легко изменить реализацию связанного списка. Часто для этого требуется только изменить typedef. Например: // создать стек целых чисел, загрузить его и вывести содер- // жимое #include <classlib\stacks.h> #include <iostream.h> // описание контейнерных типов typedef TStackAsVector<int> IntStack; int main() { IntStack intStack; for ( int i = 0; i < 10; i++ ) intStack.Push( i ); for ( i = 0; i < 10; i++ ) cout << intStack.Pop() << " "; cout << endl; return(0); } Вывод будет следующим: 9 8 7 6 5 4 3 2 1 0

Таким образом реализуется стек значений int, а в качестве соответствующей FDS используется вектор. Если стек нужно реализовать с помощью списка, вы можете заменить typedef: typedef TStackAsList<int> IntStack;

Все остальное будет работать правильно. Однако, чтобы изменить это на косвенный контейнер, потребуется больше изменений: // создать стек целочисленных указателей, загрузить стек и // вывести содержимое #include <classlib\stacks.h> #include<iostream.h> // изменить typedef как обычно typedef TStackAsVector<int> IntStack; int main() { IntStack intStack; for ( int i = 0; i < 10; i++ ) intStack.Push( &i ); // воспринимает указатель for ( i = 0; i < 10; i++ ) cout << *intStack.Pop() << " "; cout << endl; return(0); } Результат будет тот же.

Каталоги контейнеров

Библиотеки для классов контейнеров, построенных на базе шаблонов, отличаются по префиксу BIDS: это BIDSx.LIB, где x представляет модель памяти, и BIDSDBx.LIB для диагностических версий. Поддержка классов контейнеров включает в себя каталоги, содержащие файлы заголовков, библиотеки, исходные файлы и примеры.

Имя файла Описание
BIDSF.LIB 32-битовая (плоская модель).
BIDSDF.LIB 32-битовая (плоская модель), диагн. версия.
BIDS40F.DLL 32-битовая DLL (плоская модель).
BIDS40DF.DLL 32-битовая DLL (плоская модель), диагностическая версия.
BIDSFI.LIB 32-битовая библиотека импорта (плоская модель).
BIDSDFI.LIB 32-битовая библиотека импорта (плоская модель), диагностическая версия.
BIDSS.LIB 16-битовая малая модель.
BIDSDBS.LIB 16-битовая малая модель, диагностическая версия.
BIDSC.LIB 16-битовая, компактная модель.
BIDSDCC.LIB 16-битовая, компактная модель, диагностическая версия.
BIDSL.LIB 16-битовая, большая модель.
BIDSDBL.LIB 16-битовая, большая модель, диагностическая версия.
BIDS40.DLL 16-битовая DLL.
BIDS40D.DLL 16-битовая DLL, диагностическая версия.
BIDSI.LIB 16-битовая библиотека импорта.
BIDSI.LIB 16-битовая библиотека импорта, диагностическая версия.

Библиотека INCLUDE

Библиотека INCLUDE\CLASSLIB содержит файлы заголовков, необходимые для компиляции программы, которая использует классы контейнеров. Для каждого ADT или FDS в этом каталоге имеется соответствующий файл заголовка. Убедитесь, что вы включили INCLUDE в маршрут доступа и явно ссылаетесь на файл заголовка.

Каталог SOURCE

Каталог SOURCE\CLASSLIB содержит исходные файлы, реализующие многие функции-элементы библиотечных классов. Эти исходные файлы могут вам потребоваться при построении библиотеки.

Каталог EXAMPLES

Каталог EXAMPLES\CLASSLIB\BIDS содержит несколько примеров программ, использующих контейнерные классы:

Программа Описание
STRNGMAX Пример программы работы со строками.
REVERSE Промежуточные пример, использующий TStack (псевдоним TStackAsVector) и String. Этот пример позволяет пользователю вводить строки, а затем выводить их в обратном порядке.
LOOKUP Промежуточный пример, использующий TDictionary-AHashTable и TDDAssociation.
QUEUETST Промежуточный пример, использующий TQueue (псевдоним TQueueAsVector) и неирархический класс TTime.
DIRECTRY Продвинутый пример, иллюстрирующий производные пользовательские классы, использующие TISArrayAsVector и исходные файлы FILEDATA.CPP и TESTDIR.CPP.
Для отладки контейнеров Borland предусматривает специальные библиотеки.

Библиотека классов постоянных потоков

Опишем, что нового появилось в объектной поддержке потоков Borland, а затем поясним, как сделать объекты потоковыми.

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

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

Построить собственные потоковые классы достаточно просто и не потребует особых издержек. Чтобы сделать класс потоковым, вам нужно добавить специфические элементы данных, функции-элементы и операции. Свой производный класс вы должны построить (прямо или косвенно) ил TStreamableBase. Любой производный класс также является потоковым.

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

Для облегчения их использования и расширения функциональных возможностей объектов потоки в Borland C++ 4.0 существенно изменены. Эти изменения совместимы с существующим кодом ObjectWindows и кодом Turbo Vision.

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

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

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

В следующих разделах описываются изменения и новые возможности потоковых средств.

Отслеживание версий объектов

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

Считывание и запись базовых классов

В своем текущем программном коде вы можете считывать и записывать базовые классы непосредственно, например: void Derived::write( opstream& out ) { Base::write( out ); ... } void *Derived::read( ipstream& in ) { Base::read( in ); ... }

Этот метод продолжает работать, но не будет записывать в базовый класс никакого номера версии. Чтобы использовать все преимущества отслеживания версий, измените эти вызовы и используете новые шаблоны функций, которые работают с версиями: void Derived::Write( opstreams& out ) { WriteBaseObject( (Base *)this, out ); ... } void *Derived::Read( ipstream& in, uint32 ver ) { ReadBaseObject( (Base *)this, out ); ... } Важно привести указатель к базовому классу, иначе ваша программа может аварийно завершиться.

Чтение и запись целых чисел

Старые потоки записывают данные типов int и unsigned как двухбайтовые значения. Чтобы облегчить переход на 32-разрядные платформы, новые потоки записывают значения int и unsigned как 4-байтовые значения. Новые потоки могут считывать старые потоки и будут корректно обрабатывать 2-байтовые значения.

Старые потоки предусматривают две функции-элемента для чтения и записи целых значений: void writeWord(unsigned); unsigned readWord(); В новых потоках они изменились: void writeWord(uint32); uint 32 readWord(); Существующий програмный код, который использует эти функции, после перекомпиляции и перекомпоновки будет продолжать работать корректно, хотя вызов readWord будет генерировать предупреждения о потере точности (когда возвращаемое значение в 16-разрядном приложении присваивается переменной типа int или unsigned). Однако в новых программах этих функций следует избегать. В общем случае истинный размер записываемых данных вам вероятно известен, поэтому библиотека потоков предусматривает теперь отдельные функции для каждого размера: void writeWord16(uint16); void writeWord32(uint32); uint16 readWord16(uint16); uint32 writeWord32(uint32);

Множественное наследование и поддержка виртуальной базы

Потоковый код предусматривает теперь 4 шаблона функций, которые поддерживают виртуальные базовые классы и множественное наследование.

В любом классе с прямой виртуальной базов следует использовать новые шаблоны функций ReadVirtualBase и WriteVirtualBase: void Derived:Write( opstream& out ) { WriteVirtualBase( (VirtualBase *)this, out); ... } void *Derived::Read( ipstream& in, uint32 ver ) { ReadVirtualBase( (VirtualBase *)this, in ); ... }

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

Объектные потоки поддерживают теперь множественное наследование. Чтобы читать и записывать множественные базы, используйте новые шаблоны функций WriteBaseObject и ReadBaseObject: void Derived::Write( opstream& out ) { WriteBaseObject( (Base1 *)this, out ); WriteBaseObject( (Base2 *)this, out ); ... } void *Derived::Read( ipstream& in, uint32 ver ) { ReadBaseObject( (Base1 *)this, in ); ReadBaseObject( (Base2 *)this, in ); ... }

Создание потоковых объектов

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

Чтобы определить потоковый класс, вам нужно:

Классы, шаблоны и макрокоманды, необходимые для определения потокового класса, предусмотрены в файле objstrm.h. Каждый потоковый класс должен наследовать (прямо или косвенно) из класса TStreamableBase. В данном примере класс Sample наследует непосредственно из TStreamable. Класс, производный из Sample, не будет явно наследовать из TStreamableBase, поскольку это уже делает Sample. Если вы используете множественное наследование, то следует сделать TStreamableBase виртуальной базой. Это несколько увеличит классы, но никакого отрицательного эффекта не окажет.

В большинстве случае для определения потокового класса достаточно макрокоманды DECLARE_STREAMABLE. Эта макрокоманда воспринимает три параметра. Первый из них используется при компиляции DLL. Второй параметр - это имя определяемого класса, а третий номер версии для этого класса. Потоковый код не обращает внимание на номер версии класса.

DECLARE_STREAMABLE добавляет в ваш класс конструктор, воспринимающий параметр типа Streamable. Он предназначен для применения в потоковом коде, и непосредственно не используется. DECLARE_STREAMABLE создает также для вашего класса два экстрактора, так что вы можете записывать объекты для считывания их из постоянных потоков. Для класса приведенного выше примера класса Sample эти функции имеют следующие прототипы: opstream& operator << { opstream&, const Sample& }; opstream& operator << { opstream&, const Sample* }; opstream& operator >> { opstream&, Sample& }; opstream& operator >> { opstream&, Sample*& };

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

Наконец, DECLARE_STREAMABLE создает на базе класса TStreamer вложенный класс с именем Streamer, который определяет ядро потокового кода.

Реализация потоковых классов

Большинство функций-элементов, добавляемых к вашему классу с функцией DECLARE_STREAMABLE, - это поставляемые функции. Однако некоторые из них являются исключением и должны реализовываться вне класса. Для таких определений также предусмотрены макрокоманды.

Макрокоманда IMPLEMENT_CASTABLE обеспечивает рудиментарный надежный по типу механизм приведения. При построении с помощью Borland C++ 4.0 вам не нужно это использовать, так как Borland C++ 4.0 поддерживают информацию о типе этапа выполнения. Однако, если вам нужно построить свой программный код с помощью компилятора, не поддерживающего информацию о типе, то нужно использовать макрокоманду IMPLEMENT_CASTABLE. Макрокоманда DECLARE_CASTABLE имеет несколько вариантов: DECLARE_CASTABLE( cls ) DECLARE_CASTABLE( cls, base1 ) DECLARE_CASTABLE( cls, base1, base2 ) DECLARE_CASTABLE( cls, base1, base2, base3 ) DECLARE_CASTABLE( cls, base1, base2, base3, base4 ) DECLARE_CASTABLE( cls, base1, base2, base3, base4, base5 )

В некоторой точке своей программы вам следует вызвать эту макрокоманду, указав в качестве параметра имя своего потокового класса, а в качестве последующих параметров - имена потоковых базовых классов, например: class Base1 : public virtual TStreamableBase { ... DECLARE_STREAMABLE( IMPEXMACRO, Base1, 1 ); }; IMPLEMENT_CASTABLE( Base1 ); // нет потоковой базы class Derived : public Base1, public virtual Base2 { ... DECLARE_STREAMABLE( IMPEXMACRO, Derived1, 1 ); }; IMPLEMENT_CASTABLE2( Derived, Base1, Base2 ); // две потоковых базы class MostDerived : public Derived { ... DECLARE_STREAMABLE( IMPEXMACRO, MostDerived, 1 ); }; IMPLEMENT_CASTABLE1( MostDerived, Derived ); // одна потоковая база

Класс Derived использует IMPLEMENT_CASTABLE2, т.к. имеет два потоковых базовых класса.

Кроме макрокоманд IMPLEMENT_CASTABLE где-либо в программе вам следует вызвать макрокоманды IMPLEMENT_STREAMABLE. Эти макро- команды выглядят аналогично: DECLARE_STREAMABLE( cls ) DECLARE_STREAMABLE( cls, base1 ) DECLARE_STREAMABLE( cls, base1, base2 ) DECLARE_STREAMABLE( cls, base1, base2, base3 ) DECLARE_STREAMABLE( cls, base1, base2, base3, base4 ) DECLARE_STREAMABLE( cls, base1, base2, base3, base4, base5 )

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

Вложенный класс Streamer

Вложенный класс Streamer - это ядро потокового кода для ваших объектов. Макрокоманда DECLARE_STREAMABLE создает класс Streamer внутри вашего класса. Это защищенный член, поэтому классы, производные от вашего класса, также могут к нему обращаться. Streamer - это наследник TNewStreamer, а TNewStreamer является внутренним для системы потоков объекта. Он наследует две чистых виртуальных функции: virtual void Write( opstream& ) const = 0; virtual void *Read( ipstream& , sint32 ) const = 0; Streamer переопределяет эти две функции, но не предусматривает для них определений.

Вы должны написать эти функции: Write должна записывать любые данные, которые нужно считывать обратно для воссоздания объекта, а Read должна считывать эти данные. Streamer:GetObject возвращает указатель на потоковый объект.

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

Затем реализуется функция Write, которая работает параллельно с Read и формирует данные, считываемые позднее Read. Это легче сделать с помощью специальных операций, предусмотренных в потоковых классах. Например, opstream обеспечивает инсертеры для всех встроенных типов, так же как и ostream. Поэтому все, что вам нужно сделать для записи любых встроенных типов - это включение их в поток.

Вам нужно также написать базовые классы. В старых потоках ObjectWindows и Turbo Vision это делалось путем прямого вызова базовых функций Read и Write. В том программном коде, который использует новые потоки, это не работает (из-за поддержки версий класса).

Библиотека потоков предусматривает шаблоны функций, используемых для чтения и записи базовых классов. ReadVirtualBase и WriteVirtualBase используются для виртуальных базовых классов, а ReadBaseObject и WriteBaseObject - для невиртуальных.

При написании базового класса не забывайте приводить тип указателя this. Без приведения типа шаблон функции будет думать, что он записывает ваш класс, а не базовый. Результатом будет вызов не базовых, а ваших функций Read или Write. Длинная последовательность рекурсивных вызовов в итоге приведет к краху программы.

Версии объектов

Различным реализациям одного и того же класса, изменяемым при обслуживание, вы можете присваивать различные номера версий. Это не означает, что в одной и той же программе вы можете использовать различные версии одного класса, но позволяет писать потоковый код таким образом, что программа, использующая более новую версию класса, может считывать поток, содержащий данные для старой версии класса, например: class Sample : public TStreamableBase { int i; DECLARE_STREAMABLE( IMPEXMACRO, Sample, 1 ); }; IMPLEMENT_CASTABLE( Sample ); IMPLEMENT_STREAMABLE( Sample ); void Sample::Streamer::Write( opstream& out ) const { out << GetObject()->i; } void *Sample::Streamer::Read( ipstream& in, uint32 ) const { in >> GetObject()->i; return GetObject(); }

Предположим, вы записываете в файл несколько объектов данного типа и обнаруживаете, что вам нужно изменить определение класса. Это можно сделать так: class Sample : public TStreamableBase { int i; int j; // новый элемент данных DECLARE_STREAMABLE( IMPEXMACRO, Sample, 2 ); // новая версия }; IMPLEMENT_CASTABLE( Sample ); IMPLEMENT_STREAMABLE( Sample ); void Sample::Streamer::Write( opstream& out ) const { out << GetObject()->i; out << GetObject()->j; } void *Sample::Streamer::Read( ipstream& in, uint32 ) const { in >> GetObject()->i; if ( ver > 1 ) in >> GetObject()->j; else GetObject()->j = 0; return GetObject(); }

Потоки, записываемые со старой версией Sample, будет иметь для всех объектов типа Sample номер версии 1. Потоки, записываемые с новой версией, будут иметь для всех объектов типа Sample номер версии 2. Код в Read проверяет номер версии и определяет, какие данные представляет поток.

Библиотека потоков, используемая в предыдущих версиях ObjectWindows и Turbo Vision, не поддерживает версий объектов. Если вы используете эту библиотеку, функции Read будет передаваться номер версии 0.

Программирование для Windows

В данном разделе описываются различные вопросы, касающиеся 16- и 32-разрядному программированию в Windows, включая файлы сценариев ресурсов, файлы определения модулей, библиотеки импорта, файлы проектов и администратор проектов, администратор динамически распределяемой области памяти и 32-разрядное программирование в Windows. Кроме компиляции исходного кода и компоновки файлов .OBJ, программист, работающий в Windows, должен компилировать файлы сценария ресурсов и связывать ресурсы с выполняемым файлом. Он должен также иметь представление о динамической компоновки, библиотеках динамической компоновки (DLL) и библиотеках импорта. Кроме того, при работе с интегрированной средой Borland C++ (IDE) полезно знать, как использовать администратор проектов, который автоматизирует построение приложения. Процесс построения приложения в Windows иллюстрируется следующей схемой: 4 .C v .ASM RW 1 .CPP BCC TASM .INC v .H v v .RC .H .H v v v v 2 .DEF TLINK .LIB BRC RW - v v v .EXE .RES 5 v v BRC v .EXE 6 При компиляции и компоновке программы Windows выполняются следующие шаги:

  1. Исходный код компилируется или ассемблируется и создаются файлы .OBJ.
  2. Файлы определения модулей (.DEF) сообщают компоновщику, какой вид выполняемых файлов вы хотите получить.
  3. Resource Workshop (или какой-либо другой редактор ресурсов) создает ресурсы, такие как пиктограмммы или битовые массивы. Создается файл ресурса (.RC).
  4. Файл .RC компилируется компилятором ресурсов или с помощью Resource Workshop. На выходе получается двоичный файл .RES.
  5. В результате компоновке создается выполняемый файл .EXE и связанные с ним ресурсы.

Файлы сценариев ресурсов

Приложения Windows обычно используют ресурсы. Ресурсы - это пиктограммы, меню, диалоговые окна, шрифты, курсоры, битовые массивы и ресурсы, определенные пользователем. Ресурсы определяются в файле, который называется файлом сценария ресурса. Эти файлы имеют расширение .RC.

Для использования ресурсов вам потребуется компилятор ресурсов Borland BRC (Borland Resource Compiler) или BRC32, который компилирует файл .RC в двоичных формат. При компиляции ресурсов создается файл .RES. Компоновщик TLINK или TLINK32 позволяет затем связать файл .RES с генерируемым им файлом .EXE. При этом файл .EXE отмечается также как выполняемый файл Windows.

Файлы определения модуля

Файл определения модуля .DEF обеспечивает информацию о содержимом файла и требованиях к системе приложения Windows. Эта информацию используется компоновщиком и включает в себя размер динамически распределяемой памяти и стека, а также характеристики кода и данных. Файл .DEF перечисляет также функции, которые должны быть доступными для других модулей (экспортируемые функции), и используемые функции других модулей (импортируемые функции). Так как компоновщик Borland имеет и другие способы получения этой информации, файл .DEF для него не обязателен. Файл .DEF содержит несколько операторов. Перечислим некоторые из них:

Оператор Функция
NAME Задает имя программы. Если нужно построить DLL, используйте оператор LIBRARY. Каждый файл .DEF обязательно должен иметь оператор NAME или LIBRARY. Заданное имя должно совпадать с именем выполняемого файла. WINDOWAPI идентифицирует программу, как выполняемую программу Windows.
DESCRIPTION Позволяет вам задать строку, описывающую ваше приложение или библиотеку.
EXETYPE Помечает файл, как выполняемый файл Windows (это необходимо для всех выполняемых файлов Windows.
CODE Описывает атрибуты выполняемого сегмента кода Параметр PRELOAD указывает загрузчику, что при загрузке приложения в память нужно загрузить эту часть образа файла. MOVEABLE указывает, что Windows может перемещать код в памяти.
HEAPSIZE Задает размер локальной динамически распределяе- мой памяти приложения.
STACKSIZE Задает размер стека приложения. Для создания DLL этот оператор использовать нельзя.

Кроме указанных в файлах .DEF используются операции EXPORTS и IMPORTS. Оператор EXPORTS перечисляет функции в программе или DLL, которые будут вызываться другими приложениями или Windows (экспортируемые функции или функции обратного вызова). Экспортируемые функции идентифицируются компоновщиком и вводятся в таблицу экспорта.

Чтобы избежать создания в файлах .DEF длинных секций EXPORTS, в Borland C++ предусмотрено ключевое слово _export. Отмеченные этим ключевым словом функции будут идентифицироваться компоновщиком и вводиться компоновщиком в таблицу экспорта.

Библиотеки импорта

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

Библиотека импорта содержит определения импорта для всех или некоторых экспортируемых функций для одной или более DLL. Утилита IMPLIB создает для DLL библиотеки импорта. IMPLIB создает библиотеки импорта непосредственно из DLL или из файлов определения модуля DLL (либо из их комбинации).

Функция WinMain

В качестве основной точки входа приложения Windows вы должны предусмотреть функцию WinMain. Некоторые приложения (например, ObjectWindows) инкапсулируют эту точку входа. В WinMain передаются следующие параметры: WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)

Параметр hInstance - это описатель экземпляра приложения. Каждый экземпляр приложения Windows имеет уникальный описатель экземпляра, используемый в качестве аргумента в нескольких функциях Windows. Он может также использоваться, чтобы различать несколько экземпляров данного приложения.

Параметр hPrevInstance - это описатель предыдущего экземпляра данного приложения. hPrevInstance равно NULL, если это первый экземпляр (в Win32 это значение всегда равно 0).

lpCmdLine представляет собой указатель (в 16-разрядной Windows указатель типа far) на командную строку с завершающим нулем. Это значение может задаваться при вызове приложения из администратора программ или через вызов WinExec.

nCmdShow - это целое значение, определяющее, как выводить на экран окно приложения (например, в виде пиктограммы).

Возвращаемое WinMain значение в данный момент Windows не используется, однако оно может быть полезным при отладке.

Начальный и завершающий код

Когда вам нужно скомпилировать модуль для Windows, компилятору требуется знать, какой начальный и завершающих код требуется создавать для каждой функции модуля. Созданием этого кода управляют установки IDE и параметры компилятора режима командной строки. Начальный и завершающий код выполняет несколько функций, включая обеспечение активности корректного сегмента при обратном вызове и поддержку границ стека в механизме обеспечения стека Windows.

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

Ключевое слово _import в функции или классе сообщает компилятору, что функция или класс будут импортироваться из DLL. _import также указывается перед именем функции или класса.

Параметры экспорта и импорта

Экспорт функции состоит из двух этапов. Во-первых, компилятор должен создавать для функции корректный начальный и завершающий код. Благодаря этому функция становится экспортируемой. Во-вторых, компоновщик должен для каждой экспортируемой функции создавать запись в заголовке выполняемого файла. Это позволяет в 16-разрядном режиме Windows связать с функцией на этапе выполнения корректный сегмент данных. В 32-разрядной Windows связывание сегментов данных не применяется. Однако DLL должны иметь записи в заголовке, благодаря которым загрузчик сможет найти функции для компоновки при загрузке в .EXE библиотеки DLL.

Параметры -W и -WC

Параметр -W создает прикладной объектный модуль Windows, в котором все функции будут экспортироваться. Компилятор генерирует начальный и завершающий код для каждой функции, обеспечивая ее экспортируемость. Это не означает, что все функции действительно будут экспортироваться. Для фактического экспорта одной из этих функций вы должны использовать ключевое слово _export или добавить запись этой функции в секцию EXPORTS файла определения модуля. Эквивалентный параметр -WC используется для 32-разрядных приложений консольного режима.

Параметр -WE

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

Параметр -WE работает только в сочетании с ключевым словом _export. Этот параметр не экспортирует данные функции, перечисленные в секции EXPORTS или в файле определения модуля. Фактически, этот параметр не работает в сочетании с секцией EXPORTS. В этом случае компилятор будет генерировать начальный и завершающий код, не совместимый с экспортируемыми функциями, и вызовы их приведут к некорректному поведению.

Параметр -WS

Параметр -WS создает объектный модуль с функциями, использующими эффективные обратные вызовы. Эта форма начального и завершающего кода подразумевает, что DS==SS, то есть используемый по умолчанию сегмент данных совпадает с сегментом стека. Это устраняет необходимость использовать специальный код Windows, создаваемый для экспортируемых функций. Использование эффективных обратных вызовов может улучшить производительность, так как вызовы функций в модуле не перенаправляются через специальный код Windows).

Экспортируемым функциям, которые используют этот параметр, не требуется ключевое слово _export. Их не нужно также перечислять в секции EXPORTS файла определения модуля, так как компоновщику не нужно создавать для них запись в выполняемом файле.

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

Для DLL эффективные обратные вызовы отсутствуют, так как для DKLL DS != SS. Из-за предположения о равенстве DS == SS вы можете использовать этот параметр только в приложениях. Кроме того DS не следует изменять в программе явным образом (в любом случае в Windows этого делать не следует).

Параметр -WSE

Параметр -WSE создает объектный модуль с функциями, использующими эффективные обратные вызовы, и явными экспортируемыми функциями. Этот параметр используется только в BCC (16-разрядный режим).

Параметры -WD и -WCD

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

Параметры -WDE и -WCDE

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

Выводы

Если функция отмечена ключевым словом _export, и используется любой из параметров компилятора Windows, то функция компилируется как экспортируемая и компонуется как функция экспорта. Если функция не помечена ключевым словом _export, Borland C++ будет выполнять одно из следующих действий:

Результат комбинирования параметров компилятора с ключевым словом _export описывается следующей таблицей: Функция помечена + + + + - - - - словом _export? Функция перечислена + + - - + + - - в EXPORTS Параметр -W -WE -W -WE -W -WE -W -WE компилятора -WD -WDE -WD -WDE -WD -WDE -WD -WDE Функция + + + + + - + - экспортируема? Функция будет + + + + + + (1) - (2) - экспортируемой?

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

2 - эта комбинация также имеет смысл. Нет смысла компилировать все функции как экспортируемые, если реально вы экспортируете только некоторые из них.

Файлы описания проектов

Файлы описания проекта автоматизируют процесс построения приложений Windows при использовании IDE Borland C++. Файлы описания проектов с расширением .PRJ содержат информацию о том, как построить конкретное приложение. Используя такое инструментальное средство как администратор проектов, вы можете создавать и обслуживать файлы проектов, описывающие каждое из разрабатываемых приложений. Файлы описания проектов содержат список обрабатываемых файлов и параметры каждого используемого инструментального средства. Эта информация используется администратором проектов для автоматического построения приложения. Файлы описания проектов и администратор проектов - это эквиваленты формирующих файлов и утилиты Make, но их легче обслуживать и использовать, чем формирующие файлы. Для установки параметров проекта можно использовать диалоговое окно Project Options интегрированной среды.

Администратор динамически распределяемой области памяти

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

Глобальная динамически распределяемая область - это пул памяти, доступной для всех приложений. Хотя могут распределяться глобальные блоки памяти любого размера, глобальная динамически распределяемая область предназначена только для больших блоков памяти (256 байт или более). Каждый блок глобальной памяти дополнительно использует не менее 20 байт. В соответствии со стандартом Windows и улучшенными режимами процессора 386 существует системное ограничение в 8192 блока глобальной памяти, только некоторые из которых будут доступны для любого конкретного приложения.

Локальная динамически распределяемая область - это пул памяти, доступной только для вашего приложения. Она существует только в верхней части сегмента данных приложения. Общий размер блоков локальной памяти, которые могут распределяться в локальной динамической области - это 64 минус размер стека приложения и его статических данных. По этой причине локальная динамически распределяемая область лучше подходит для небольших блоков памяти (256 байт и менее). По умолчанию локальная динамически распределяемая область имеет размер 4К, но в файле .DEF приложения его можно изменить.

Borland C++ включает в себя администратор памяти, реализующий функции new, delete, malloc и free. Этот администратор динамически распределяемой памяти использует для всех приложений глобальную динамически распределяемую область. Поскольку такая область имеет системное ограничение, администратор динамически распределяемой памяти включает в себя механизм вторичного распределения, улучшающий производительность и позволяющий выделять существенно большее число блоков.

Алгоритм вторичного распределения работает следующим образом: при выделении большого блока администратор динамически распределяемой памяти с помощью подпрограммы Windows GlobalAlloc выделяет блок глобальной памяти. При выделении небольшого блока администратор памяти выделяет более крупный блок глобальной памяти, а затем при необходимости разбивает этот блок на блоки меньшего размера. При выделении небольших блоков перед тем, как администратор выделит новый блок глобальной памяти (который также будет вторично распределяться), повторно используется все доступное пространство вторичного распределения.

Пороговое значение между большим и малым блоком динамически распределяемой памяти определяет переменная HeapLimit. Она устанавливается равной 64 байтам. Переменная HeapBlock определяет размер, используемый администратором динамически распределяемой памяти при вторичном распределении. Ей присваивается значение 4096 байт.

Назад | Содержание | Вперед

Copyright © CIT