This code will not compile if the -Werror and -Wall flags are passed to the compiler.
Without them, it will compile and run, but the result may not be what you expect.
a is an uninitialized variable.
It is important to understand that this does not mean that a has no value.
a must have a value, since a is merely an abstraction of a
memory cell, and a memory cell cannot be empty.
An uninitialized variable in C++ is a variable
into which no value has yet been deliberately written.
Declaring a variable merely allocates a memory cell for that variable.
That cell may have been used by another variable before.
Such memory can retain its old value
from a previous use.
For this reason, a variable may contain any number,
and, as the programmer, you cannot rely on what it will contain.
The contents of an uninitialized variable
are also called garbage data.
Why can a memory cell not be empty?
Because memory consists of bytes, and each byte consists of 8 bits.
Bits can store either 0 or 1, and nothing else.
They cannot store “nothing”.
Accordingly, a byte is made up entirely of bits,
each of which is either 0 or 1.
You might decide that 0 is “nothing”, but that is not always the case.
0 can be deliberately written into a bit.
If 0 and “nothing” were the same thing,
you would not be able to distinguish the two
by reading a bit in isolation.
Was it 0 because nothing has been written there yet,
or because someone deliberately wrote it there earlier?
What happens if you read from variable a?
Since a is uninitialized, you will obtain whatever value
was in the memory before it was allocated to a.
However, reading from an uninitialized variable is considered UB (undefined behavior),
which by definition means that anything can happen,
and the compiler is allowed to assume that such a read is impossible.
On line 2, the result of the expression on the right-hand side of the assignment (a + 6) is written to b.
Evaluating this expression means turning it into a single value.
a + 6 -> 5 + 6 -> 11
The result of evaluating the expression is the value 11, which is written to cell b.
Further changes to a do not affect the previous operation, since its result has already been stored in b.
What counts as an expression here?
A literal (5, 6, 7), a variable read (a).
Each of them evaluates to a single value with its own type (all int here).
Even the whole assignment (a = 7) is an expression.
Why? Because its result can be assigned to e.g. another variable.
For example the following code assigns 7 to a while evaluating a = 7,
which itself evaluates to 7 (whatever both of them became after the assignment),
which is then assigned to c.
intc= (a=7);
Even b on std::cout << b << std::endl; is an expression,
because its value is going to be passed to the printing function,
and it has to be evaluated to e.g. a number before getting sent to the print function.
This syntax is largely equivalent to the following:
inta=5;
It differs in that the compiler reports an error
when an assignment could result in a loss of information.
For example, the following code compiles if no compiler flags are provided.
When run, the number 5 will be stored in a (the fractional part will be discarded).
inta=5.6;
If curly braces are used instead, it will not compile.
This strictness can help us notice possible mistakes.
sizeof is evaluated at compile time and yields the size, in bytes, of a variable or type.
It does not execute at run time: the compiler replaces sizeof(...) with a plain number,
so sizeof itself does not even exist as a function at run time.
For example:
sizeof(int) produces 4;
sizeof(a) is equivalent to sizeof(type of a), that is, sizeof(int), that is, 4;
sizeof(uint8_t) produces 1 (8 bits — 1 byte).
The operand is never actually computed.
In sizeof(a + 1), the expression a + 1 is not evaluated;
the compiler only looks at the type its result would have (here, int),
because types are known only at compile time and do not survive to run time.
auto means that the type is automatically inferred from the type of the initializer expression,
not just from the text on the right-hand side.
Since the expression 5 has type int, a will have type int.
You can think of auto as being replaced with int during compilation.
The inferred type is static (fixed at compile time) and cannot change later:
autoa=5;
a="abc"; // error: `a` is `int`, it cannot become a string later
This will not compile: auto needs an initializer expression to infer the type from.
With no expression, the compiler has nothing to replace auto with.
Assigning 5 on the next line does not fix it —
the type must be fixed at the definition and cannot be inferred retroactively from a later assignment.
auto looks at the type of the whole initializer expression, not just at whether it is a literal.
a + 5 has type int, so b becomes int with value 10.
The same holds for any expression: auto c{ a }; would also give c the type int.
An implicit conversion equivalent to static_cast<int> occurs here, even though it is not written explicitly.
Since every value that can be stored in a also fits in b,
you can assign a directly to b,
which performs the conversion from uint8_t to int automatically.
In this example, static_cast<uint8_t> takes only the least significant byte of the number in a,
discarding the upper 3 bytes. This is called truncation.
Without static_cast<uint8_t>,
the conversion occurs implicitly.
The compiler does not report an error in this case
if no warning flags are passed during compilation.
To have the compiler detect and reject such situations,
pass warning flags during compilation, for example:
g++ test.cpp -Wall -Werror -Wconversion
In addition, you can use brace initialization.
The following will also not compile:
uint8_tb { 256 }; // narrowing conversion
And the following will probably produce a warning:
uint32_ta { 256 };
uint8_tb { a }; // narrowing conversion
Correct answer:
static_cast<uint8_t> truncates the value in a,
leaving only the last byte (the least significant byte).
The result is 0, because 256 is represented as 1 0000 0000 in binary,
and truncating this number to 8 bits leaves only 0000 0000,
discarding the leading 1.
What happens if a different value is stored in a?
Value 257: 1 0000 0001 is stored, becoming 0000 0001 after truncation.
Value 258: 1 0000 0010 is stored, becoming 0000 0010 after truncation.
Value 511: 1 1111 1111 is stored, becoming 1111 1111 after truncation.
Value 512: 10 0000 0000 is stored, becoming 0000 0000 after truncation.
It is a bitwise operator that inverts every bit in the binary representation of a number:
it turns each 0 into 1 and each 1 into 0.
For example, 1010 0011 -> 0101 1100.
Correct answer
0 is stored in an 8-bit variable.
The ~ operator is applied to the 8-bit value 0: 0000 0000 -> 1111 1111 (255).
The result is stored in the 8-bit variable b.
The result is stored unchanged in the 32-bit variable c (for printing).
In this example, it interprets the unchanged bit representation of the number stored in a as a signed integer.
For example, if a is 0, the result will be 0, because 0000 0000
is 0 as both a signed and an unsigned integer.
If a is 128, that is, 1000 0000, it becomes -128, because 1000 0000
represents -128 as a signed number.
What happens when a smaller signed int8_t value is assigned to int32_t?
If the value is negative, the result will also be negative (the upper bits are filled with 1s).
For example, -1 is written as 1111 1111 in 8 bits,
and becomes 1111 1111 1111 1111 1111 1111 1111 1111 in 32 bits,
which is also -1.
If the value is positive, the result will also be positive (the upper bits are filled with 0s).
For example, 10 is written as 0000 1010 in 8 bits,
and becomes 0000 0000 0000 0000 0000 0000 0000 1010 in 32 bits,
which is also 10.
In short, int32_t will always store the same numeric value.
Correct answer:
255 is written to a as 1111 1111.
1111 1111 is converted unchanged to b, and as a signed number it is -1.
The value -1 is stored in c as -1 (see the explanation above for how).