Этапы компиляции

Компиляция исходных текстов на Си в исполняемый файл происходит в три этапа.

Препроцессинг

Эту операцию осуществляет текстовый препроцессор.

Исходный текст частично обрабатывается — производятся:

Компиляция

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

  1. Лексический анализ. Последовательность символов исходного файла преобразуется в последовательность лексем.
  2. Синтаксический анализ. Последовательность лексем преобразуется в дерево разбора.
  3. Семантический анализ. Дерево разбора обрабатывается с целью установления его семантики (смысла) — например, привязка идентификаторов к их декларациям, типам, проверка совместимости, определение типов выражений и т. д.
  4. Оптимизация. Выполняется удаление излишних конструкций и упрощение кода с сохранением его смысла.
  5. Генерация кода. Из промежуточного представления порождается объектный код.

Результатом компиляции является объектный код.

Объектный код — это программа на языке машинных кодов с частичным сохранением символьной информации, необходимой в процессе сборки.

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

Компоновка

Также называется связывание, сборка или линковка.

Это последний этап процесса получения исполняемого файла, состоящий из связывания воедино всех объектных файлов проекта.

При этом возможны ошибки связывания.

Если, допустим, функция была объявлена, но не определена, ошибка обнаружится только на этом этапе.

Особенность подключения пользовательских библиотек в Си

Подключение пользовательской библиотеки в Си на самом деле не так просто, как кажется.

Рассмотрим пример: есть желание вынести часть кода в отдельный файл — пользовательскую библиотеку.

program.c

#include "mylib.h"
const int MAX_DIVISORS_NUMBER = 10000;

int main()
{
    int number = read_number();

    int Divisor[MAX_DIVISORS_NUMBER];
    size_t Divisor_top = 0;
    factorize(number, Divisor, &Divisor_top);

    print_array(Divisor, Divisor_top);
    return 0;
}

 

Сама библиотека должна состоять из двух файлов: mylib.h и mylib.c.

mylib.h

#ifndef MY_LIBRARY_H_INCLUDED
#define MY_LIBRARY_H_INCLUDED

#include <stdlib.h>

//считываем число
int read_number();

//получаем простые делители числа
// сохраняем их в массив, чей адрес нам передан
void factorize(int number, int *Divisor, int *Divisor_top);

//выводим число
void print_number(int number);

//распечатывает массив размера A_size в одной строке через TAB
void print_array(int A[], size_t A_size);

#endif // MY_LIBRARY_H_INCLUDED

 

mylib.c

#include <stdio.h>

#include "my_library.h"

//считываем число
int read_number()
{
    int number;
    scanf("%d", &number);
    return number;
}

//получаем простые делители числа
// сохраняем их в массив, чей адрес нам передан
void factorize(int x, int *Divisor, int *Divisor_top)
{
    for (int d = 2; d <= x; d++) {
        while (x%d == 0) {
            Divisor[(*Divisor_top)++] = d;
            x /= d;
        }
    }
}

//выводим число
void print_number(int number)
{
    printf("%d\n", number);
}

//распечатывает массив размера A_size в одной строке через TAB
void print_array(int A[], size_t A_size)
{
    for(int i = A_size-1; i >= 0; i--)
    {
        printf("%d\t", A[i]);
    }
    printf("\n");
}

 

Препроцессор Си, встречая #include "mylib.h", полностью копирует содержимое указанного файла (как текст) в место вызова директивы. Благодаря этому на этапе компиляции не возникает ошибок типа Unknown identifier при использовании функций из библиотеки.

Файл mylib.c компилируется отдельно.

А на этапе компоновки полученный файл mylib.o должен быть включен в исполняемый файл program.exe.

Cреда разработки обычно скрывает весь этот процесс от программиста, но для корректного анализа ошибок сборки важно представлять себе как это делается.