Теперь мы с вами подошли к такой ситуации, что код наших проектов достиг такой величины, что уже сложно стало его читать, потому что все функции, причём разнообразного назначения, все константы, макросы, глобальные переменные у нас находятся в одном файле 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 в папке с нашим проектом вот с таким содержимым
1 2 3 4 5 |
#ifndef MAIN_H_ #define MAIN_H_ //------------------------------------------------ //---------------------------------------------- #endif /* MAIN_H_ */ |
С помощью данных директив мы дадим команду компилятору при случайной попытке подключения нашего заголовочного файла не включать его код в программу. Есть ещё один инструмент для данного действия, но о нём мы узнаем позже.
Перенесём из файла main.c в наш хедер-файл вот этот код
1 2 3 |
#include <stdio.h> #include <string.h> #include <stdlib.h> |
Чтобы код заголовочного файла включился в общий код сборки, данный файл необходимо подключить в файл с исходным кодом, поэтому в самом верху нашего файла main.c подключим данный хедер-файл
1 |
#include "main.h" |
Я думаю, вы обратили внимание, что вместо треугольных скобок мы поставили кавычки. Это делается в том случае, когда мы подключаем файлы, находящиеся не там, где находятся стандартные библиотечные файлы, а файлы находящиеся в дополнительных подключенных директориях. Также мы должны в данном случае «сказать» компилятору об этих путях. Только мы этого сейчас делать не будем, так как в том случае, когда подключаемые файлы находятся в корне папки с проектом, они и так будут «видны».
Если мы сохраним оба наших файла и попробуем собрать проект командой build, то он отлично соберётся, тем самым подчёркивая, что наш хедер виден компилятору gcc.
Также давайте проверим работоспособность нашего проекта, добавив в функцию main() вот такой вот код из урока 17
1 2 3 4 5 6 7 8 9 |
int main() { float xf = 8; float yf = 3; float zf = 2; float res = xf + yf + zf; printf ("Value is %.5f\n", res); res = xf + yf - zf; printf ("Value is %.5f\n", res); |
Соберём код и проверим
Закомментируем данный код и добавим ещё вот такой
1 2 3 4 5 6 7 |
float xf = 8; float yf = 3; float zf = 2; float res = xf + yf * zf; printf ("Value is %.5f\n", res); res = xf - yf / zf; printf ("Value is %.5f\n", res); |
Данный код также работает
Тем самым мы проверили, что функция prinf, которая находится в библиотеке, подключенной в заголовочном файле main.h «видна», что подтверждает удачное подключение заголовочного файла, но пока никак не показывает нам принцип модульного программирования, а уж тем более раздельной компиляции ибо хоть у нас и два файла для сборки в нашем проекте, но они оба — часть одного модуля.
Закомментируем и этот код и добавим теперь следующий из того же урока
1 2 3 4 5 6 7 |
float xf = 8; float yf = 3; float zf = 2; float res = xf + my_div(yf, zf); printf ("Value is %.5f\n", res); res = xf / my_sum(yf, zf); printf ("Value is %.5f\n", res); |
Здесь у нас уже используется функции my_sum и my_div, которые у нас никак в нашем проекте не реализованы. Поэтому, если мы сейчас попытаемся собрать наш код, то мы получим ошибку на этапе компиляции, а вернее предупреждение, хотя оно равнозначно ошибке, так как в данном случае линковка у нас вообще не произойдёт
И, чтобы нам поработать с модульным программированием, данные функции мы реализуем в отдельном модуле, в который мы и добавим данные арифметические функции. Пока создадим заголовочный файл с именем ariph.h следующего содержания
1 2 3 4 5 6 7 |
#ifndef ARIPH_H_ #define ARIPH_H_ //------------------------------------------------ float my_div(float a, float b); float my_sum(float a, float b); //------------------------------------------------ #endif /* ARIPH_H_ */ |
В данном хедере мы пока только объявили прототипы наших функций. Подключим данный файл в файле main.h
1 2 |
#include <stdlib.h> #include "ariph.h" |
Сохраним наши файлы и попробуем теперь собрать наш проект
Как мы видим, компиляция у наш прошла успешно, а линковка дала ошибку, ибо линкер не нашел реализации наших функций.
Почему же всё-таки мы не получили ошибку на этапе компиляции? А потому, что засчёт прототипов функций в объектный модуль main.o встроились пока заглушки на наши функции с их адресами, ведущими пока в никуда. Если бы это было не так и тела функций, а вернее их объектные коды, встраивались бы непосредственно в объектный файл main.o, то мы бы не смогли раздельно скомпилировать модули, так как модуль, компилируемый отдельно, «не знает» о существовании других модулей.
Теперь добавим в наш проект файл ariph.c следующего содержания
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include "ariph.h" //---------------------------------------------- float my_div(float a, float b) { return a/b; } //---------------------------------------------- float my_sum(float a, float b) { return a+b; } //---------------------------------------------- |
В данном файле мы также подключаем заголовочный файл модуля, а также добавляем наши функции с телами.
Сохраним данный файл и добавим теперь его сборку также в файл build.cmd, а также добавим объектный файл данного модуля в строку с линковкой. Файл build.cmd будет теперь иметь теперь вот такое содержимое
1 2 3 4 5 6 7 8 |
@CHCP 1251>NUL gcc -Wall -E main.c -o main.i gcc -Wall -E ariph.c -o ariph.i gcc -Wall -S main.i -o main.s gcc -Wall -S ariph.i -o ariph.s gcc -Wall -g3 -c main.c gcc -Wall -g3 -c ariph.c gcc -Wall main.o ariph.o -o myprog19 |
В нашем файле мы раздельно компилируем оба наших модуля, а затем уже их вместе компонуем.
Попробуем собрать и запустить теперь наш проект
Процесс прошёл удачно. Всё собралось и программа наша работает.
Также для полноты картины мы в нашем уроке подключим ещё один модуль.
Для начала в функции main() мы закомментируем наш код и добавим следующий из того же урока
1 2 3 4 5 6 7 8 9 10 |
char str1[35] = {}; int a = 0b00111000, b = 0b10000010; int res = a | b >> 1; int_to_binary(a, str1); printf ("Value is %s\r\n", str1); int_to_binary(b, str1); printf ("Value is %s\r\n", str1); printf("==========\r\n"); int_to_binary(res, str1); printf ("Value is %s\r\n", str1); |
Собирать мы проект пока не будем, так как у нас пока нет функции int_to_binary и результат такой сборки мы уже знаем.
Поэтому добавим в наш проект ещё один заголовочный файл utils.h следующего содержания
1 2 3 4 5 6 7 8 |
#ifndef UTILS_H_ #define UTILS_H_ //------------------------------------------------ #include <string.h> //------------------------------------------------ void int_to_binary(int x, char* in_str); //------------------------------------------------ #endif /* UTILS_H_ */ |
Также добавим файл utils.c следующего содержания
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#include "utils.h" //---------------------------------------------- void int_to_binary(int x, char* in_str) { char str_tmp[9] = {}; unsigned char i, j, k; unsigned char bt[4] = {0}; for (j=0; j<4; j++) { if(((x >> j*8)==0) && (j>0)) break; bt[j] = (unsigned char) (x >> j*8); } strcpy(in_str,"0b"); for (k=0; k<j; k++) { for (i=0; i<8; i++) { switch ((bt[j-k-1] >> i) & 0b00000001) { case 1: str_tmp[7-i] = '1'; break; case 0: str_tmp[7-i] = '0'; break; } } strcat(in_str,str_tmp); } } //---------------------------------------------- |
Функцию вместе с телом мы взяли из того же 17 урока.
Теперь в файле main.h подключим также наш новый заголовочный файл
1 2 |
#include "ariph.h" #include "utils.h" |
Сохраним все наши файлы.
Теперь нам надо включить наш модуль в сборку, для чего добавим его сборку в файл build.cmd, содержимое которого теперь будет следующим
1 2 3 4 5 6 7 8 9 10 11 |
@CHCP 1251>NUL gcc -Wall -E main.c -o main.i gcc -Wall -E ariph.c -o ariph.i gcc -Wall -E utils.c -o utils.i gcc -Wall -S main.i -o main.s gcc -Wall -S ariph.i -o ariph.s gcc -Wall -S utils.i -o utils.s gcc -Wall -g3 -c main.c gcc -Wall -g3 -c ariph.c gcc -Wall -g3 -c utils.c gcc -Wall main.o ariph.o utils.o -o myprog19 |
То есть наш командный файл сначала компилирует по очереди все модули, а затем вместе их компонует, для чего мы их включаем в одну строку с компоновкой все вместе через пробел.
Сохраним наш командный файл, попробуем собрать и запустить нашу программу
Всё отлично собралось и запустилось.
Попробуем на всякий случай ещё и вот такой код
1 2 3 4 5 6 7 8 9 10 11 12 |
char str1[35] = {}; int a = 0b00111000, b = 0b10000010, c = 0b01000001; int_to_binary(a, str1); printf ("Value is %s\r\n", str1); int_to_binary(b, str1); printf ("Value is %s\r\n", str1); int_to_binary(c, str1); printf ("Value is %s\r\n", str1); printf("==========\r\n"); int res = a | b ^ c; int_to_binary(res, str1); printf ("Value is %s\r\n", str1); |
Соберём его и запустим
Всё отлично работает.
Итак, на данном уроке мы научились добавлять в наши проекты новые модули, познакомившись с принципом модульного программирования и раздельной компиляции, что позволило нам распределить наш код по его функциональному назначению в различные модули и теперь код стал менее нагромождённым и, следовательно, более читабельным.
Всем спасибо за внимание!
Предыдущий урок Программирование на C Следующий урок
Смотреть ВИДЕОУРОК в RuTube (нажмите на картинку)
Смотреть ВИДЕОУРОК в YouTube (нажмите на картинку)
Здравствуйте, очень нравятся ваши уроки!) Но столкнулся с проблемой после подключения .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 вот и не очень хочу таким заниматься.
Вот может вы знаете в чем проблема. Хотелось бы продолжить ваши уроки.
Все, нашел глупую ошибку. Оказывается имеет место как называется основной код int main() или int menu(). gcc воспринимает только main, а если он отсутствует то он думает что программа и не является программой так как кода нет, очень странно но ладно.
Что означает gcc -Wall -E, gcc -Wall -S, gcc -Wall -g3 -c
Не нашел точного описания этих флагов
-Wall — это опция включения всех предупреждений при компиляции (отлавливает большинство ошибок в коде)
-Е — это опция управляющая видом ввода. Остановиться после стадии препроцессирования; не запускать собственно компилятор.
-S — так же опция управляющая видом ввода. Остановиться после собственно компиляции; не ассемблировать.
-g3 — это опция для отладки. Порождает отладочную информацию в родном формате операционной системы. Уровень 3 включает дополнительную информацию, такую как все макро определения встречающиеся в программе. Некоторые отладчики поддерживают макро расширения при использовании '-g3'.
-c — это опция управляющая видом ввода. Компилировать или ассемблировать исходные файлы, но не линковать.
Более подробно можно ознакомиться тут:
http://linux.yaroslavl.ru/docs/prog/gcc/gcc1-2.html