C Урок 19. Модульное программирование. Раздельная компиляция



Теперь мы с вами подошли к такой ситуации, что код наших проектов достиг такой величины, что уже сложно стало его читать, потому что все функции, причём разнообразного назначения, все константы, макросы, глобальные переменные у нас находятся в одном файле main.c. Дальше такое продолжаться не может, и нам нужно теперь будет как-то разбить наш проект на какие то части по их функциональному назначению. Такие части в языке C существуют, они также поддерживаются всеми средами программирования, системами сборки и компиляторами. Они именуются модулями.

Модуль в языке C — это как правило совокупность файла с исходным кодом, как правило имеющим расширение c, и заголовочного файла. Также модуль может быть и с закрытым исходным кодом. Это статическая библиотека. Но о них будет отдельный разговор скорее всего в отдельном занятии.

Заголовочный файл, или как его ещё называют header-файл — это файл, в котором обычно находятся подключения всяческих других заголовочных файлов, библиотек, прототипы функций, некоторые глобальные переменные, структуры, массивы, указатели, макросы и прочие объявления, которые вполне могли бы находиться и в файле с исходным кодом, но, во-первых они его загромождают чрезмерной информационной нагрузкой, а также, благодаря заголовочному файлу, при его подключении в другие файлы становятся доступными многие ресурсы из модуля, частью которого данный файл является. Заголовочные файлы как правило имеют расширение h.

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

Процесс раздельной компиляции можно изобразить в виде вот такой диаграммы

 

 

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

Пока мы создаём проект, как и прежде, из проекта прошлого занятия с именем MYPROG18 и присвоим ему имя MYPROG19.

Откроем файл main.c и в функции main(), как обычно, удалим весь код тела кроме возврата нуля, останется от него вот это

 

int main()

{

   return 0; //Return an integer from a function

}

 

Функцию menu() тоже удалим.

Давайте теперь создадим заголовочный файл main.h в папке с нашим проектом вот с таким содержимым

 

 

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

Перенесём из файла main.c в наш хедер-файл вот этот код

 

 

Чтобы код заголовочного файла включился в общий код сборки, данный файл необходимо подключить в файл с исходным кодом, поэтому в самом верху нашего файла main.c подключим данный хедер-файл

 

 

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

Если мы сохраним оба наших файла и попробуем собрать проект командой build, то он отлично соберётся, тем самым подчёркивая, что наш хедер виден компилятору gcc.

Также давайте проверим работоспособность нашего проекта, добавив в функцию main() вот такой вот код из урока 17

 

 

Соберём код и проверим

 

 

 

Закомментируем данный код и добавим ещё вот такой

 

 

Данный код также работает

 

 

Тем самым мы проверили, что функция prinf, которая находится в библиотеке, подключенной в заголовочном файле main.h «видна», что подтверждает удачное подключение заголовочного файла, но пока никак не показывает нам принцип модульного программирования, а уж тем более раздельной компиляции ибо хоть у нас и два файла для сборки в нашем проекте, но они оба — часть одного модуля.

Закомментируем и этот код и добавим теперь следующий из того же урока

 

 

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

 

 

И, чтобы нам поработать с модульным программированием, данные функции мы реализуем в отдельном модуле, в который мы и добавим данные арифметические функции. Пока создадим заголовочный файл с именем ariph.h следующего содержания

 

 

В данном хедере мы пока только объявили прототипы наших функций. Подключим данный файл в файле main.h

 

 

Сохраним наши файлы и попробуем теперь собрать наш проект

 

 

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

Почему же всё-таки мы не получили ошибку на этапе компиляции? А потому, что засчёт прототипов функций в объектный модуль main.o встроились пока заглушки на наши функции с их адресами, ведущими пока в никуда. Если бы это было не так и тела функций, а вернее их объектные коды, встраивались бы непосредственно в объектный файл main.o, то мы бы не смогли раздельно скомпилировать модули, так как модуль, компилируемый отдельно, «не знает» о существовании других модулей.

 

 

Теперь добавим в наш проект файл ariph.c следующего содержания

 

 

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

Сохраним данный файл и добавим теперь его сборку также в файл build.cmd, а также добавим объектный файл данного модуля в строку с линковкой. Файл build.cmd будет теперь иметь теперь вот такое содержимое

 

 

