Сборка программы (компиляция)
Last updated
Last updated
Сборка (build) – процесс получения программы или библиотеки из исходного кода. Сборку разделяют на 3 крупных этапа: препроцессинг, компиляцию, линковку. Если один из этапов завершился неуспешно, то будет выведено сообщение компилятора или линковщика с ошибками.
Сборка состоит из нескольких этапов, среди которых выделяют 3 крупных:
Preprocessing – Препроцессирование, препроцессинг. Выполнение директив препроцессора (#include
, #define
и пр.). Обычно расширение *.i
.
Compiling – Компиляция. Перевод в машинный код. Обычно расширение *.obj
или *.o
(он же объектный файл).
LinkingLinking – Линковка (компоновка). Обычно расширение *.exe/*.out
(исполняемый файл) и *.dll/*.so
(файл динамической библиотеки).
Пусть имеется файл main.c
, содержащий следующий код:
Компиляция из терминала с использованием компилятора Clang:
На выходе будет создан файл a.exe
или a.out
в зависимости от системы (Windows или Linux соответственно).
Как правило, компилятор – это консольное приложение с интерфейсом опций. Для задания параметров работы компиляторы имеют набор специальных опций (ключей), которые указываются в командной строке.
Зададим компилятору имя результирующего файла как main.exe
:
Современные компиляторы обладают большими возможностями по оптимизации генерируемого кода и "нацеливание" его на конкретные архитектуры процессоров. Помимо -o
компиляторы поддерживают большое число других ключей.
Рассмотрим следующий пример:
Существует несколько уровней комплексной оптимизации. Каждый следующий уровень обычно включает все техники оптимизации с предыдущего уровня + некоторые новые. Кроме того, могут быть и отдельные наборы для особых случаев (например, оптимизация по размеру исполняемого кода).
Для задания уровня оптимизации используется ключ -O*
: -O0
, -O1 (или -O)
, -O2
, -Ofast
и т.д. Набор таких ключей для каждого компилятора свой.
Зададим компилятору ключ оптимизации:
Рассмотрим ряд других ключей для компилятора clang
и llvm-based intel
:
Ключ | Задаёт | Пример | По умолчанию |
---|---|---|---|
| имя результирующего файла |
|
|
| уровень оптимизации |
|
|
| include directory |
|
|
| library directory |
|
|
| library file |
| |
| стандарт языка |
| зависит от версии компилятора |
| preprocessor definitions |
| |
| вывод подробного лога сборки |
| |
| битность целевой платформы |
|
|
| enable the specified warning |
| зависит от версии компилятора |
| disable the specified warning |
| зависит от версии компилятора |
Debug – код не оптимизируется (-O0
), работает медленно, но хорошо работает отладчик.
Release – код оптимизируется (-O2
или -O3
), работает быстро, но под отладчиком не видно значения переменных, не работают точки остановок отладки и прочее.
В Debug конфигурации неинициализированные локальные переменные нередко инициализируются специальным, так называемым, "мусором" (0xBAADF00D, 0xCCCCCCCC и другие) для упрощения отладки.
В Release же ничего не инициализируется и неинициализированные переменные содержат то, что оказалось в памяти (часто 0, но это не гарантируется).
Предупреждение – это потенциальная ошибка. Предупреждение – это сигнал от компилятора о том, что написано одно, а требуется, возможно, что-то совершенно иное.
Поэтому программист должен помочь компилятору понять, как трактовать спорную ситуацию. То есть либо поправить свою ошибку, либо сообщить компилятору явно о том, что нужно верить программисту и делать именно то, что написано.
Ключ | Назначение |
---|---|
| делает все предупреждения ошибками |
| делает только указанное предупреждение ошибкой |
| "агрегатор" базовых предупреждений (не всех) |
| дополнительные предупреждения, которых нет в |
| проверяет соответствие кода стандарту ISO C++, сообщает об использовании запрещённых расширений, о наличии лишних точек с запятой, нехватке переноса строки в конце файла и прочих полезных штуках. |
| использование всех предупреждений |
Ключ | Назначение |
---|---|
| выход за пределы массивов |
| выход за границы массивов при использовании арифметике указателей |
| не все ветви исполнения кода возвращают значение |
| использование VLA (VLA + VMT) |
| объявлена неиспользуемая переменная |
| набор предупреждений |
|
|
Пример кода, на котором появляется сообщение с предупреждением:
Предупреждение -Wstring-compare
Компиляция с игнорированием этого исключения (лог пустой, сборка прошла успешно):
Имена элементов программы, таких как переменные, функции, классы и т. д., должны быть объявлены перед их использованием. Например, вы не можете просто написать x = 42
, не объявив x
.
Рассмотрим пример кода, представленного в двух разных файлах: main.c
и print_hello_world.c
.
При попытке скомпилировать файлы будет ошибка компиляции:
При попытке скомпилировать файлы будет ошибка компиляции:
Для того, чтобы вызвать функцию, надо что-то про неё знать – а именно объявление функции (declaration):
Объявление говорит о том, что где-то в программе такая функция есть. А когда мы пишем тело функции в фигурных скобках – это определение (definition).
Следуя этому правилу можно писать код, в котором все функции определены ниже их первого вызова, указав до этого первого вызова объявления этих функций:
Когда исходного кода становится много, то делать предварительные объявления каждой функции, которую вы хотите использовать, и которая определена в другом файле, становится всё труднее.
Заголовочные файлы "подключаются" в файлы с исходным кодом при помощи директивы препроцессора #include
. Используются #include <...>
или #include "..."
в зависимости от того, где компилятору следует искать этот файл – в своих include directories или относительно текущей директории.
Подключение СИСТЕМНЫХ и СТАНДАРТНЫХ (поставляемых вместе с компилятором) заголовочных файлов – указанный заголовочный будет искаться в папках с заголовчными компилятора, системных папках и в тех папках, который указаны при компиляции через ключ -I
.
Компилятор не будет искать заголовочный файл в каталоге исходного кода вашего проекта и ему нужно задать директории, где они лежат.
Подключение остальных (ВАШИХ) заголовочных. Имя указаывается относительно текущей директории.
Таким образом:
системные/стандартные заголовочные файлы подключаем через #include <...>
заголовочные файлы из библиотек/сторонних проектов подключаем через #include <...>
+ добавляем пути до этих файлов в параметры компиляции
свои заголовочные подключаем через #include "..."
рекомендуется указывать пути с использованием /
, а не обратных косых черт.
return_codes.h
Там лежат объявления макросов. В своём коде вы используете именно макросы, а не числа, которые рядом с ним написаны!
Следующий пример состоит из 3 файлов.
grandparent.h parent.h child.c |
В результате препроцессинга:
child.i |
А теперь скомпилируем это в объектный файл
(」°ロ° )
Ошибка компиляции redefinition of 'smth'
Для исправления такой ошибки необходимо сделать так, чтобы код из файла grandparent.h
попал в итоговый child.c
один раз. Для этого используют защиту от повторного включения или include guard.
Old-style: всё содержимое заголовочного "обернуть" в
New-style: в начале заголовочного указать
Последнее является нестандартной, но широкораспространённой директивой препроцессора.
Не следует делать!
Оставлять названия макросов, которые вам сгенерировала IDE на основе названия проекта
или что-то не относящееся к самому заданию
Подключать файлы с исходным кодом (*.c/*.cpp) через #include
– зло (вас будут карать баллами)
୧((#Φ益Φ#))୨
Каталоги включаемых файлов настраиваются как часть вашего проекта / настроек IDE / настроек компилятора. Компилятор не будет искать заголовочный файл в каталоге исходного кода вашего проекта и ему нужно задать директории, где они лежат.
Вынесем определение (definition) функций foo
и bar
в отдельный исполняемый файл и создадим соответсвующий заголовочный:
Чтобы задать компилятору include directories нужно использовать ключ -I
.
Сборка без -I
Сборка с -I
Следующий пример: вы хотите использовать в своей программу библиотеку, представляющую собой набор заголовочных файлов и файла статической библиотеки. В таком случае, помимо указания путей заголовочных файлов нужно будет указать где искать файл библиотеки и как он называется.
Для подключения сторонних библиотек помимо указания директорий, где следует искать заголовочные файлы (чаще всего это директории include
или inc
) может потребоваться указание директорий библиотек (lib
). Для этого используются ключи -L
и -l
.
Рассмотрим пример кода, использующий фреймворк OpenCL. Для компиляции такого кода помимо необходимого заголовочного файла, расположение которого укажем через ключ -I
, необходимо указать, где располагается файл статической библиотеки opencl.lib
и сам этот файл.
Структура проекта
Сборка без указания директории заголовочных и библиотек (без -I и -L и -l)
Видно, что в первую очередь не хватает заголовочного файла. Исправим ситуацию, указан компилятору include directory opencl_root/include
.
Сборка без указания директории библиотек (с -I и без -L и -l)
Видно, что все текущие ошибки – ошибки линковки. Линкеру необходимо явно указать library directory и сами файлы библиотек.
Сборка без указания имени библиотеки (с -I и -L и без -l)
Несмотря на указанную директорию всё ещё видим ошибки линковки. Укажем явно файл библиотеки, который лежит в указанной library directory. Файл библиотеки указывается без расширения, а также ключ -l
с названием файла библиотеки должен идти последним параметром.
Сборка с -I и -L и -l
Лог может отличаться в зависимости от версии компилятора и системы (может выводиться меньше информации). Главное, что никаких итогов с ошибками не наблюдается.
Но собранный исполняемый файл не означает, что на этапе исполнения не будет возникать ошибок.
При попытке запустить полученный ocl.exe
будет ошибка. Чтобы программа отработала необходимо поместить рядом с исполняемым файлом файлы динамически подключаемых библиотек (.dll
для Windows; os
для UNIX систем; dylib
для MacOS). Такие файлы чаще всего располагаются в директории bin
библиотеки.
Если же библиотека будет использоваться в нескольких проектах, то следует задуматься о добавлении пути до файлов динамически подключаемых библиотек в переменные окружения, чтобы не копировать их каждый раз к исполняемым файлам.