Код, который писали на первом занятии. Исходник лежит в базе как 30-Код/C++/L01 - main.cpp и компилируется как есть; на сайт он не выкладывается, поэтому все нужные фрагменты приведены здесь.

Что демонстрирует

Одним файлом — почти весь материал первого занятия: размеры типов, переполнение знаковых и беззнаковых, арифметику с символами, целочисленное продвижение, чтение неинициализированной переменной, указатели и ветвление. Файл устроен как «проговорить и тут же показать», поэтому часть строк намеренно содержит ошибочный код.

Это код с занятия, а не эталон

Файл собирается g++ -std=c++20, но выдаёт четыре предупреждения, а часть комментариев в нём расходится с лонгридом. Разбор ниже отмечает такие места: в спорных случаях правы лонгрид и стандарт, а не комментарий в коде. Сам исходник в базе лежит без правок — его не «улучшаем».

Разбор

Переполнение: знаковое против беззнакового

int x = 2'000'000'000;
int y = 2'000'000'000;
std::cout << x + y << '\n';          // UB
 
unsigned int ux = 3'000'000'000;
unsigned int uy = 3'000'000'000;
std::cout << ux + uy << '\n';        // 1705032704

Две внешне одинаковые строки с принципиально разным статусом. Вторая полностью определена: беззнаковая арифметика идёт по модулю 2³², и 6 000 000 000 − 2³² даёт 1 705 032 704 на любой системе с 32-битным unsigned int.

Первая — Неопределённое поведение. На моём запуске она напечатала -294967296, и это число ничего не доказывает: оно не гарантировано ни стандартом, ни повторным запуском, ни другим уровнем оптимизации. Ровно тот случай, когда «я проверил, работает» не является проверкой.

Целочисленное продвижение

short sx = 1;
char cx = 'a';
int ix = 1;
std::cout << sizeof(ix) << '\n';        // 4
std::cout << sizeof(ix + ix) << '\n';   // 4
std::cout << sizeof(sx) << '\n';        // 2
std::cout << sizeof(sx + sx) << '\n';   // 4
std::cout << sizeof(cx) << '\n';        // 1
std::cout << sizeof(cx + cx) << '\n';   // 4

Переменные остались прежнего размера, а суммы выросли до четырёх байт: перед арифметикой short и char продвигаются до int. Пары sizeof подряд — самый наглядный способ это увидеть. Разбор правил — Арифметические преобразования типов.

Символы как числа

char v = 'v';
char v118 = 118;
char V = v + 'A' - 'a';
std::cout << v << V << v118 << '\n';   // vVv

v118 инициализирован числом, а печатается буквой: в ASCII 118 это и есть v. Выражение v + 'A' - 'a' вычисляется уже в int (снова продвижение) и при записи в char сужается обратно. Подробнее — Символьный тип char.

Строковый литерал с нулём внутри

char* greet = "Hello, Vova!\n\0Goodbye, Vova!\n";
std::cout << greet;                     // Hello, Vova!

Печатается только первая половина: вывод char* идёт до первого нулевого байта, а \0 поставлен ровно посередине. Прощание в памяти есть, но для std::cout строка уже закончилась. Это тот же сюжет, что в вопросе про "A\0B" в конце лонгрида.

Строка не компилируется по стандарту

char* greet = "..."; — преобразование строкового литерала к char*, запрещённое в C++11 и дальше. GCC пропускает это с предупреждением ISO C++ forbids converting a string constant to 'char*', Clang с -pedantic-errors выдаёт ошибку. Правильно — const char* greet = .... В своём коде так и пишем.

Литералы шире, чем кажутся

std::cout << (10 + 3'000'000'000) << '\n';   // 3000000010
std::cout << (0.1f + 0.1) << '\n';           // 0.2
std::cout << (10u + 10) << '\n';             // 20
std::cout << (1.0 + 10) << '\n';             // 11

Первая строка — не переполнение: литерал 3'000'000'000 в int не помещается, поэтому его типом выбирается long (на 64-битном Linux он 64-битный), и 10 приводится туда же. На платформе с 32-битным long тип был бы long long. Остальные строки показывают приведение к общему типу: float с double дают double, unsigned со знаковым — unsigned int, дробное с целым — дробное.

0.1f + 0.1 печатается как 0.2, хотя точного 0.2 там нет: std::cout по умолчанию показывает шесть значащих цифр и прячет разницу. Это иллюстрация не к точности, а к тому, что вывод по умолчанию округляет.

Объявление, определение, присваивание

int var;                   // объявление
std::cout << var << '\n';  // UB
var = 0;                   // определение
std::cout << var << '\n';  // OK
var = 1;                   // присвоение значение

Комментарии здесь — то, что писалось на занятии вживую, и они не сходятся с лонгридом.

Расхождение с лонгридом

Строка int var; это и объявление, и определение сразу: объект уже существует, просто без полезного значения. Строки var = 0; и var = 1; — обе присваивания, первая не становится определением оттого, что она первая. Раздел 5.2 лонгрида разбирает это специально. Верна версия лонгрида.

Само UB в первой строке подано верно: чтение неинициализированной локальной переменной в C++20 — неопределённое поведение. На запуске она напечатала 0, что как раз и делает ошибку живучей.

Звёздочка относится к имени, а не к типу

long long lly = 0;
long long *plong, llx;
plong = &llx;

В одной строке объявлены разные типы: plong — указатель на long long, а llx — обычный long long. Звёздочка привязана к имени, а не к типу слева. Отсюда правило кодстайла про одну переменную на строку: так эта ловушка просто не возникает. См. Инициализация переменных.

Ветвление

std::cin >> llx;
 
if (llx == 0) {
  std::cout << "This is zero!!!\n";
} else if (llx == 1) {
  std::cout << "This is one!!!\n";
} else {
  std::cout << "Oh no... this is neither zero nor one :(\n";
}

Обычная цепочка: условия проверяются сверху вниз, после сработавшей ветви остальные пропускаются. Подробнее — Условный оператор if.

Что будет, если сделать иначе

  • Если раскомментировать ux / uz при uz == 0 — программа упадёт с Floating point exception. Беззнаковый тип не делает деление на ноль безопасным, а сообщение оболочки к дробным числам отношения не имеет.
  • Если заменить x + y на static_cast<long long>(x) + y — UB исчезает, и получится честные 4 000 000 000. См. static_cast.
  • Если заменить char* на const char* — предупреждение уходит, поведение не меняется.
  • Если собрать с -Wall -Wextra, всплывут ещё три неиспользуемые переменные (lx, lly, plong) — на результат они не влияют, но показывают, что часть строк осталась от проговаривания темы вслух.

Комментарии-конспект внутри файла

Файл начинается и заканчивается большими блоками /* ... */ — это конспект занятия, надиктованный по ходу. Три места в них стоит читать с поправкой:

В файлеКак на самом деле
беззнаковое переполнение «по модулю 2^{sizeof(type)}−1»по модулю 2^N, где N — ширина в битах
«если один операнд беззнаковый, оба приведутся к беззнаковому»зависит от рангов типов; лонгрид прямо называет это правило неверным
sizeof(void) == 1по стандарту ошибка компиляции, единицу даёт GCC как расширение
суффикс 0.1lfтакого суффикса нет, lf — из printf; для long double это L

Остальное в комментариях совпадает с лонгридом.