Программирование на C и C++

Онлайн справочник программиста на C и C++

Компоновка, библиотеки и заголовочные файлы

При создании компилятора С или С++ решались две задачи. Во-первых, разрабатывался сам компилятор. Во-вторых, создавалась библиотека функций. Поскольку в библиотеке Borland содержится очень много функций, можно не сомневаться, что ее создание потребовало от программистов серьезных усилий. (Одно только описание этих функций занимает несколько сотен страниц!) Любая С или С++ программа опирается на эти функции при выполнении множества задач. Ввиду столь фундаментальной роли библиотеки для выполнения программ важно познакомиться с описанием ее работы. В частности, необходимо разобраться в работе компоновщика, понять, чем библиотеки отличаются от объектных файлов и какова роль заголовочных файлов. Эти вопросы и рассматриваются ниже.

Компоновщик (LINKER)

Результатом работы компилятора является перемещаемый объектный файл, а результатом работы компоновщика — файл, готовый к исполнению. Компоновщик выполняет двойную роль. Во-пер­вых, он физически объединяет указанные в списке связей файлы в один программный файл. Во- вторых, он решает проблему внешних ссылок и обращений к памяти. Внешняя ссылка делается каждый раз, когда в коде одного файла упоминается код из другого файла. Это случается либо при вызове функции, либо при упоминании глобальной переменной. Например, при установлении связей между двумя приведенными ниже файлами нужно реализовать обращение файла Two к переменной count. Именно компоновщик «сообщает» файлу Two, где в памяти располагается count.
Файл 1
int count;
extern void display(void);
int main(void)
{
count = 10;
display();
return 0;
}
Файл 2
#include <stdio.h>
extern int count;
void display(void)
{
printf("%d", count);
}

Подобным же образом компоновщик «сообщает» файлу One, где располагается функция display, чтобы ее можно было вызвать.
При формировании объектного кода файла Two компоновщик на место адреса count подстав­ляет «пустую» команду, поскольку он не знает, в каком месте памяти расположена count. При­мерно то же происходит при компилировании файла One. Адрес display() неизвестен, поэтому используется «пустая» команда. Такой прием обеспечивает возможность создания перемещаемо­го кода (relocatable code). После связывания файлов компоновщиком «пустые» команды замеща­ются относительными адресами.

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

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

Компоновщик (LINKER)

Результатом работы компилятора является перемещаемый объектный файл, а результатом работы компоновщика — файл, готовый к исполнению. Компоновщик выполняет двойную роль. Во-пер­вых, он физически объединяет указанные в списке связей файлы в один программный файл. Во- вторых, он решает проблему внешних ссылок и обращений к памяти. Внешняя ссылка делается каждый раз, когда в коде одного файла упоминается код из другого файла. Это случается либо при вызове функции, либо при упоминании глобальной переменной. Например, при установлении связей между двумя приведенными ниже файлами нужно реализовать обращение файла Two к переменной count. Именно компоновщик «сообщает» файлу Two, где в памяти располагается count.

Файл 1
int count;
extern void display(void);
int main(void)
{
count = 10;
display();
return 0;
}

Файл 2
#include <stdio.h>
extern int count;
void display(void)
{
printf("%d", count);
}

Подобным же образом компоновщик «сообщает» файлу One, где располагается функция display, чтобы ее можно было вызвать.

При формировании объектного кода файла Two компоновщик на место адреса count подстав­ляет «пустую» команду, поскольку он не знает, в каком месте памяти расположена count. При­мерно то же происходит при компилировании файла One. Адрес display() неизвестен, поэтому используется «пустая» команда. Такой прием обеспечивает возможность создания перемещаемо­го кода (relocatable code). После связывания файлов компоновщиком «пустые» команды замеща­ются относительными адресами.

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

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

Библиотечные файлы в сравнении с объектными файлами

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

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

Поскольку функции хранятся в библиотеке, то в исполняемый код программы войдут лишь действительно используемые в программе функции. (Если бы они входили в состав объект­ных файлов, программа была бы длиннее на несколько сот килобайт!)

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

Многие библиотечные функции работают со своими особыми типами данных и со структурами, к которым программа должна иметь доступ. Эти структуры и типы определяются в заголовочных файлах, поставляемых с компилятором, и они (заголовочные файлы) должны включаться (с по­мощью #include) в каждый файл, использующий функции, на которые они ссылаются. Кроме того, у всех библиотечных функций имеются прототипы, определенные в заголовочном файле. Это сделано по двум причинам. Во-первых, в (С++ все функции должны иметь прототипы. Во-вторых, хотя в С создание прототипов и не является обязательным, их использование настоятель­но рекомендовано, поскольку оно обеспечивает средство для более тщательного контроля типов. Включая в С-программу заголовочные файлы, которые соответствуют стандартным функ­циям программы, можно обнаружить потенциальные ошибки несовпадения типов. Например, включение string.h (заголовочного файла для работы с функциями обработки строк) в нижесле­дующий код приведет к выдаче при компилировании предупреждения:

#include <string.h>
char s1[20] = "hello ";
char s2[] = "there.";
int main(void)
{
int p;
p = strcat(s1, s2);
return 0;
}

Поскольку функция strcat() в заголовочном файле объявлена как возвращающая указатель на char, то компилятор может зарегистрировать как потенциальную ошибку присвоение целочис­ленной переменной р значения этого указателя.

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

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

Таблица: Наиболее употребительные заголовочные файлы
ALLOC.Н Функции динамического выделения памяти
ASSERT.H  Определяет макрос assert() (ANSI С)
BIOS.H Функции ROM-BIOS
CONIO.H Функции для работы с экраном
CTYPE.H Функции для работы с символами (ANSI С)
DIR.H Функции для работы с каталогами
DOS.H Функции интерфейса DOS
ERRNO.H Определяет коды ошибок (ANSI С)
FCNTL.H Определяет константы, используемые функцией ореn()
FLOAT.H Определяет зависящие от реализации переменные с плавающей точкой (ANSI С)
FSTREAM.H Файл определений ввода/вывода (С++)
GRAPHICS.Н Графические функции
IO.Н UNIX-подобные процедуры ввода/вывода
IOMANIP.H Определяет манипуляторы ввода/вывода (С++)
IOSTREAM.H Определяет классы потоков ввода/вывода (С++)
LIMITS.Н Определяет различные, зависящие от реализации, пределы (ANSI С)
LOCALE.Н Функции, зависящие от стран и языков (ANSI С)
МАТН.Н Разные определения, используемые математической библиотекой (ANSI С)
PROCESS.H Функции spawn() и ехес()
SETJMP.H Нелокальные переходы (ANSI С)
SHARE.H Совместное использование файлов
SIGNAL.H Определяет величины сигналов (ANSI С)
STDARG.H Списки аргументов длин переменных (ANSI С)
STDDEF.H Определяет некоторые общеупотребительные константы (ANSI С)
STDIO.H Объявления для стандартных потоков ввода/вывода (ANSI С)
STDLIB.H Различные определения (ANSI С)
STRING.H Обработка строк (ANSI С)
STRSTREA.H  Классы потоков ввода/вывода на базе массивов
TIME.H Функции системного времени

Макросы в заголовочных файлах

Многие библиотечные функции в действительности являются вообще не функциями, а скорее параметризованными макроопределениями, содержащимися в заголовочном файле. Последствия этого, в общем-то, незначительны, однако эта разница будет подчеркиваться при обсуждении таких «функций». Если по каким-либо причинам необходимо избежать использования стандартного макроса, можно отменить его определение, используя препроцессорную директиву #undef.