Язык c. Лекция 2
1. Компиляция программы
1.1. Общая схема процесса получения выполняемой программы
Процесс создания выполняемого файла (т.е. программы в машинном коде) из исходного текста на языке C состоит из следующих шагов:
Зачем нужны два раздельных шага — компиляция и компоновка1 программы?
1. Стандартные библиотечные функции скомпилированы заранее и прилагаются к компилятору в виде объектных библиотек. Требуемые функции отыскиваются в библиотеках и присоединяются к программе на этапе сборки (компоновки). Поручать эту работу компилятору нерационально — он ориентирован на обработку исходного текста, тогда как объектные файлы и библиотеки содержат не текст, а двоичный машинный код. У компилятора и компоновщика (линкера) разная специализация — они имеют дело с информацией, представленной в принципиально отличных формах, и выполняют работу, непохожую по содержанию.
2. Возможность раздельной компиляции. Программа на языке C обычно состоит из нескольких исходных файлов (у больших программ число файлов достигает нескольких десятков или даже сотен). Каждый из файлов можно компилировать отдельно и независимо от других. Когда в какой-то один файл вносятся изменения, нет необходимости компилировать всю программу заново (при условии, что сохранились все объектные файлы). Достаточно перекомпилировать только измененный файл, а затем заново скомпоновать программу из объектных файлов (компоновка выполняется гораздо быстрее, чем компиляция).
3. Программисты-профессионалы довольно часто пишут отдельные части программы на других языках (Фортране, Ассемблере и т.д.). Для каждого языка используется свой компилятор, а полученные объектные файлы связываются в единую исполняемую программу с помощью компоновщика. Объектный файл не содержит «языковой» специфики, поскольку он состоит в основном из машинных команд. Поэтому компоновщик (линкер) вполне может работать с объектными файлами от разных компиляторов.
Примечание. Строго говоря, не всякие объектные файлы совместимы между собой. Существуют несколько разных (более или менее стандартизованных) форматов объектных файлов и библиотек. Как минимум, необходимо, чтобы все используемые файлы имели одинаковый формат. Однако, и этого бывает еще недостаточно. Требуется, чтобы компиляторы придерживались согласованных правил в отношении передачи параметров, использования регистров процессора, распределения памяти и т. п. либо позволяли явным образом задавать (менять) эти правила. В полной мере совместимыми на уровне объектного кода обычно бывают компиляторы, выпускаемые одной фирмой либо созданные (разными разработчиками) в рамках общего проекта.
1.2. Две фазы компиляции
Компиляция программы на языке C происходит в два этапа:
Препроцессирование (предварительная обработка текста)
Собственно компиляция (вообще говоря, тоже делится на две стадии — грамматический анализ и генерацию машинного кода).
Первая фаза выполняется препроцессором. Вначале из текста удаляются все комментарии. Затем препроцессор обрабатывает строки, начинающиеся с символа # — директивы препроцессора. К ним, в частности, относятся:
#include <имя-файла>
или
#include "имя-файла"
Это директива включения файла. Обычно ее применяют для включения т. наз. заголовочных файлов, содержащих наборы общих описаний. Два варианта записи имени файла (в угловых скобках или в кавычках) влияют на то, где именно препроцессор будет искать указанный файл.
#define имя подставляемый-текст
Это директива макроподстановки в простейшей форме (бывает еще макроподстановка с параметрами; мы не будем пока ее рассматривать). Здесь имя — любое имя (идентификатор), т.е. последовательность букв и цифр, начинающаяся с буквы, а подставляемый-текст — любой текст без каких-либо ограничений (все, что находится в директиве после имени до конца строки). Если препроцессор находит имя в лежащем ниже тексте программы, он заменяет его на подставляемый текст. (Имя должно входить в текст как самостоятельный лексический элемент. Если имя является частью более длинного имени, то замена не производится. Не анализируется также содержимое текстовых констант.)
Препроцессор не анализирует грамматику и семантику (смысловое содержание) текста, он работает на уровне лексических элементов (т.е. умеет выделять в тексте отдельные «слова», но не интересуется смыслом этих слов).
После завершения работы препроцессора начинается собственно компиляция — анализируется грамматическая структура, и если в ней не найдены ошибки, то генерируется эквивалентная последовательность машинных команд.