Сборка программы
Сборка (build) – процесс получения программы или библиотеки из исходного кода. Сборку разделяют на 3 крупных этапа: препроцессинг, компиляцию, линковку. Если один из этапов завершился неуспешно, то будет выведено сообщение компилятора или линковщика с ошибками.
Общие сведения
Сборка состоит из нескольких этапов, среди которых выделяют 3 крупных:
Preprocessing – Препроцессирование, препроцессинг. Выполнение директив препроцессора (
#include,#defineи пр.). Обычно расширение*.i.clang -E main.c -o main.i # (preprocessing)Compiling – Компиляция. Перевод в машинный код. Обычно расширение
*.objили*.o(он же объектный файл).clang -c main.c -o main.obj # (preprocessing + compile)Linking – Линковка (компоновка). Обычно расширение
*.exe/*.out(исполняемый файл) и*.dll/*.so(файл динамической библиотеки).lld-link /subsystem:console /defaultlib:libcmt main.obj # (linking)
clang main.c -o main.exe # (all steps)
Сборка из терминала
Пусть имеется файл main.c, содержащий следующий код:
Компиляция из терминала с использованием компилятора Clang:
На выходе будет создан файл a.exe или a.out в зависимости от системы (Windows или Linux соответственно).
Как правило, компилятор – это консольное приложение с интерфейсом опций. Для задания параметров работы компиляторы имеют набор специальных опций (ключей), которые указываются в командной строке.
Зададим компилятору имя результирующего файла как main.exe:
Ключи компиляции
Современные компиляторы обладают большими возможностями по оптимизации генерируемого кода и "нацеливание" его на конкретные архитектуры процессоров. Помимо -o компиляторы поддерживают большое число других ключей.
Рассмотрим следующий пример:
Существует несколько уровней комплексной оптимизации. Каждый следующий уровень обычно включает все техники оптимизации с предыдущего уровня + некоторые новые. Кроме того, могут быть и отдельные наборы для особых случаев (например, оптимизация по размеру исполняемого кода).
Для задания уровня оптимизации используется ключ -O*: -O0, -O1 (или -O), -O2, -Ofast и т.д. Набор таких ключей для каждого компилятора свой.
Зададим компилятору ключ оптимизации:
Рассмотрим ряд других ключей для компилятора clang и llvm-based intel:
-o
имя результирующего файла
-o main.exe
a.exe или a.out для Windows или Linux соответственно
-O*
уровень оптимизации
-O0, -O1 (или -O), -O2, -Ofast ...
-O0
-I
include directory
-I include
-I .
-L
library directory
-L lib
-L .
-l
library file
-l lab2.lib
-std=
стандарт языка
-std=c17
зависит от версии компилятора
-D
preprocessor definitions
-D_CRT_SECURE_NO_WARNINGS
-v
вывод подробного лога сборки
-v
-m
битность целевой платформы
-m32, -m64
-m64
-W
enable the specified warning
-Wall
зависит от версии компилятора
-Wno
disable the specified warning
-Wno-endif-labels
зависит от версии компилятора
debug VS release
Debug – код не оптимизируется (-O0), работает медленно, но хорошо работает отладчик.
Release – код оптимизируется (-O2 или -O3), работает быстро, но под отладчиком не видно значения переменных, не работают точки остановок отладки и прочее.
В Debug конфигурации неинициализированные локальные переменные нередко инициализируются специальным, так называемым, "мусором" (0xBAADF00D, 0xCCCCCCCC и другие) для упрощения отладки.
В Release же ничего не инициализируется и неинициализированные переменные содержат то, что оказалось в памяти (часто 0, но это не гарантируется).
Предупреждение – это потенциальная ошибка. Предупреждение – это сигнал от компилятора о том, что написано одно, а требуется, возможно, что-то совершенно иное.
Поэтому программист должен помочь компилятору понять, как трактовать спорную ситуацию. То есть либо поправить свою ошибку, либо сообщить компилятору явно о том, что нужно верить программисту и делать именно то, что написано.
Общие предупреждения
-Werror
делает все предупреждения ошибками
-Werror=[error-type]
делает только указанное предупреждение ошибкой
-Wall
"агрегатор" базовых предупреждений (не всех)
-Wextra
дополнительные предупреждения, которых нет в -Wall
-Wpedantic
проверяет соответствие кода стандарту ISO C++, сообщает об использовании запрещённых расширений, о наличии лишних точек с запятой, нехватке переноса строки в конце файла и прочих полезных штуках.
-Weverything
использование всех предупреждений
-Warray-bounds
выход за пределы массивов
-Warray-bounds-pointer-arithmetic
выход за границы массивов при использовании арифметике указателей
-Wreturn-type
не все ветви исполнения кода возвращают значение
-Wvla
использование VLA (VLA + VMT)
-Wunused-variable
объявлена неиспользуемая переменная
-Wunused
набор предупреждений -Wunused-argument, -Wunused-but-set-variable, -Wunused-function, -Wunused-label, -Wunused-lambda-capture, -Wunused-local-typedef, -Wunused-private-field, -Wunused-property-ivar, -Wunused-value, -Wunused-variable.
-Wshift-op-parentheses
warning: operator ‘A’ has lower precedence than ‘B’; ‘B’ will be evaluated first
Пример кода, на котором появляется сообщение с предупреждением:
Предупреждение -Wstring-compare
Компиляция с игнорированием этого исключения (лог пустой, сборка прошла успешно):
Сборка программы из нескольких исходников без заголовочных файлов
Имена элементов программы, таких как переменные, функции, классы и т. д., должны быть объявлены перед их использованием. Например, вы не можете просто написать x = 42, не объявив x.
Рассмотрим пример кода, представленного в двух разных файлах: main.c и print_hello_world.c.
При попытке скомпилировать файлы будет ошибка компиляции:
При попытке скомпилировать файлы будет ошибка компиляции:
Для того, чтобы вызвать функцию, надо что-то про неё знать – а именно объявление функции (declaration):
Объявление говорит о том, что где-то в программе такая функция есть. А когда мы пишем тело функции в фигурных скобках – это определение (definition).
Следуя этому правилу можно писать код, в котором все функции определены ниже их первого вызова, указав до этого первого вызова объявления этих функций:
Подключение заголовочных файлов #include
Когда исходного кода становится много, то делать предварительные объявления каждой функции, которую вы хотите использовать, и которая определена в другом файле, становится всё труднее.
Заголовочные файлы "подключаются" в файлы с исходным кодом при помощи директивы препроцессора #include. Используются #include <...> или #include "..." в зависимости от того, где компилятору следует искать этот файл – в своих include directories или относительно текущей директории.
Подключение СИСТЕМНЫХ и СТАНДАРТНЫХ (поставляемых вместе с компилятором) заголовочных файлов – указанный заголовочный будет искаться в папках с заголовчными компилятора, системных папках и в тех папках, который указаны при компиляции через ключ -I.
Компилятор не будет искать заголовочный файл в каталоге исходного кода вашего проекта и ему нужно задать директории, где они лежат.
Подключение остальных (ВАШИХ) заголовочных. Имя указаывается относительно текущей директории.
Таким образом:
системные/стандартные заголовочные файлы подключаем через
#include <...>заголовочные файлы из библиотек/сторонних проектов подключаем через
#include <...>+ добавляем пути до этих файлов в параметры компиляциисвои заголовочные подключаем через
#include "..."
рекомендуется указывать пути с использованием /, а не обратных косых черт.
return_codes.h
return_codes.hТам лежат объявления макросов. В своём коде вы используете именно макросы, а не числа, которые рядом с ним написаны!
Повторное подключение заголовочных файлов
Следующий пример состоит из 3 файлов.

В результате препроцессинга:

А теперь скомпилируем это в объектный файл
(」°ロ° )
Ошибка компиляции redefinition of 'smth'
Для исправления такой ошибки необходимо сделать так, чтобы код из файла grandparent.h попал в итоговый child.c один раз. Для этого используют защиту от повторного включения или include guard.
Include guard (#define)
Old-style: всё содержимое заголовочного "обернуть" в
New-style: в начале заголовочного указать
Последнее является нестандартной, но широкораспространённой директивой препроцессора.
Не следует делать!
Оставлять названия макросов, которые вам сгенерировала IDE на основе названия проекта
или что-то не относящееся к самому заданию
Подключать файлы с исходным кодом (*.c/*.cpp) через #include – зло (вас будут карать баллами)
୧((#Φ益Φ#))୨
Include directories
Каталоги включаемых файлов настраиваются как часть вашего проекта / настроек IDE / настроек компилятора. Компилятор не будет искать заголовочный файл в каталоге исходного кода вашего проекта и ему нужно задать директории, где они лежат.
Вынесем определение (definition) функций foo и bar в отдельный исполняемый файл и создадим соответсвующий заголовочный:
Чтобы задать компилятору include directories нужно использовать ключ -I.
Сборка без -I
Сборка с -I
Library directories и library files
Следующий пример: вы хотите использовать в своей программу библиотеку, представляющую собой набор заголовочных файлов и файла статической библиотеки. В таком случае, помимо указания путей заголовочных файлов нужно будет указать где искать файл библиотеки и как он называется.
Для подключения сторонних библиотек помимо указания директорий, где следует искать заголовочные файлы (чаще всего это директории 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 библиотеки.
Last updated