Компилятор превращает текст программы в исполняемый файл. В курсе это делается одной командой:

g++ main.cpp -o main && ./main

Не из лекции

Разделы про санитайзеры, про разные компиляторы и большая часть таблицы флагов — не материал занятия, а инструментальная справка. На занятии разбирались этапы сборки, -std=c++20, -o и код завершения.

g++ main.cpp — собрать файл, -o main — назвать результат main (без этого флага получится a.out), && ./main — запустить, если сборка прошла успешно. Именно &&, а не ;: при ошибке компиляции запускать нечего, и оболочка сама остановится.

Что происходит внутри

Формально это четыре стадии, и каждую можно посмотреть отдельно:

СтадияЧто делаетФлаг
ПрепроцессорРаскрывает #include и #define-E
КомпиляцияПереводит C++ в ассемблер-S
АссемблированиеПолучает объектный файл .o-c
ЛинковкаСшивает объектные файлы и библиотеки в программу(по умолчанию)

Всю цепочку можно пройти руками и посмотреть, что получается на каждом шаге:

g++ -std=c++20 -E main.cpp -o main.ii   # препроцессор
g++ -std=c++20 -S main.ii -o main.s     # трансляция в ассемблер
as main.s -o main.o                     # ассемблирование
g++ main.o -o main                      # компоновка

main.cpp → main.ii → main.s → main.o → main

В main.ii вместо десятка написанных строк окажутся тысячи строк из подключённых заголовков — они сами подключают другие заголовки, и такое разрастание нормально. main.s ещё текстовый, но описывает программу инструкциями целевой машины. В main.o уже машинный код, однако запустить его в общем случае нельзя: обращения к другим частям программы и к библиотекам не связаны с реализациями. Этим занимается компоновщик.

Знать это полезно ради диагностики: по тексту ошибки видно, на какой стадии всё сломалось. error: 'cout' was not declared — компиляция, не хватает объявления. undefined reference to 'Foo()' — линковка: объявление есть, а тела функции нет ни в одном файле. А если программа собралась, запустилась и упала только потом — это уже время выполнения.

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

Сборка и запуск — разные действия

Во время сборки тело main не выполняется: компиляция строки с std::cout сама по себе ничего не печатает. Кнопка «запустить» в IDE прячет оба действия, и из-за этого легко не заметить, что новая версия не собралась, и запустить старый исполняемый файл.

Результат работы программы можно посмотреть и без вывода — через код завершения:

g++ main.cpp -o main
./main
echo $?     # код завершения; 0 означает успех

Как способ выдачи ответа он не годится: через него не вернуть ни дробное число, ни строку, ни несколько значений. Для этого есть Потоки ввода и вывода.

Какие бывают компиляторы

КомпиляторКомандаГде
GCCg++Linux по умолчанию, MinGW на Windows
Clang / LLVMclang++macOS по умолчанию, есть везде
MSVCcl.exeWindows, Visual Studio

Код, написанный по стандарту, собирается любым из них. Различия проявляются в другом: Clang даёт более внятные сообщения об ошибках, GCC чуть агрессивнее оптимизирует, MSVC живёт по своим правилам с заголовками. У всех есть собственные расширения языка — ими лучше не пользоваться, иначе программа перестанет собираться на другой машине.

Проверяющая система может использовать не тот компилятор и не ту версию, что стоит у вас. Поэтому «у меня собирается» — не гарантия; а -std стоит указывать явно, если код опирается на возможности конкретного стандарта.

Флаги

ФлагЗачем
-o имяимя результата (иначе a.out)
-std=c++20версия стандарта языка
-Wall -Wextraвключить предупреждения
-Werrorсчитать предупреждения ошибками
-O0 … -O3уровень оптимизации (-O0 по умолчанию)
-gотладочная информация для gdb
-cтолько объектный файл, без линковки
-I путьгде искать заголовки
-l имяподключить библиотеку
-fsanitize=address,undefinedпроверки во время выполнения

Про стандарт

Без -std компилятор берёт свой стандарт по умолчанию, и он зависит от версии: у GCC 16 это C++20, у более старых — C++17 или C++14. Один и тот же код у вас и в проверяющей системе может собираться по разным стандартам.

Про предупреждения

-Wall -Wextra заставляют компилятор рассказывать о подозрительных местах: сравнение знакового с беззнаковым, неиспользуемая переменная, переполнение в константном выражении. Ошибками они не являются — программа соберётся и запустится. Но если поведение расходится с ожидаемым, включить их стоит первым делом: часто ответ уже лежит в выводе компилятора.

Про санитайзеры

-fsanitize=undefined,address встраивает в программу проверки, которые во время работы ловят Неопределённое поведение: выход за границы массива, чтение неинициализированной памяти, переполнение. Программа при этом работает медленнее, поэтому такую сборку делают для отладки, а не для сдачи.

g++ -std=c++20 -g -fsanitize=undefined,address main.cpp -o main && ./main

Стоит вспоминать про это, когда ошибка воспроизводится «через раз» или результат зависит от того, добавили вы вывод в консоль или нет.

Тонкости

  • Исполняемый файл — не текст: его нельзя открыть редактором и нельзя сдать вместо исходника. Имя main без расширения на Linux — обычное дело.
  • Пересобирать нужно после каждой правки: ./main запускает то, что собрано в прошлый раз.
  • Несколько файлов собираются одной командой: g++ a.cpp b.cpp -o app.
  • В проекте посерьёзнее эту команду прячут за сборочную систему — make или CMake; для одного файла они излишни.

Частые ошибки

  • Запустить ./main после неудачной сборки и смотреть на результат прошлой версии — от этого спасает && в команде.
  • Забыть -o и потерять программу среди файлов под именем a.out.
  • Считать предупреждение ошибкой (программа собралась) или, наоборот, игнорировать его совсем.