Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ЯП - ПОИТ (Бахтизин) часть 1 редакт.doc
Скачиваний:
1
Добавлен:
01.04.2025
Размер:
1.76 Mб
Скачать

11.5. Директивы # и ##

Ранее нами уже были рассмотрены макросы и способы их применения в программах на языке Си. Для расширения возможностей макросов существуют ещё две директивы: # и ##.

Оператор # превращает аргумент, которому он предшествует, в строку, заключенную в кавычки. Например:

#define mkstr(s) # s

Данный оператор преобразует аргумент в строку. Вызов mkstr(123) раскроется в ("123").

Оператор ## используется для конкатенации двух лексем. Например:

#define concat(a,b) a ## b

Данный оператор соединяет ("склеивает") свои аргументы, может использоваться для формирования идентификаторов. Вызов concat(x,4) раскроется в (x4).

Допускается ставить по одному необязательному пробелу слева и справа от знака ##. При раскрытии макроопределения препроцессор удалит пробелы. Вложенные маровызовы, расположенные в строке макроопределения, раскрываются только в момент раскрытия самого макроса, а не в момент его определения.

12. Модульное программирование

В общем случае, программа на Си может состоять из нескольких отдельно компилируемых модулей. Каждый из них находится в своем текстовом файле, а после компиляции, «превращается» в т.н. объектный файл с расширением .obj. Вот, например, как может выглядеть программа печати строки, если ее исходный код разбить на два модуля:

Module1.c

Module2.c

int main()

{

myprint(“Joe”);

}

void myprint(char* s)

{

while (*s)

putch(s++);

}

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

Несомненно, двухэтапный подход к созданию выполнимого файла открывает множество возможностей, таких, например, как создание библиотек. Технически, связывание отдельно скомпилированных модулей, возможно благодаря тому, что в объектные файлы кроме машинных кодов команд и данных программы заносятся т.н. неразрешенные символьные ссылки. В нашем случае объектный файл Module1.obj содержал вместо конкретного указателя имя функции myprint(), а модуль Module2.obj информацию для линковщика о том, что в Module2.obj есть функция myprint(). Линковщику оставалось лишь подставить вместо неразрешенной ссылки из первого модуля адрес функции из второго.

Однако у такого подхода есть и обратная сторона: тот факт, что перед вызовом функции не обязательно иметь её прототип, делает невозможной проверку типов во время компиляции. Так, функция myprint может быть вызвана из модуля Module1.c (не содержащего ее прототипа) с параметрами не только неверного типа, но и количества. Очень остро эта проблема встает в программах, с большим количеством функций, особенно ввиду того, что, обычно, чем больше проект, тем активней в нем используется модульное программирование.

Однако способ устранения данного недостатка модульного программирования лежит на поверхности, и заключается в использовании прототипов (заголовков). Если бы текст Module1.c выглядел так:

Module1.c

void myprint(char *s)

int main()

{

myprint(“Joe”);

}

никаких проблем с неверными параметрами функции возникнуть бы не могло. Встретив прототип, компилятор не допустит неверного преобразования типов. Но это еще не способ решения проблемы: во-первых, переписывая из раза в раз заголовок можно и в нем допустить ошибку, а во-вторых, такой подход просто не удобен, и значит, программист просто поленится переписать заголовки всех функций, которые собирается использовать. Перечислять недостатки можно было бы и дальше, но намного проще устранить их, создав текстовый файл, в котором будут храниться заголовки всех функций данного модуля. Читатель, наверное, уже догадался, что, раз файл должен хранить заголовки функций, его нужно называть заголовочным, и дать ему расширение “h” (от английского слова header – заголовок). В нашем случае заголовочный файл будет иметь имя Module2.h, что вполне логично, т.к. содержать он будет прототипы функций из Module2.c:

Module2.h

Module2.c

void myprint(char *s);

#include “Module2.h”

void myprint(char* s)

{

while (*s)

putch(s++);

}

Теперь, добавив соответствующую строку в module1.c, получим намного более удобную и надежную конструкцию, чем та, что была получена вначале.

Module1.c

// Вместо этой строки в файл будет подставлено содержимое

// заголовочного файла, т.е. прототипы функций из Module2.c

#include “Module2.h”

int main()

{

myprint(“Joe”);

}

Заметьте, что заголовочный файл включен в Module2.c не случайно: это не даст кодировщику забыть изменить прототипы, если он изменит заголовок одной из функций модуля. Так, если

void myprint (char* s) {}

по каким-то причинам, будет заменен, на

char* myprint (char* s, int *res)

а заголовок останется прежним, программист увидит сообщение компилятора о том, что прототип из Module2.h и заголовок функции в Module2.c не совпадают.

Обычно IDE предоставляет удобный механизм создания многомодульных программ так, в Borland C++ 3.1 это можно сделать, выбрав в меню пункт project -> open project и введя несуществующее в данном каталоге имя. При этом будут созданы файлы имя.prj и имя.dsk, в которых IDE сохранит служебную информацию о проекте. Далее, в служебном окне project, можно, нажав insert, ввести имена файлов, которые будут содержать исходный код (существующие, или несуществующие). Подробнее о работе с проектами в вашей среде разработки можно прочесть в файле справки, или в документации по системе.

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

Program.c

Modl.h

#include “Modl.h”

int main()

