Переменные и базовые типы данных

Урок разбирает переменные и базовые типы C: int, char, float, double, как они хранятся в памяти и почему в C размер типа — это не абстракция, а конкретные байты.
В C переменная — это именованный участок памяти фиксированного размера. Тип переменной определяет и сколько байтов она занимает, и как эти байты интерпретируются.

В отличие от Python, где переменная — это просто метка, способная указывать на что угодно, в C переменная жёстко привязана к типу. Когда вы пишете int x;, компилятор резервирует под x ровно столько байтов, сколько занимает целое число на вашей платформе (обычно 4 байта), и навсегда запоминает: здесь лежит целое. Поменять тип нельзя.

#include <stdio.h>

int main(void) {
    int age = 25;            // целое число
    float price = 19.99f;    // вещественное (одинарная точность)
    double pi = 3.14159265;  // вещественное (двойная точность)
    char grade = 'A';        // один символ (хранится как число!)

    printf("Возраст: %d\n", age);
    printf("Цена: %.2f\n", price);
    printf("Оценка: %c (код %d)\n", grade, grade);
    return 0;
}

Обратите внимание: char хранит символ, но физически это маленькое целое число — код символа в таблице ASCII. Буква 'A' — это число 65. Поэтому grade можно вывести и как символ (%c), и как число (%d).

Как работает под капотом

Каждый тип занимает определённое число байтов. Посмотрим, как переменные раскладываются в памяти (размеры типичны для 64-битной системы):

Имя      Тип      Размер   Память (байты)
-------  -------  -------  ------------------------
grade    char     1 байт   [65]
age      int      4 байта  [00][00][00][25]
price    float    4 байта  [.. .. .. ..]
pi       double   8 байт   [.. .. .. .. .. .. .. ..]

Размер типа можно узнать оператором sizeof. Именно размер задаёт диапазон значений: int в 4 байта (32 бита) вмещает примерно от минус двух до плюс двух миллиардов. Если число выйдет за этот предел, произойдёт переполнение — значение «завернётся» и станет некорректным. Это одна из коварных особенностей C.

printf("int   занимает %zu байт\n", sizeof(int));
printf("char  занимает %zu байт\n", sizeof(char));
printf("double занимает %zu байт\n", sizeof(double));

Частые ошибки

  • Использование неинициализированной переменной. int x; без присваивания содержит мусор — то, что случайно лежало в этой памяти. Чтение даст непредсказуемый результат.
  • Путаница = и ==. Первое — присваивание, второе — сравнение. if (x = 5) вместо if (x == 5) — классическая ошибка.
  • Переполнение целого. Прибавление к максимальному int единицы даёт большое отрицательное число, а не ошибку.
  • Сравнение float на точное равенство. Из-за двоичного представления 0.1 + 0.2 не равно ровно 0.3.

Best practices

  • Инициализируйте переменные при объявлении: int count = 0;. Это убирает целый класс багов.
  • Выбирайте тип под смысл: для денег и точных расчётов — double, для счётчиков — int, для размеров и индексов — size_t.
  • Для целых фиксированного размера в современном C используйте <stdint.h>: int32_t, uint8_t — их размер одинаков на всех платформах.

Идею «у каждого типа свой размер и диапазон» удобно прочувствовать на Python, где мы вручную смоделируем переполнение 8-битного беззнакового числа.

# Эмулируем uint8_t: диапазон 0..255, переполнение "заворачивается"
def add_uint8(a, b):
    return (a + b) % 256

print("250 + 10 в uint8_t =", add_uint8(250, 10))  # не 260, а 4!
print("255 + 1   в uint8_t =", add_uint8(255, 1))   # заворот в 0

Та же логика на Python ▶ — в реальном C переполнение происходит само, без всякого % 256, и часто незаметно. Понимание границ типа спасает от таких сюрпризов.

Знаковость и беззнаковость

У целочисленных типов есть важное измерение, которое легко упустить: знаковость. int хранит и отрицательные, и положительные числа, а unsigned int — только неотрицательные, зато вдвое больший положительный диапазон. Смешивать их в одном выражении опасно: при сравнении signed и unsigned компилятор молча приводит знаковое к беззнаковому, и отрицательное число превращается в огромное положительное. Классическая ловушка — цикл for (unsigned i = n; i >= 0; i--): он никогда не закончится, потому что unsigned не бывает меньше нуля. Поэтому для счётчиков, которые могут стать отрицательными, берут знаковый тип, а для размеров и индексов — size_t (он беззнаковый и точно вмещает любой размер объекта в памяти).

Итоги

Переменная в C — это типизированный участок памяти фиксированного размера. Базовые типы: int (целые), float и double (вещественные), char (символ-число). Размер типа определяет диапазон значений и узнаётся через sizeof. Главные опасности — неинициализированные переменные и переполнение; защита от них — инициализация при объявлении и продуманный выбор типа.

Проверьте себя
1. Как физически хранится переменная типа char со значением 'A'?
AКак строка из одного символа
BКак число — код символа в таблице ASCII (для 'A' это 65)
CКак указатель на букву
DКак тип double
2. Почему опасно использовать неинициализированную переменную int x; в C?
AПрограмма не скомпилируется
Bx всегда равен нулю
Cx содержит случайный мусор из памяти, и чтение даёт непредсказуемый результат
Dx автоматически станет указателем