JavaRush /Курсы /C++ SELF /Структура репозитория: include/, src/, tests/ и build/

Структура репозитория: include/, src/, tests/ и build/

C++ SELF
26 уровень , 4 лекция
Открыта

1. Базовая схема папок проекта

Когда проект маленький, кажется, что структура папок — это чистая эстетика: мол, «ну лежит всё рядом, и ладно». Но как только в проекте появляется больше трёх файлов, мозг начинает работать как компилятор без оптимизаций: медленно, нервно и с подозрением. Хорошая структура репозитория нужна не для красоты, а чтобы вы быстро понимали, где что искать, и чтобы проект не развалился, когда вы добавите десятый модуль.

Представьте, что вы открыли чужой проект. У вас нет телепатии, но есть проводник файлов. Если структура адекватная, вы буквально за минуту угадаете: где точка входа, где заголовки, где реализация, где тестовая проверка. Если структуры нет, вы начинаете искать main() по всему проекту как иголку в стоге сена, причём иголка ещё и переименована в main_final_final2.cpp.

В учебном курсе структура важна особенно: вы будете постепенно добавлять функциональность в одно приложение. И если папки и имена согласованы, то новые куски кода ложатся на место естественно.

Сейчас мы договоримся об очень простой, но жизнеспособной «географии» проекта. Она не единственная в мире, но для обучения и для большинства небольших приложений — отличная. И главное: она хорошо дружит с нашей моделью «модуль = .hpp + .cpp» и с идеей «интерфейс отдельно от реализации».

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

Папка Что там лежит Как это читать “по-человечески”
include/
Заголовки .hpp «Витрина проекта»: объявления типов и функций, которые можно подключать из разных .cpp
src/
Исходники .cpp (и обычно main.cpp) «Кухня проекта»: реализации функций, логика, детали
tests/
Маленькие проверочные программы (часто отдельные main) «Проверим модуль отдельно»: быстро запустить и убедиться, что базово работает
build/
Артефакты сборки (то, что создаёт компилятор/IDE) «Мастерская»: нужное для сборки, но не исходники

Почему именно так? Потому что это разделяет роли: исходники — отдельно, интерфейс — отдельно, эксперименты/проверки — отдельно, результаты сборки — отдельно. И да, это убирает вечный хаос «а что из этого вообще нужно отправлять другу/преподавателю/в репозиторий».

include/: заголовки и публичный интерфейс

Когда вы кладёте заголовки в include/, вы как бы говорите: «Вот этим можно пользоваться». Это особенно удобно, когда у вас несколько модулей, и они подключают заголовки друг друга. Тогда include/ становится единой точкой правды, и проект меньше зависит от случайного порядка файлов или от того, «в какой папке я сейчас открыл IDE».

Важная мысль: заголовок — это не место, где вы «прячете побольше кода». Заголовок — это место, где вы объясняете остальной программе, какие типы и функции существуют и как ими пользоваться. Мы уже обсуждали границу «интерфейс/реализация»: в заголовке обычно находятся struct, enum class и объявления функций, а реализация — в .cpp.

Ещё одна практичная причина держать заголовки в include/: когда у вас появятся отдельные «проверочные» программы в tests/, они будут подключать тот же интерфейс, что и основное приложение. Это резко снижает шанс, что тесты «проверяют что-то не то».

src/: реализации и точка входа

Папка src/ — это место, где живёт то, что реально выполняется: тела функций, алгоритмы, обработка ввода, вывод, работа с контейнерами и так далее. И почти всегда именно тут лежит main.cpp, потому что main — это часть приложения, то есть реализации, а не интерфейса.

Если вы положите main.cpp куда попало, проект не сломается физически (компилятор не обидится), но сломается морально: вы будете каждый раз заново вспоминать, где вход в программу. В нормальной структуре «вход» должен быть очевиден: открыл src/ — нашёл main.cpp — понял, откуда стартуем.

Практическая дисциплина, которую полезно держать в голове: в src/ можно подключать что угодно, а в include/ нужно быть сдержаннее. Мы не будем сейчас углубляться в тонкости «что именно подключать» (это отдельная тема дальше по курсу), но общий принцип такой: заголовки должны быть максимально простыми и стабильными, а реализация может позволить себе больше деталей.

tests/: быстрые проверки модулей

