Данный код не скомпилируется, если подключить флаги -Werror и -Wall при компиляции.
Если их не дать, скомпилируется и запустится, но результат возможно будет не такой,
какой вы ожидаете.
a является неинициализированной переменной.
Важно понимать, что это не значит, что в a нет значения.
В a не может не быть значения, поскольку a это всего лишь абстракция для
ячейки памяти, а в ячейке памяти не может не быть ничего.
Неинициализированная переменная в C++ это та переменная,
куда еще не было нарочно записано значение.
Объявление переменной всего лишь выделяет ячейку памяти для этой переменной.
Заметьте, что ячейка приходит из общей памяти и могла
до этого использоваться другой переменной.
Такие ячейки памяти остаются с их старым значением,
которое было там при прошлом их использовании.
По этой причине, в переменной может быть любое число,
а вы как программист не можете полагаться на то, что там будет.
То, что находится в неинициализированной переменной
по другому называется мусорные данные.
Почему в ячейке памяти не может не быть ничего?
Потому что память состоит из байтов, которые состоят из 8 битов каждый.
Биты способны хранить в себе или 0, или 1, и ничто другое.
Они не могут хранить в себе “ничего”.
Соответственно и байты, состоящие строго из битов,
не способны хранить что либо кроме 0 или 1 составляющих их битов.
Вы могли бы решить, что 0 это “ничего”, но это не всегда так.
0 можно вписать в бит нарочно.
Если 0 и “ничего” это одно и то же,
то вы не сможете различить, который из них в бите,
если в изоляции считаете 0 из бита.
Был ли там 0 потому что туда еще ничего не вписали,
или потому что кто-то туда нарочно его вписал ранее?
Что будет если считать из переменной a?
Поскольку a неинициализированная, вы получите то значение,
которое было в памяти, выделенной a еще до того, как она была выделена a.
Однако, считывание из неинициализированной переменной считается UB (undefined behavior),
что по определению означает, что может произойти все что угодно,
а компилятору разрешается предположить, что такое действие невозможно.
На строчке 2 в b записывается результат выражения с правой части присваивания (a + 6).
Вычисление результата этого выражения означает его превращение в одно значение.
a + 6 -> 5 + 6 -> 11
Результатом вычисления выражения является значение 11, которое и будет записано в ячейку b.
Дальнейшее изменение a не влияет на предыдущую операцию, поскольку ее результат уже был сохранен в b.
Что здесь считается выражением?
Литерал (5, 6, 7), чтение переменной (a).
Каждое из них при вычислении даёт одно значение со своим типом (здесь все int).
Даже всё присваивание целиком (a = 7) — это выражение.
Почему? Потому что его результат можно присвоить, например, другой переменной.
Например, следующий код записывает 7 в a, вычисляя a = 7,
которое само даёт 7 (то значение, которое установилось после присваивания),
а оно затем записывается в c.
intc= (a=7);
Даже b на строчке std::cout << b << std::endl; — это выражение,
потому что его значение будет передано в функцию печати,
а для этого оно должно быть сначала вычислено — например, в число —
прежде чем отправиться в функцию печати.
Этот синтаксис по большей части эквивалентен следующему:
inta=5;
Разница в том, что он заставляет компилятор дать ошибку в том случае,
когда такое присваивание влечет за собой возможную потерю информации.
Например, следующий код скомпилируется, если не дать компилятору никаких флагов.
При запуске, в a будет сохранено число 5 (дробная часть будет отсечена).
inta=5.6;
Если использовать фигурные скобки, не скомпилируется.
Эта строгость может помочь нам заметить возможные ошибки.
sizeof вычисляется во время компиляции, давая размер переменной или типа в байтах.
Во время выполнения его уже нет: компилятор заменяет sizeof(...) на обычное число,
так что никакого sizeof в выполняемом коде уже не остаётся.
Например:
sizeof(int) даст 4;
sizeof(a) эквивалентен sizeof(тип_а), то есть sizeof(int), то есть 4;
sizeof(uint8_t) даст 1 (8 бит — 1 байт).
Операнд при этом на самом деле не вычисляется.
В sizeof(a + 1) выражение a + 1 не вычисляется;
компилятор определяет только тип его результата (здесь int),
потому что типы существуют только во время компиляции и к моменту выполнения уже недоступны.
auto значит, что тип автоматически выводится из типа выражения, которым инициализируется переменная,
а не просто из текста справа.
Так как у выражения 5 тип int, у a тип автоматически станет int.
Можете представить, что auto как бы заменится на int во время компиляции.
Выведенный тип статический (фиксируется при компиляции) и поменяться позже не может:
autoa=5;
a="abc"; // ошибка: `a` — это `int`, позже он не может стать строкой
Это не скомпилируется: auto нужно выражение, из которого выводится тип.
Без выражения компилятору нечем заменить auto.
Присваивание a = 5; на следующей строке этого не исправляет — тип должен быть зафиксирован при определении и не может быть выведен из последующего присваивания.
auto учитывает тип всего выражения, а не то, является ли оно литералом.
У a + 5 тип int, поэтому b станет int со значением 10.
То же верно для любого выражения: auto c{ a }; тоже даст c тип int.
static_cast<int> здесь произойдет даже если его не написать.
Так как любое число, сохраненное в a, всегда влезет в b,
можно просто присвоить к b переменную a,
что совершит конверсию из uint8_t в int автоматически.
В данном примере, он берет только младший байт числа от a,
отсекая старшие 3 байта. Это называется truncation.
Если записать это без static_cast<uint8_t>,
по умолчанию он произойдет автоматически.
При этом компилятор не дает ошибок,
если не переданы флаги предупреждения при компиляции.
Чтобы подобные ситуации определялись и останавливались компилятором,
необходимо передать флаги предупреждений, компилируя, например, вот так:
g++ test.cpp -Wall -Werror -Wconversion
Помимо этого, можно использовать инициализацию через фигурные скобки.
Следующее тоже не скомпилируется:
uint8_tb { 256 }; // narrowing conversion
А следующее вероятно даст предупреждение:
uint32_ta { 256 };
uint8_tb { a }; // narrowing conversion
Правильный ответ:
static_cast<uint8_t> обрезает значение a (truncation),
оставляя только последний байт (наименьший байт).
Ответ будет 0, потому что число 256 имеет запись 1 0000 0000 в двоичной системе,
а обрезание этого числа до 8 бит оставит лишь 0000 0000,
удалив старшую 1.
Что будет, если в a сохранить другое значение?
Значение 257: Сохранится 1 0000 0001, что оставит 0000 0001 при отсечении.
Значение 258: Сохранится 1 0000 0010, что оставит 0000 0010 при отсечении.
Значение 511: Сохранится 1 1111 1111, что оставит 1111 1111 при отсечении.
Значение 512: Сохранится 10 0000 0000, что оставит 0000 0000 при отсечении.
В данном примере, он воспринимает представление числа сохраненное в a как есть, но со знаком.
Например, если a равен 0, в ответе будет 0, потому что 0000 0000
что как целое со знаком, что без знака, является значением 0.
Если a равен 128, то есть 1000 0000, станет -128, потому что 1000 0000
как число со знаком это -128.
Что делает присваивание к int32_t для меньшего типа со знаком int8_t?
Если значение отрицательное, ответ тоже будет отрицательным (верхние биты заполнятся 1).
Например, -1 записывается как 1111 1111 в 8 битах,
станет 1111 1111 1111 1111 1111 1111 1111 1111 в 32 битах,
что тоже равно -1.
Если значение положительное, ответ тоже будет положительным (верхние биты заполняются 0).
Например, 10 записывается как 0000 1010 в 8 битах,
станет 0000 0000 0000 0000 0000 0000 0000 1010 в 32 битах,
что тоже равно 10.
Короче говоря, в int32_t будет всегда храниться то же числовое значение.
Правильный ответ:
255 записывается в a как 1111 1111.
1111 1111 конвертируется как есть в b, а 1111 1111 как число со знаком — -1
значение -1 сохраняется в c как значение -1 (см. выше объяснение как).