{

myprint(“Joe”);

}

void myprint(char *s)

{

while (*s)

putch(s++);

}

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

Чтобы проверить работоспособность примера программы из нескольких модулей, создадим проект с именем test, а Module1.c, Module2.c и Module2.h переименуем в main.c, unit.c и unit.h соответственно. Если сохранить все файлы в одном каталоге, должна получиться примерно такая картина:

P:\TC\C\Projects>dir

Содержимое папки P:\TC\C\Projects

22.02.2003 19:45 <DIR> .

22.02.2003 19:45 <DIR> ..

22.02.2003 19:43 180 Main.c

22.02.2003 19:45 27 533 Test.dsk

22.02.2003 19:45 4 143 Test.prj

22.02.2003 19:43 82 Unit.c

22.02.2003 19:43 23 Unit.h

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

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

Program.c

Modl.h

#include “Modl.h

#include “Another.h”

int main()

{

myprint(“Joe”);

}

void myprint(char *s);

В данном примере таким заголовочным файлом является Another.h – он необходим как в Modl.h, так и в Program.c. С одной стороны, если Modl.h построен правильно и не содержит определений, это не грозит никакими ошибками, т.к. непротиворечивые объявления, могут встретиться в модуле несколько раз. С другой стороны, многократное включение одного и того же файла, ведет к неоправданным потерям времени при компиляции.

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

Another.h

#ifndef ANOTHERH

#define ANOTHERH

//-------------------------------------

int *someheader(int, int*, char);

void oneheader(char*);

const int *twoheader(int, char);

// ...

//-------------------------------------

#endif

Компилятор заходит в блок #ifndef, если символическая константа ANOTHERH еще не объявлена, и, первым делом, объявляет ее. Теперь, сколько бы раз не встретился заголовочный файл, работа по включению его содержимого в модуль будет произведена лишь однажды.

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

  • Необходимость совместной работы множества программистов.

  • Возможность разбить исходный код на несколько частей.

  • Удобство создания библиотек общеиспользуемых функций.

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

Очень часто, для сокрытия исходного кода распространяемых на коммерческой основе библиотек, lib.c компилируют и распространяют вместо него lib.obj и, естественно, lib.h.

Следует помнить о том, что модуль может содержать не только функции, но и переменные, доступ к которым может быть получен извне. Однако, т.к. тип переменной компилятор должен «знать» до первого ее использования опустить объявление переменной нельзя:

Lib.h

Lib.c

#ifndef LIBH

#define LIBH

//-----------------------

// Заголовочный файл не

// содержит объявления

// useful_var

//-----------------------

#endif

#include “Lib.h”

int useful_var;

Однако объявить в заголовочном файле переменную не так просто, ведь при включении заголовочного файла в программу, будет создана локальная копия переменной:

Lib.h

Program.c

#ifndef LIBH

#define LIBH

//-----------------------

#include “Lib.h”

// Объявление?

int useful_var;

//-----------------------

#endif

#include “Lib.h

void someFunction(void)

{

useful_var = 0;

}

В приведенном примере включение в program.c файла lib.h равносильно включению строки “int useful_var;”:

Program.c

int useful_var; // Локальная копия переменной

void somefunction(void)

{

useful_var = 0;

}

В памяти будет создана еще одна переменная, к которой и будет обращаться somefunction из примера. Обойти эту проблему можно при помощи ключевого слова extern:

Lib.h

#ifndef LIBH

#define LIBH

//-------------------------------------

#include “Lib.h”

// Объявление!

extern int useful_var;

//-------------------------------------

#endif

Теперь somefunction изменит именно переменную из модуля lib.h. Следует помнить, что объявление, содержащее инициализацию, становится определением:

extern int useful_var = 0; // Определение!

Ключевое слово extern можно использовать и с объявлениями функций, хотя и является там избыточным:

extern void somefunction(void);

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

Lib.c

void f1(void); // Для общего пользования

void f2(void); // Для пользования в специальных модулях

void f3(void); // Для внутреннего пользования

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

PublicLib.h

ProtectedLib.h

#ifndef PUBLICLIB

#define PUBLICLIB

//----------------------

void f1(void);

//----------------------

#endif

#ifndef PROTECTEDLIB

#define PROTECTEDLIB

//-----------------------

void f1(void);

void f2(void);

//-----------------------

#endif

Не следует забывать и о том, что сам библиотечный модуль должен включать оба заголовочных файла:

Lib.c

#include “PublicLib.h”

#include “ProtectedLib.h”

void f1(void);

void f2(void);

void f3(void);

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

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

#include <ConIO.h>

в lib.c или в lib.h? Ответ не однозначен, но обычно, директива помещается в заголовочный файл только в том случае, если без этого не обойтись:

Lib.h

//-----------------------------------------------------------

#include <ConIO.h>

/* Структура text_info объявлена в ConIO.h */

somefunction(struct text_info* sti);

/* Перечислимый тип text_modes также объявлен в ConIO.h */

enother_function(text_modes tm);

//-----------------------------------------------------------

Следует заметить, что т.к. lib.h включен в lib.c подключать сторонние библиотеки в обоих файлах не стоит:

Lib.h

Program.c

//----------------------

#include <ConIO.h>

//----------------------

#include “Lib.h”

// Нежелательно...

#include <ConIO.h>

// ...

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