Опубликован: 14.12.2010 | Уровень: для всех | Доступ: платный
Лекция 18:

Программы на языке С, состоящие из нескольких файлов

< Лекция 17 || Лекция 18: 12345 || Лекция 19 >
Аннотация: В лекции рассматриваются вопросы сборки программы, состоящей из нескольких функций, расположенных в разных файлах, а также дополнительные обращения к функциям.

Теоретическая часть

Программа на языке С – это совокупность функций. Запуск любой программы начинается с запуска главной функции, содержащей в себе всю остальную часть программы. Внутри главной функции для реализации заданного алгоритма вызываются все другие необходимые функции. Часть функций создается самим программистом, другая часть – библиотечные функции – поставляется пользователю со средой программирования и используется в процессе разработки программ (например, 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 не было бы необходимости.

При компиляции выполняются следующие правила:

  1. Если компилятор находит переменную, не объявленную внутри блока, он ищет ее объявление во внешних блоках.
  2. Если не находит ее там, то ищет среди объявлений глобальных переменных.

Спецификатор 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

Результат выполнения программы, состоящей из двух файлов

Рис. 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" \to "File", и далее в списке Installed Templates выбрать Visual C++ \to Header File (.h). Откроется окно, показанное на рис. 17.2.

Процесс создания нового файла с расширением .h

Рис. 17.2. Процесс создания нового файла с расширением .h

Далее в правом нижнем углу нажмем клавишу Open. Откроется пустое поле – заготовка для набора необходимого кода. По умолчанию этот файл имеет имя Header1.h. При повторном создании заголовочного файла это будет Header2.h и т.д. После написания кода можно сохранить этот заголовочный файл по желанию в любом каталоге с любым (допустимым) именем (а расширение остается .h ).

< Лекция 17 || Лекция 18: 12345 || Лекция 19 >
Мухаммадюсуф Курбонов
Мухаммадюсуф Курбонов
Александр Соболев
Александр Соболев
Россия
Артем Полутин
Артем Полутин
Россия, Саранск