, и через пару секунд появился app.exe, который печатает Hello, Engine!.
Но процессор не понимает C++. Процессор выполняет только машинные команды
Числа, которые процессор умеет выполнять напрямую: например, «сложи два числа» или «перейди к другой команде». Для каждого вида процессоров набор команд свой.
, а строка std::cout << "Hello, Engine!\n"; для него просто набор букв. Что же происходит за эти секунды, пока текст превращается в программу?
Что происходит при сборке
Что происходит за эти секунды, показано на схеме ниже. Рядом со схемой твой main.cpp: наведи на слово в коде, и схема покажет, где это слово оказывается; щёлкни по слову, и откроется реплика про это слово.
Слева твой main.cpp: 7 строк текста на C++. Справа app.exe: файл с машинными командами, который можно запустить. Между ними работают две программы, и у каждой свой шаг: cl.exe переводит C++ в машинные команды, а link.exe собирает из результата готовую программу. На схеме обе программы пока скрыты в пустых рамках со знаком вопроса, но в выводе сборки
PowerShell
PS D:\dev\MyEngine> cmake --build buildВерсия MSBuild 18.10.1-1.26427.6+3cd27c13e для .NET Framework 1>Checking Build System Building Custom Rule D:/dev/MyEngine/CMakeLists.txt main.cpp app.vcxproj -> D:\dev\MyEngine\build\Debug\app.exe Building Custom Rule D:/dev/MyEngine/CMakeLists.txt
ты уже видел следы работы обеих программ: строку main.cpp написала первая из них, cl.exe, а строка app.vcxproj -> появилась после работы второй, link.exe. Обе программы запускает сборщик MSBuild: версию MSBuild печатает первая строка того же вывода.
Первым работает препроцессор — первая часть cl.exe. C++ препроцессор не понимает и обращается с main.cpp как с обычным текстом: находит строки, которые начинаются со знака #, и выполняет их. Такие строки называются директивами
#include — вставить сюда текст другого файла.
#define — задать имя, которое препроцессор заменит текстом.
#if — оставить кусок текста только при условии, например только в Debug-сборке.
#pragma — особое указание компилятору, например #pragma once: «не вставлять этот файл второй раз».
. В твоём файле директива одна: #include <iostream>. На место этой строки препроцессор вставляет весь текст заголовочного файлаiostream, который приходит вместе с Visual Studio. Файл iostream сам подключает другие файлы, и текст этих файлов тоже вставляется, поэтому на схеме за main.cpp вырастает стопка листов. Было 7 строк, стало около 84 тысяч
У нас вышло 84 257 строк, и 37 170 из этих строк — с кодом, остальные — пустые строки и служебные пометки препроцессора о том, из какого файла пришёл текст. Число зависит от версии Visual Studio.
.
Текст main.cpp со вставленными заголовками получает компилятор — вторая часть cl.exe. В отличие от препроцессора, компилятор понимает C++: он проверяет, правильно ли написан код, и переводит код в машинные команды. Результат компилятор записывает в файл, и на схеме этот файл появляется справа от cl.exe. Если в коде ошибка, cl.exe останавливается, пишет об ошибке в вывод сборки, и файл не появляется. У каждой ошибки есть код, и у ошибок cl.exe он начинается с буквы C, например C2065
Undeclared identifier: в коде встретилось имя, которого компилятор нигде не нашёл. Чаще всего это опечатка или забытый #include.
. Коды ошибок препроцессора тоже начинаются с C, например C1083 — «не найден файл из #include», потому что препроцессор — часть той же программы cl.exe.
Получился main.obj — объектный файл с машинными командами. Запустить main.obj пока нельзя, потому что в этом файле не хватает кода. Твоя функция main в main.obj есть, и оператор << тоже: его код пришёл с текстом заголовочных файлов
Код этого оператора написан прямо в заголовочных файлах, которые вставил препроцессор. Компилятор получил этот код вместе с твоим кодом и перевёл в машинные команды.
. А вот самого объекта std::cout в тексте после препроцессора нет: в заголовках только объявление
Запись «такое имя существует, и вот его тип» без самого объекта или кода функции. Компилятору этого хватает, чтобы проверить строку и перевести строку в машинные команды. Сам объект или код лежит в другом месте, например в библиотеке.
. Поэтому компилятор не жалуется, а оставляет в main.obj пометку: «здесь используется std::cout, а где он лежит — не знаю». Такие же пометки стоят на служебных функциях библиотеки, например на flush
Функция, которая отправляет в консоль текст, накопленный в памяти std::cout: то же делает std::endl. Функцию flush вызывает код оператора <<. Сам ты flush не вызывал, поэтому в main.cpp этого имени не видно.
. На схеме пометки подписаны знаком вопроса.
Дальше main.obj получает линкер — программа link.exe. Линкер закрывает пометки
Записи в main.obj вида «здесь используется std::cout, а где он лежит — не знаю». На схеме пометки подписаны знаком вопроса.
: для каждой пометки находит нужный код и подставляет на место пометки, где этот код лежит. Ищет линкер во всех файлах .obj проекта (у тебя пока есть только main.obj, а в движке таких файлов будут десятки) и в библиотеках.lib
Файл-библиотека. В файле .lib лежит либо готовый скомпилированный код, либо только запись о том, где этот код лежит: в файле .dll. Файл .dll — тоже библиотека, но он остаётся отдельным файлом и подключается к программе уже при запуске.
Файлы .libстандартной библиотеки лежат в папке, куда установилась Visual Studio. Путь к этим файлам нигде записывать не нужно: папки библиотек линкеру передаёт сборщик, а имена нужных библиотек компилятор сам записал в main.obj.
. Когда пометок не осталось, линкер склеивает main.obj и найденный в библиотеках код в один файл — app.exe. Если для какой-то пометки код не нашёлся, линкер останавливается с ошибкой, код которой начинается с LNK, от слова linker. Такая ошибка, LNK2019, появляется, например, если в программе нет функции main: код запуска
Часть стандартной библиотеки, которую линкер сам добавляет в каждую программу на C++ из файла MSVCRTD.lib. Код запуска готовит программу к работе, вызывает main, а после main завершает программу.
вызывает main, а линкер не находит, что подставить.
app.exe — исполняемый файл, то есть файл, который Windows умеет запускать. Внутри app.exe два вида записей: машинные команды, которые получились из твоего main.cpp и из кода запуска стандартной библиотеки, и список файлов .dll, которые понадобятся программе. При запуске Windows загружает app.exe в память, подключает файлы из этого списка и передаёт управление коду запуска, а код запуска вызывает main. Поэтому для запуска app.exe уже не нужны ни main.cpp, ни main.obj, ни cl.exe, ни link.exe: app.exe можно скопировать в любую папку и запустить, а исходники удалить. Нужна только часть стандартной библиотеки
Код std::cout не копируется в app.exe. В файле .lib записано, что этот код лежит в MSVCP140D.dll, и линкер переносит эту запись в программу. При запуске Windows находит файл .dll и подключает этот файл к программе. Так CMake настраивает сборку по умолчанию; сборку можно настроить и так, чтобы код библиотеки копировался в сам app.exe.
Буква D в конце имени значит Debug. Этот файл ставится вместе с Visual Studio, поэтому на компьютере без Visual Studio твоя Debug-сборка не запустится.
, но эта часть ставится вместе с Visual Studio и уже есть на твоём компьютере.
Зачем два шага, компиляция и линковка, а не одна программа, которая делает всё сразу? В движке будут сотни файлов .cpp, и компилировать их все после каждой правки пришлось бы минутами. Поэтому компилятор обрабатывает каждый .cpp вместе со вставленными в него заголовками отдельно от остальных, и из каждого .cpp получается свой .obj. Эти файлы остаются в папке сборки. Поменял один .cpp — перекомпилируется только этот файл, а линкер быстро склеивает новый .obj с остальными, уже готовыми. На схеме так выглядит будущий движок: изменённый файл проходит весь путь от main.cpp до app.exe, а остальные файлы в этот путь не попадают: готовые .obj этих файлов линкер берёт из папки сборки без перекомпиляции. Что именно пересобрать, решает сборщик, сейчас это MSBuild
Сборщик, который входит в Visual Studio. MSBuild работает потому, что при настройке CMake выбрал генератор Visual Studio, а генератор Visual Studio пишет список дел для MSBuild.
: сборщик сравнивает даты изменения файлов
Если файл .cpp или один из подключённых к нему файлов изменён позже, чем записан .obj, этот .cpp компилируется заново. Иначе сборщик оставляет готовый .obj.
. А CMake только записал для сборщика список дел
Файл app.vcxproj, который CMake записал при первой команде, настройке: в нём сказано, какие файлы .cpp компилировать и что склеить в app.exe.
— файл app.vcxproj в папке build.
Проверка на своём проекте
Если не получилось1
После добавления пустой строки в выводе всё равно нет строки main.cpp.
Почему
Сборщик сравнивает дату изменения main.cpp с датой main.obj. Если правка не сохранена на диск, файл остался прежним, main.obj новее main.cpp, и компилятор запускать незачем.
Что сделать
Сохрани main.cpp в редакторе и снова выполни cmake --build build. В заголовке вкладки с файлом при несохранённой правке стоит знак: в Visual Studio звёздочка, в VS Code белая точка.