Учимся компилировать C++ программу

В преддверии старта нового потока по курсу «Разработчик C++» подготовили перевод еще одного полезного материала. Данный материал не является хардкорным, но наверняка будет полезен джунам и даже специалистам middle уровня.



Одна из самых сложных тем, с которыми я столкнулся в начале изучении C++, помимо указателей и управления памятью, заключалась в том, как успешно компилировать код с использованием сторонних библиотек.

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

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

Эти знания не из тех, которые обычно входят в обучающие программы. Основное внимание уделяется решению проблем и синтаксису C++, и, тем не менее, если вы хотите заниматься серьезным программированием игр на C++ без написания всего с нуля, эти знания необходимы.
Поэтому я решил написать серию статей, в которых рассматриваются эти, обсуждаемые мной, вопросы. Первая статья будет посвящена изучению того, как скомпилировать программу на C++.

Трехэтапный процесс




Компиляция программы на C++ включает взятие написанного нами исходного кода (файлы .cpp, .c, .h, .hpp) и преобразование их в исполняемый файл или библиотеку, которая может работать на указанной платформе.
Этот процесс можно разделить на три основных этапа:

  1. Препроцесинг
  2. Компиляция
  3. Компоновка


Препроцесинг


В C++ есть директивы препроцессора (идентифицируются в коде префиксом #), определяющие преобразования, которые должны выполняться в исходном коде до его компиляции.

Вы можете узнать больше о директивах препроцессора здесь. Первый этап компиляции программы на C++ с использованием препроцессора предполагает выполнение этих преобразований.

Что именно делает препроцессор, зависит от директивы.

Например, мы часто разбиваем код на отдельные файлы, чтобы упростить организацию и чтение. Чтобы связать код в одном файле с кодом из другого мы используем директиву #include.

При компиляции нашей программы на C++ препроцессор берет этот #include и копирует код, определенный в хедере, в файл, который его включает. Это экономит наше время и исключает возможность возникновения ошибок, которые могли бы возникнуть при ручном копировать кода между файлами.

Директива include является лишь одним примером предопределенных директив, другие примеры вы можете посмотреть в этой статье.

К концу стадии препроцессинга все директивы препроцессора в вашем коде будут обработаны препроцессором компилятора, и результирующий код будет готов к компиляции.

Компиляция


Компиляция является следующим этапом в процессе и связана с превращением исходного кода, который мы пишем, в нечто, понятное компьютеру — машинный код.

Компиляция C++ сама по себе является двухэтапным процессом. Во-первых, компилятор берет исходный код и конвертирует его в язык ассемблера. Ассемблер является языком программирования низкого уровня, который в большей степени похож на машинные инструкции процессора.

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

Примечание: машинный код состоит из команд, написанных в двоичном формате, в терминах машинного языка, потому что именно это тот код, который фактически понимает процессор.

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

На этапе компиляции мы будем предупреждены о любых ошибках в нашем коде, которые приводят к тому, что наш код не компилируется. Все ошибки на этом этапе вызваны тем, что компилятор не понимает написанный нами код.
Код не будет распознаваемым C++, если мы где-то перепутали наш синтаксис. В типичных примерах неудачной компиляции не хватает точки с запятой, неправильно написано ключевое слово C++ или добавлено слишком многих фигурных скобок в конце метода.

Если обнаружена ошибка, компиляция полностью останавливается. Вы не сможете скомпилировать свой C++ код, пока все ошибки не будут исправлены.

Компоновка


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

Примечание: библиотека — это просто повторно используемая коллекция функций, классов и объектов, которые имеют общее назначение, например, математическая библиотека.

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

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

Сборка


Я думаю, стоит упомянуть еще одну вещь: в IDE, такой как Visual Studio, описанные шаги компиляции сгруппированы в процесс, называемый сборка (build). Типичный рабочий процесс при создании программы — сборка, а затем отладка (debug).

Происходит следующее: сборка создает исполняемый файл (путем компиляции и компоновки кода) или список ошибок в зависимости от того, насколько хорошо мы справились с написанием кода со времени нашей последней сборки. Когда мы нажмем Start Debugging, Visual Studio запустит созданный исполняемый файл.

Компиляция простой программы на C++


Теперь мы знаем основные этапы компиляции C++ программ. Я подумал, что мы могли бы закончить эту статью, рассмотрев простой пример, который поможет закрепить то, что мы только что изучили.

В этом примере я планирую использовать набор инструментов MSCV и собирать его из командной строки разработчика.

Это не руководство по настройке и использованию набора инструментов MSCV из командной строки, поэтому, если вам это интересно, вы можете найти больше информации здесь.

Шаги, которые мы собираемся выполнить:

  1. Создадим папку для нашей C++ программы.
  2. Перейдем в эту папку.
  3. Создадим нашу C++ программу в редакторе (я использовал Visual Studio Code).
  4. Скомпилируем наш исходный код в объектные файлы.
  5. Скомпануем наши объектные файлы, чтобы создать исполняемый файл.


Создаем место для хранения нашей C++ программы




Все, что мы делаем на этом шаге, это используем команду Windows md для создания каталога по указанному пути с именем HelloWorld. Мы могли бы просто создать папку из проводника, но это не так круто.

Переходим в папку



Все, что мы делаем на этом шаге, — это перемещаемся в нашу папку с помощью команды cd, за которой следует путь, по которому мы хотим перейти. В нашем случае это папка, которую мы создали на предыдущем шаге.

Мы делаем это, чтобы облегчить себе жизнь.

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

Пишем код на C++


class HelloWorld
{
public:
void PrintHelloWorld();
};
#include "HelloWorld.h"
#include <iostream>
using namespace std;
void HelloWorld::PrintHelloWorld()
{
std::cout << "Hello World";
}
#include "HelloWorld.h"
int main()
{
HelloWorld hello;
hello.PrintHelloWorld();
return 0;
}


Приведенный выше код представляет собой очень простую программу, содержащую три файла: main.cpp, HelloWorld.h, и HelloWorld.cpp.

Наш заголовочный файл HelloWorld определяет единственную функцию PrintHelloWorld(), реализация этой функции определена в HelloWorld.cpp, а фактическое создание объекта HelloWorld и вызов его функции выполняется из main.cpp.

Примечание. Эти файлы сохраняются в папке, которую мы создали ранее.

Компиляция программы


Чтобы скомпилировать и скомпоновать нашу программу, мы просто используем команду cl, за которой следуют все файлы .cpp, которые мы хотим скомпилировать. Если мы хотим скомпилировать без компоновки, нужно использовать команду cl /c.

Примечание: мы не включаем файл .h в компиляцию, потому что его содержимое автоматически включается в main.cpp и HelloWorld.cpp препроцессором благодаря директиве препроцессора #include.






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

Компоновка


На этом финальном этапе нам нужно скомпоновать наши объектные файлы для получения окончательного исполняемого файла.
Для этого мы используем команду LINK, за которой указываем созданные объектные файлы.





Теперь все, что нам нужно сделать, это дважды кликнуть по helloworld.exe, чтобы запустить нашу программу.
Стоит отметить, что, учитывая, что наша программа производит вывод в консоль как раз перед вызовом return из main, консоль может не появиться, или же она будет отображена очень недолго.

Распространенным решением для обеспечения того, чтобы консоль оставалась открытой, является запрос пользовательского ввода в конце программы с использованием cin.

Это очень примитивный пример, но я надеюсь, что он поможет понять, как компилируется программа на C++.

Есть еще много вещей, которые мы не рассмотрели, например, как скомпоновать внешние библиотеки, как наш код может быть скомпилирован на нескольких платформах, и как лучше справляться с компиляцией больших программ на C++.

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

Резюме


Компиляция программы на C++ — это трехэтапный процесс: препроцессинг, компиляция и компоновка.

Препроцессор обрабатывает директивы препроцессора, такие как #include, компиляция преобразует файлы исходного кода в машинный код, хранимый в объектных файлах, а компоновка связывает ссылки на объектные файлы и внешние библиотеки для создания исполняемого файла или файла библиотеки.

Ссылки




Пройти тестирование и узнать подробнее о курсе.
Источник: habr.ru