Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Numeral Systems
Write numbers in binary, octal, and hexadecimal formats for bit manipulation and memory work.
Numeral systems: decimal, binary, hexadecimal, and octal
This lesson is optional. Future lessons reference hexadecimal numbers, so you should at least understand the basic concept before continuing.
In everyday life, we use decimal numbers, where each digit can be 0 through 9. Decimal is also called "base 10" since there are 10 possible digits. We count: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ... By default, C++ assumes all numbers are decimal.
int score { 75 }; // 75 is assumed to be a decimal number
In binary, only 2 digits exist: 0 and 1, making it "base 2". In binary, we count: 0, 1, 10, 11, 100, 101, 110, 111, ... Longer binary numbers are often grouped by fours for readability (e.g., 1010 0011).
Decimal and binary are examples of numeral systems - collections of symbols (digits) used to represent numbers. C++ supports four main numeral systems in order of popularity: decimal (base 10), binary (base 2), hexadecimal (base 16), and octal (base 8).
Nomenclature note: In both decimal and binary, 0 and 1 mean the same thing. We call these "zero" and "one."
But what about 10? The number 10 appears after the last single-digit number. In decimal, 10 equals nine plus one, and we call it "ten."
In binary, 10 represents one plus one (equivalent to decimal two). Calling binary 10 "ten" would be confusing since "ten" means nine plus one, not one plus one.
Therefore, names like "ten," "eleven," "twelve" are reserved for decimal. In non-decimal systems, we say "one-zero," "one-one," "one-two," etc. Binary 101 isn't "one hundred one" - it's "one-zero-one."
Octal and hexadecimal literals
Octal is base 8, using only digits: 0, 1, 2, 3, 4, 5, 6, and 7. In octal, we count: 0, 1, 2, 3, 4, 5, 6, 7, 10, 11, 12, ... (notice: no 8 or 9, jumping from 7 to 10).
| Decimal | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Octal | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 13 |
To create an octal literal, prefix your number with 0 (zero):
#include <iostream>
int main()
{
int x { 015 }; // 0 prefix means this is octal
std::cout << x << '\n';
return 0;
}
This program prints:
13
Why 13 instead of 15? Because numbers are displayed in decimal by default, and 15 octal equals 13 decimal.
Octal is rarely used in modern C++. Avoid using octal literals, as the `0` prefix can be confusing and lead to unexpected bugs.
Hexadecimal is base 16. In hexadecimal, we count: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F, 10, 11, 12, ...
| Decimal | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Hexadecimal | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F | 10 | 11 |
Lowercase letters (a-f) also work, though uppercase is more common.
To create a hexadecimal literal, prefix your number with 0x:
#include <iostream>
int main()
{
int x { 0x2A }; // 0x prefix means this is hexadecimal
std::cout << x << '\n';
return 0;
}
This program prints:
42
You can use 0X prefix, but 0x is conventional and more readable.
Numeral systems comparison table
Here are the four numeral systems side-by-side:
Decimal 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Binary 0 1 10 11 100 101 110 111 1000 1001 1010 1011 1100 1101 1110 1111
Octal 0 1 2 3 4 5 6 7 10 11 12 13 14 15 16 17
Hexadecimal 0 1 2 3 4 5 6 7 8 9 A B C D E F
Decimal 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
Binary 10000 10001 10010 10011 10100 10101 10110 10111 11000 11001 11010 11011 11100 11101 11110 11111
Octal 20 21 22 23 24 25 26 27 30 31 32 33 34 35 36 37
Hexadecimal 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F
Each row follows the same pattern: The rightmost digit increments from 0 to (base-1). When reaching (base), it resets to 0 and the left digit increments by 1. If that left digit reaches (base), it resets to 0 and the digit to its left increments. This pattern continues indefinitely.
Using hexadecimal to represent binary
Since hexadecimal has 16 possible values per digit, one hexadecimal digit represents exactly 4 bits. Therefore, two hexadecimal digits represent one full byte precisely.
| Hexadecimal | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Binary | 0000 | 0001 | 0010 | 0011 | 0100 | 0101 | 0110 | 0111 | 1000 | 1001 | 1010 | 1011 | 1100 | 1101 | 1110 | 1111 |
Consider a 32-bit integer with binary value 1011 0101 1110 0011 0100 1101 0001 1010. Because of length and repetition, that's hard to read. In hexadecimal, this same value is: B5E3 4D1A, which is much more concise. Hexadecimal is often used to represent memory addresses or raw memory data.
Binary literals
Before C++14, binary literals weren't supported. Hexadecimal provided a workaround (still seen in older code):
#include <iostream>
int main()
{
int flags {}; // assume 16-bit ints
flags = 0x0001; // assign binary 0000 0000 0000 0001
flags = 0x0004; // assign binary 0000 0000 0000 0100
flags = 0x0010; // assign binary 0000 0000 0001 0000
flags = 0x0080; // assign binary 0000 0000 1000 0000
flags = 0x00FF; // assign binary 0000 0000 1111 1111
flags = 0x0C7A; // assign binary 0000 1100 0111 1010
flags = 0xAB12; // assign binary 1010 1011 0001 0010
return 0;
}
In C++14 and later, use binary literals with the 0b prefix:
#include <iostream>
int main()
{
int flags {}; // assume 16-bit ints
flags = 0b1; // assign binary 0000 0000 0000 0001
flags = 0b11; // assign binary 0000 0000 0000 0011
flags = 0b1100; // assign binary 0000 0000 0000 1100
flags = 0b10101010; // assign binary 0000 0000 1010 1010
return 0;
}
Digit separators
Long literals can be hard to read. C++14 adds the ability to use single quotation marks (') as digit separators:
#include <iostream>
int main()
{
int flags { 0b1010'1100 }; // assign binary 1010 1100
long population { 7'800'000'000 }; // much easier than 7800000000
return 0;
}
Note: The separator cannot appear before the first digit:
int flags { 0b'1010'1100 }; // error: ' used before first digit
Digit separators are purely visual and don't affect the literal's value.
Use digit separators to make long literals more readable. For binary, group by 4 bits (nibbles). For decimal, group by 3 digits (thousands).
Outputting values in decimal, octal, or hexadecimal
By default, C++ outputs values in decimal. However, you can change output format using std::dec, std::oct, and std::hex I/O manipulators:
#include <iostream>
int main()
{
int value { 42 };
std::cout << value << '\n'; // decimal (by default)
std::cout << std::hex << value << '\n'; // hexadecimal
std::cout << value << '\n'; // still hexadecimal
std::cout << std::oct << value << '\n'; // octal
std::cout << std::dec << value << '\n'; // back to decimal
std::cout << value << '\n'; // still decimal
return 0;
}
This prints:
42
2a
2a
52
42
42
Note: Once applied, the I/O manipulator remains active until changed again.
Outputting values in binary
Outputting binary is trickier since std::cout doesn't include built-in binary support. Fortunately, the C++ standard library provides std::bitset (in the <bitset> header).
To use std::bitset, define a std::bitset variable and specify how many bits to store. The bit count must be a compile-time constant. std::bitset can be initialized with any integral value in any format (decimal, octal, hex, or binary).
#include <bitset> // for std::bitset
#include <iostream>
int main()
{
// std::bitset<8> means we want to store 8 bits
std::bitset<8> pattern1 { 0b1100'1001 }; // binary literal for binary 1100 1001
std::bitset<8> pattern2 { 0xC9 }; // hexadecimal literal for binary 1100 1001
std::cout << pattern1 << '\n' << pattern2 << '\n';
std::cout << std::bitset<4>{ 0b1101 } << '\n'; // create temporary std::bitset and print it
return 0;
}
This prints:
11001001
11001001
1101
The last line creates a temporary (unnamed) std::bitset object with 4 bits, initializes it with binary literal 0b1101, prints it in binary, then discards the temporary object.
Outputting values in binary using Format/Print Library (Advanced)
In C++20 and C++23, better options exist for printing binary via the Format Library (C++20) and Print Library (C++23):
#include <format> // C++20
#include <iostream>
#include <print> // C++23
int main()
{
std::cout << std::format("{:b}\n", 0b1101); // C++20, {:b} formats as binary digits
std::cout << std::format("{:#b}\n", 0b1101); // C++20, {:#b} formats as 0b-prefixed binary
std::println("{:b} {:#b}", 0b1101, 0b1101); // C++23, format/print two arguments with newline
return 0;
}
This prints:
1101
0b1101
1101 0b1101
Summary
- Numeral systems are collections of symbols used to represent numbers
- Decimal (base 10): The default system in C++, uses digits 0-9
- Binary (base 2): Uses only 0 and 1, created with
0bprefix (e.g.,0b1010) - Hexadecimal (base 16): Uses digits 0-9 and letters A-F, created with
0xprefix (e.g.,0x2A) - Octal (base 8): Uses digits 0-7, created with
0prefix - avoid using this - Hexadecimal and binary: One hexadecimal digit represents exactly 4 binary bits (a nibble)
- Digit separators: Use single quotes (
') to make long literals readable (e.g.,0b1010'1100or7'800'000) - Output formatting: Use
std::hex,std::oct, andstd::decto change output format - Binary output: Use
std::bitsetfor binary output, orstd::formatwith{:b}in C++20+
Understanding different numeral systems, especially hexadecimal and binary, is essential for low-level programming, bit manipulation, and working with memory addresses.
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!