Integer Representation
A fixed-width byte string is just a pattern; what makes it a number is the rule we read it by. We define unsigned encoding and two's complement — where the top bit carries a negative weight — derive the ranges UMax, TMin, and TMax, and show how the same bits reinterpret between signed and unsigned, how widening sign-extends, and what truncation throws away.
╌╌╌╌
The previous lesson left a -bit string as a raw pattern with no meaning of its own. Two rules turn that pattern into an integer. The unsigned rule reads it as a plain base-2 magnitude; the two's-complement rule reads it as a signed integer by making the top bit count against the total. Both rules cover all patterns, neither wastes one, and the machine implements both with the same adder. This lesson fixes those two encodings, the ranges they span, and the three operations — reinterpretation, extension, and truncation — that move a value between widths and meanings.
Unsigned encoding
Let be a bit vector, the most significant bit. The unsigned value reads each bit at its positional weight and sums the ones, exactly as the byte figure from the last lesson did:
Every bit carries a non-negative weight, so ranges from (all bits clear) up to a maximum when every bit is set. That maximum is one of the three numbers worth memorizing for the rest of the course:
The map is a bijection: it pairs each of the bit patterns with a distinct value in and leaves nothing out. That property, full coverage with no collisions, is what lets us speak of the unsigned value of a pattern, and it will hold for two's complement too.
Two's complement and the sign bit's weight
To represent negatives we keep every bit's weight the same except the most significant, whose weight we negate. That single change defines two's complement:
The leading term is the only difference from : the top bit, the sign bit, now contributes when set instead of . Everything below it is an ordinary unsigned magnitude. So a negative value is the sign bit's plus an unsigned offset from the lower bits.
Like , the map is a bijection onto its range, but the range is shifted to straddle zero. Setting only the sign bit gives the most negative value; clearing the sign bit and setting everything below gives the most positive:
Two facts follow, and both matter in practice. First, the range is asymmetric: there is one more negative number than positive, because zero occupies a slot on the non-negative side. So , and is not representable. Second, , since flipping the sign bit's weight from to shifts every negative value up by .
For the landmarks are , , ; for
, , ,
. These are the limits a C unsigned, int,
UINT_MAX, and INT_MIN/INT_MAX actually take on a typical machine.1
A short table of 4-bit values shows the two readings side by side and exactly where they diverge. The first eight patterns (sign bit clear) read the same under both rules; the last eight (sign bit set) diverge by .
| Bits | Unsigned | Two's comp. |
|---|---|---|
0000 | 0 | 0 |
0001 | 1 | 1 |
0111 | 7 | 7 (= ) |
1000 | 8 | (= ) |
1001 | 9 | |
1110 | 14 | |
1111 | 15 (= ) |
The pattern 1111 is worth a second look: it is read one way and read
the other, and the reason is all ones in two's complement is that borrows through every column, filling the whole word. This is worth
committing to memory — an all-ones word is signed and unsigned at
every width — because it turns up constantly in masks and error returns.
The number wheel: same bits, two readings
Because both encodings are bijections over the same patterns, they differ only in where they cut the circle. Counting bit patterns and reading each as unsigned gives a monotone climb; reading the top half as two's complement wraps those patterns around to the negative side.
The dashed boundary marks the sign bit: every pattern in the lower half has , and exactly there the two readings diverge by . Converting between the two encodings never touches the bits, only the rule we apply to them.
The asymmetry of TMin
The signed range holds one more negative value than positive, and the odd one out is itself. Every other value has a negation partner across zero: pairs with , with . The partner of would be , one step past , and no -bit pattern encodes it.
Ask the machine for anyway and fixed-width arithmetic (the subject of the next lesson) wraps the missing around to : negating returns . For , negate by flipping the bits and adding one: , the same pattern. Three practical consequences follow.
abs(INT_MIN)cannot produce the right answer, since is out of range; on a two's-complement machine the call comes back negative, and the C standard leaves it undefined.- The innocuous normalization
if (x < 0) x = -x;fails to make one input non-negative, a real bug in binary-search midpoints and absolute-difference code. - The literal
-2147483648is a pitfall in C source: it parses as negation applied to the constant2147483648, which does not fit in anint, solimits.hspells the constant as(-INT_MAX - 1).2
#include <limits.h>
#include <stdlib.h>
int a = abs(INT_MIN); /* undefined; wraps back to INT_MIN on x86-64 */
int m = -INT_MAX - 1; /* the portable way to write TMin */
Signed ↔ unsigned: same bits, different meaning
A C cast between int and unsigned of the same width is a reinterpretation:
the bit pattern is left alone and only the reading rule changes. For a value
in two's complement, the unsigned reading of the identical bits is
Negative values gain because the sign bit's weight flips from to , a swing of exactly . Going the other way, an unsigned value at or above reads back as negative:
This conversion is usually silent. In C, an expression mixing signed and unsigned operands converts the signed one to unsigned, which can flip comparisons.
#include <stdio.h>
int main(void) {
int a = -1; /* bits: 0xffffffff */
unsigned b = 1u;
/* -1 is converted to unsigned 4294967295, so the test is FALSE */
printf("%d\n", a < b); /* prints 0, not 1 */
printf("%u\n", (unsigned) a); /* prints 4294967295 */
return 0;
}
The comparison a < b looks like and should be true, but C promotes a
to unsigned, turning its bits 0xffffffff into ,
which is not less than . The same bits, read under the unsigned rule, give
the opposite answer.3
Widening: sign extension and zero extension
Moving a value into a wider type must preserve its numeric value, and the bits needed to do so depend on the encoding. For an unsigned value, prepend zeros (zero extension), since leading zeros never change a base-2 magnitude. For a two's-complement value, replicate the sign bit (sign extension), copying into every new high position.
Sign extension keeps the value intact: the high
copies of the sign bit re-create the telescoping
that leaves unchanged. A C compiler picks the right one automatically from
the source type — int to long sign-extends, unsigned to unsigned long
zero-extends — which is why declaring a width's signedness correctly matters.4
Truncation: dropping the high bits
The reverse, casting to a narrower type, simply discards the high-order bits. This is truncation, and unlike widening it can change the value. For an unsigned value, dropping to bits computes the result modulo :
For a signed value, the kept bits are read by on the narrower width, so the
result is — the same low bits, but their top
surviving bit may now act as a sign bit. Casting int to a 16-bit short,
for instance, keeps 0xCFC7 and reinterprets it as the negative .
int x = 53191; /* 0x0000cfc7 */
short s = (short) x; /* keeps 0xcfc7 */
/* low 16 bits 0xcfc7 read as signed short = -12345 */
Truncation is information loss: the high bits are gone, and whether the surviving value is the one you wanted depends entirely on whether it fit in the narrower range. Mixing it with the signedness reinterpretation above is a reliable source of bugs, which is why the next lesson treats overflow as a first-class topic.
Why two's complement won
CS:APP presents two's complement as the signed encoding, which is accurate for every machine you will program — but it was not the only candidate, and seeing the alternatives explains why the industry converged on it. Three schemes competed in early computers, all of which reserve the top bit as a sign.
Sign-magnitude stores the magnitude in the low bits and a sign in the top
bit, exactly as humans write numbers: is 1011 (sign 1, magnitude 011).
It is easy to read but has two flaws that sank it: there are two zeros (0000
and 1000, positive and negative zero), wasting a pattern and complicating
equality; and addition needs to inspect the signs and choose between adding and
subtracting, so it cannot reuse the plain adder. One's complement negates by
flipping every bit, which also produces a negative zero (1111) and needs an
awkward end-around carry
to add correctly. Two's complement negates by
flipping and adding one, and its single decisive advantage is the one the next
lesson develops in full: the same adder circuit adds signed and unsigned values
with no special cases, because the encoding is arithmetic modulo .5
The design is old and its origins are usually credited to von Neumann's 1945 report on the EDVAC, which proposed complement arithmetic for exactly the hardware-economy reason above.6 A representation is a hardware decision as much as a mathematical one: two's complement won because it lets one adder, one comparison, and one multiply serve both signed and unsigned data, a theme that runs through the whole next lesson.
With both encodings in hand, the next lesson asks what happens when fixed-width arithmetic runs off the end of its range — the wrap-around behavior of integer arithmetic.
Footnotes
- Bryant & O'Hallaron, CS:APP, §2.2.1 — Integral Data Types: the C
limits.hconstantsUINT_MAX,INT_MIN,INT_MAXand the typical 32- and 64-bit widths. ↩ - Bryant & O'Hallaron, CS:APP, §2.2.3 — Two's-Complement Encoding: the asymmetric range , and the aside on why C headers write as
(-INT_MAX - 1). ↩ - Bryant & O'Hallaron, CS:APP, §2.2.5 — Signed vs. Unsigned in C: implicit conversion of operands to unsigned and the comparison pitfalls it causes. ↩
- Bryant & O'Hallaron, CS:APP, §2.2.6 — Expanding the Bit Representation of a Number: sign extension preserves the two's-complement value; zero extension preserves the unsigned value. ↩
- Bryant & O'Hallaron, CS:APP, §2.2.2–2.2.3 — the aside contrasting two's complement with sign-magnitude and one's complement, and the observation that two's complement is the near-universal choice because it has a single zero and a uniform adder. ↩
- von Neumann, First Draft of a Report on the EDVAC (1945): the early proposal to use complement arithmetic so that subtraction could be performed by the addition hardware, the hardware-economy argument that still explains two's complement today. ↩
╌╌ END ╌╌