Лекция 19. Раздельная компиляция, указатели на функции

Схема работы компилятора при сборке наших программ до этого момента выглядела так:

_images/01.png

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

_images/02.png

Для того, чтобы файлы с исходным кодом (.c-файлы) имели представление о функциях, глобальный переменных и типах данных, используемых в других файлах с исходным кодом, краткая информация обо всём этом выносится в заголовочные файлы (.h-файлы).

.h-файлы используются для храниения прототипов функций, объявлений глобальных переменных с extern и определения новых типов данных.

#include <stdio.h> // Поиск stdio.h ведётся в системных директориях компилятора

#include "mymodule.h" // Поиск myfile.h ведётся в той же директории, где находится
            // компилируемый файл
_images/03.png

Компиляция программы, разбитой на несколько файлов и показанной на рисунке выше, происходит так:

gcc -o proga mymodule.c main.c -lm -Wall

Заметьте, что .h-файлы напрямую не компилируются, так как уже подставлены в .c файлы.

_images/04.png

main.c подключает mymodule1.h два раза: прямо и косвенно (через mymodule2.h). Если в mymodule1.h есть определения типов, то повторное определение типа будет подставлено в компилируемый файл дважды, что приведёт к ошибке. Чтобы избежать этого, надо использовать защиту от множественного включения.

Защита от множественного включения

Для защиты от множественного включения module1.h в другие файлы применяется следующий трюк, который позволяет включать содержимое mymodule1.h в каждый из подключающих его файлов только один раз:

#ifndef __MYMODULE1_H_
#define __MYMODULE1_H_
 // Здесь будут находится прототипы функций,
 // определения новых типов данных,
 // глобальные переменные с пометкой extern
#endif

Разберёмся подробнее с незнакомой нам директивой препроцессора #ifndef:

#ifndef КОНСТАНТА
  // Весь код, заключённый между директивой ifndef и закрывающей его
  // директивой endif будет включён в компилируемый файл только тогда,
  // когда КОНСТАНТА не определена (ifndef == if not defined).
#endif

Кроме того, так же существует похожий оператори ifdef, который работает противоположным образом:

#ifdef КОНСТАНТА
  // Весь код между ifdef и endif будет включён в компилируемый файл
  // только тогда, когда КОНСТАНТА определена
#endif

Внешние (extern) переменные

Иногда бывает необходимость воспользоваться одной и той же глобальной переменной в нескольких .c-файлах. Можно было бы написать определение глобальной переменной напрямую в .h-файле и подключать его из нескольких .c-файлов. Однако, такие действия приведут к тому, что это определение будет скомпилировано несколько раз и будет создано несколько глобальных переменных с одинаковым именем. На этапе компоновки это вызовет ошибку.

Поэтому в языке C существует модификатор extern, который говорит о существовании глобальной переменной, но не создаёт её.

В объявлениях переменных с extern нельзя инициализировать переменные. Создаются и инициализируются глобальные перменные в одном и только одном из .c-файлов (без extern).

Указатели на функции

void apply_func(int *in, int *out, int n, int (*f)(int))
// Обозначение int(*f)(int) означает, что аргумент f
// -- это указатель на функцию, принимающую и возвращающую
// тип int.
{
    int i;
    for (i = 0; i < n; ++i) {
        out[i] = f(in[i]);
    }
}

int square(int x)
{
    return x * x;
}

int div2(int x)
{
    return x / 2;
}

int main(void)
{
    int a[4] = {10, 20, 30, 40};
    int b[4];
    int choice;
    scanf("%d", &choice);
    if (choice == 1) {
        apply_func(a, b, 4, square);
    } else {
        apply_func(a, b, 4, div2);
    }
    int (*f)(int) = square;
    printf("%d\n", f(10));      // 100
    f = div2;
    printf("%d\n", f(10));      // 5
    return 0;
}

Функция qsort

В библиотеке языка C есть функция qsort, которая реализует алгоритм под названием “быстрая сортировка” и работает за время O(n log(n)):

void qsort(void *ptr, size_t n_elem, size_t elem_size,
        int (*compar) (void *, void *));

n_elem – число элементов

elem_size – размер элемента массива в байтах

compar – указатель на функцию, принимающую указатели на два
элемента массива. Она должна возвращать число < 0, == 0 или > 0, если первый её аргумент <, == или > второго соответственно.
int compar_int(void *xp, void *yp)
{
    int x = *((int *) xp);
    int y = *((int *) yp);
    return x - y;
}

int compar_str(void *s1, void *s2)
{
    return strcmp((char *) s1, (char *) s2);
}

int main(void)
{
    int a[10] = {1, 23, 7, 9, 4, 2, 5, 16, 10, 3};
    qsort(a, 10, sizeof(int), compar_int);
    char *str_arr[5] = { "hello", "world", "apple", "orange", "cat" };
    qsort(a, 5, sizeof(char *), compar_str);
    return 0;
}