Типы данных в C: int, char, float, double и их размеры

Типы данных в C: int, char, float, double и их размеры Полезное

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

Ниже разберем встроенные арифметические типы языка C: целочисленные (char, short, int, long, long long), модификаторы знака (signed/unsigned), вещественные (float, double, long double), а также void и _Bool. Посмотрим, почему размеры зависят от платформы и что происходит при приведении и переполнении. Речь про типы языка C, а не про абстрактные структуры данных.

Целочисленные типы и их размеры

Базовые целочисленные типы упорядочены по неубыванию размера: char <= short <= int <= long <= long long. Стандарт задает не точные размеры, а минимальные гарантии — конкретные величины выбирает реализация (компилятор плюс платформа).

Ключевой факт: точные размеры платформозависимы. Ходовое «int — это 4 байта» верно на большинстве настольных систем, но стандарт гарантирует лишь минимум 16 бит. В таблице — минимумы стандарта и типичные значения на 64-битных Linux и macOS (модель LP64).

Тип Минимум по стандарту Типично (LP64) Типичный диапазон printf
char 8 бит 1 байт знаковость платформозависима %c
signed char 8 бит 1 байт -128..127 %hhd
unsigned char 8 бит 1 байт 0..255 %hhu
short 16 бит 2 байта -32768..32767 %hd
unsigned short 16 бит 2 байта 0..65535 %hu
int 16 бит 4 байта -2147483648..2147483647 %d
unsigned int 16 бит 4 байта 0..4294967295 %u
long 32 бита 8 байт +-9.2e18 %ld
unsigned long 32 бита 8 байт 0..1.8e19 %lu
long long 64 бита 8 байт +-9.2e18 %lld
unsigned long long 64 бита 8 байт 0..1.8e19 %llu

Диапазоны в колонке «типично» приведены для дополнительного кода (two’s complement) — так знаковые целые представлены на массовых платформах. Минимальные гарантии стандарта чуть уже (для int это -32767..32767): исторически допускались и другие представления знака.

Строка long показательна: на 64-битных Linux и macOS это 8 байт, а на 64-битной Windows (модель LLP64) — 4 байта. Размер типа нельзя «запоминать числом» без привязки к платформе; когда нужен точно 64-битный тип везде, берут long long или int64_t из <stdint.h>.

Модификаторы signed и unsigned

Модификаторы задают, отведен ли старший бит под знак. signed хранит и отрицательные значения, unsigned — только неотрицательные, зато верхняя граница примерно вдвое больше.

Тонкость про char. В C это три РАЗНЫХ типа: char, signed char и unsigned char. Обычный char совпадает по значениям с одним из знаковых вариантов, но с каким — решает платформа (на x86 обычно знаковый, на части ARM — беззнаковый). Поэтому для байтовой арифметики пишут явно signed char или unsigned char, а char оставляют для текстовых символов. Слово int можно опускать: unsigned значит unsigned int, а long int и long — один тип.

sizeof: размер на своей платформе

Оператор sizeof возвращает размер типа или объекта в байтах. По определению sizeof(char) всегда равен 1, а размер байта в битах хранится в макросе CHAR_BIT (почти везде 8). Результат имеет тип size_t и печатается через %zu:

#include <stdio.h>

int main(void) {
    printf("char       %zu\n", sizeof(char));
    printf("int        %zu\n", sizeof(int));
    printf("long       %zu\n", sizeof(long));
    printf("long long  %zu\n", sizeof(long long));
    printf("double     %zu\n", sizeof(double));
    return 0;
}

На типичной 64-битной Linux или macOS (LP64) вывод такой:

char       1
int        4
long       8
long long  8
double     8

На 64-битной Windows та же программа напечатает для long значение 4 — это не ошибка, а другая модель данных (LLP64). Правило: если размер важен, проверяйте sizeof или берите типы точной ширины.

Вещественные типы: float, double, long double

Вещественные типы хранят числа с плавающей запятой — с дробной частью и в широком диапазоне порядков. Их три: float (одинарная точность), double (двойная) и long double (расширенная).

На большинстве платформ float — это 32-битный формат IEEE 754 (около 7 значащих десятичных цифр), а double — 64-битный (около 15-16 цифр). Стандарт C, однако, не требует именно IEEE 754, а long double бывает разным: 80-битным на x86 или совпадающим с double. Это упрощенная модель — точные параметры берут из <float.h>.

Следствие двоичного представления: значения вроде 0.1 хранятся приближенно, поэтому сравнивать вещественные на строгое равенство (a == b) ненадежно — сравнивают по допуску. В printf и для float, и для double используют %f.

void и _Bool

void — это «отсутствие типа». Он служит тремя способами: функция без возврата (void f(void)), функция без параметров (int main(void)) и обобщенный указатель void *, который может указывать на данные любого типа.

_Bool появился в C99 и хранит логическое значение: любое присваивание, отличное от нуля, сохраняется как 1, ноль остается 0. Через <stdbool.h> доступны привычные имена bool, true, false. Например, bool flag = 5; сохранит в flag единицу.

