Hexadecimal is base sixteen. Its digits are 0–9 and A–F, with A=10 through F=15. Each hex digit represents exactly four binary positions, called a nibble. You can translate each group independently because 16 is 2^4.
1. Group from the right
For an unsigned spelling, group binary positions in fours starting at the right. Pad the leftmost group with zeroes if necessary. The binary 101101 becomes 0010 1101: 2D in hex, 45 in decimal. The number 0x2D is the same value as 0b101101. Hexadecimal is a notation, not a signedness decision.
| Hex | Binary | Hex | Binary |
|---|---|---|---|
| 0 | 0000 | 8 | 1000 |
| 1 | 0001 | 9 | 1001 |
| 2 | 0010 | A | 1010 |
| 3 | 0011 | B | 1011 |
| 4 | 0100 | C | 1100 |
| 5 | 0101 | D | 1101 |
| 6 | 0110 | E | 1110 |
| 7 | 0111 | F | 1111 |
An octet is exactly eight bits. A C++ byte is the size of char and has CHAR_BIT bits, at least eight. The course platform has eight-bit bytes and provides uint8_t. Keep this assumption explicit when describing files and protocols rather than claiming every possible C++ byte is an octet.
2. Separate digit order from byte order
The most-significant hex digit is written on the left, just as in decimal. That convention does not tell you how a multibyte value is stored in memory. The numeric value 0x1234 can be serialized as bytes {0x12,0x34} in big-endian order or {0x34,0x12} in little-endian order.
Endianness orders bytes, not the printed bits inside every digit. A hex dump shows a byte sequence; you need the file or protocol's word boundaries and byte-order contract to turn that sequence into integers. Printing an integer in hex does not reverse its digits on a little-endian machine.
#include <array>
#include <cstdint>
#include <print>
int main() {
const std::array<std::uint8_t, 2> bytes{0x12, 0x34};
auto big = (std::uint64_t{bytes[0]} << 8) | bytes[1];
auto little = (std::uint64_t{bytes[1]} << 8) | bytes[0];
std::println("{:04X} {:04X}", big, little);
}These arithmetic readers work independently of the host CPU's byte order. Avoid reinterpreting a pointer into an arbitrary byte buffer as an integer pointer: alignment, object lifetime and byte order are separate concerns.
3. Extract and combine nibbles
For a value in 0–255, byte >> 4 selects the high nibble, and byte & 0x0F selects the low one. To combine validated nibbles, shift the high one left four and OR in the low one. Validate each digit before that operation.
#include <print>
int main() {
constexpr unsigned byte = 0xA7;
std::println("{} {}", byte >> 4, byte & 0x0F);
std::println("{:02X}", (10u << 4) | 7u);
}Use an unsigned value wide enough for the final shift before performing the shift. A uint8_t input often promotes to int; a large shift of that promoted int is not a 64-bit operation. For an eight-byte little-endian reader, uint64_t{bytes[i]} << (8*i) has the intended width, and the last count is 56.
4. Specify a text codec instead of guessing
The lab accepts complete pairs of hex digits, case-insensitively, and emits uppercase pairs. Empty byte text is valid. Odd length, non-hex digits, separators, spaces and the characters 0x are rejected. This wire-text API is not the C++ literal parser: its prefix and separator rules are intentional.
A leading zero byte must survive a round trip. For example 00AF10 denotes three bytes, even though an integer formatter might print only AF10. The word readers accept one through eight bytes and reject empty or longer input. Their explicit endian names tell callers how to interpret the sequence.
Test all 256 possible byte values, uppercase/lowercase input, zero bytes, malformed pairs, one-byte words and an eight-byte word whose high bit is set. Confirm that 12 34 decodes differently through the two word readers on the same machine. Then continue to one's complement.
Reading: Cambridge's number-system introduction connects bases and four-bit grouping. The number course keeps these lessons in prerequisite order.