В нашем файле мы раздельно компилируем оба наших модуля, а затем уже их вместе компонуем.

Попробуем собрать и запустить теперь наш проект

 

 

Процесс прошёл удачно. Всё собралось и программа наша работает.

Также для полноты картины мы в нашем уроке подключим ещё один модуль.

Для начала в функции main() мы закомментируем наш код и добавим следующий из того же урока

 

 

Собирать мы проект пока не будем, так как у нас пока нет функции int_to_binary и результат такой сборки мы уже знаем.

Поэтому добавим в наш проект ещё один заголовочный файл utils.h следующего содержания

 

 

Также добавим файл utils.c следующего содержания

 

 

Функцию вместе с телом мы взяли из того же 17 урока.

Теперь в файле main.h подключим также наш новый заголовочный файл

 

 

Сохраним все наши файлы.

Теперь нам надо включить наш модуль в сборку, для чего добавим его сборку в файл build.cmd, содержимое которого теперь будет следующим

 

 

То есть наш командный файл сначала компилирует по очереди все модули, а затем вместе их компонует, для чего мы их включаем в одну строку с компоновкой все вместе через пробел.

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

 

Всё отлично собралось и запустилось.

Попробуем на всякий случай ещё и вот такой код

 

 

Соберём его и запустим

 

 

Всё отлично работает.

Итак, на данном уроке мы научились добавлять в наши проекты новые модули, познакомившись с принципом модульного программирования и раздельной компиляции, что позволило нам распределить наш код по его функциональному назначению в различные модули и теперь код стал менее нагромождённым и, следовательно, более читабельным.

Всем спасибо за внимание!

 

 

Предыдущий урок Программирование на C Следующий урок

 

Исходный код

 

 

Смотреть ВИДЕОУРОК (нажмите на картинку)

 

C Модульное программирование, раздельная компиляция

4 комментария на “C Урок 19. Модульное программирование. Раздельная компиляция
  1. Rustam:

    Здравствуйте, очень нравятся ваши уроки!) Но столкнулся с проблемой после подключения .h файла я не могу скомпилировать программу.
    В командной строке пишет:
    D:\ProjectsC\prog19_Modul>gcc -Wall -E main.c -o main.i
    D:\ProjectsC\prog19_Modul>gcc -Wall -S main.i -o main.s
    D:\ProjectsC\prog19_Modul>gcc -Wall -c main.c
    D:\ProjectsC\prog19_Modul>gcc -Wall main.o -o myprog19
    d:/mingw/bin/../lib/gcc/mingw32/9.2.0/../../../../mingw32/bin/ld.exe: d:/mingw/bin/../lib/gcc/mingw32/9.2.0/../../../libmingw32.a(main.o):(.text.startup+0xc0): undefined reference to `WinMain@16'
    collect2.exe: error: ld returned 1 exit status

    Полазил по форумам, везде пишут переименовать .c в .cpp, но разве мы изучаем cpp вот и не очень хочу таким заниматься.
    Вот может вы знаете в чем проблема. Хотелось бы продолжить ваши уроки.

    • Rustam:

      Все, нашел глупую ошибку. Оказывается имеет место как называется основной код int main() или int menu(). gcc воспринимает только main, а если он отсутствует то он думает что программа и не является программой так как кода нет, очень странно но ладно.

  2. Konstantin:

    Что означает gcc -Wall -E, gcc -Wall -S, gcc -Wall -g3 -c
    Не нашел точного описания этих флагов

    • Arkadiy:

      -Wall — это опция включения всех предупреждений при компиляции (отлавливает большинство ошибок в коде)
      -Е — это опция управляющая видом ввода. Остановиться после стадии препроцессирования; не запускать собственно компилятор.
      -S — так же опция управляющая видом ввода. Остановиться после собственно компиляции; не ассемблировать.
      -g3 — это опция для отладки. Порождает отладочную информацию в родном формате операционной системы. Уровень 3 включает дополнительную информацию, такую как все макро определения встречающиеся в программе. Некоторые отладчики поддерживают макро расширения при использовании '-g3'.
      -c — это опция управляющая видом ввода. Компилировать или ассемблировать исходные файлы, но не линковать.

      Более подробно можно ознакомиться тут:

      http://linux.yaroslavl.ru/docs/prog/gcc/gcc1-2.html

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*