Numeral Systems
Write numbers in binary, octal, and hexadecimal formats for bit manipulation and memory work.
What Is a Numeral System?
A numeral system is a set of digit symbols plus a rule for combining them into numbers. The rule is always the same shape: pick a base, allow that many digit symbols, and let each position in the number stand for the next power of the base.
Everyday counting uses decimal, base 10, with the ten digits 0 through 9. C++ accepts three other bases in source code: binary (base 2, digits 0 and 1), octal (base 8, digits 0 through 7), and hexadecimal (base 16, digits 0 through 9 followed by A through F). All four describe the same integers. They differ only in how those integers are spelled.
Hexadecimal turns up constantly once you reach memory addresses, colour values, and bit manipulation, and binary literals are how bit patterns get written down. You do not need to be fluent in base conversion to continue, but you do need to recognise these spellings when they appear.
One Value, Four Spellings
The four bases are not four kinds of number. They are four ways to write one number, and the compiler converts every one of them to the same bits.
#include <iostream>
int main()
{
int fromDecimal{ 206 };
int fromBinary{ 0b11001110 };
int fromOctal{ 0316 };
int fromHex{ 0xCE };
std::cout << fromDecimal << ' ' << fromBinary << ' '
<< fromOctal << ' ' << fromHex << '\n';
return 0;
}
206 206 206 206
Four different-looking literals, one stored value, and one printed form. The base you write in is a property of the source text, not of the variable. Once the compiler has read the literal, the original spelling is gone, which is why every variable above prints as 206.
Telling the Compiler Which Base You Meant
Digits alone are ambiguous: 11 could be three in binary, nine in octal, eleven in decimal, or seventeen in hexadecimal. C++ resolves this with a prefix on the literal.
| Base | Prefix | Example literal | Decimal value |
|---|---|---|---|
| Binary (2) | 0b |
0b11001110 |
206 |
| Octal (8) | 0 |
0316 |
206 |
| Decimal (10) | none | 206 |
206 |
| Hexadecimal (16) | 0x |
0xCE |
206 |
Two details are worth noting. Hexadecimal digits may be written in either case, so 0xce and 0xCE are the same literal, and the prefix itself may be written 0X, though almost nobody does. Binary literals arrived in C++14; older code that needed bit patterns wrote them in hexadecimal instead, which is why you still meet constants like 0xCE where 0b11001110 would read more clearly.
Octal is the odd one out, because its marker is not a letter but a bare leading zero. That collides with a habit people bring from everywhere else: padding numbers with zeros to line them up.
#include <iostream>
int main()
{
int paddedCode{ 0415 };
std::cout << paddedCode << '\n';
return 0;
}
269
Nothing here looks wrong, and nothing is reported as wrong. The leading zero silently made this an octal literal, and octal 415 is decimal 269.
Never write a leading zero on a number you mean as decimal. The compiler will read it as octal without complaint, and the value will be wrong by an amount that depends on the digits. This is the entire reason octal has a bad reputation: it is rarely wanted and easy to trigger by accident.
Reach for decimal by default, binary when the individual bits are the point, and hexadecimal when you are writing a byte pattern or an address. Leave octal alone.
Reading a Number Off Its Digits
Every position in a number carries a weight, and the weights are the powers of the base counting right to left. In hexadecimal, those weights are 1, 16, 256, and so on, so 0xCE works out as C times 16 plus E times 1, which is 12 times 16 plus 14, which is 206. In binary the weights are 1, 2, 4, 8, 16, and upward, so 0b11001110 adds 128, 64, 8, 4, and 2 to reach the same 206.
Counting works the same way in every base too. Increment the rightmost digit until it runs out of symbols, then reset it to 0 and carry 1 into the position on its left, repeating leftward whenever a position runs out. All that changes is how soon a position runs out. A binary digit is spent after 1, an octal one after 7, a decimal one after 9, and a hexadecimal one holds on until F.
Read non-decimal numbers digit by digit rather than as quantities. Binary `101` is "one-zero-one", not "one hundred one", because words like "ten" and "hundred" carry decimal weights baked into them. Reading the digits individually keeps the base you are working in from getting lost in the words.
The Sixteen Patterns Worth Knowing
The first sixteen values are where the four systems visibly diverge, and they are the only conversions you ever need to memorise.
| Decimal | Binary | Octal | Hexadecimal |
|---|---|---|---|
| 0 | 0000 | 0 | 0 |
| 1 | 0001 | 1 | 1 |
| 2 | 0010 | 2 | 2 |
| 3 | 0011 | 3 | 3 |
| 4 | 0100 | 4 | 4 |
| 5 | 0101 | 5 | 5 |
| 6 | 0110 | 6 | 6 |
| 7 | 0111 | 7 | 7 |
| 8 | 1000 | 10 | 8 |
| 9 | 1001 | 11 | 9 |
| 10 | 1010 | 12 | A |
| 11 | 1011 | 13 | B |
| 12 | 1100 | 14 | C |
| 13 | 1101 | 15 | D |
| 14 | 1110 | 16 | E |
| 15 | 1111 | 17 | F |
Octal is the first to run out of symbols and roll over, at 8. Decimal rolls over at 10. Hexadecimal keeps a single digit all the way to 15, which is exactly what makes it useful.
Larger values follow the same rules, and a few landmarks are worth recognising on sight:
| Decimal | Binary | Octal | Hexadecimal |
|---|---|---|---|
| 32 | 10 0000 | 40 | 20 |
| 64 | 100 0000 | 100 | 40 |
| 100 | 110 0100 | 144 | 64 |
| 128 | 1000 0000 | 200 | 80 |
| 255 | 1111 1111 | 377 | FF |
| 256 | 1 0000 0000 | 400 | 100 |
| 1000 | 11 1110 1000 | 1750 | 3E8 |
Why Hexadecimal and Binary Fit Together
Look again at the sixteen-row table and read the binary column. Every value from 0 to 15 fits in exactly four bits, and every four-bit pattern maps to exactly one hexadecimal digit. That is not a coincidence: 16 is 2 to the fourth power.
The consequence is that converting between binary and hexadecimal needs no arithmetic at all. Split the bits into groups of four from the right, replace each group with its hexadecimal digit, and you are done. Two hexadecimal digits therefore cover exactly one 8-bit byte, and eight of them cover a 32-bit integer.
Consider a 32-bit value whose bits are 0110 1101 0010 1111 1000 0100 1110 0011. Thirty-two ones and zeros are hard to read and harder to compare against another thirty-two. Group and translate them and the same value becomes 6D2F 84E3, which fits in a glance. This is why memory addresses, raw memory dumps, and colour codes are almost always shown in hexadecimal: it is binary with the redundancy squeezed out, and nothing about the underlying bits is lost.
Octal has the same property with three bits per digit, which is why it survives at all. It suits systems built on 3-bit fields, and Unix file permissions are the one place most programmers still meet it.
Making Long Literals Readable
Long runs of digits are hard to scan, so C++14 allows a single quotation mark as a digit separator anywhere between two digits of a literal.
#include <iostream>
int main()
{
int flowRate{ 0b1101'0110 };
long distanceKm{ 1'429'000'000 };
std::cout << flowRate << ' ' << distanceKm << '\n';
return 0;
}
214 1429000000
The separators exist purely for the reader. They never change the literal's value, and they leave no trace in the program's output, as the printed 1429000000 shows.
The one placement rule is that a separator cannot come before the first digit. Writing 0b'1101'0110 puts one straight after the base prefix, and GCC rejects it with error: digit separator after base indicator. Group binary by four bits so each group matches one hexadecimal digit, and group decimal by three so the groups match thousands.
Changing the Base on the Way Out
Choosing a base for a literal affects only how you write the value. Choosing a base for output is a separate decision, made at the point of printing, and std::cout defaults to decimal for both.
The manipulators std::hex, std::oct, and std::dec change that default. They are sticky: applying one changes every subsequent insertion until another one replaces it.
#include <iostream>
int main()
{
int cargoWeight{ 206 };
std::cout << cargoWeight << '\n';
std::cout << std::hex << cargoWeight << '\n';
std::cout << cargoWeight << '\n';
std::cout << std::oct << cargoWeight << '\n';
std::cout << std::dec << cargoWeight << '\n';
return 0;
}
206
ce
ce
316
206
The third line prints ce even though it names no manipulator, because std::hex from the line before is still in force. Returning to decimal takes an explicit std::dec; nothing resets the stream for you.
Binary is missing from that set, so std::cout cannot print it directly. std::bitset, from the <bitset> header, fills the gap. A std::bitset is told at compile time how many bits it holds, and it can be initialized from an integer written in any base.
#include <bitset>
#include <iostream>
int main()
{
std::bitset<8> fromBinary{ 0b1100'1110 };
std::bitset<8> fromHex{ 0xCE };
std::cout << fromBinary << '\n';
std::cout << fromHex << '\n';
std::cout << std::bitset<6>{ 0b100110 } << '\n';
return 0;
}
11001110
11001110
100110
The first two lines print identically, which is the point: 0b1100'1110 and 0xCE were never different values. The third line builds an unnamed six-bit std::bitset, prints it, and discards it immediately, which is the shortest way to show a bit pattern without keeping a variable around.
C++20 adds a more direct route through std::format, where a format specifier selects the base for that one argument. The b, x, and o specifiers request binary, hexadecimal, and octal, and prefixing any of them with # includes the corresponding literal prefix in the output.
#include <format>
#include <iostream>
int main()
{
std::cout << std::format("{:b}", 0xCE) << '\n';
std::cout << std::format("{:#b}", 0xCE) << '\n';
std::cout << std::format("{:#x} {:#o}", 206, 206) << '\n';
return 0;
}
11001110
0b11001110
0xce 0316
Unlike the stream manipulators, a format specifier applies to one argument and nothing else, so there is no sticky state to undo. C++23 goes one step further with std::println, which formats and prints in a single call, though the platform compiler here targets C++20.
Looking Forward
Bit patterns become a working tool rather than a curiosity once you meet the bitwise operators, and std::bitset reappears there as the standard way to hold and inspect a set of bit flags. Hexadecimal shows up again as soon as pointers are printed, since addresses are conventionally displayed in base 16.
Summary
A numeral system is a base plus a set of digits, with each position weighted by the next power of the base. C++ reads four of them and tells them apart by prefix alone: 0b opens a binary literal, a bare leading 0 an octal one, 0x a hexadecimal one, and no prefix at all leaves you in base 10. The prefix affects only the source text, since every literal becomes the same bits and prints in decimal by default.
The leading-zero rule for octal is the trap to remember. A zero-padded number such as 0415 is octal, and it converts silently to the wrong decimal value.
Hexadecimal earns its place because 16 is 2 to the fourth: one hexadecimal digit is exactly four bits, two digits are exactly one byte, and converting between the two bases is a lookup rather than a calculation. That is why byte patterns and addresses are written in hexadecimal.
Digit separators, written as single quotation marks between digits, make long literals readable without changing their value. They may go anywhere except before the first digit. Group binary by four and decimal by three.
For output, std::hex, std::oct, and std::dec switch the base of a stream and stay switched until replaced. std::bitset prints binary, which the stream manipulators cannot. std::format selects a base per argument with {:b}, {:x}, and {:o}, adding the literal prefix when written as {:#b} and so on.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Numeral Systems - Quiz
Test your understanding of the lesson.
Practice Exercises
Number System Conversions
Practice representing the same value in different number systems: decimal, binary, hexadecimal, and octal.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!