logo
Языки программирования

13.1. Раздельная компиляция

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

Раздельная компиляция в языке Fortran

Когда был разработан Fortran, программы вводились в компьютер с помощью перфокарт, и не было никаких дисков или библиотек программ, которые из­вестны сегодня.

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

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

Поскольку каждая подпрограмма компилируется независимо, нельзя со­вместно использовать глобальные объявления данных. Вместо этого опреде­лены общие (common) блоки:

subroutine S1

common /block1/distance(100), speed(100), time(100)

real distance, speed, time

end

Это объявление требует выделить 300 ячеек памяти для значений с плаваю­щей точкой. Все другие объявления для этого же блока распределяются в те же самые ячейки памяти, поэтому, если другая подпрограмма объявляет:

subroutine S2

common /block1/speed(200), time(200), distance(200)

integer speed, time, distance

….

End

то две подпрограммы будут использовать различные имена и различные типы для доступа к одной и той же памяти! Отображение common-блоков друг на друга делается по их расположению в памяти, а не по именам переменных. Если для переменной типа real выделяется столько памяти, сколько для двух пере­менных типа integer, speed(8O) в подпрограмме S2 размещается в той же са­мой памяти, что и половина переменной distance(40) в S1. Эффект подобен неаккуратному использованию типов union в языке С или вариантных запи­сей в языке Pascal.

Независимая компиляция и общие блоки вряд ли создадут проблемы для отдельного программиста, который пишет небольшую программу, но с боль­шой вероятностью вызовут проблемы в группе из десяти человек; придется организовывать встречи или контроль, чтобы гарантировать, что интерфейсы реализованы правильно. Частичное решение состоит в том, чтобы использо­вать включаемые (include) файлы, особенно для общих блоков, но вам все равно придется проверять, что вы используете последнюю версию включае­мого файла, и удостовериться, что какой-нибудь умный программист не иг­норирует объявления в файле.

Раздельная компиляция в языке С

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

Вначале немного терминологии: объявление вводит имя в программу:

void proc(void);

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

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

/* File main.c */

int global; /* Внешняя по умолчанию */

int func(int); /* Внешняя по умолчанию */

int main(void)

{

global = 4;

return func(global);

}

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

/* File func.c */

extern int global; /* Внешняя, только объявление */

int func(int parm)

{

return parm + global:

}

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

Компиляция в языке С независима в том смысле, что результат одной ком­пиляции не сохраняется для использования в другой. Если кто-то из вашей группы случайно напишет:

/* File func.c */

extern float global; /* Внешняя, только объявление */

int func(int parm) /* Внешняя по умолчанию */

{

return parm + global;

}

программа все еще может быть откомпилирована и скомпонована, а ошибка произойдет только во время выполнения. На моем компьютере целочисленное значение 4, присвоенное переменной global в main, воспринимается в файле func.c как очень малое число с плавающей точкой; после обратного преобразо­вания к целому числу оно становится нулем, и функция возвращает 4, а не 8.

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

/* File main.h */

extern int global; /* Только объявление */

/* File func.h */

extern int func(int parm); /* Только объявление */

/* File main.c */

#include "main.h"

#include "func.h"

int global; /* Определение */

int main(void)

{

return func(global) + 7;

}

/* File func.c */

#include "main.h"

#include "func.h"

int func(int parm) /* Определение */

{

return parm + global;

}

Спецификатор static

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

static int g 1; /* Глобальная переменная только в этом файле */

int g2; /* Глобальная переменная для всех файлов */

static int f1 (int i) {...}; /* Глобальная функция только в этом файле */

intf2(int i) {...}; /* Глобальная функция для всех файлов */

Здесь уместно говорить об области действия файла (file scope), которая выступает в роли области действия модуля (module scope), используемой в других языках. Было бы, конечно, лучше, если бы по умолчанию принимался спецификатор static, а не extern; однако нетрудно привыкнуть приписывать к каждому глобальному объявлению static.

Источником недоразумений в языке С является тот факт, что static имеет другое значение, а именно он определяет, что время жизни переменной явля­ется всем временем выполнения программы. Как мы обсуждали в разделе 7.4, локальные переменные внутри процедуры имеют время жизни, ограниченное одним вызовом процедуры. Глобальные переменные, однако, имеют стати­ческое время жизни, то есть они распределяются, когда программа начинает­ся, и не освобождаются, пока программа не завершится. Статическое время жизни — нормальный режим для глобальных переменных; на самом деле, гло­бальные переменные, объявленные с extern, также имеют статическое время жизни!

Спецификатор static также можно использовать для локальных перемен­ных, чтобы задать статическое время жизни:

void proc(void)

{

static bool first_time = true;

if (first_time) {

/* Операторы, выполняемые при первом вызове proc */

first_time = false;

}

….

}

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

Yandex.RTB R-A-252273-3
Yandex.RTB R-A-252273-4