Папка tests/ в нашем курсе — это не про сложные тест‑фреймворки и не про промышленный CI. Это про очень простую идею: иногда удобно проверить модуль отдельно от всего приложения. Например, вы написали функцию форматирования или печати модели — и хотите убедиться, что она работает, не прогоняя весь сценарий приложения.

Обычно такие «проверки» выглядят как отдельный файл с int main(), который подключает заголовок модуля и делает пару вызовов. По сути это мини‑программа «для себя», но хранимая рядом с проектом. И это резко ускоряет разработку: вы не тратите время на ввод данных, меню, сложный сценарий — просто запускаете крошечный тестовый main и смотрите результат.

Самый важный плюс tests/ как папки: вы заранее приучаете проект к мысли «модуль можно использовать извне». А значит, вы (почти автоматически) начинаете писать более аккуратные интерфейсы в заголовках.

build/: артефакты сборки и почему их не коммитят

Папка build/ — это место, куда попадают результаты сборки: временные файлы, объектные файлы, кэш сборки, иногда сгенерированные файлы проекта и, в конце концов, собранный исполняемый файл. Она появляется, потому что сборка — это отдельный процесс с кучей технических шагов.

Почему build/ не хранят в репозитории? Потому что это не исходники, а «продукты производства». Они, во‑первых, часто зависят от вашей машины и настроек (другая ОС, другой компилятор, другой путь к проекту — и содержимое уже не то). Во‑вторых, они могут весить много. В‑третьих, они легко пересоздаются: исходники важнее.

Очень полезная привычка: относиться к build/ как к папке, которую не страшно удалить. Если исходники на месте, вы в любой момент всё пересоберёте заново. Это снижает страх «я что-то сломал», потому что вы понимаете: вы не удаляете код, вы удаляете результат сборки.

И ещё один момент: build/ лучше держать отдельной папкой, чтобы исходники и «производственный мусор» не смешивались. Тогда вы не увидите в src/ загадочные файлы с расширениями, которые вы не создавали, и не начнёте подозревать компьютер в одержимости.

Соглашение «одна сущность — одна пара файлов»

Когда вы делаете структуру include/ и src/, очень удобно мыслить так: каждый модуль — это пара файлов. Например, есть модуль task — значит, есть task.hpp и task.cpp. Это не закон физики, но это хорошее соглашение, которое держит проект в порядке.

Внутри include/ вы держите заголовок, а внутри src/ — реализацию. Тогда проект читается почти как словарь: «ищу интерфейс — иду в include/; ищу, как сделано — иду в src/». А ещё это снижает шанс, что вы случайно начнёте подключать .cpp вместо .hpp (да, люди так делают, обычно в панике, и обычно это заканчивается странными ошибками).

Идея «одна сущность — одна пара» особенно полезна для новичков, потому что она убирает лишний выбор. Не нужно каждый раз решать: «куда бы мне положить эту функцию?» — вы просто спрашиваете: «она относится к модулю task?» Если да — значит в task.hpp объявление, в task.cpp реализация.

3. Практический пример: мини‑проект Tasky

Сейчас соберём маленький «скелет» проекта, который можно расширять дальше по курсу. Приложение будет простым: хранить задачу (Task) и печатать её. Наша цель не в крутой функциональности, а в том, чтобы увидеть, как файлы и папки работают вместе.

Дерево проекта

Сначала — структура. Её удобно держать в голове как карту:

tasky/
  include/
    app/
      task.hpp
      task_print.hpp
  src/
    task_print.cpp
    main.cpp
  tests/
    task_print_smoke.cpp
  build/
    (создаётся сборкой, в репозиторий не кладём)

Обратите внимание на include/app/...: это не обязательно, но приятно. Так вы сразу видите, что заголовки относятся к вашему приложению (а не к чужой библиотеке), и имена меньше конфликтуют.

Модель данных: Task

include/app/task.hpp

#pragma once // (пока просто как маркер; детали разберём в другой день)

#include <string>

namespace app {

struct Task {
    int id = 0;
    std::string title;
    bool done = false;
};

} // namespace app

Здесь мы сделали простую модель. Она лежит в include/, потому что Task нужен «всем»: и приложению, и тестам, и любым будущим модулям.

Если вы заметили #pragma once и думаете «а мы это не проходили» — вы правы. В рамках этой лекции можете воспринимать это как техническую пометку, чтобы файл случайно не подключали дважды. Подробно мы будем разбирать гигиену заголовков отдельно, позже.

Интерфейс печати: отдельный заголовок

include/app/task_print.hpp

#pragma once

