This lesson is not translated yet – the Russian original is below.
Программа на 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 или просто выделяют блок и добавляют // редактором.