CodeRigor

FREE LESSON

Binary: bits, place values & unsigned words

Convert binary and decimal, distinguish a value from its spelling, and validate fixed-width unsigned words without overflowing or shifting by the type width.

Binary is base two. Each digit is a bit, either 0 or 1. Moving one position to the left doubles its weight. Count positions from zero at the right: bit 0 has weight 1, bit 1 has weight 2, bit 2 has weight 4, then 8, 16, 32 and so on. The most-significant bit (MSB) is the leftmost bit of the selected word; the least-significant bit (LSB) is the rightmost.

Start here after the C++ values and sequences lessons. These exercises use strings, string_view, integer arithmetic and explicit errors. You do not need assembly, templates or an electronics background.

1. Read powers of two

For the spelling 101101, add only the weights whose bits are 1:

Weight 32 16 8 4 2 1
Digit 1 0 1 1 0 1

That is 32 + 8 + 4 + 1 = 45. Leading zeroes do not change an unsigned value: 00101101 is also 45. They can still matter to a protocol that requires a particular display width.

One value, several spellings
The subscript names the base. Changing the spelling does not change the integer value.
One value, several spellings
The subscript names the base. Changing the spelling does not change the integer value.

C++ uses 0b for binary literals and 0x for hexadecimal. A leading zero without b or x marks an octal literal: 010 is eight, not ten. Apostrophes may separate literal digits for readability; they do not add bits. A prefix chooses the base, not a fixed-width storage type. Choose a type or suffix deliberately.

C++
#include <print>
int main() {
    constexpr auto bits = 0b0010'1101;
    std::println("{} {} {}", bits, bits == 45, 010);
}

2. Write a binary spelling

Repeatedly divide a nonnegative integer by two and record the remainders. The remainders arrive least-significant first, so read them backwards. For 13: 13/2 gives remainder 1; 6/2 gives 0; 3/2 gives 1; 1/2 gives 1. Read back 1101.

For parsing in the other direction, scan left to right. Each new digit extends the prefix: value = value*2 + digit. Parsing 1101 visits 1, 3, 6, 13. Validate that a character is 0 or 1 before using it. Treat malformed text as an error; subtracting '0' from an arbitrary character does not validate a digit.

A parser extends its prefix
The next digit moves every old place value left by one binary position.
A parser extends its prefix
The next digit moves every old place value left by one binary position.

The build uses a deliberately small text grammar: 1–64 ASCII bits with no prefix, spaces, sign or separators. This differs from C++ source-literal syntax. Its maximum length makes every parsed unsigned prefix representable in uint64_t. The exact-width types are available on this course's platform; not every C++ implementation supplies every exact-width typedef.

3. Width determines the unsigned range

A w-bit unsigned word has 2^w possible patterns, representing 0 through 2^w−1. Four bits cover 0–15; eight cover 0–255. The mathematical value 256 exists, but it needs nine unsigned bits. The value and the chosen container width are separate decisions.

Add binary digits with carries: 0+0=0; 0+1=1; 1+1 produces a zero and a carry of one; 1+1+carry produces one and a carry. A carry between positions is ordinary. A carry leaving the selected word says the unsigned sum needs more bits. In an eight-bit model, 250+10=260 leaves the low word 4 and an outgoing carry of 1.

An eight-bit boundary
The ninth bit is an outgoing carry. Keeping eight bits discards that bit; a checked API could instead reject the operation.
An eight-bit boundary
The ninth bit is an outgoing carry. Keeping eight bits discards that bit; a checked API could instead reject the operation.

4. The C++ expression has its own type

Unsigned arithmetic is modulo 2^N for its actual result type. Smaller integer types commonly undergo integer promotion first. On this platform uint8_t promotes to int, so adding one to its value 255 computes the int value 256. A subsequent conversion back to uint8_t yields zero. Do not describe that expression as an eight-bit addition all the way through.

C++
#include <cstdint>
#include <print>
int main() {
    std::uint8_t byte = 255;
    auto next = byte + 1;
    std::println("{} {}", next, static_cast<unsigned>(static_cast<std::uint8_t>(next)));
}

For bit operations, use an appropriate unsigned type. Validate a shift count before shifting: a count at least the promoted left operand's width is undefined behavior. uint64_t{1} << 63 is useful here; shifting it by 64 is not a way to make a 64-bit mask. Use the maximum value for the full-width case. The standard's shift rules define the operand promotions and count limit.

5. Build a strict converter

Implement parse and format with explicit width and syntax checks. The formatter must emit most-significant-first digits, preserve leading zeroes and reject a value that does not fit. Test zero, one, a mixed pattern, maximum values, invalid digits and widths 0, 1, 64 and 65. For example, format(45,8) is 00101101, while format(16,4) must fail instead of losing the fifth bit.

Next: hexadecimal and byte order, then the two signed encoding models. See the number course for the sequence.

Reading: C++ integer literal syntax.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Why hardware uses two states; pair this introduction with the place-value and conversion exercises.

Follow the carries in a small fixed-width word. The lesson separately explains C++ promotions and arithmetic rules.