#include <iosfwd>
#include "app/task.hpp"

namespace app {

void print_task(std::ostream& out, const Task& t);

} // namespace app

Заметьте, заголовок не печатает сам. Он лишь говорит: «Есть функция print_task, вот её сигнатура». Это и есть «витрина».

Реализация печати в src/

src/task_print.cpp

#include "app/task_print.hpp"

#include <ostream>

namespace app {

void print_task(std::ostream& out, const Task& t) {
    out << "#" << t.id << " [" << (t.done ? 'x' : ' ') << "] " << t.title;
}

} // namespace app

Ключевая мысль: реализация живёт в src/. Снаружи пользователям модуля не важно, как именно вы печатаете — им важно, что функция существует и как её вызвать.

Тонкий main.cpp в src/

src/main.cpp

#include <iostream>
#include "app/task_print.hpp"

int main() {
    app::Task t{.id = 1, .title = "Сдать домашку", .done = false};

    app::print_task(std::cout, t);
    std::cout << '\n'; // #1 [ ] Сдать домашку

    return 0;
}

Этот main действительно «тонкий»: он создаёт пример задачи и вызывает функцию печати. Позже вы добавите ввод, хранение списка задач и т.д., но структура останется.

Мини‑проверка в tests/

tests/task_print_smoke.cpp

#include <iostream>
#include "app/task_print.hpp"

int main() {
    app::Task t{.id = 42, .title = "Проверка печати", .done = true};

    app::print_task(std::cout, t);
    std::cout << '\n'; // #42 [x] Проверка печати

    return 0;
}

Это не «серьёзный юнит‑тест», но это полезный «дымовой» запуск: вы быстро убеждаетесь, что печать работает, и вам не нужно запускать весь будущий интерфейс приложения.

4. Типичные ошибки

Ошибка №1: складывать всё в src/ и считать, что заголовки не нужны.
На первых шагах так действительно проще: один файл, всё рядом. Но как только вам нужно переиспользовать тип или функцию из другого .cpp, вы начинаете копировать объявления вручную, а потом ловите рассинхронизацию: в одном месте сигнатура поменялась, в другом — нет. include/ дисциплинирует: «объявления живут в одном месте».

Ошибка №2: держать заголовки рядом с .cpp без понятной границы.
Физически это возможно, но ментально тяжело: вы открываете папку и видите 30 файлов вперемешку. Глаз цепляется за случайные вещи, и поиск превращается в квест. Разделение include/ и src/ — это способ сделать границу «что можно подключать» и «что является реализацией» очевидной без объяснений.

Ошибка №3: подключать .cpp через #include, чтобы «оно увиделось».
Это почти всегда симптом того, что нарушена модель раздельной компиляции (которую мы проходили раньше). .cpp предназначены быть отдельными единицами компиляции; подключать их как текст — значит устроить себе дублирование кода и очень странные ошибки. Если вам «не видно функцию», лечится это заголовком с объявлением, а не подключением .cpp.

Ошибка №4: хранить build/ рядом с исходниками и ещё и отправлять её в репозиторий/на проверку.
Это выглядит заманчиво («ну там же всё уже собрано!»), но обычно приводит к тому, что проект разрастается мусором, а у другого человека эти файлы всё равно не подходят под его окружение. Нам важно хранить исходники, а build/ — пересоздавать. Психологически это сложно первые пару раз, но потом становится привычкой, которая экономит часы жизни.

Ошибка №5: делать тесты, которые лезут в «внутренности» модуля вместо его интерфейса.
Когда проверки подключают какие-то случайные .cpp или начинают зависеть от деталей реализации, вы теряете главный смысл tests/: проверять модуль как чёрный ящик. Гораздо полезнее, когда tests/ подключает заголовок из include/ — тот же интерфейс, которым пользуется и приложение.

1
Задача
C++ SELF, 26 уровень, 4 лекция
Недоступна
Паспорт сборки
Паспорт сборки
1
Задача
C++ SELF, 26 уровень, 4 лекция
Недоступна
Дымовой тест
Дымовой тест
1
Задача
C++ SELF, 26 уровень, 4 лекция
Недоступна
Бейдж и печать
Бейдж и печать
1
Задача
C++ SELF, 26 уровень, 4 лекция
Недоступна
Конвертер шкал
Конвертер шкал
1
Опрос
Файлы проекта, 26 уровень, 4 лекция
Недоступен
Файлы проекта
Файлы проекта
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