Точные размеры и границы: stdint.h, limits.h, float.h

Когда программа должна вести себя одинаково на разных платформах (частый случай во встраиваемых системах и сетевых протоколах), берут типы точной ширины из <stdint.h>: int8_t, int16_t, int32_t, int64_t и беззнаковые uint8_t..uint64_t. Их размер фиксирован именем: int32_t — всегда ровно 32 бита.

Границы значений не заучивают, а читают из заголовков: целочисленные — из <limits.h>, вещественные — из <float.h>.

#include <stdio.h>
#include <limits.h>

int main(void) {
    printf("CHAR_BIT = %d\n", CHAR_BIT);
    printf("INT_MIN  = %d\n", INT_MIN);
    printf("INT_MAX  = %d\n", INT_MAX);
    printf("UINT_MAX = %u\n", UINT_MAX);
    return 0;
}

Типичный вывод при 32-битном int:

CHAR_BIT = 8
INT_MIN  = -2147483648
INT_MAX  = 2147483647
UINT_MAX = 4294967295

Приведение типов и переполнение

Когда в выражении встречаются разные типы, C приводит их к общему по правилам обычных арифметических преобразований. Частый сюрприз новичка — целочисленное деление: если оба операнда целые, дробная часть отбрасывается до присваивания.

#include <stdio.h>

int main(void) {
    int a = 7, b = 2;
    printf("%d\n", a / b);         // 3 - целочисленное деление
    printf("%f\n", (double)a / b); // 3.500000 - приведение к double
    return 0;
}

Вывод:

3
3.500000

Явное приведение (double)a делает один операнд вещественным, и деление становится вещественным. Это и есть «каст» — управляемая смена типа.

Переполнение зависит от знаковости. Для беззнаковых типов оно определено — результат берется по модулю 2^N (арифметика «по кругу»).

#include <stdio.h>

int main(void) {
    unsigned char u = 255;  // максимум для unsigned char
    u = u + 1;              // перенос по модулю 256
    printf("%u\n", u);      // 0
    return 0;
}

Вывод:

0

А переполнение ЗНАКОВОГО целого (например, INT_MAX + 1) — это неопределенное поведение (undefined behavior): стандарт не гарантирует результата, и полагаться на «оно просто завернется в минус» нельзя — компилятор с оптимизацией может повести себя неожиданно. Поэтому перед сложением проверяют границы через INT_MAX/INT_MIN из <limits.h> либо осознанно берут беззнаковый тип там, где перенос по модулю — нужное поведение.

Выводы

  • Тип в C задает и размер памяти, и трактовку битов; стандарт фиксирует минимальные гарантии, а точные размеры выбирает платформа.
  • «int равен 4 байтам» — лишь типичный случай; гарантирован минимум 16 бит, а long на Windows и Linux отличается. Проверяйте sizeof или берите int32_t, int64_t из <stdint.h>.
  • signed/unsigned управляют знаком; у char знаковость платформозависима, поэтому для байтовой арифметики пишут signed char или unsigned char.
  • Границы и параметры точности не заучивают, а читают из <limits.h> и <float.h>.
  • Целочисленное деление отбрасывает дробь; беззнаковое переполнение определено (по модулю 2^N), знаковое — неопределенное поведение.

Где применяется / связь с практикой

Понимание системы типов C — база для низкоуровневого программирования: работы с памятью, битовыми операциями, сетевыми протоколами и драйверами, где размер каждого поля важен побайтно. Ошибка в выборе типа (знаковый вместо беззнакового, узкий тип для большого счетчика) — классический источник багов, которые всплывают только на больших данных.

Освойте тему на практике

Разобрать эти механики с компилятором и отладчиком удобнее на структурированном курсе. На курсе «Программист C» типы, память и приведения отрабатываются на реальных задачах. Посмотреть формат и уровень занятий можно на открытых уроках Otus — это бесплатно и помогает выбрать направление.

FAQ

Почему sizeof(int) на моей машине равен 4, а где-то пишут 2?
Стандарт гарантирует для int только минимум 16 бит (2 байта). На старых и микроконтроллерных платформах int бывает 2 байта, на современных настольных — обычно 4. Точное значение всегда покажет sizeof на вашей системе.

Чем int32_t лучше обычного int?
int32_t гарантированно ровно 32 бита там, где он определен, а int может быть 16 или 32 бита. Когда важен точный размер поля (формат файла, сетевой пакет, регистр устройства), берут типы из <stdint.h>; для обычных счетчиков достаточно int.

Что будет, если сложить два больших int и выйти за диапазон?
Для знакового int это неопределенное поведение — результат не гарантирован. Для беззнаковых типов сложение определено и идет по модулю 2^N. Если перенос нежелателен, проверяйте границы через INT_MAX или берите более широкий тип.

OTUS Журнал
Бесплатные открытые уроки (поп-ап)