Тип в 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), а также содержащие двойное подчёркивание, зарезервированы за реализацией. Компилироваться будет, но может конфликтовать с внутренностями стандартной библиотеки.