Добавить в корзинуПозвонить
Найти в Дзене
Информатика

Компилятор C не читает мысли: почему 'A' ≠ "A" и как это ломает код

Привет! Сегодня разберём тему, которая звучит скучно — «присваивание и литералы в C». Но на самом деле это фундамент, без которого ты не поймёшь, как работают игровые движки, как шифруются твои сообщения в Telegram и почему твой код иногда ведёт себя как будто у него своя жизнь. Поехали 🚀 Все знают, что = это присваивание. Написал x = 5; — и переменная получила значение. Элементарно, Ватсон. Но вот факт, который взрывает мозг: в C присваивание — это операция, которая возвращает значение. Не просто записывает число в память, а ещё и отдаёт его дальше по цепочке. Поэтому такой код работает: int a, b, c;
a = b = c = 42; Компилятор читает справа налево: Это как эстафета 🏃‍♂️ — каждый участник передаёт палочку дальше, но при этом ещё и пробежал свой этап. Зачем это знать? Потому что без этого понимания ты не поймёшь, почему можно писать while ((c = getchar()) != EOF) — тут присваивание внутри условия, и оно работает именно благодаря тому, что возвращает значение. int x = 10; // инициализ
Оглавление

Привет! Сегодня разберём тему, которая звучит скучно — «присваивание и литералы в C». Но на самом деле это фундамент, без которого ты не поймёшь, как работают игровые движки, как шифруются твои сообщения в Telegram и почему твой код иногда ведёт себя как будто у него своя жизнь.

Поехали 🚀

Присваивание — это не то, что ты думаешь

Все знают, что = это присваивание. Написал x = 5; — и переменная получила значение. Элементарно, Ватсон.

присваивание — это операция, которая возвращает значение
присваивание — это операция, которая возвращает значение

Но вот факт, который взрывает мозг: в C присваивание — это операция, которая возвращает значение. Не просто записывает число в память, а ещё и отдаёт его дальше по цепочке.

Поэтому такой код работает:

int a, b, c;
a = b = c = 42;

Компилятор читает справа налево:

  1. c = 42 → записывает 42 в c и возвращает 42
  2. b = 42 → записывает 42 в b и возвращает 42
  3. a = 42 → и так далее

Это как эстафета 🏃‍♂️ — каждый участник передаёт палочку дальше, но при этом ещё и пробежал свой этап.

Зачем это знать? Потому что без этого понимания ты не поймёшь, почему можно писать while ((c = getchar()) != EOF) — тут присваивание внутри условия, и оно работает именно благодаря тому, что возвращает значение.

Инициализация ≠ Присваивание (и это важно)

Инициализация ≠ Присваивание
Инициализация ≠ Присваивание
int x = 10; // инициализация
int y;
y = 10; // присваивание

Выглядит одинаково? Для компилятора — две разные вселенные.

Инициализация — это когда переменная получает значение в момент рождения. Присваивание — когда ты меняешь значение уже существующей переменной.

Почему это не просто формальность:

  • Инициализация работает для массивов и структур: int arr[] = {1, 2, 3};
  • Присваивание так не работает: arr = {4, 5, 6}; ❌ — компилятор пошлёт тебя

Это как разница между «родиться с именем» и «сменить имя через паспортный стол» — формально вроде одно и то же, но процедуры разные 🎭

Литералы: когда 100 это не просто сто

Число 100 в коде — это литерал. Компилятор видит его и думает: «Окей, целое число, значит int, займу под него 4 байта».

Но если напишешь 100000000000 — компилятор подумает: «В int не влезет... Возьму long. Не влезет? Возьму long long». И так, пока не найдёт подходящий тип.

Системы счисления: один литерал, три лица

Системы счисления
Системы счисления

int dec = 100; // десятичная (обычная)
int hex = 0x64; // шестнадцатеричная
int oct = 0144; // восьмеричная

Внимание! 0144 ≠ 144. Ноль в начале — это магический префикс, который говорит компилятору: «Это восьмеричное число».

Почему это важно? Потому что в реальном коде можно случайно написать int time = 0900; (хотел указать 9 утра) — и получишь ошибку компиляции, потому что в восьмеричной системе нет цифры 9 😅

Хекс (0x) используется везде:

  • Цвета в веб-дизайне: #FF5733 — это те же хексы
  • Адреса памяти в отладчике
  • Битовые маски для флагов в настройках

