Когда объём программы достигает сотен и тысяч строк кода, возникает проблема поддержки этого кода. Для программы вполне естественно быть одним большим файлом, но с точки зрения программиста страдают и структурированность кода, и навигация по нему.
Так как программа обычно состоит из тематически сгруппированных функций, вроде работы с графикой или с базой данных, то логично вынести эти функции в отдельные файлы и разрабатывать их там, обособленно.
Но эти отдельные файлы надо потом свести обратно в единую программу. Для чего у каждого языка программирования есть свои способы, которые в целом мало отличаются.
В C можно использовать инструкцию #include, которая вставляет содержимое файла прямо по месту. Причём типичный пример всегда будет с файлом типа .h, но не возбраняется вставлять файлы типа .c, а в чём разница, мы обсудим ниже.
Таким образом, можно вставлять в программу на C куски кода, из которых образуется общая большая программа.
Но так как язык C компилируемый, то есть два варианта:
Можно скомпилировать одну общую программу, в исходный код которой всё включено.
Но также можно скомпилировать каждый кусок-файл по отдельности, а потом собрать программу из уже скомпилированных кусков. Этот процесс и называется собственно сборкой:
Но в чём смысл такого подхода?
Когда программа включает в себя куски исходного кода, то при любом изменении в любом файле вся программа перекомпилируется целиком.
Если же каждый файл компилируется отдельно, то перекомпилировать надо только тот файл, который изменился. Это существенно сокращает время компиляции для больших проектов.
Кроме того, скомпилированные файлы можно использовать в других проектах.
Давайте попробуем сделать свой проект по двум сценариям. Сначала напишем программу с функцией реверса строки:
Программа находится целиком в одном файле main.c, и её можно скомпилировать как есть:
gcc main.c
Результат работы:
!dlroW olleH
Теперь вынесем функцию str_reverse() в отдельный файл mystringlib.c и включим этот файл в main.c:
Разницы для компилятора вообще никакой, так как файл включается в программу ещё до того, как начнётся компиляция (это делает препроцессор) и мы получаем точно такую же программу, как и выше.
Теперь уберём из main.c включение mystringlib.c:
и попробуем скомпилировать каждый файл отдельно. Для этого укажем компилятору все имена файлов, которые нужно компилировать:
gcc main.c mystringlib.c
или даже вот так:
gcc *.c
Но возникает ошибка, а точнее, предупреждение:
Дело в том, что мы используем функцию str_reverse() в main.c, но она там не описана, так как мы туда ничего не включали. Тем не менее, она подключается позже, на этапе сборки, и поэтому программа создаётся и работает.
Чтобы избавиться от предупреждения, достаточно в main.c поместить не саму функцию, а её определение:
Для подобных определений принято создавать файлы типа .h (header), они называются заголовочными, потому что содержат "заголовки" функций, определения типов и прочее. Но на деле это обычные файлы на языке C, так что расширение .h может быть любым другим, просто так принято.
Поместим определение void str_reverse(char* str); в отдельный файл mystringlib.h и включим этот файл в main.c:
Теперь можно повторить компиляцию и получить тот же результат.
Казалось бы, мы недалеко ушли от первого варианта, и даже усложнили его. Там мы включали в main.c саму функцию str_reverse() и компилировали один файл main.c, а здесь сделали ещё один файл .h, включаем его, а компилировать приходится уже два файла.
Действительно, проще сразу включать в main.c необходимые функции, не заморачиваясь с заголовочными файлами. Но посмотрим дальше.
Попробуем скомпилировать mystringlib.c отдельно:
gcc mystringlib.c
Мы встретим такую ошибку (в среде Windows):
undefined reference to `WinMain'
Дело в том, что для получения работающей программы требуется наличие функции main() в одном из компилируемых файлов. В файле mystringlib.c её, понятно, нет. Поэтому компиляция не удаётся.
Но нам и не надо получать из mystringlib.c работающую программу. Надо получить просто скомпилированный файл, который можно затем использовать.
Для этого используем ключ -c (compile):
gcc -c mystringlib.c
В результате получился т.н. объектный файл mystringlib.o, который уже откомпилирован, и теперь можно компилировать main.c вместе с ним:
gcc main.c mystringlib.o
И вот мы добились того, что при изменениях в файле main.c будет перекомпилироваться только он.
Библиотека
Как вы поняли, файл mystringlib.c посвящён работе со строками и может содержать другие строковые функции. Аналогичным образом сделаем файл mymathlib.c и поместим в него какую-нибудь математическую функцию:
Добавим определение функции в mymathlib.h, включим в main.c и т.д.
Теперь откомпилируем mymathlib.c, получив объектный файл, и соединим всё вместе:
gcc main.c mystringlib.o mymathlib.o
Как видим, количество объектных файлов растёт, но мы можем объединить их в одну библиотеку. Для этого используется отдельная утилита ar в составе пакета компилятора gcc. По сути это просто архиватор. Команда:
ar cr libmylib.a mystringlib.o mymathlib.o
имеет два ключа cr:
c: создать архив, если он ещё не существует
r: заменить файлы в архиве, если они там есть
В результате мы получим библиотеку-архив libmylib.a. Обратите внимание, что файл библиотеки называется не просто mylib.a, а libmylib.a – у названия в начале есть префикс lib для служебных целей, что мы увидим ниже.
Теперь можно собрать программу с библиотекой:
gcc main.c -L. -lmylib
Ключ -L задаёт папку, в которой ищутся библиотеки. В нашем случае это текущая папка, которая обозначается точкой. Ключ -l (это не большая i, а маленькая L) задаёт библиотеку, которую надо подключить. И ещё раз обратите внимание, что там написано mylib, но сборщик будет искать libmylib.a. Из-за этого и требуется префикс lib в имени файла, но не в параметре -l (исторически сложилось).
Альтернативный способ это просто указать путь к файлу библиотеки без всяких ключей:
gcc main.c libmylib.a
В этом случае, так как используется прямой путь к файлу, наличие префикса lib в нём необязательно и вы можете назвать его как хотите.
Теперь вы можете скомпилировать любую другую программу с этой же самой библиотекой. Только создайте файл mylib.h с определениями всех функций, которые есть в библиотеке, и включите его в свою программу.
Статические и динамические сборки
В вышеуказанном примере используется статическая сборка. Это значит, что содержимое библиотеки помещается непосредственно в программу. Программа может запускаться самостоятельно, так как имеет все необходимые функции.
В случае динамической сборки библиотека остаётся внешней по отношению к программе. В момент запуска программа ищет библиотеку и подгружает функции из неё, после чего может работать.
У такого подхода есть плюсы. Программа становится меньше. Библиотеку можно, к примеру, обновлять, а программа всё равно будет с ней работать без перекомпиляции. С одной и той же динамической библиотекой могут работать разные программы.
Есть и минусы. Программа должна распространяться вместе с библиотекой, либо устанавливаться в систему, где такая библиотека уже есть, иначе она работать не будет. Также при загрузке программа должна настроить адреса вызываемых из библиотеки функций. Это делается через таблицу указателей и приводит к некоторому замедлению работы, хотя и практически незаметному. Ну и сам старт программы становится более долгим из-за подгрузки библиотеки.
Динамическую сборку разберём как-нибудь потом.