Программы на языке С, состоящие из нескольких файлов
Теоретическая часть
Программа на языке С – это совокупность функций. Запуск любой программы начинается с запуска главной функции, содержащей в себе всю остальную часть программы. Внутри главной функции для реализации заданного алгоритма вызываются все другие необходимые функции. Часть функций создается самим программистом, другая часть – библиотечные функции – поставляется пользователю со средой программирования и используется в процессе разработки программ (например, printf(), sqrt() и др.).
Простейший метод использования нескольких функций требует их размещения в одном и том же файле. Затем выполняется компиляция этого файла, как если бы он содержал единственную функцию [17.1]. Другие подходы к решению этой проблемы существенно зависят от конкретной операционной системы (Unix-подобные системы, Windows, Macintosh). Компиляторы операционных систем Windows и Macintosh представляют собой компиляторы, ориентированные на проекты [17.1]. Проект описывает ресурсы, используемые программой. Эти ресурсы включают файлы исходного программного кода.
Если поместить главную функцию main() в один файл, а определения собственной функции программиста – во второй файл, то первому файлу нужны прототипы функций. Для этого можно хранить прототипы функций в одном из заголовочных файлов.
Хороший тон в программировании рекомендует размещать прототипы функций и объявлять их константы в заголовочном файле [17.1]. Назначение отдельных задач отдельным функциям способствует улучшениям программы.
Функция может быть либо внешней (по умолчанию), либо статической. К внешней функции доступ могут осуществлять функции из других файлов, в то же время статическая функция может использоваться только в файле, в котором она определена [17.1]. Например, возможны следующие объявления функций:
double gamma(); // внешняя функция по умолчанию static double beta(); extern double delta();
Функция gamma() и delta() могут использоваться функциями из других файлов, которые являются частью программы, тогда как beta() – нет. В силу этого применение функции beta() ограничено одним файлом, поэтому в других файлах можно использовать функции с тем же именем. Одна из причин использования класса статической памяти заключается в необходимости создания функций, приватных для конкретных модулей, благодаря чему во многих случаях удается избежать конфликта имен [17.1].
Обычная практика состоит в том, что при объявлении функции, определенной в другом файле, указывается ключевое слово extern. При этом просто достигается большая ясность, поскольку при объявлении функция и предполагается как extern, если только не задано ключевое слово static.
Одним из золотых правил для надежного программирования есть принцип "необходимости знать", или принцип минимально необходимой области видимости [17.1]. Рекомендуется держать всю внутреннюю работу каждой функции максимально закрытой по отношению к другим функциям, используя совместно только те переменные, без которых нельзя обойтись по логике программы. Другие классы памяти полезны, и ими можно воспользоваться. Однако всякий раз следует задать вопрос: а есть ли в этом необходимость?
Память, использованная для хранения данных, которыми манипулирует программа, может быть охарактеризована продолжительностью хранения, областью видимости и связыванием [17.1]. Продолжительность хранения может быть статической, автоматической или распределенной. Если продолжительность хранения статическая, память распределяется в начале выполнения программы и остается занятой на протяжении всего выполнения. Если продолжительность хранения автоматическая, то память под переменную выделяется в момент, когда выполнение программы входит в блок, в котором эта переменная определена, и освобождается, когда выполнение программы покидает этот блок. Если память выделяется, то она выделяется с помощью функции malloc() (или родственной функции) и освобождается посредством функции free(). Область видимости определяет, какая часть программы может получить доступ к данным. Переменные, определенные вне пределов функции, имеют область видимости в пределах файла и видимы в любой функции, определенной после объявления этой переменной. Переменная, определенная в блоке или как параметр функции, видима только в этом блоке и в любом из блоков, вложенных в этот блок.
Связывание описывает экстент (протяжение, пространство), в пределах которого переменная, определенная в одной части программы, может быть привязана к любой другой части программы. Переменная с областью видимости в пределах блока, будучи локальной, не имеет связывания. Переменная с областью видимости в пределах файла имеет внутреннее или внешнее связывание. Внутреннее связывание означает, что переменная может быть использована в файле, содержащем ее определение. Внешнее связывание означает, что переменная может быть использована в других файлах.
Стандарт языка С поддерживает 4 спецификатора класса памяти [17.2]:
- extern
- static
- register
- auto
Спецификаторы сообщают компилятору, как он должен разместить соответствующие переменные в памяти. Спецификатор класса памяти в объявлении всегда должен стоять первым [2].
Приведем характеристику спецификаторов классов памяти [17.2].
Спецификатор extern
В языке С при редактировании связей к переменной может применяться одно из трех связываний: внутреннее, внешнее или же не относящееся ни к одному из этих типов. В общем случае к именам функций и глобальных переменных применяется внешнее связывание. Это означает, что после компоновки они будут доступны во всех файлах, составляющих программу. К объектам, объявленным со спецификатором static и видимым на уровне файла, применяется внутреннее связывание, после компоновки они будут доступны только внутри файла, в котором они объявлены. К локальным переменным связывание не применяется и поэтому они доступны только внутри своих блоков.
Спецификатор extern указывает на то, что к объекту применяется внешнее связывание, именно поэтому они будут доступны во всей программе. Объявление ( декларация ) объявляет имя и тип объекта. Описание (определение, дефиниция) выделяет для объекта участок памяти, где он будет находиться. Один и тот же объект может быть объявлен неоднократно в разных местах, но описан он может быть только один раз.
Пример использования спецификатора extern при использовании глобальных переменных:
#include <stdio.h> #include <conio.h> // Главная функция int main (void) { // объявление глобальных переменных extern int a, b; printf("\n\t a = %d; b = %d\n", a, b); printf("\n Press any key: "); _getch(); return 0; } // инициализация (описание) глобальных переменных int a = 33, b = 34;
Описание глобальных переменных дано за пределами главной функции main(). Если бы их объявление и инициализация встретились перед main(), то в объявлении со спецификатором extern не было бы необходимости.
При компиляции выполняются следующие правила:
- Если компилятор находит переменную, не объявленную внутри блока, он ищет ее объявление во внешних блоках.
- Если не находит ее там, то ищет среди объявлений глобальных переменных.
Спецификатор extern играет большую роль в программах, состоящих из многих файлов [17.3]. В языке С программа может быть записана в нескольких файлах, которые компилируются раздельно, а затем компонуются в одно целое. В этом случае необходимо как-то сообщить всем файлам о глобальных переменных программы. Самый лучший (и наиболее переносимый) способ сделать это – определить (описать) все глобальные переменные в одном файле и объявить их со спецификатором extern в остальных файлах, например, как это сделано в следующей программе:
Первый файл ( main.c ) | Второй файл ( second.h ) |
#include <stdio.h> #include <conio.h> #include "D:\second.h" int x = 99, y = 77; char ch; void func1(void); int main(void) { ch = 'Z'; func1(); printf("\n Press any key: "); _getch(); return 0; } void func1(void) { func22(); func23(); printf("\n\t x = %d; y = %d;\ ch = %c\n", x, y, ch); } |
extern int x, y; extern char ch; void func22(void) { y = 100; } void func23(void) { x = y/10; ch = 'R'; } |
В программе первый файл – это основная часть программного проекта. Второй файл создан как текстовый файл (с помощью блокнота) с расширением *.h. Список глобальных переменных ( x, y, ch ) копируется из первого файла во второй, а затем добавляется спецификатор extern. Он сообщает компилятору, что имена и типы переменных, следующих далее, объявлены в другом месте. Все ссылки на внешние переменные распознаются в процессе редактирования связей. Подключение второго файла выполнено с указанием имени диска (D:), на котором расположен файл second.h.
Для подключения имени файла, созданного пользователем, его заключают в двойные кавычки.
Результат выполнения программы показан на рис. 17.1
В общем случае h -файл (например, second.h ) формируется редактором кода: надо создать заготовку обычным способом, очистить все поле редактора и записать в это поле необходимые данные (программный код созданной функции). Затем выполнить команду главного меню: File/Save as и выбрать для сохраняемого файла расширение .h в раскрывающемся списке типов сохраняемого файла: C++ Header Files (*.h; *.hh; *.hpp; *.hxx; *.inl; *.tlh; *.tli). Сохраненный файл с расширением .h следует подключить к проекту. Для этого потребуется в узле Solution Explorer навести курсор мыши к папке Header Files и правой кнопкой мыши выбрать Add – Existing Item сохраненный файл second.h. Затем с помощью оператора #include файл следует включить в основную программу.
Другой способ, реализуемый в Microsoft Visual Studio 2010, состоит в том, что сразу через пункт меню "File" выбрать "New" "File", и далее в списке Installed Templates выбрать Visual C++ Header File (.h). Откроется окно, показанное на рис. 17.2.
Далее в правом нижнем углу нажмем клавишу Open. Откроется пустое поле – заготовка для набора необходимого кода. По умолчанию этот файл имеет имя Header1.h. При повторном создании заголовочного файла это будет Header2.h и т.д. После написания кода можно сохранить этот заголовочный файл по желанию в любом каталоге с любым (допустимым) именем (а расширение остается .h ).