Суффиксы: когда нужно говорить компилятору напрямую

100U // unsigned int
100L // long
100ULL // unsigned long long
10.0F // float (а не double!)

Зачем? Чаще всего — чтобы избежать предупреждений. Например, если у тебя переменная типа float, а ты пишешь:

float pi = 3.14; // компилятор: "Эй, 3.14 это double, могут быть потери!"

Правильно:

float pi = 3.14F; // компилятор: "Понял, принял"

Лайфхак: Всегда используй заглавную L, а не строчную l — потому что l легко спутать с единицей: 100l выглядит как 1001 🤦

Символы: когда 'A' это число, а не буква

Вот это реально ломает шаблон первый раз:

char letter = 'A';
printf("%c\n", letter); // выведет: A
printf("%d\n", letter); // выведет: 65

Что происходит? Символ 'A' — это на самом деле число 65 (его код в таблице ASCII). Компьютер не знает букв, он знает только числа. Когда ты пишешь 'A', компилятор переводит это в 65.

Одинарные vs двойные кавычки

Одинарные vs двойные кавычк
Одинарные vs двойные кавычк

'A' // символ (тип int, значение 65)
"A" // строка (массив из двух элементов: 'A' и '\0')

Это не взаимозаменяемо. Если напишешь:

char c = "A"; // ❌ ОШИБКА

Компилятор скажет: «Ты пытаешься засунуть массив в переменную под один символ. Ты серьёзно?»

Почему это важно? Потому что все текстовые протоколы, все форматы данных, вся работа с текстом в C строится на понимании этой разницы. HTTP-запросы, JSON, XML — всё это манипуляции символами как числами.

Бонус: символы в шифровании

Хочешь простейший шифр? Сдвиг символов:

char secret = 'H' + 3; // теперь это 'K'

Это основа шифра Цезаря — того самого, которым пользовались древние римляне (и который взламывается за 5 секунд, но это уже детали 😏).

Вещественные числа: когда 10 ≠ 10.0

int a = 10; // целое число (int)
double b = 10.0; // вещественное (double)

Для математики это одно и то же. Для компилятора — принципиально разные типы.

Почему? Потому что:

  • Целые числа хранятся точно
  • Вещественные числа хранятся приблизительно

Вот почему 0.1 + 0.2 в любом языке программирования даёт не 0.3, а 0.30000000000000004 🤯

Экспоненциальная форма

double big = 1e9; // 1 × 10⁹ = 1 000 000 000
double small = 5e-3; // 5 × 10⁻³ = 0.005

Где используется:

  • Физические расчёты (скорость света: 3e8 м/с)
  • Работа с очень большими числами (количество операций GPU)
  • Научные вычисления (постоянная Планка: 6.62607015e-34)

sizeof: оператор, который показывает правду

sizeof
sizeof
printf("%zu байт\n", sizeof(int)); // обычно 4
printf("%zu байт\n", sizeof(char)); // всегда 1
printf("%zu байт\n", sizeof(double)); // обычно 8

Зачем это нужно? Потому что размеры типов не фиксированы стандартом C. На твоём компе int может быть 4 байта, а на каком-нибудь микроконтроллере — 2 байта.

sizeof — это способ писать переносимый код, который будет работать везде.

Применение в жизни:

  • Выделение памяти: malloc(n * sizeof(int))
  • Копирование данных: memcpy(dest, src, sizeof(data))
  • Проверка размеров структур для оптимизации

Главный инсайт 💡

Компилятор C не угадывает твои намерения. Он берёт то, что ты написал, и интерпретирует строго по правилам.

  • 'A' — это символ
  • "A" — это строка
  • 100 — это int
  • 100.0 — это double
  • 0100 — это восьмеричное ~~и потенциальная головная боль~~

Понимание этих деталей — это не зубрёжка ради зубрёжки. Это ключ к тому, чтобы писать код, который делает то, что ты имел в виду, а не то, что получилось случайно.

Это разница между программистом, который пишет код, и программистом, который понимает, что происходит на уровне железа. А понимание = власть 🔥

🔥 Хочешь копнуть глубже? Полный учебный материал с детальными примерами, схемами и крутыми иллюстрациями ждёт тебя на нашем сайте!