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

Типы, переменные и инициализация

This lesson is not translated yet – the Russian original is below.

Тип в C++ отвечает не только на вопрос «что тут лежит», но и на вопрос «сколько это занимает байт». Второе в языках со сборщиком мусора обычно скрыто, здесь – видно и имеет последствия.

Объявление

int age = 19;

Тип, имя, значение. С этого момента age – целое число, и другим оно не станет: тип фиксируется при объявлении.

Объявить без значения можно, но результат опасен:

int count;              // значение – мусор, который лежал в этой памяти
std::cout << count;     // undefined behavior

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

Встроенные типы

Тип Что хранит Обычный размер Пример
bool истина или ложь 1 байт true
char один байт / символ 1 байт 'A'
int целое 4 байта 42
long long целое побольше 8 байт 9000000000LL
unsigned int целое без знака 4 байта 42u
float дробное 4 байта 3.14f
double дробное точнее 8 байт 3.14

Колонка «обычный размер» названа так не случайно. Стандарт фиксирует не размеры, а соотношения: char ≤ short ≤ int ≤ long ≤ long long. Размер конкретного типа на конкретной платформе узнают у компилятора:

std::cout << sizeof(int) << " " << sizeof(void*) << std::endl;   // 4 8 на типичной 64-битной системе

Когда размер существенен – формат файла, сетевой протокол – берут типы с шириной в имени:

#include <cstdint>

std::int32_t id;      // ровно 32 бита, где бы ни собирали
std::uint8_t flags;   // ровно 8 бит, без знака

У char в стандарте не определена даже знаковость: на x86 он обычно знаковый, на ARM – беззнаковый. Если байт нужен как число, пишут std::uint8_t или явное signed char.

Границы и переполнение

У каждого типа есть предел, и он ближе, чем кажется:

#include <limits>

std::cout << std::numeric_limits<int>::max() << std::endl;   // 2147483647

Два миллиарда – это меньше, чем миллисекунд в двух месяцах, и меньше, чем байт в трёхгигабайтном файле. Что произойдёт при выходе за границу, зависит от знаковости, и различие тут не академическое:

unsigned int u = 4294967295u;
u = u + 1;                   // 0 – определено стандартом, беззнаковые закольцовываются

int i = 2147483647;
i = i + 1;                   // undefined behavior

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

if (i + 1 < i) {             // проверка на переполнение
    std::cout << "переполнение";
}

Этот if оптимизатор вправе выкинуть целиком: раз переполнения не бывает, i + 1 всегда больше i, условие всегда ложно. Проверка, ради которой всё писалось, исчезает из машинного кода.

Проверять надо до операции, а не после: if (i > std::numeric_limits<int>::max() - 1). Либо брать типы пошире и не подходить к границе.

Дробные числа

double хранит число в двоичном виде, и десятичные дроби в него попадают неточно:

std::cout << (0.1 + 0.2 == 0.3);        // 0 – ложь
std::cout << (0.1 + 0.2);               // 0.3, потому что печать округляет

Это не особенность C++ – так устроен стандарт IEEE 754 во всех языках. Следствие одно и то же: дробные числа не сравнивают на равенство, сравнивают разность с допуском.

#include <cmath>

if (std::abs(a - b) < 1e-9) { ... }

Для денег double не годится – ошибка округления накапливается. В C++ нет встроенного десятичного типа, как decimal в C#: деньги считают в минимальных единицах целым числом (копейки в std::int64_t) либо берут стороннюю библиотеку.

Четыре способа инициализации

Здесь у C++ репутационная проблема: способов несколько, и различаются они не только видом.

int a = 5;        // копирующая
int b(5);         // прямая
int c{5};         // списком, с C++11
int d = {5};      // копирующая списком

Для встроенных типов разницы почти нет, но фигурные скобки дают одну важную гарантию: они запрещают сужающее преобразование.

int x = 3.9;      // 3, молча – дробная часть отброшена
int y{3.9};       // ошибка компиляции: сужение double до int

Второе – ровно то поведение, которого хочется: если потеря данных случайна, узнать о ней лучше при сборке.

Ещё фигурные скобки решают проблему, которая иначе выглядит как чёрная магия:

Widget w1(7);     // объект Widget с аргументом 7
Widget w2();      // не объект! это объявление функции без аргументов, возвращающей Widget
Widget w3{};      // объект, инициализированный по умолчанию

Практическое правило: по умолчанию фигурные скобки. Исключение – контейнеры, где список означает содержимое: std::vector<int> v(5) – это пять нулей, а std::vector<int> v{5} – это один элемент со значением 5.

auto

Когда тип очевиден из правой части, его разрешено не повторять:

auto age = 19;                       // int
auto ratio = 1.5;                    // double
auto name = std::string{"Anya"};     // std::string
auto it = values.begin();            // std::vector<int>::iterator – а вот это писать не хочется

auto – не «переменная без типа». Тип есть и зафиксирован, просто его вывел компилятор. Реальную пользу видно на последней строке: имена типов итераторов и лямбд бывают такими, что их физически невозможно набрать.

auto отбрасывает ссылки и const. auto x = obj.Name(); сделает копию, даже если метод вернул ссылку. Нужна ссылка – пишут явно: const auto& x = ....

const и constexpr

Значение, которое не должно меняться, помечают const:

const double kPi = 3.14159;
kPi = 3;                     // ошибка компиляции

В C++ на const держится многое: константные методы, константные ссылки, обещание «эту функцию я вам дам посмотреть, но менять не дам». Целый урок этому посвящён в третьем модуле.

constexpr – более сильное утверждение: значение вычислимо на этапе компиляции.

constexpr int kMaxUsers = 1000;
constexpr int kBufferSize = kMaxUsers * 64;   // посчитано компилятором, в машинном коде уже число

int users = ReadFromConfig();
const int limit = users * 2;       // const – можно: значение не поменяется
constexpr int bad = users * 2;     // ошибка: во время компиляции users неизвестен

Число, смысл которого понятен только автору, называют «магическим». if (attempts > 3) через месяц читается как загадка, if (attempts > kMaxAttempts) – как предложение. Константа стоит одной строки.

Как называть

Единого общепринятого стиля в C++ нет – их несколько, и они несовместимы. В стандартной библиотеке snake_case, в Google Style PascalCase для функций, в Qt camelCase. Важно не какой выбрать, а не смешивать в одном проекте.

В этом курсе: snake_case для переменных, PascalCase для функций и типов, префикс k для констант, суффикс _ для полей класса.

Имя должно объяснять смысл: int d заставляет искать объявление, int days_left – нет. Исключение одно – счётчик цикла i, который живёт три строки.

Имена, начинающиеся с подчёркивания и заглавной буквы (_Value), а также содержащие двойное подчёркивание, зарезервированы за реализацией. Компилироваться будет, но может конфликтовать с внутренностями стандартной библиотеки.