Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Кетков.doc
Скачиваний:
17
Добавлен:
27.09.2019
Размер:
2.22 Mб
Скачать

12.3. Динамически загружаемые библиотеки

В довольно ранних версиях операционных систем наряду со статическими библиотеками объектных модулей появились динамически загружаемые библиотеки с расширением .dll (от Dynamic-link libraries). Динамически загружаемые библиотеки Windows могут иметь и другие расширения – .exe или .drv.

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

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

Раздел 13. Дополнительные сведения о системе программирования Borland C++ 3.1

13.1. Препроцессор и условная компиляция

В состав системы программирования BC 3.1 входит препроцессор cpp.exe, который выполняет следующую подготовительную работу перед компиляцией программы:

  • включает в программу тексты указанных файлов;

  • исключает из программы фрагменты, не удовлетворяющие заданным условиям;

  • осуществляет предусмотренную замену (макроподстановка);

  • заменяет Esc-последовательности их числовыми кодами;

  • объединяет смежные символьные строки и устраняет символы переноса строк.

Все действия препроцессора диктуются директивами, которые программист включает в текст своей программы. Первым символом директивы является символ #.

Для включения в текст программы указанных файлов используется директива #include (от англ. – включить), допускающая два следующие формата:

#include <file_name>

#include "file_name"

Угловые скобки являются указанием препроцессору, что поиск файла с заданным именем надо начинать с системного каталога (например, с каталога c:\bc\include). Если в указанном каталоге файл file_name не обнаружен, то поиск продолжается сначала с текущего каталога, а затем по всем каталогам, перечисленным в директиве PATH операционной системы. Если имя файла заключено в двойные кавычки, то поиск начинается с текущего каталога.

Обычно, с помощью директивы #include к программе подключаются системные и пользовательские заголовочные файлы с расширением .h (от header – заголовок). Однако точно так же к программе можно подключить ранее заготовленный фрагмент исходного кода, оформленный в виде текстового файла с расширением .inc.

Замена одной цепочки символов в тексте программы на другую цепочку символов реализуется с помощью макроподстановки #define (от англ. – определить):

#define s1s2s3...sn q1q2...qm

При этом цепочка символов s1s2s3...sn в тексте исходной программы будет заменена на цепочку q1q2...qm . Пробелы перед замещающей цепочкой и в ее конце игнорируются. Замене не подвергаются значения строк и комментарии. Заменяющий фрагмент может оказаться и многостроковым. В этом случае в конце каждой строки помещается символ переноса – "\". В приведенной ниже программе содержится несколько наиболее характерных примеров использования директивы #define:

#include <stdio.h>

#include <conio.h>

#define Nmax 100

#define max(a,b) ((a)>(b))?(a):(b)

#define print(a) printf("\n%s=%d\n",#a,a);\

getch()

void main()

{ int x=5,y=8;

int z=Nmax;

int w=max(x*y,z);

print(x);

print(y);

print(z);

print(w);

}

//=== Результат работы ===

x=5

y=8

z=100

w=100

Обратите внимание на некоторые тонкости в приведенных подстановках.

Во-первых, аргументы макроопределения-функции max в замещающем выражении заключены в круглые скобки. Это позволяет использовать в конкретных обращениях нормальные арифметические выражения. Представим себе, что в программе достаточно часто приходится использовать операцию возведения в квадрат. Если для этой цели макроподстановку Square(x) определить без использования скобок (#define Square(x) x*x), то для арифметического выражения Square(1+z) результат такой подстановки даст 1+z*1+z=2*z+1, т.е. заведомо неправильное значение.

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

w=max(x+=2,y+=3); //результат подстановки w=14

w=max(x+2,y+3); //результат правильный w=11

В бесскобочном макроопределении первое обращение привело бы к синтаксической ошибке (Lvalue required – требуется левое значение).

Во-вторых, макроопределение max не зависит от типа используемых данных.

В-третьих, в макроопределении print использована довольно редко описываемая возможность вывода имени переменной (#a – интерпретируется препроцессором как имя переменной a). Если бы мы включили эту переменную в форматную строку (printf("\na=%d",a);), то ничего хорошего из этого бы не вышло. Так как на содержимое строк макроподстановка не распространяется, то при каждом обращении вместо имени очередной переменной выводился бы символ 'a'.

В операторе макроподстановки иногда используется операция склейки лексем (аналог того, что при работе со строками называют конкатенацией):

#define Paste(a,b) a##b

В результате такой подстановки строка Paste(x,4) будет заменена на x4.

Две группы следующих директив используются для организации "условной компиляции":

#define...#ifdef...ifndef...#undef

#if...#elif...#elif...#else...#endif

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

Если заглянуть в любой заголовочный файл из каталога ...BC\INCLUDE, например, в файл math.h, то в самом его начале (исключая комментарий по поводу авторских прав) находятся следующие строки:

#ifndef __MATH_H

#define __MATH_H

#if !defined(___DEFS_H)

#include <_defs.h>

#endif

В чем смысл двух первых строк? Сначала проверяется, была ли ранее объявлена подстановка для идентификатора __MATH_H (этот идентификатор является уникальным, т.к. он повторяет имя заголовочного файла). Если такого указания еще не было, то следующая строка объявляет о необходимости такой подстановки в строках программы, следующих ниже по тексту (не важно, что замещающее выражение пусто), и все остальное содержимое файла, который мы присоединяем по директиве #include <math.h>, будет включено в текст наше программы. Но если такая подстановка ранее была заявлена, то повторное подключение файла math.h не произойдет. Это позволяет избежать дублирования констант и других макроопределений, которые могли бы появиться из-за повторения заголовочного файла.

Примерно такую же функцию выполняют три следующие строки. Первая из них проверяет, не состоялось ли ранее присвоение значения идентификатору ___DEFS_H. Если такого действия еще не было, то файл defs.h будет загружен в оперативную память. В противном случае фрагмент программы до строки #endif не будет передан компилятору. Кстати, подобная тройка строк присутствует во многих заголовочных файлах, и такая проверка предупреждает повторную загрузку файла defs.h.

Более детально, первая группа директив препроцессора выполняет следующие действия:

Таблица 13.1

Директива

Пояснение

#define name value

Объявляет о необходимости замены выражения name на значение value в строках программы, расположенных ниже

#ifdef name

Проверяет, была ли объявлена замена выражения name (true, если была)

#ifndef name

Проверяет, была ли объявлена замена выражения name (true, если не была)

#undef name

Отменяет указание о замене выражения name в строках программы, расположенных ниже

Вторая группа директив предназначена для включения или отключения фрагментов программы пользователя средствами, напоминающими действие условного оператора if – then – else:

#if (условие_1) //если выполнено условие_1

фрагмент_1

#elif (условие_2) //если выполнено условие_2

фрагмент_2

#elif (условие_3) //если выполнено условие_3

фрагмент_3

.............

#else //если не выполнено ни одно из предшествующих условий

фрагмент_k

#endif //конец проверок

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

#define name 1

.............

#if (name==1)

Одним из достаточно частых применений группы #if...#endif является подключение или отключение отладочных выдач.