Why data representation matters
Inside a computer, everything is a pattern of bits: zeros and ones. A bit is a single binary digit. A group of 8 bits is a byte. The same 32-bit pattern can mean an unsigned integer, a negative integer, a floating-point number, four ASCII characters, or a machine instruction. The hardware does not know which; the meaning comes from how the program uses it.
Understanding representation explains bugs you will meet as a software engineer: integer overflow that turns a large positive balance negative, 0.1 + 0.2 not equalling 0.3, garbled text when a file is read with the wrong encoding, and network data that looks byte-swapped. It is also standard fare in placement aptitude tests and interviews. Interviewers typically probe:
- base conversions done by hand, quickly,
- two's complement: how to negate, the range, and why it is used,
- overflow detection rules,
- IEEE 754 encoding and decoding, special values, and floating-point equality,
- endianness, and
- how UTF-8 encodes characters.
Number systems
A positional number system with base (or radix) b uses digits 0 to b - 1, and each position is worth a power of b. The rightmost integer digit is worth b^0, the next b^1, and so on. Digits after the point are worth b^-1, b^-2, and so on.
| System | Base | Digits | Example |
|---|---|---|---|
| Binary | 2 | 0, 1 | 1011 |
| Octal | 8 | 0 to 7 | 234 |
| Decimal | 10 | 0 to 9 | 156 |
| Hexadecimal | 16 | 0 to 9, A to F (A = 10, ..., F = 15) | 9C |
Programmers use hexadecimal because each hex digit maps to exactly 4 bits, so long binary patterns become short and readable. Octal digits map to exactly 3 bits.
Any base to decimal
Multiply each digit by its place value and add.
Example: 10011100 in binary.
1x2^7 + 0x2^6 + 0x2^5 + 1x2^4 + 1x2^3 + 1x2^2 + 0x2^1 + 0x2^0
= 128 + 0 + 0 + 16 + 8 + 4 + 0 + 0
= 156
Example: 3F7 in hexadecimal = 3 × 256 + 15 × 16 + 7 × 1 = 768 + 240 + 7 = 1015.
Example with a fraction: 101.101 in binary = 4 + 0 + 1 + 0.5 + 0 + 0.125 = 5.625.
Decimal to binary: integer part
Divide repeatedly by 2 and record the remainders. The remainders, read from bottom to top, are the binary digits.
Example: convert 156.
156 / 2 = 78 remainder 0 <- least significant bit
78 / 2 = 39 remainder 0
39 / 2 = 19 remainder 1
19 / 2 = 9 remainder 1
9 / 2 = 4 remainder 1
4 / 2 = 2 remainder 0
2 / 2 = 1 remainder 0
1 / 2 = 0 remainder 1 <- most significant bit
Reading upward: 156 = 10011100 in binary.
The same method works for any base: divide by 8 for octal, by 16 for hex.
Decimal to binary: fractional part
Multiply the fraction by 2 repeatedly. The integer part of each product (0 or 1) is the next bit; keep only the fractional part for the next step. Read the bits from top to bottom.
Example: convert 0.625.
0.625 x 2 = 1.25 -> bit 1, keep 0.25
0.25 x 2 = 0.5 -> bit 0, keep 0.5
0.5 x 2 = 1.0 -> bit 1, keep 0.0 (stop)
So 0.625 = 0.101 in binary.
Example: convert 0.1.
0.1 x 2 = 0.2 -> 0
0.2 x 2 = 0.4 -> 0
0.4 x 2 = 0.8 -> 0
0.8 x 2 = 1.6 -> 1 keep 0.6
0.6 x 2 = 1.2 -> 1 keep 0.2
0.2 x 2 = 0.4 -> 0 (repeats from here)
...
So 0.1 = 0.000110011001100... in binary, with 0011 repeating forever. A decimal fraction has a finite binary form only if its denominator (in lowest terms) is a power of 2. 0.1 = 1/10 is not, so 0.1 cannot be stored exactly in binary. Remember this; it is the root cause of the floating-point surprise later.
Binary, octal and hex shortcuts
To go from binary to hex, group bits in fours starting from the point; for octal, group in threes.
Binary: 1001 1100 10 011 100
Hex: 9 C Octal: 2 3 4
So 156 = 0x9C = octal 234. To go back, expand each hex digit to 4 bits (or octal digit to 3).
Interview tip
Memorize powers of 2 up to 2^16 = 65,536, plus 2^10 ≈ one thousand, 2^20 ≈ one million and 2^32 ≈ 4.29 billion. Hex-to-binary by grouping is much faster than going through decimal, and interviewers notice when you do it fluently.
Unsigned integers
With n bits you can represent 2^n different patterns. Treated as unsigned (non-negative) integers, they cover 0 to 2^n - 1. An 8-bit unsigned value ranges from 0 to 255; a 32-bit one from 0 to 4,294,967,295.
Signed integers
To represent negative numbers, we need to give some bit patterns negative meanings. Three schemes have been used. In each, the leftmost bit (the most significant bit, MSB) acts as a sign indicator: 0 for non-negative, 1 for negative.
Sign-magnitude
The MSB is the sign; the remaining bits are the magnitude (absolute value).
- +5 in 8 bits:
0000 0101 - −5 in 8 bits:
1000 0101
It is easy for humans, but has two problems. There are two zeros (0000 0000 = +0 and 1000 0000 = −0), and addition needs special logic: to add numbers with different signs you must compare magnitudes and subtract.
One's complement
To negate, invert every bit.
- +5:
0000 0101 - −5:
1111 1010
There are still two zeros (0000 0000 and 1111 1111), and addition needs an end-around carry: if a carry comes out of the MSB, add it back into the least significant bit.
Two's complement
To negate, invert every bit and add 1.
- +5:
0000 0101 - Invert:
1111 1010 - Add 1:
1111 1011= −5
A second way to see it: in n-bit two's complement, the MSB has weight −2^(n−1) instead of +2^(n−1); all other bits have their usual positive weights. Check −5 = 1111 1011:
-128 + 64 + 32 + 16 + 8 + 0 + 2 + 1 = -128 + 123 = -5
Worked example: −45 in 8-bit two's complement.
- +45 in binary: 32 + 8 + 4 + 1 →
0010 1101. - Invert:
1101 0010. - Add 1:
1101 0011.
So −45 = 1101 0011 = 0xD3. Check with weights: −128 + 64 + 16 + 2 + 1 = −45. Correct.
A handy shortcut: copy bits from the right up to and including the first 1, then invert all bits to its left. For 0010 1101, the first 1 is the last bit; keep it and invert the rest: 1101 0011.
Why two's complement wins
Every modern computer uses two's complement for signed integers, for three reasons:
- One zero.
0000 0000is the only zero, so no special cases. - The same adder works for signed and unsigned numbers. Adding bit patterns with an ordinary binary adder and discarding the carry out gives the correct two's complement result (as long as there is no overflow). The hardware does not need to know the numbers are signed.
- Subtraction is addition.
A − B = A + (invert B) + 1. An adder with a carry-in of 1 and inverters on one input does subtraction.
Ranges compared
For n bits:
| Scheme | Minimum | Maximum | Zeros |
|---|---|---|---|
| Unsigned | 0 | 2^n − 1 | 1 |
| Sign-magnitude | −(2^(n−1) − 1) | 2^(n−1) − 1 | 2 |
| One's complement | −(2^(n−1) − 1) | 2^(n−1) − 1 | 2 |
| Two's complement | −2^(n−1) | 2^(n−1) − 1 | 1 |
For 8 bits: two's complement covers −128 to 127. For 32 bits: −2,147,483,648 to 2,147,483,647, which you see as Integer.MIN_VALUE and Integer.MAX_VALUE in Java.
The range is asymmetric: there is one more negative number than positive. Negating the minimum value overflows back to itself: in 8 bits, −(−128) gives 1000 0000 again, which is −128. In Java, Math.abs(Integer.MIN_VALUE) returns a negative number for exactly this reason.
Sign extension
To widen a two's complement number (for example from 8 to 16 bits), copy the sign bit into all the new high bits. −5 = 1111 1011 becomes 1111 1111 1111 1011; +5 = 0000 0101 becomes 0000 0000 0000 0101. The value is unchanged. Unsigned numbers are widened with zeros instead (zero extension). Load instructions such as RISC-V lb (sign-extend) and lbu (zero-extend) exist for exactly this reason.
Binary arithmetic and overflow
Addition
Add column by column from the right, as in decimal: 0 + 0 = 0, 0 + 1 = 1, 1 + 1 = 0 carry 1, 1 + 1 + 1 = 1 carry 1.
Subtraction with two's complement
Example: 25 − 12 in 8 bits.
25 = 0001 1001
12 = 0000 1100 -> -12 = 1111 0100
0001 1001
+ 1111 0100
-----------
1 0000 1101 carry out of MSB discarded -> 0000 1101 = 13
The result is 13, correct. The carry out is ignored in signed arithmetic.
Overflow
Overflow happens when the true result does not fit in the available bits. The stored result is then wrong.
For signed (two's complement) addition, overflow occurs only when both operands have the same sign and the result has the opposite sign. Adding numbers with different signs can never overflow.
Equivalent hardware rule: overflow = (carry into the MSB) XOR (carry out of the MSB).
Worked example: 100 + 50 in 8-bit signed.
100 = 0110 0100
50 = 0011 0010
---------
1001 0110 = -106 as two's complement
Both inputs are positive but the result's sign bit is 1. Overflow. The true answer, 150, exceeds the maximum of 127. Carry into the MSB is 1, carry out is 0; 1 XOR 0 = 1, confirming overflow.
Worked example: −100 + (−50).
-100 = 1001 1100
-50 = 1100 1110
---------
1 0110 1010 -> 0110 1010 = +106
Two negatives produced a positive: overflow (true answer −150 is below −128). Carry into MSB = 0, carry out = 1, XOR = 1.
Worked example: carry is not overflow.
1111 0000 (unsigned 240, signed -16)
+ 0010 0000 (unsigned 32, signed +32)
-----------
1 0001 0000 -> 0001 0000 = 16
As unsigned numbers, 240 + 32 = 272 does not fit; the carry out of 1 signals unsigned overflow. As signed numbers, −16 + 32 = 16 is exactly right; carry in to MSB = 1 and carry out = 1, XOR = 0, so no signed overflow.
This is why processors keep separate flags: the carry flag (C) reports unsigned overflow, and the overflow flag (V) reports signed overflow.
| Situation | Signed overflow? | Unsigned overflow? |
|---|---|---|
| Positive + positive gives negative | Yes | Not necessarily |
| Negative + negative gives positive | Yes | Yes (carry out) |
| Different signs | Never | Possible |
| Carry out of MSB | Not by itself | Yes (for addition) |
Common mistake
"A carry out of the top bit means overflow" is true only for unsigned numbers. For signed numbers, look at the signs (or carry-in XOR carry-out). In C, signed integer overflow is undefined behavior, while unsigned arithmetic wraps around modulo 2^n. In Java, both wrap silently; Math.addExact throws instead.
Fixed-point numbers
A fixed-point number is an integer with an implied binary point at a fixed position. The hardware just does integer arithmetic; the programmer remembers where the point is.
A common notation is Qm.n: m integer bits and n fraction bits. In an 8-bit Q4.4 format (4 integer bits including sign, 4 fraction bits), the value is the stored integer divided by 2^4 = 16.
Example: store 5.75 in Q4.4.
- 5.75 × 16 = 92.
- 92 in 8 bits =
0101 1100. Read as0101.1100= 4 + 1 + 0.5 + 0.25 = 5.75.
Example: store −2.5 in Q4.4.
- −2.5 × 16 = −40.
- −40 in 8-bit two's complement: 40 =
0010 1000, invert =1101 0111, add 1 =1101 1000(0xD8).
The range of Q4.4 is −8 to 7.9375 in steps of 1/16 = 0.0625. Precision is uniform across the range, which is both the strength and the weakness of fixed point. It is used where floating-point hardware is absent or too costly (small microcontrollers, some DSP code) and in money handling, where amounts are stored as integer paise or cents to avoid rounding errors.
Floating point and IEEE 754
Fixed point cannot cover both very large and very small values. Floating point solves this the same way scientific notation does: store a significand (the digits) and an exponent (where the point goes). For example, 6.02 × 10^23 and 1.6 × 10^−19.
In binary, any non-zero number can be written in normalized form:
value = (-1)^sign x 1.fraction x 2^exponent
Because the leading digit of a normalized binary number is always 1, it does not need to be stored. This free extra bit is called the hidden bit (or implicit leading 1).
The IEEE 754 formats
The IEEE 754 standard (first published in 1985, revised in 2008 and 2019) defines the formats used by essentially all modern hardware.
Single precision (32 bits, float):
+---+----------+-------------------------+
| S | Exponent | Fraction |
| 1 | 8 bits | 23 bits |
+---+----------+-------------------------+
Double precision (64 bits, double):
+---+-------------+----------------------------------+
| S | Exponent | Fraction |
| 1 | 11 bits | 52 bits |
+---+-------------+----------------------------------+
| Property | Single (float) | Double (double) |
|---|---|---|
| Total bits | 32 | 64 |
| Exponent bits | 8 | 11 |
| Fraction bits | 23 (24 with hidden bit) | 52 (53 with hidden bit) |
| Bias | 127 | 1023 |
| Normal exponent range | −126 to +127 | −1022 to +1023 |
| Largest finite value | about 3.4 × 10^38 | about 1.8 × 10^308 |
| Smallest normal positive | about 1.18 × 10^−38 | about 2.2 × 10^−308 |
| Decimal digits of precision | about 7 | about 15 to 16 |
The biased exponent
The exponent is stored as an unsigned number with a bias added: stored exponent = actual exponent + bias. For single precision the bias is 127, so an actual exponent of 3 is stored as 130, and −2 as 125. Biasing (instead of two's complement) means that positive floats compare in the same order as their bit patterns read as integers, which makes comparison hardware simpler.
Stored exponents of all zeros and all ones are reserved for special values (below), so normal numbers use stored exponents 1 to 254.
Worked example: encode −13.625 as a single-precision float
- Sign: negative, so S = 1.
- Convert the magnitude to binary. 13 =
1101. 0.625 =0.101(shown earlier). So 13.625 =1101.101. - Normalize: move the point 3 places left:
1.101101 × 2^3. - Exponent: actual 3, stored 3 + 127 = 130 =
1000 0010. - Fraction: the bits after the leading 1:
101101, padded to 23 bits with zeros:101 1010 0000 0000 0000 0000. - Assemble:
S Exponent Fraction
1 10000010 10110100000000000000000
Grouped in 4s: 1100 0001 0101 1010 0000 0000 0000 0000
Hex: C 1 5 A 0 0 0 0
So −13.625 is stored as 0xC15A0000.
Worked example: decode 0x41460000
- Binary:
0100 0001 0100 0110 0000 0000 0000 0000. - Split: S = 0; exponent =
1000 0010= 130; fraction =100 0110 0000 .... - Actual exponent: 130 − 127 = 3.
- Significand:
1.100011(hidden 1 plus fraction). - Value:
1.100011 × 2^3=1100.011= 8 + 4 + 0.25 + 0.125 = 12.375.
You can check both examples in Python:
import struct
print(struct.pack('>f', -13.625).hex()) # c15a0000
print(struct.unpack('>f', bytes.fromhex('41460000'))[0]) # 12.375
Special values
The reserved exponent patterns encode special cases.
| Exponent bits | Fraction | Meaning |
|---|---|---|
| All zeros | All zeros | ±0 (signed zero) |
| All zeros | Non-zero | Denormal (subnormal) number |
| 1 to 254 (single) | Anything | Normal number |
| All ones | All zeros | ±infinity |
| All ones | Non-zero | NaN (Not a Number) |
- Signed zero: +0 is
0x00000000, −0 is0x80000000. They compare as equal, but1 / +0 = +∞and1 / −0 = −∞. - Infinity: produced by overflow or dividing a non-zero number by zero. +∞ is
0x7F800000. Arithmetic with infinity behaves sensibly: ∞ + 1 = ∞. - NaN: the result of invalid operations such as 0 / 0, ∞ − ∞, or the square root of a negative number. A NaN is not equal to anything, including itself:
x != xis true exactly whenxis NaN. That is howisnancan be implemented. - Denormal (subnormal) numbers: with exponent bits all zero, the hidden bit becomes 0 and the exponent is fixed at −126 (single). Value =
0.fraction × 2^−126. These fill the gap between zero and the smallest normal number (about 1.18 × 10^−38), so very small results fade gradually to zero instead of jumping to it. This is called gradual underflow. The smallest positive single-precision denormal is0x00000001= 2^−149 ≈ 1.4 × 10^−45. On many processors denormal arithmetic is much slower, which is why some performance-sensitive code enables "flush to zero" modes.
Rounding
Most real numbers fall between two representable floats, so results must be rounded. IEEE 754 defines several rounding modes:
- Round to nearest, ties to even (the default): choose the nearer representable value; on an exact tie, choose the one whose last bit is 0. Rounding ties to even avoids the upward bias that "always round half up" accumulates over many operations.
- Round toward zero (truncate).
- Round toward +∞ (ceiling).
- Round toward −∞ (floor).
To round correctly, hardware keeps a few extra bits beyond the fraction during computation, commonly called the guard, round and sticky bits.
A consequence: single precision has 24 significant bits, so integers above 2^24 = 16,777,216 cannot all be represented. Storing 16,777,217 in a float gives 16,777,216.
Why 0.1 + 0.2 != 0.3
Now the famous puzzle. As we saw, 0.1 has an infinite repeating binary expansion. The double closest to 0.1 is slightly larger than 0.1; so is the double closest to 0.2. Their exact sum rounds to a double that is not the same as the double closest to 0.3.
from decimal import Decimal
print(0.1 + 0.2) # 0.30000000000000004
print(0.1 + 0.2 == 0.3) # False
print(Decimal(0.1)) # 0.1000000000000000055511151231257827021181583404541015625
print(Decimal(0.1 + 0.2)) # 0.3000000000000000444089209850062616169452667236328125
print(sum(0.1 for _ in range(10))) # 0.9999999999999999
import math
print(math.isclose(0.1 + 0.2, 0.3)) # True
Decimal(0.1) shows the exact value of the stored double. The tiny errors are not bugs in Python; every language using IEEE 754 doubles (Java, JavaScript, C) behaves the same way.
How to handle it:
- Compare with a tolerance, not
==:abs(a - b) <= tolerance, preferably a relative tolerance likemath.isclose. - Use integers for money (store paise or cents) or a decimal type such as Python's
decimal.Decimalor Java'sBigDecimalbuilt from strings. - Be aware that floating-point addition is not associative.
(a + b) + cmay differ froma + (b + c), so summing in a different order (for example in parallel) can change the last digits.
Common mistake
Floating point is not "random" or "inaccurate". Each operation is exactly the correctly rounded result of the exact operation on the stored inputs. The surprise comes from decimal inputs like 0.1 not being exactly representable in the first place.
BCD (Binary-Coded Decimal)
BCD stores each decimal digit in its own 4-bit group. 59 becomes 0101 1001 (5 then 9), not the binary 0011 1011. Only patterns 0000 to 1001 are valid; 1010 to 1111 are unused, so BCD wastes about 17% of the space compared with pure binary.
Why use it? Conversion to and from decimal display is trivial, and decimal fractions such as 0.1 are exact. It was common in calculators, clocks and older financial systems, and x86 still has a few legacy BCD adjustment instructions.
BCD addition: add digit groups in binary; if a group's result exceeds 9 or produces a carry, add 6 (0110) to correct it.
Example: 59 + 36.
Units: 1001 (9) + 0110 (6) = 1111 (15) > 9, so add 0110
1111 + 0110 = 1 0101 -> digit 0101 (5), carry 1
Tens: 0101 (5) + 0011 (3) + carry 1 = 1001 (9) valid
Result: 1001 0101 = 95
Adding 6 skips the six unused patterns 1010 to 1111.
Endianness
A 32-bit integer occupies 4 bytes. Endianness is the order in which those bytes are stored at increasing memory addresses.
- Big-endian: the most significant byte goes at the lowest address ("big end first").
- Little-endian: the least significant byte goes at the lowest address.
Example: store 0x12345678 at address 0x100.
Address: 0x100 0x101 0x102 0x103
Big-endian: 12 34 56 78
Little-endian: 78 56 34 12
Where you meet each:
- x86 and x86-64 are little-endian. ARM and RISC-V are typically run little-endian (ARM supports both).
- Network byte order is big-endian. Protocol headers such as IP and TCP send multi-byte fields most significant byte first, which is why C code calls
htonlandntohl(host-to-network and network-to-host conversions). - Java's
DataOutputStreamand the JVM class file format use big-endian.
Endianness matters when you read binary files, parse network packets, or reinterpret memory (for example, casting a pointer to a byte array). It does not affect arithmetic inside registers. A quick C test:
#include <stdio.h>
#include <stdint.h>
int main(void) {
uint32_t x = 0x12345678;
uint8_t *p = (uint8_t *)&x;
printf("%s-endian\n", p[0] == 0x78 ? "little" : "big");
return 0;
}
On a typical laptop this prints little-endian.
Character encodings
Text is stored by giving each character a number (its code point) and then deciding how to store that number as bytes (the encoding).
ASCII
ASCII (American Standard Code for Information Interchange) uses 7 bits for 128 characters: control characters (0 to 31 and 127, such as newline = 10), digits, English letters and punctuation. Useful values to know:
| Character | Decimal | Hex |
|---|---|---|
'0' | 48 | 0x30 |
'A' | 65 | 0x41 |
'a' | 97 | 0x61 |
| Space | 32 | 0x20 |
| Newline (LF) | 10 | 0x0A |
Upper- and lower-case letters differ by exactly 32 (one bit, 0x20), which is why case conversion can be done with a bit operation. Digits are contiguous, so c - '0' converts a digit character to its value.
Extended 8-bit "code pages" used values 128 to 255 for different languages, but different systems assigned them differently, causing garbled text when they mismatched.
Unicode
Unicode solves this by assigning one code point to every character in every writing system, written U+ followed by hex digits. A is U+0041, é is U+00E9, the Devanagari letter क is U+0915, the euro sign € is U+20AC, and the emoji 😀 is U+1F600. Code points range from U+0000 to U+10FFFF.
Unicode is a numbering, not a storage format. Encodings decide the bytes:
| Encoding | Unit size | Bytes per character | Notes |
|---|---|---|---|
| UTF-8 | 8 bits | 1 to 4 | ASCII-compatible; dominant on the web and in Linux |
| UTF-16 | 16 bits | 2 or 4 | Used internally by Java strings, JavaScript and Windows APIs; characters above U+FFFF use two units (a surrogate pair) |
| UTF-32 | 32 bits | 4 | Fixed width, simple, wasteful |
How UTF-8 works
UTF-8 uses the high bits of the first byte to say how many bytes the character takes. Continuation bytes always start with 10.
| Code point range | Bytes | Byte pattern (x = payload bits) |
|---|---|---|
| U+0000 to U+007F | 1 | 0xxxxxxx |
| U+0080 to U+07FF | 2 | 110xxxxx 10xxxxxx |
| U+0800 to U+FFFF | 3 | 1110xxxx 10xxxxxx 10xxxxxx |
| U+10000 to U+10FFFF | 4 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
Worked example: encode € (U+20AC).
- U+20AC is between U+0800 and U+FFFF, so it needs 3 bytes with pattern
1110xxxx 10xxxxxx 10xxxxxx(4 + 6 + 6 = 16 payload bits). 0x20ACin 16 bits:0010 0000 1010 1100.- Split into 4 + 6 + 6 bits:
0010|000010|101100. - Fill the pattern:
1110+0010=1110 0010=0xE210+000010=1000 0010=0x8210+101100=1010 1100=0xAC
- Result:
E2 82 AC.
Worked example: encode é (U+00E9).
- U+00E9 is in U+0080 to U+07FF: 2 bytes, pattern
110xxxxx 10xxxxxx(11 payload bits). 0xE9in 11 bits:000 1110 1001→ split 5 + 6:00011|101001.- Bytes:
110 00011=0xC3,10 101001=0xA9. Result:C3 A9.
The emoji U+1F600 needs 4 bytes: F0 9F 98 80. Check in Python:
print('€'.encode('utf-8').hex(' ')) # e2 82 ac
print('é'.encode('utf-8').hex(' ')) # c3 a9
print('😀'.encode('utf-8').hex(' ')) # f0 9f 98 80
print(len('😀'), len('😀'.encode('utf-8'))) # 1 4
Why UTF-8 won:
- ASCII compatibility: every ASCII file is already valid UTF-8, byte for byte.
- No byte-order problem: it is a sequence of bytes, so endianness does not apply (UTF-16 and UTF-32 need a byte order mark or an agreed order).
- Self-synchronizing: from any byte you can find the start of the character (skip bytes beginning with
10). - No zero bytes except for the NUL character itself, so C string functions keep working.
Interview tip
"String length" is ambiguous. In Python len('😀') is 1 (code points); in UTF-8 it is 4 bytes; in Java "😀".length() is 2 (UTF-16 units). Mentioning this shows real understanding, and it matters for database column sizes and API limits.
Interview questions
Q1. Why do computers use two's complement for signed integers?
It has a single representation of zero, and ordinary binary addition gives the right answer for both signed and unsigned operands, so one adder circuit serves both. Subtraction becomes addition of the negated value (invert and add 1), which needs only inverters and a carry-in. Sign-magnitude and one's complement both have two zeros and need extra correction logic.
Q2. What is the range of an n-bit two's complement number? Why is it asymmetric?
It is −2^(n−1) to 2^(n−1) − 1, for example −128 to 127 for 8 bits. Half the patterns have sign bit 1 (negative), and the other half include zero, which leaves one fewer positive value. As a result, negating the minimum value overflows back to itself.
Q3. How do you detect overflow in two's complement addition?
Overflow happens only when both operands have the same sign and the result has a different sign. In hardware, overflow equals the carry into the most significant bit XOR the carry out of it. A carry out alone signals unsigned overflow, not signed overflow; processors keep both a carry flag and an overflow flag.
Q4. Convert −45 to 8-bit two's complement.
45 is 00101101. Inverting gives 11010010, and adding 1 gives 11010011, which is 0xD3. Checking with weights: −128 + 64 + 16 + 2 + 1 = −45.
Q5. Explain the layout of an IEEE 754 single-precision number.
It has 1 sign bit, 8 exponent bits stored with a bias of 127, and 23 fraction bits. A normal value equals (−1)^S × 1.fraction × 2^(exponent − 127), with the leading 1 implied rather than stored. All-zero and all-one exponents are reserved for zero, denormals, infinity and NaN.
Q6. Why is the exponent biased rather than stored in two's complement?
With a bias, larger exponents have larger unsigned bit patterns. Because the exponent sits above the fraction, positive floats then sort in the same order as their bit patterns viewed as integers. Comparison hardware can therefore reuse integer comparison, apart from handling the sign.
Q7. Why does 0.1 + 0.2 not equal 0.3 in most languages?
0.1, 0.2 and 0.3 have infinite repeating binary expansions, so each is stored as the nearest double. The rounding errors in the stored 0.1 and 0.2 add up to a result that rounds to a different double than the one stored for 0.3. Compare floats with a tolerance, and use integers or decimal types for money.
Q8. What are NaN, infinity and denormal numbers?
Infinity (exponent all ones, fraction zero) comes from overflow or dividing a non-zero number by zero. NaN (exponent all ones, fraction non-zero) results from invalid operations such as 0/0 and is not equal even to itself. Denormals (exponent all zeros, fraction non-zero) drop the hidden 1 to represent values smaller than the smallest normal number, giving gradual underflow.
Q9. What is the default IEEE 754 rounding mode, and why ties to even?
The default is round to nearest, ties to even. When a value lies exactly halfway between two representable numbers, it picks the one with an even last bit. Always rounding halves up would bias long computations upward, while ties to even balances the errors out on average.
Q10. What is endianness? Which do x86 and network protocols use?
Endianness is the byte order of multi-byte values in memory. Big-endian puts the most significant byte at the lowest address; little-endian puts the least significant byte there. x86 is little-endian, while network protocols use big-endian (network byte order), so code converts with functions like htonl and ntohl.
Q11. How does UTF-8 encode a character, and why is it popular?
It uses 1 to 4 bytes. The leading bits of the first byte give the length (0, 110, 1110, 11110), and continuation bytes start with 10. It is backward compatible with ASCII, has no byte-order issues, is self-synchronizing, and is compact for ASCII-heavy text, which is why the web standardized on it.
Q12. What is BCD and when is it used?
Binary-coded decimal stores each decimal digit in 4 bits. It makes decimal display and exact decimal fractions easy but wastes space and needs a correction step (adding 6) during addition. It shows up in calculators, real-time clock chips and legacy financial systems.
Q13. What is sign extension and why is it needed?
When a signed value is widened to more bits, the sign bit is copied into all the new high bits so the value stays the same: 8-bit −5 (11111011) becomes 16-bit 11111111 11111011. Padding with zeros instead would turn it into a large positive number. Processors provide separate signed and unsigned load instructions for this reason.
Q14. When would you choose fixed point over floating point?
Choose fixed point when the range of values is known and limited, when you need uniform absolute precision (such as money in paise), or on hardware without a floating-point unit, such as small microcontrollers. Fixed point uses ordinary integer arithmetic, which is fast and deterministic, but you must manage scaling and overflow yourself.
Key takeaways
- Convert integers by repeated division (read remainders bottom-up) and fractions by repeated multiplication (read bits top-down). Group bits in 4s for hex and 3s for octal.
- A decimal fraction is exact in binary only if its denominator is a power of 2; 0.1 is not.
- Two's complement (invert and add 1) has one zero, shares the adder with unsigned arithmetic, and covers −2^(n−1) to 2^(n−1) − 1.
- Signed overflow: same-sign inputs, different-sign result (carry-in XOR carry-out of the MSB). A carry out alone means unsigned overflow.
- IEEE 754 single uses 1 + 8 + 23 bits with bias 127; double uses 1 + 11 + 52 with bias 1023. Normal values have a hidden leading 1.
- Special values: signed zero, denormals for gradual underflow, infinity, and NaN, which is not equal to itself.
- Never compare floats with
==; use a tolerance, and use integers or decimal types for money. - Little-endian (x86) stores the least significant byte first; network byte order is big-endian.
- Unicode assigns code points; UTF-8 stores them in 1 to 4 bytes, is ASCII-compatible and has no byte-order issues.
Next lesson
Continue with Digital logic.

