Компилятор превращает текст программы в исполняемый файл. В курсе это делается одной командой:
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 означает успехКак способ выдачи ответа он не годится: через него не вернуть ни дробное число, ни строку, ни несколько значений. Для этого есть Потоки ввода и вывода.
Какие бывают компиляторы
| Компилятор | Команда | Где |
|---|---|---|
| GCC | g++ | Linux по умолчанию, MinGW на Windows |
| Clang / LLVM | clang++ | macOS по умолчанию, есть везде |
| MSVC | cl.exe | Windows, 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. - Считать предупреждение ошибкой (программа собралась) или, наоборот, игнорировать его совсем.