У каждой переменной в C есть тип, который вы выбираете при объявлении и который остаётся с ней навсегда. Тип решает три вещи: сколько байт занимает переменная, как эти байты интерпретируются и какие операции над ней осмысленны.
Список типов в C короткий. Вся сложность — в модификаторах.
Четыре базовых типа
charзанимает один байт. Используется для отдельных символов ('A'), для байтов строки и иногда как крошечное целое.int— рабочая лошадка среди целочисленных типов. Счётчики циклов, размеры, идентификаторы.floatиdoubleхранят вещественные числа. Уdoubleпримерно вдвое больше точность.voidв некотором смысле четвёртый тип, но он означает «нет значения» — переменную типаvoidобъявить нельзя, его используют только как тип возврата функции, как пустой список параметров или как нетипизированный указатель.
Обратите внимание на суффикс f в 3.14f. Без него литерал имеет тип double, и присваивание его переменной float молча теряет точность. Некоторые компиляторы об этом предупреждают.
Модификаторы: short, long, unsigned
У базовых целочисленных типов есть модификаторы, меняющие размер или знак.
Каждому целочисленному типу нужен свой спецификатор формата: %hd для short, %d для int, %ld для long, %lld для long long, %u для unsigned. Использовать не тот — это неопределённое поведение, а не ошибка округления.
unsigned убирает знаковый бит и удваивает положительный диапазон. unsigned int хранит значения от 0 до примерно 4,3 млрд вместо диапазона от -2,1 до +2,1 млрд. Это правильный выбор для того, что действительно не может быть отрицательным — размеров, количества байт, битовых масок, — и ловушка для всего, что может быть уменьшено ниже нуля.
signed подразумевается по умолчанию для int, short, long и long long, поэтому его почти никогда не пишут. Единственное место, где это важно, — char: знаковый ли простой char или беззнаковый, определяется реализацией, поэтому пишите signed char или unsigned char, когда знак имеет значение.
Размеры: что вы получаете на самом деле
Стандарт C задаёт минимумы, а не точные размеры. На любой современной 64-битной машине с Linux, macOS или Windows вы увидите вот это:
| Тип | Типичный размер | Типичный диапазон |
|---|---|---|
char | 1 байт | от -128 до 127 (или от 0 до 255) |
short | 2 байта | от -32 768 до 32 767 |
int | 4 байта | от -2 147 483 648 до 2 147 483 647 |
long | 8 байт (4 в Windows) | примерно ±9,2 квинтиллиона |
long long | 8 байт | примерно ±9,2 квинтиллиона |
float | 4 байта | ~7 значащих цифр |
double | 8 байт | ~15 значащих цифр |
long double | 16 байт (зависит от платформы) | больше, чем у double |
Строка про long — та самая, на которой попадаются: это 8 байт в Linux и macOS и 4 байта в 64-битной Windows. Код, который считает, что long вмещает 64-битное значение, непереносим. Используйте long long или типы точной ширины из stdint.h (int32_t, uint64_t), когда размер — часть требований.
sizeof: спросите у компилятора
Никогда не угадывайте размер — измеряйте его:
sizeof — это оператор, а не функция, и вычисляется он на этапе компиляции. Он даёт значение типа size_t, которое печатается через %zu.
sizeof(char) гарантированно равен ровно 1 — это и есть определение байта в C. Всё остальное измеряется относительно него.
limits.h и float.h
Точные диапазоны для вашего компилятора доступны в виде именованных констант:
Именно с этими значениями нужно сравнивать, когда вы хотите понять, не переполнится ли операция. Проверка if (a > INT_MAX - b) перед вычислением a + b — способ обнаружить переполнение до того, как оно случится, а это важно, потому что знаковое переполнение нельзя обнаружить постфактум.
Переполнение целых чисел
Что произойдёт, когда значение выйдет за пределы диапазона своего типа, целиком зависит от знака.
Беззнаковое переполнение определено: значение сворачивается по модулю 2^N.
Второй случай — настоящий источник багов. Цикл вида for (unsigned i = n - 1; i >= 0; i--) никогда не закончится, потому что беззнаковое значение всегда >= 0.
Знаковое переполнение — неопределённое поведение. Не «сворачивается», а именно неопределённое. Компилятору разрешено считать, что оно никогда не случается, и оптимизировать исходя из этого, а значит, проверку на переполнение, написанную после факта, он может удалить:
int sum = a + b;
if (sum < a) { /* компилятор вправе полностью убрать эту проверку */ }
Проверяйте заранее, используя пределы:
if (b > 0 && a > INT_MAX - b) {
/* a + b переполнится - обработайте это */
}
Точность чисел с плавающей точкой
float и double хранят числа в двоичном виде, а у большинства десятичных дробей нет точной двоичной записи — ровно так же, как у 1/3 нет точной десятичной.
Отсюда правило: никогда не сравнивайте числа с плавающей точкой через ==. Сравнивайте абсолютную разницу с небольшим допуском.
И никогда не используйте плавающую точку для денег. Храните копейки как целое: long long копеек точен там, где double рублей — нет.
Как выбрать тип
Короткий список решений, покрывающий большую часть кода:
- Целые числа:
int, если нет причины взять другое. Это тип, который процессор обрабатывает эффективнее всего и вокруг которого построены все правила арифметики. - Что-то больше 2 миллиардов:
long longилиint64_tизstdint.h. - Размеры, длины, индексы массивов из
sizeofилиstrlen:size_t. Он беззнаковый и гарантированно вмещает размер любого объекта. - Дробные числа:
double. Беритеfloatтолько чтобы вдвое сократить память в больших массивах или на встраиваемом железе без блока двойной точности. - Отдельные символы и сырые байты:
charдля текста,unsigned charдля двоичных данных. - Истина/ложь:
boolизstdbool.h— см. логический тип в C. - Точная разрядность (форматы файлов, сетевые протоколы, регистры устройств):
stdint.h—uint8_t,int16_t,uint32_tи так далее.
Смешивание типов
Когда вы соединяете в одном выражении два разных типа, C незаметно преобразует их перед вычислением. Обычно это удобно, но иногда катастрофично:
Первое — целочисленное деление: оба операнда типа int, поэтому результат тоже int, а дробная часть отбрасывается. Третье хуже: при сравнении знакового значения с беззнаковым знаковое преобразуется в беззнаковое, и -1 превращается в огромное положительное число.
Эти правила преобразования и то, как взять их под контроль с помощью явных приведений, — тема страницы о приведении типов в C.
Часто задаваемые вопросы
Какие базовые типы данных есть в C?
Четыре базовых типа: char для отдельных символов и байтов, int для целых чисел, float и double для дробных. Модификаторы меняют их размер и знак — short, long, long long, signed и unsigned — и дают полный набор.
Сколько байт занимает int в C?
Почти всегда 4 байта (32 бита) на современных настольных и серверных системах, что даёт диапазон примерно от -2,1 млрд до 2,1 млрд. Стандарт гарантирует лишь 2 байта минимум, и 16-битные микроконтроллеры действительно используют 2. Если нужно знать точно, используйте sizeof(int).
В чём разница между float и double в C?
float — 4 байта и около 7 значащих десятичных цифр; double — 8 байт и около 15. double используется по умолчанию для литералов с плавающей точкой и математических функций, и если вы не храните миллионы значений и не пишете под встраиваемый чип, правильный выбор — double.
Что происходит при переполнении int в C?
Для знакового int переполнение — это неопределённое поведение: компилятор может свернуть значение, насытить его или вовсе выбросить вашу проверку. Для беззнакового int всё определено строго: значение сворачивается по модулю 2^N, поэтому UINT_MAX + 1 равно 0. Никогда не полагайтесь на знаковое переполнение.