Введение
Весь предыдущий план статей предполагал базовое знакомство с синтаксисом C/C++ — настало время вернуться к самым основам и разобрать их, начиная с того, как устроена программа на C/C++ на самом фундаментальном уровне: функция main как точка входа, роль заголовочных файлов, и то, что происходит до того, как выполнится первая строка пользовательского кода.
Концепция
Каждая программа на C/C++ обязана содержать ровно одну функцию main() — именно с неё начинается выполнение пользовательского кода (хотя до её вызова операционная система и системная библиотека выполняют собственную, не видимую разработчику инициализацию). main() может принимать аргументы командной строки (int argc, char *argv[]) или не принимать их вовсе, и обычно возвращает int, интерпретируемый операционной системой как код завершения процесса (0 традиционно означает успешное завершение, любое другое значение — некоторую форму ошибки). Заголовочные файлы (.h/.hpp) содержат объявления функций, классов и типов, и директива #include (часть препроцессора) текстуально вставляет содержимое указанного файла в точку включения перед началом реальной компиляции.
Пример кода
// Минимальная программа на C++ — структура, общая для абсолютно любой программы
#include <iostream> // заголовочный файл стандартной библиотеки, объявляющий std::cout и аналогичные сущности
int main()
{
std::cout << "Привет, мир!" << std::endl;
return 0; // явный код завершения — 0 означает успешное выполнение программы
}
// main с аргументами командной строки
#include <iostream>
int main(int argc, char *argv[])
{
std::cout << "Программа запущена с " << argc << " аргументами:" << std::endl;
for (int i = 0; i < argc; ++i) {
std::cout << " argv[" << i << "] = " << argv[i] << std::endl;
// argv[0] традиционно — само имя запущенной программы, а не первый "реальный" аргумент
}
return 0;
}
// Пользовательский заголовочный файл — math_utils.h
#ifndef MATH_UTILS_H // include guard — защита от множественного включения
#define MATH_UTILS_H
int square(int value); // ОБЪЯВЛЕНИЕ функции — компилятор узнаёт сигнатуру, но не "тело" функции
#endif
// math_utils.cpp — соответствующее ОПРЕДЕЛЕНИЕ
#include "math_utils.h"
int square(int value)
{
return value * value;
}
// main.cpp — использование функции из отдельного файла через её заголовок
#include "math_utils.h"
#include <iostream>
int main()
{
std::cout << square(5) << std::endl; // линковщик находит реальное определение
return 0;
}
Пояснения к коду
Минимальная программа показывает абсолютный, неснижаемый минимум — без main() компоновщик (линковщик) не сможет создать исполняемый файл, поскольку операционная система не будет знать, с какой именно функции начинать выполнение процесса. Пример с argc/argv показывает, как программа получает доступ к аргументам, переданным при её запуске из командной строки — argc (count) указывает количество аргументов, а argv (vector) — массив C-строк с самими значениями этих аргументов, включая само имя запускаемой программы как нулевой элемент. Разделение math_utils.h/math_utils.cpp показывает фундаментальный принцип организации C/C++ кода — заголовочный файл объявляет интерфейс (что доступно для использования и с какой сигнатурой), тогда как файл реализации (.cpp) содержит реальное тело функции, и любой другой файл, желающий использовать square(), подключает именно заголовочный файл, не нуждаясь в доступе к исходному коду самой реализации.
Подводные камни
- Возврат из
main()без явногоreturnоператора — стандарт C++ (хотя не стандарт чистого C) допускает отсутствие явногоreturnв самом конце функцииmain(), неявно подразумеваяreturn 0, но это исключение применимо только кmain()и не работает для любых других функций, возвращающих не-voidтип, что может ввести в заблуждение начинающего разработчика, ожидающего такого же поведения для собственных функций. - Путаница между объявлением и определением функции, особенно при первом знакомстве с разделением на заголовочные файлы и файлы реализации — объявление (
int square(int value);с точкой запятой вместо тела функции) сообщает компилятору только сигнатуру, тогда как реальный код, выполняющий вычисление, находится в определении, и попытка вызвать функцию, для которой существует только объявление без соответствующего определения где-либо в проекте, приводит к ошибке линковки , а не ошибке компиляции, что может смутить разработчика, ожидающего более раннего обнаружения проблемы. - Игнорирование кода возврата
main()как значимой, проверяемой информации — для скриптов, CI-пайплайнов и других автоматизированных систем, вызывающих программу, конкретное значение, возвращённоеmain(), часто используется для определения успешности выполнения программно (через проверку кода завершения процесса операционной системой), и игнорирование этого механизма (всегда возвращая 0 независимо от реального результата работы программы, или не задумываясь о семантике разных кодов ошибки) лишает вызывающий код важной диагностической информации. - Подключение избыточного количества заголовочных файлов «на всякий случай», не используемых реально в конкретном файле — это не только засоряет код неинформативными, лишними включениями, но и может замедлять компиляцию (особенно для крупных, «тяжёлых» заголовочных файлов), и хорошая практика — подключать только те заголовочные файлы, чьё содержимое реально используется непосредственно в данном конкретном файле.