Программа курса← Программирование на языке C++
1Введение в язык C++
2Как выполняются программы на C++
3Структуры и классы
4Объектно-ориентированное программирование
5Дополнительные возможности C++
6Шаблоны
7Методические материалы

Структура кода на C++

Программа на C++ – это не один файл, а набор файлов, которые собираются вместе. Прежде чем разбирать синтаксис, разберём, из каких частей вообще состоит код и почему их именно столько.

Минимальная программа

#include <iostream>

int main() {
    std::cout << "Hello" << std::endl;

    return 0;
}

Четыре строки, и в каждой есть что объяснить.

main: точка входа

Выполнение начинается с функции main. Не с первой строки файла, не с первого класса – именно с неё. Функция обязана быть ровно одна на всю программу, и называться должна именно так.

int main() { ... }                             // без аргументов
int main(int argc, char* argv[]) { ... }       // с аргументами командной строки

Возвращаемый тип – int, и это не формальность: число уходит операционной системе как код завершения. Ноль – успех, всё остальное – ошибка. На этом построены скрипты: ./program && echo ok напечатает ok только если программа вернула ноль.

У main одна привилегия: return можно не писать, компилятор сам подставит return 0. Любая другая функция с типом int обязана что-то вернуть явно.

#include: это подстановка текста

Строки, начинающиеся с #, – это команды препроцессора, отдельного инструмента, который работает до компилятора. Он не знает синтаксиса C++ и умеет ровно три вещи: подставлять текст, разворачивать макросы и вырезать куски по условию.

#include <iostream> означает буквально: «возьми файл iostream и вставь его содержимое сюда, вместо этой строки». После препроцессора никакой строки с #include не остаётся – на её месте несколько тысяч строк из стандартной библиотеки.

Две формы записи различаются тем, где искать файл:

#include <iostream>    // системные пути: стандартная библиотека и установленные библиотеки
#include "point.h"     // сначала рядом с текущим файлом, потом системные пути

Раз это подстановка, порядок #include влияет на результат, а один и тот же файл легко подставить дважды – через два разных заголовка. От повторной подстановки защищаются: об этом ниже.

Объявление и определение

Это различие в C++ фундаментально, и без него не читаются сообщения об ошибках.

Объявление говорит, как сущность выглядит: имя, типы аргументов, тип результата. Тела нет.

double Distance(Point a, Point b);   // объявление: заканчивается точкой с запятой

Определение даёт тело – то, что функция на самом деле делает.

double Distance(Point a, Point b) {  // определение
    return std::sqrt(std::pow(b.x - a.x, 2) + std::pow(b.y - a.y, 2));
}

Компилятору для вызова достаточно объявления: чтобы сгенерировать вызов, нужно знать сигнатуру, а не содержимое. Тело потом найдёт компоновщик. Отсюда два разных сообщения об ошибке, которые путают в первый месяц:

Сообщение Кто выдал Что случилось
'Distance' was not declared in this scope компилятор нет объявления – он не знает, как вызывать
undefined reference to 'Distance' компоновщик объявление есть, тела нигде нет

Зачем два файла

Отсюда сложившееся разделение: объявления кладут в заголовочный файл (.h или .hpp), определения – в файл реализации (.cpp).

// point.h – что умеет модуль
struct Point {
    double x = 0;
    double y = 0;
};

double Distance(Point a, Point b);
// point.cpp – как он это делает
#include "point.h"
#include <cmath>

double Distance(Point a, Point b) {
    const double dx = b.x - a.x;
    const double dy = b.y - a.y;

    return std::sqrt(dx * dx + dy * dy);
}
// main.cpp – кто этим пользуется
#include "point.h"
#include <iostream>

int main() {
    std::cout << Distance({0, 0}, {3, 4}) << std::endl;
}

Пользователю модуля достаточно point.h – он подставляется дёшево. Если бы #include подставлял реализацию целиком, каждая правка внутри функции заставляла бы пересобирать всё, что её вызывает.

Проверка на «что положить в заголовок»: туда идёт то, что нужно знать вызывающему коду. Вспомогательная функция, которой пользуется только point.cpp, в заголовке не нужна.

Защита от повторного включения

point.h подставится дважды, если два заголовка включают его каждый по-своему. Компилятор увидит два определения struct Point и откажется собирать. Защищаются одной строкой:

// point.h
#pragma once

struct Point { ... };

#pragma once говорит препроцессору: этот файл подставлять один раз за единицу трансляции. Формально в стандарте его нет, фактически он работает во всех компиляторах, которыми пользуются.

Классический вариант того же самого – include guard, он встречается в любом старом коде:

#ifndef POINT_H
#define POINT_H

struct Point { ... };

#endif  // POINT_H

При первой подстановке POINT_H не определён – содержимое проходит, макрос определяется. При второй условие ложно, и препроцессор вырезает весь блок.

Пространства имён

Стандартная библиотека живёт в пространстве имён std, поэтому пишут std::cout, std::string, std::vector. Двоеточия – это оператор разрешения области видимости.

Импортировать всё пространство целиком можно, но не нужно:

using namespace std;   // так делать не стоит

В std несколько тысяч имён, среди них count, distance, size, data. Втащив их все, вы рискуете столкнуться с собственными. Правильнее импортировать точечно или писать std:: – четыре лишних символа читаются лучше, чем ошибка неоднозначности на двадцать строк.

using std::cout;   // если очень мешает

Свои имена группируют так же:

namespace geometry {

double Distance(Point a, Point b);

}  // namespace geometry

Комментарии

// однострочный, до конца строки

/* многострочный,
   вкладывать нельзя */

Многострочные комментарии не вкладываются: первый же */ закрывает всё. Закомментировать кусок кода, внутри которого уже есть /* */, не выйдет – для этого пользуются #if 0 ... #endif или просто выделяют блок и добавляют // редактором.