Из каких частей состоит программа на C/C++ [2.1]
В этом уроке вы увидите, из каких частей состоит программа на C/C++, зачем делить проект на файлы .h и .cpp, как и зачем писать комментарии (включая doxygen), и что делает препроцессор. В конце есть практические задачи и чек-лист самопроверки.
Главная мысль урока: программа на C++ строится из инструкций, выражений и функций. Интерфейс объявляем в заголовках (.h), реализацию пишем в исходниках (.cpp), а препроцессор помогает подключать файлы и управлять сборкой через директивы #include, #define и т.д.
Содержание
- 1. Цели урока
- 2. Что такое программа на C/C++
- 3. Инструкции, выражения, функции, библиотеки
- 4. Комментарии и документирование кода (включая doxygen)
- 5. Разделение на .h и .cpp, сборка проекта по шагам
- 6. Директивы препроцессора: #include, #define, #ifdef, #pragma once
- 7. Практика: типовые задачи (с решениями)
- 8. Чек-лист самопроверки знаний
1. Цели урока
- Понять, из чего состоит программа на C++ и как она выполняется.
- Научиться разделять интерфейс и реализацию (.h и .cpp).
- Писать понятные комментарии и готовить doxygen-документацию.
- Применять директивы препроцессора на практике.
- Собрать простой проект и избежать типичных ошибок.
2. Что такое программа на C/C++
Программа на C++ обычно состоит из нескольких файлов. Основные из них: файлы исходного кода (.cpp) и заголовочные файлы (.h). Выполнение программы начинается с функции main(), которая называется точкой входа.
main(). Если вы собираете библиотеку, main() обычно не нужен.#include <iostream>
int main() {
std::cout << "Hello, robotics!" << std::endl;
return 0;
}
Программа и запуск -- не одно и то же
Мини-словарь терминов
| Термин | Простое объяснение |
|---|---|
| Исходный код | Текст программы, который пишет человек (файлы .cpp и .h). |
| Компиляция | Перевод исходного кода в объектные файлы (.o/.obj). |
| Линковка | Сборка объектных файлов и библиотек в один исполняемый файл или библиотеку. |
| Интерфейс | То, как другие части программы "видят" ваш код (объявления функций/классов в .h). |
| Реализация | То, как именно это работает (тела функций в .cpp). |
| Препроцессор | Этап до компиляции, который подставляет файлы и заменяет макросы. |
3. Инструкции, выражения, функции, библиотеки
3.1. Инструкция (statement)
Инструкция (statement) -- минимальная завершенная команда в C++. Почти все инструкции заканчиваются точкой с запятой ;. Если провести аналогию с текстом, инструкция похожа на законченное предложение.
int a; // объявление переменной
a = 2; // присваивание
std::cout << a; // вывод в консоль
Важные слова из примера: int -- тип данных (целое число), a -- имя переменной, = -- оператор присваивания (кладет значение справа в переменную слева).
3.2. Выражение (expression)
Выражение (expression) -- вычисление, которое дает результат. Выражение может быть частью инструкции.
int a = 2 + 2; // выражение 2 + 2 дает 4
int b = a + 5; // выражение a + 5 дает 9
int c = (a + 3) * (b + 1); // выражение со скобками дает 70
3.3. Функция и main()
Функция -- именованный блок кода, который можно вызывать многократно. У функции есть параметры (входные данные) и возвращаемое значение (результат).
main() -- особая функция. Это точка входа программы: с нее начинается выполнение. Если вы пишете программу для ПК, у вас должен быть int main().
#include <iostream>
int sum(int x, int y)
{
return x + y;
}
int main()
{
std::cout << sum(2, 3) << std::endl;
return 0;
}
3.4. Библиотека
Библиотека -- это заранее написанный и проверенный код для повторного использования. В C++ часто подключают стандартную библиотеку. Например, <iostream> дает доступ к std::cout.
#include <iostream> // std::cout, std::cin, std::endl
std:: -- это пространство имен стандартной библиотеки. Оно нужно, чтобы имена не конфликтовали. Например, ваш проект тоже может иметь что-то с именем cout, но std::cout всегда означает объект из стандартной библиотеки.
3.5. Типичные ошибки
Нет точки с запятой
int a = 5 // ошибка: нужен ';'
int b = 6;
Почему так происходит: компилятор читает код слева направо. Если нет ;, он пытается "склеить" строки. В итоге сообщение об ошибке может указывать на следующую строку, хотя причина выше.
Нет функции main()
undefined reference to `main'
Это ошибка линковщика: он собрал объектные файлы, но не нашел точку входа. Исправление: добавьте int main() в один из .cpp файлов (если вы собираете исполняемый файл).
4. Комментарии и документирование кода
Комментарий -- это текст в исходном коде, который компилятор игнорирует. Комментарии пишут для людей: чтобы через неделю, месяц или год вы могли быстро понять логику, а коллега мог безопасно изменить код.
В C++ есть два вида комментариев:
// Однострочный комментарий
/* Многострочный
комментарий */
4.1. Как писать понятно
Хороший комментарий отвечает на вопрос "зачем" или "какой смысл", а не переписывает код. Если комментарий можно удалить и ничего не потеряется, он, скорее всего, лишний.
Плохо
// Присваиваем переменной day значение 0
day = 0;
Дублирует то, что и так видно.
Хорошо
// Датчик света показал темноту -- переходим в ночной режим
day = 0;
Объясняет причину действия.
4.2. Уровни "ЧТО" и "КАК"
- "ЧТО делает" -- над функцией/в начале файла: назначение, входы, выходы, ограничения.
- "КАК делается" -- перед сложным местом: кратко про алгоритм, математику, шаги, крайние случаи.
4.3. Doxygen
Doxygen -- инструмент, который создает документацию по коду на основе комментариев. Для doxygen используют блоки вида /** ... */ и теги @brief, @param, @return.
/**
* @brief Сумма двух целых
* @param a Первое число
* @param b Второе число
* @return a + b
*/
int sum(int a, int b) { return a + b; }
4.4. Ошибки комментирования
- Дублирование кода словами вместо объяснения смысла.
- Устаревшие комментарии после правок кода.
- Слишком много мелких комментариев справа от каждой строки. Лучше один комментарий над блоком.
5. Разделение на .h и .cpp, сборка проекта по шагам
5.1. Что хранить в .h
.h хранит объявления (declarations): прототипы функций, классы, константы. Задача заголовка -- сообщить компилятору и другим .cpp файлам, что существует такая функция/класс и как им пользоваться.
// math_utils.h
#pragma once
int sum(int a, int b);
5.2. Что хранить в .cpp
.cpp хранит определения (definitions): тела функций, реализацию алгоритмов. Здесь находится код, который реально выполняется.
// math_utils.cpp
#include "math_utils.h"
int sum(int a, int b) { return a + b; }
// main.cpp
#include <iostream>
#include "math_utils.h"
int main() {
std::cout << sum(3, 4) << std::endl; // 7
return 0;
}
5.3. Объявление и определение
- Объявление говорит: "объект существует и выглядит так".
- Определение дает "тело" объекта: реальный код функции или место в памяти под переменную.
Пример
// Объявление (прототип) в .h:
int sum(int a, int b);
// Определение (тело) в .cpp:
int sum(int a, int b) { return a + b; }
Важно: определение должно быть одно, иначе при линковке вы получите ошибку "multiple definition".
5.4. Компиляция и линковка
# Схема сборки (упрощенно)
# .cpp --компилятор--> .o --линковщик--> .exe
- Компилятор читает каждый .cpp файл и превращает его в объектный файл (.o).
- Линковщик объединяет объектные файлы и библиотеки в единый результат (например, .exe), а также проверяет, что для каждого вызова функции есть соответствующее определение.
5.5. Типичные ошибки
Определения в .h
// bad.h
int a = 5; // при подключении в несколько .cpp получите "multiple definition"
Почему это ошибка: заголовок может подключиться сразу в несколько .cpp файлов, и каждый получит свою копию определения. Исправление: в .h только объявления, определения держим в одном .cpp.
Забыли #include
'sum' was not declared in this scope -- компилятор не знает о функции без объявления из заголовка.
Исправление: подключите заголовок, где объявлена функция: #include "math_utils.h".
6. Директивы препроцессора: #include, #define, #ifdef, #pragma once
Препроцессор -- этап обработки текста до компиляции. Он: подключает файлы (#include), заменяет макросы (#define) и может включать или исключать части кода (#ifdef).
6.1. #include
#include подставляет содержимое файла в место директивы. Обычно системные заголовки подключают через <...>, а свои через "...".
#include <iostream> // системный заголовок
#include "math_utils.h" // ваш заголовок
6.2. #define
#define создает макрос -- правило текстовой замены. Препроцессор заменяет имя макроса на значение. В современных проектах для констант часто используют const и constexpr, но базовое понимание макросов важно, потому что они встречаются в коде и документации.
#define MY_NUMBER 9
std::cout << MY_NUMBER; // станет std::cout << 9;
6.3. #ifdef / #ifndef / #endif
Это условная компиляция. Она позволяет включать код только если макрос определен (или не определен). Частый сценарий: включить отладочный вывод только в debug.
#define PRINT_JOE
#ifdef PRINT_JOE
std::cout << "Joe" << std::endl;
#endif
6.4. #pragma once
#pragma once защищает заголовок от повторного включения. Это помогает избежать ошибок повторного объявления (redefinition).
// header.h
#pragma once
// объявления...
6.5. Ошибки и как их избежать
- Нет защиты заголовка -- дублирующиеся объявления при множественных include.
- Слишком много #define для "констант" -- лучше const/constexpr.
- Скрытая логика через #ifdef без комментариев -- сложно сопровождать.
7. Практика: типовые задачи (с решениями)
Базовые задачи
Задача 1: Hello, Robotics!
Вывести строку в консоль.
#include <iostream>
int main() {
std::cout << "Hello, Robotics!" << std::endl;
return 0;
}
Задача 2: Сумма
Напишите функцию и вызовите ее из main().
#include <iostream>
int sum(int a, int b) { return a + b; }
int main() {
std::cout << sum(10, 25) << std::endl; // 35
return 0;
}
Файлы и doxygen
Задача 3: Вынесите функцию в .h и .cpp
Сделайте отдельный модуль greet: заголовок и реализацию.
// greet.h
#pragma once
#include <string>
std::string greeting(const std::string& name);
// greet.cpp
#include "greet.h"
std::string greeting(const std::string& name) { return "Hello, " + name + "!"; }
Задача 4: Doxygen-блок
Добавьте doxygen-комментарий к функции.
/**
* @brief Умножает два целых числа
* @param a Первый множитель
* @param b Второй множитель
* @return Произведение a и b
*/
int multiply(int a, int b) { return a * b; }
Препроцессор и диагностика
Задача 5: Условная компиляция
Сделайте отладочный вывод, который включается через макрос.
#include <iostream>
// #define DEBUG_MODE
int main() {
#ifdef DEBUG_MODE
std::cout << "Debug mode" << std::endl;
#endif
std::cout << "Program running" << std::endl;
return 0;
}
Задача 6: Проверка #pragma once
Создайте заголовок constants.h и подключите его напрямую и через другой заголовок.
// constants.h
#pragma once
const int kRobotSpeed = 50;
// sensors.h
#pragma once
#include "constants.h"
// main.cpp
#include "constants.h"
#include "sensors.h"
#include <iostream>
int main() {
std::cout << kRobotSpeed << std::endl;
return 0;
}
8. Чек-лист самопроверки знаний
Отметьте пункты, которые вы действительно понимаете и можете применить без подсказок.
| +/- | Навык | Проверка |
|---|---|---|
| Структура программы | Могу перечислить: инструкции, выражения, функции, библиотеки | |
| Точка входа | Понимаю, что main() -- точка входа исполняемой программы | |
| Инструкция | Знаю, что инструкция обычно заканчивается ; и могу привести пример | |
| Выражение | Понимаю, что выражение дает результат и может быть частью инструкции | |
| .h и .cpp | Разношу объявления в .h и реализацию в .cpp | |
| Объявление и определение | Объясняю разницу и знаю, почему "multiple definition" -- ошибка линковки | |
| Комментарии | Пишу комментарии уровня "ЧТО" и "КАК", не дублирую очевидное | |
| Doxygen | Умею оформить @brief, @param, @return для функции | |
| Препроцессор | Понимаю роль #include и #define и когда применять #ifdef | |
| #pragma once | Ставлю защиту в каждый .h и понимаю, от каких ошибок она спасает |