CMake — первая программа
Зачем CMake, если есть IDE
C++ редко запускают "двойным кликом по .cpp" как Python-скрипт. Цепочка такая:
- Компилятор (
g++,clang++,cl) превращает каждый.cppв объектный файл (.o/.obj). - Линкер склеивает объектные файлы и библиотеки в исполняемый файл (
hello.exe,hello).
В одном файле main.cpp можно вызвать вручную:
g++ main.cpp -o hello
Разбор:
g++запускает полный цикл: компиляция, ассемблирование и линковка в одной команде.main.cppвыступает единственной единицей трансляции для простого сценария.-o helloзадает имя итогового исполняемого файла.- Такой формат удобен только для мини-примеров; при росте проекта нужна декларативная сборка через CMake.
Как только появляются несколько .cpp, заголовки в include/ и разные ОС — ручные команды ломаются. CMake читает сценарий CMakeLists.txt и генерирует проект под вашу среду.
| Термин | Простыми словами |
|---|---|
| Цель (target) | то, что собираем: программа hello или библиотека greet |
| Генератор | Ninja, Make, Visual Studio — кто реально вызывает компилятор |
| Конфигурация | Debug (отладка) или Release (оптимизации) |
Теория Debug/Release и статика/динамика: Конфигурация и сборка. Выбор g++ / clang++ / cl: Компиляторы и toolchain.
Идея "исходники отдельно от сборки"
hello-cmake/ ← храните в git
├── CMakeLists.txt
├── include/
│ └── greet.hpp
└── src/
├── main.cpp
└── greet.cpp
hello-cmake/build/ ← генерируется, в git не кладут
Разбор:
- Верхняя часть дерева описывает исходники и конфигурацию, которые должны версионироваться.
include/хранит интерфейсы модулей,src/— их реализации.build/изолирует артефакты генератора и компилятора от исходного кода.- Такое разделение упрощает очистку сборки и предотвращает "мусор" в git-истории.
| Команда | Смысл |
|---|---|
cmake -S . -B build | Source = корень с CMakeLists.txt; Build = папка артефактов |
cmake --build build | собрать всё, что описано в сценарии |
./build/hello | запуск (Linux/macOS; на Windows путь может быть build\Debug\hello.exe) |
CMakeLists.txt — построчно
cmake_minimum_required(VERSION 3.20)
project(hello_cmake VERSION 0.1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(hello
src/main.cpp
src/greet.cpp
)
target_include_directories(hello PRIVATE include)
Разбор:
cmake_minimum_requiredфиксирует нижнюю границу возможностей CMake для воспроизводимой конфигурации.project(... LANGUAGES CXX)активирует язык C++ и формирует метаданные проекта.set(CMAKE_CXX_STANDARD 17)включает требования стандарта C++17 для target-ов.add_executable(hello ...)объявляет исполняемую цель и список исходных файлов.target_include_directories(... PRIVATE include)добавляет путь заголовков только для текущей цели.
| Строка | Зачем |
|---|---|
cmake_minimum_required(VERSION 3.20) | CMake ниже 3.20 остановится с ошибкой — защита от старых версий |
project(hello_cmake ... LANGUAGES CXX) | имя проекта; CXX = язык C++ |
CMAKE_CXX_STANDARD 17 | включить стандарт C++17 для всех целей |
CMAKE_CXX_STANDARD_REQUIRED ON | без C++17 сборка прервётся, а не "тихо" откатится к C++14 |
add_executable(hello ...) | цель hello — исполняемый файл из перечисленных .cpp |
target_include_directories(hello PRIVATE include) | при компиляции hello искать #include "greet.hpp" в include/ |
PRIVATE у include-путей: каталог нужен только при сборке hello. Если позже сделаете библиотеку и подключите её к другим целям — различие PRIVATE / PUBLIC станет важным (Конфигурация и сборка в C++).
Исходники и заголовок
Заголовок — объявление (что есть функция):
#pragma once
#include <string>
std::string greet(const std::string& name);
Разбор:
#pragma onceпредотвращает повторное включение заголовка в одной единице трансляции.<string>нужен для типаstd::stringв сигнатуре функции.- Объявление
greetформирует публичный контракт модуля без реализации. - Параметр передается по
const&, чтобы избежать лишнего копирования.
#pragma once — защита от повторного включения одного файла в одной единице трансляции.
Реализация — в .cpp (одно определение на всю программу):
#include "greet.hpp"
std::string greet(const std::string& name) {
return "Hello, " + name + "!";
}
Разбор:
- Файл реализации подключает собственный заголовок для проверки соответствия сигнатур.
- Оператор
+выполняет конкатенацию литерала и входной строки. - Возвращаемое значение создается как новый объект
std::string. - Функция изолирована и легко тестируется, потому что не зависит от ввода/вывода.
Точка входа:
#include "greet.hpp"
#include <iostream>
int main() {
std::cout << greet("CMake") << '\n';
return 0;
}
Разбор:
mainсвязывает модуль бизнес-логики (greet) и инфраструктуру вывода (std::cout).- Вызов
greet("CMake")демонстрирует использование функции из отдельного translation unit. '\n'завершает строку в потоке без принудительного flush, что обычно быстрееstd::endl.return 0фиксирует корректный код завершения процесса.
#include | Где ищет |
|---|---|
"greet.hpp" | каталоги из target_include_directories |
<iostream> | системные пути компилятора |
Если положить тело greet в .hpp и подключить его из двух .cpp, линкер может выдать duplicate symbol — поэтому реализацию держат в одном .cpp.
Сборка и запуск
cmake -S . -B build -G Ninja
cmake --build build
./build/hello
Разбор:
- Первая команда конфигурирует проект и генерирует файлы сборки для генератора Ninja.
- Вторая команда запускает сам процесс компиляции/линковки через выбранный генератор.
- Третья команда исполняет полученный бинарник и проверяет, что pipeline завершился успешно.
- Разделение "configure/build/run" помогает диагностировать ошибки по этапам.
На Windows без Ninja CMake выберет Visual Studio или MSBuild. Путь к exe часто build\Debug\hello.exe.
Release (оптимизации):
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
Разбор:
-DCMAKE_BUILD_TYPE=Releaseвключает профиль с оптимизациями компилятора.- Опция задается на этапе конфигурации и сохраняется в кэше папки
build. - Во втором шаге CMake использует уже сгенерированную конфигурацию Release для сборки.
- Для multi-config генераторов (MSVC) применяется
--config Releaseво время--build.
Для multi-config генераторов MSVC вместо CMAKE_BUILD_TYPE при сборке: cmake --build build --config Release.
Подключение внешней библиотеки
Учебный способ — FetchContent (скачать исходники при конфигурации):
include(FetchContent)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.2.1
)
FetchContent_MakeAvailable(fmt)
target_link_libraries(hello PRIVATE fmt::fmt)
Разбор:
include(FetchContent)подключает модуль CMake для декларативной загрузки зависимостей.FetchContent_Declareописывает источник и ревизию библиотекиfmt.FetchContent_MakeAvailable(fmt)добавляет target-ы зависимости в текущий граф сборки.target_link_librariesпривязываетfmt::fmtкhello, чтобы линкер подключил библиотеку.PRIVATEограничивает распространение зависимости только текущей целью.
Цепочка для новичка:
- Объявили зависимость (
FetchContent_Declare). - Сделали доступной (
FetchContent_MakeAvailable). - Привязали к цели (
target_link_libraries). - В C++ подключили заголовок и вызвали API (
fmt::print(...)).
В долгих проектах чаще vcpkg или Conan — стабильнее версии и кэш.
Частые ошибки
| Ошибка | Что проверить |
|---|---|
greet.hpp: No such file | target_include_directories и файл в include/ |
Undefined reference to greet | src/greet.cpp в add_executable |
| Старая версия C++ | CMAKE_CXX_STANDARD и REQUIRED ON |
Правки в CMakeLists.txt не срабатывают | снова cmake -S . -B build (реконфигурация) |
Запускаю exe из корня, а он в build/ | полный путь к собранному файлу |
Что попробовать
- Вынесите
greetвadd_library(greet STATIC src/greet.cpp)и линкуйте:target_link_libraries(hello PRIVATE greet). - Добавьте
enable_testing()иadd_test(NAME smoke COMMAND hello)— запуск черезctest. - Соберите Qt тем же
cmake -B build.
Дальше
- Google Test и Catch2 — отдельная цель для тестов;
- Qt — первая программа —
find_package(Qt6)поверх этого каркаса; - Практические задания — уровень 3.1 "библиотека + exe".
Мини-практикум — закрепление за 20 минут
Если статья читается как "много команд, мало живого смысла", пройдите этот короткий сценарий и сразу получите ощущение рабочего каркаса.
- Создайте проект из трёх файлов:
CMakeLists.txt,src/main.cpp,src/greet.cpp. - Проверьте базовый запуск:
cmake -S . -B build && cmake --build build. - Добавьте второй
.cppв цель и убедитесь, что линковка проходит безundefined reference. - Сломайте сборку намеренно: уберите
src/greet.cppизadd_executable, чтобы увидеть типичную ошибку линкера. - Верните файл и добавьте
-Wall//W4, чтобы увидеть предупреждения на раннем этапе.
Такой цикл тренирует главное: различать ошибки конфигурации, компиляции и линковки.
Типичные вопросы новичка
Почему проект собирается, а программа "не видит" заголовок
Причина почти всегда в target_include_directories. Заголовок должен лежать в директории, которую вы явно привязали к цели.
Проверка: в CMakeLists.txt путь добавлен к тому target, который реально компилирует main.cpp.
Почему после правки CMake "ничего не меняется"
CMake сначала конфигурирует проект (cmake -S . -B build), затем собирает (cmake --build build).
При изменении CMakeLists.txt нужен шаг конфигурации, иначе генератор использует старое состояние.
Почему на Windows и Linux разные пути к exe
Генераторы разные: single-config и multi-config.
На MSVC чаще путь вида build\Debug\hello.exe, на Ninja/Make обычно build/hello.
Кейс из практики — "проходит локально, падает в CI"
Ситуация: локально проект собирался через IDE, а в CI падал с ошибкой greet.hpp: No such file.
Причина: include-путь был добавлен только в настройках IDE, но не описан в CMakeLists.txt.
Решение: перенести конфигурацию в target-уровень:
target_include_directories(hello PRIVATE include)
Разбор:
- Команда добавляет каталог
includeв список include-path именно для целиhello. - Проблема "header not found" в CI часто возникает из-за отсутствия этой строки в CMake.
PRIVATEозначает, что путь не экспортируется потребителям цели.- Настройка в
CMakeLists.txtделает сборку независимой от локальных IDE-настроек.
Проверка:
- удалить папку
build; - пересобрать командами
cmake -S . -B buildиcmake --build build; - повторить на другой машине или в CI.
Вывод: вся критичная конфигурация хранится в CMake, а IDE только читает её.
В подборках
Статья входит в тематические подборки и блок "С чего начать?" на главной. Соседние шаги того же маршрута:
Первые шаги (маршрут подборки) — Razor Pages — первая программа, Qt — первая программа, .NET MAUI — первая программа, Qt Quick — первая программа на QML, Blazor — первая программа, Первая программа на Laravel.