Код, который писали на первом занятии. Исходник лежит в базе как
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'; // vVvv118 инициализирован числом, а печатается буквой: в 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 |
Остальное в комментариях совпадает с лонгридом.