Foundations · Module 1 · Lesson 4 of 9
Types Decide What Bits Mean
Learn why the same bytes can be -1 or 4294967295: how types fix size and meaning, how two's complement and floating point store numbers, and how conversions and wraparound behave.
By the end you can
- Explain why the same bytes can mean different values under different types
- Convert between a bit pattern and its signed integer value using two's complement
- Predict the result of mixed-type arithmetic, including promotion and truncation
- Choose between integer and floating-point types for a job and name the risk of each
Builds on Variables Are Names for Bytes.
01 · A question first
A single byte holds 255 and you add 1 to it. Does the result become 256, crash the program, or wrap around to 0? And why does the comparison -1 < 1u come out false when minus one is smaller than one?
Make a guess, then step through the trace below to check it.
02 · Watch it run
int n = -1;unsigned int u = n;unsigned char c = 255;c = c + 1;- byte 0--
- byte 1--
- byte 2--
- byte 3--
- byte 4--
- byte 5--
- byte 6--
- byte 7--
- byte 8--
Nine bytes of memory: n will use bytes 0 to 3, u bytes 4 to 7 and c byte 8. Memory holds only bytes. The types of n, u and c are known to the compiler alone.
Machine work: nothing has run yet
03 · The idea
Start with the problem
Four bytes sit in memory: FF FF FF FF. Are they the number -1? The number 4,294,967,295? Part of a color, or of some text? Memory cannot say. The same bytes are all of these, and the only thing that picks one is the type the program uses to read them.
int n = -1;
unsigned int u = n;
std::cout << n << " " << u << "\n";-1 4294967295Same bits, two different numbers. This lesson explains why, and how that single fact predicts most of the surprises beginners meet with numbers.
How the idea works
A type tells the compiler three things: how many bytes a value takes, how to read those bytes, and which operations make sense on them. The trace above follows three variables through four lines.
Integers. An int is typically four bytes, a long long eight, a short two and a char one. Unsigned types read all the bits as a plain binary number, so an unsigned char holds 0 to 255 and a four-byte unsigned int holds 0 to 4,294,967,295. Signed types use two's complement for negative numbers: minus one is every bit set, and each step down subtracts one from the pattern, so minus two is FE in a single byte. A four-byte int therefore covers -2,147,483,648 to 2,147,483,647, and this is why step 2 of the trace shows FF FF FF FF for -1.
Characters and booleans. A char is a one-byte integer, and the character 'A' is the number 65. A bool is a one-byte value that is 0 for false and 1 for true.
Floating point. A float (four bytes) or double (eight bytes) stores a sign, an exponent and a fraction, in the IEEE 754 format. Many decimal fractions, 0.1 among them, have no exact binary form, so the machine stores the nearest value it can represent.
Conversions. When types meet, the compiler converts silently, and two rules explain most results. First, arithmetic on types smaller than int promotes them to int before computing, which is why step 5 of the trace works in four bytes. Second, converting between signed and unsigned of the same size keeps the bit pattern, as in step 3, and converting to a smaller type keeps only the low bytes. Dividing two integers gives an integer: 7 / 2 is 3, while 7 / 2.0 is 3.5.
Why it is correct
The type exists only at compile time. After compilation the CPU runs instructions chosen by type: an integer add for an int, a floating-point add for a double. That is why the same bytes can legitimately be read in two ways without conflict, and why types cost nothing at run time.
The overflow rules differ on purpose. Unsigned arithmetic wraps around, so 255 + 1 in one byte gives 0, and the language guarantees it. For signed integers, overflow is undefined behavior: the compiler may assume it never happens, and it uses that assumption to optimize, for example by treating x + 1 > x as always true. Wraparound is a feature for hashing and checksums. The signed rule is a speed contract that your program must not break.
What it costs
Memory and cache. A million char values take 1 MB, and a million long long values take 8 MB. Smaller types let more data fit in the CPU cache, which often matters more than the arithmetic itself.
Arithmetic. Adding two int values takes about one cycle, and a byte is promoted to int for free, so using char for math saves space but not time. In vector (SIMD) instructions a 256-bit register holds 8 float values but only 4 double values, so float can process twice as many numbers per instruction.
Precision. A float keeps about 7 significant decimal digits and a double about 15 to 16. Beyond that, results are rounded.
Pitfalls and variations
Mixed comparisons. -1 < 1u is false. The signed -1 is converted to unsigned and becomes 4294967295, which is larger than 1. A loop such as for (unsigned i = n; i >= 0; --i) never ends, because an unsigned value is always at least 0. Compile with -Wall -Wextra to get warnings about comparisons between signed and unsigned values.
Float equality. 0.1 + 0.2 == 0.3 is false, because each side is a slightly different nearest binary value. Compare floating-point numbers with a tolerance rather than ==, and avoid floating point for money: keep whole cents in an integer.
Truncation. double average = total / count; with two int operands divides first and converts afterwards, so the fraction is already lost. Make one operand a double before dividing.
Assuming sizes. An int is four bytes almost everywhere, but long is eight bytes on 64-bit Linux and four on 64-bit Windows. When the size matters, use the fixed-width types such as std::int32_t and std::uint8_t from the header <cstdint>. Whether a plain char is signed is also up to the platform.
Variations. auto deduces a type from the initializer. std::bit_cast, from the header <bit> in C++20, is the well-defined way to reinterpret the bytes of one type as another.
Where it is used
File formats and network protocols specify exact widths and byte order, so parsers use fixed-width types and careful conversions all day. Hash functions and random number generators rely on unsigned wraparound. Financial software counts cents in integers, while graphics and physics code uses float for speed. Each of these choices is the decision described above: how many bytes, and how to read them.
The next lesson, "Reading Input and Surviving Bad Input", uses these types to take values from the user, and shows what happens when the user types something that does not fit.
04 · Prove it
Question 1 of 4 · answer from memory
A variable holds the bytes `FF FF FF FF`. What decides whether the program sees -1 or 4294967295?
05 · Practice
Asked in interviews
Something wrong with this lesson? Report a problem