Перейти к основному содержимому

CMake — первая программа

Разработчику Архитектору
О чём эта статья

Пошаговый первый проект C++ с CMake: от структуры папок до команды запуска. CMake не компилирует код сам — он генерирует файлы для Ninja, Make или Visual Studio. Тот же каркас понадобится для Qt и тестов.


Зачем CMake, если есть IDE

C++ редко запускают "двойным кликом по .cpp" как Python-скрипт. Цепочка такая:

  1. Компилятор (g++, clang++, cl) превращает каждый .cpp в объектный файл (.o / .obj).
  2. Линкер склеивает объектные файлы и библиотеки в исполняемый файл (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 buildSource = корень с 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 ограничивает распространение зависимости только текущей целью.

Цепочка для новичка:

  1. Объявили зависимость (FetchContent_Declare).
  2. Сделали доступной (FetchContent_MakeAvailable).
  3. Привязали к цели (target_link_libraries).
  4. В C++ подключили заголовок и вызвали API (fmt::print(...)).

В долгих проектах чаще vcpkg или Conan — стабильнее версии и кэш.


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

ОшибкаЧто проверить
greet.hpp: No such filetarget_include_directories и файл в include/
Undefined reference to greetsrc/greet.cpp в add_executable
Старая версия C++CMAKE_CXX_STANDARD и REQUIRED ON
Правки в CMakeLists.txt не срабатываютснова cmake -S . -B build (реконфигурация)
Запускаю exe из корня, а он в build/полный путь к собранному файлу

Что попробовать

  1. Вынесите greet в add_library(greet STATIC src/greet.cpp) и линкуйте: target_link_libraries(hello PRIVATE greet).
  2. Добавьте enable_testing() и add_test(NAME smoke COMMAND hello) — запуск через ctest.
  3. Соберите Qt тем же cmake -B build.

Дальше


Мини-практикум — закрепление за 20 минут

Если статья читается как "много команд, мало живого смысла", пройдите этот короткий сценарий и сразу получите ощущение рабочего каркаса.

  1. Создайте проект из трёх файлов: CMakeLists.txt, src/main.cpp, src/greet.cpp.
  2. Проверьте базовый запуск: cmake -S . -B build && cmake --build build.
  3. Добавьте второй .cpp в цель и убедитесь, что линковка проходит без undefined reference.
  4. Сломайте сборку намеренно: уберите src/greet.cpp из add_executable, чтобы увидеть типичную ошибку линкера.
  5. Верните файл и добавьте -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-настроек.

Проверка:

  1. удалить папку build;
  2. пересобрать командами cmake -S . -B build и cmake --build build;
  3. повторить на другой машине или в CI.

Вывод: вся критичная конфигурация хранится в CMake, а IDE только читает её.


В подборках

Статья входит в тематические подборки и блок "С чего начать?" на главной. Соседние шаги того же маршрута:

Первые шаги (маршрут подборки) — Razor Pages — первая программа, Qt — первая программа, .NET MAUI — первая программа, Qt Quick — первая программа на QML, Blazor — первая программа, Первая программа на Laravel.