5.2 Integer Literals: Decimal, Hex, Octal, Binary, and Byte
Right, let’s talk about how you tell a computer, “Here, have a number.” It seems simple, but like most things in computing, we’ve devised a few different ways to do it, each with its own historical baggage and modern use case. I’m going to show you the whole cast of characters: decimal (our everyday numbers), hex, octal, binary, and the slightly oddball byte literal. Pay attention; this is where a lot of subtle, head-scratching bugs are born.
The Default: Decimal Literals
You already know this one. You write a number, the compiler reads a number. It’s the path of least resistance.
let meaning_of_life = 42;
let debt_in_dollars = -15000;
There’s no special prefix here because, well, we’re human and we think in base-10. The compiler assumes any number that starts with a digit (or a minus sign, but that’s technically part of the expression, not the literal itself) is a decimal integer. The only “gotcha” to remember is that you can’t use underscores as a thousands separator like in some languages (1,000 is a syntax error). Instead, you can use underscores as a visual separator within the literal, which is brilliantly useful for large numbers.
let one_billion = 1_000_000_000; // Much clearer than 1000000000
let max_u32 = 4_294_967_295; // The underscores are purely for our benefit.
Hexadecimal (Base-16) Literals
When you need to talk directly to the machine, or more commonly, when you’re dealing with memory addresses, bit masks, or color codes, you’ll live in hexadecimal. It’s base-16, so we need digits beyond 9. We use a through f (case-insensitive, thank goodness). To signal your intent, you prefix the number with 0x.
let black_in_rgb = 0x000000;
let read_write_permissions = 0o644; // Wait, no, that's octal. See? Easy to mix up!
let read_write_permissions = 0b110_100_100; // Let's use binary instead. Better.
let magic_constant = 0xdeadbeef; // A classic. The compiler doesn't judge.
Why hex? Because one hex digit (0xF) represents exactly four bits (1111). Two hex digits (0xFF) represent exactly one byte (11111111). This makes translating between what’s in your memory and what’s on your screen almost trivial. It’s the language of bit-twiddling.
Octal (Base-8) Literals
Here’s where we start with the historical baggage. Octal is base-8, prefixed with 0o. Its heyday was with systems where a word size was a multiple of 3 bits (look, I don’t judge the past, I just explain it). This is… less common now.
let old_school_file_permission = 0o755; // rwxr-xr-x, for the curious.
The biggest pitfall here is that in some other languages (I’m looking at you, JavaScript and a bunch of legacy C code), a leading 0 alone used to imply octal. This led to absolute nightmares. Imagine let permission = 0755; meaning 493 in decimal instead of 755. Rust, in its infinite wisdom, requires the explicit 0o, saving you from that particular trauma.
Binary (Base-2) Literals
When you absolutely, positively need to specify a bit pattern directly, binary is your friend. Prefixed with 0b, it’s the most explicit way to define a number. It’s incredibly useful for hardware registers, protocol headers, or any time you’re defining a bit flag.
let flags = 0b1010001;
let mask = 0b0010000;
// Check if the fifth bit is set
if flags & mask != 0 {
println!("Yep, the flag is set!");
}
For anything more than a few bits, the underscore separator becomes your best friend. Comparing these two:
let confusing = 0b1010101010101010;
let clear = 0b1010_1010_1010_1010; // Ah, a repeating pattern!
Always use the underscores. Your future self, debugging at 2 AM, will thank you.
The Oddball: Byte Literals
This one is exclusively for u8 values (a single byte). It’s denoted by a b followed by a character, and it gives you the ASCII code (or, more accurately, the byte value) for that character.
let byte_a = b'a'; // This is a u8 with value 97
let byte_newline = b'\n'; // This is a u8 with value 10
It’s a concise and self-documenting way to get a u8 value for an ASCII character. The key thing to remember—and this is a common point of confusion—is that this is not a char and it’s not a string. A char in Rust is a 4-byte Unicode scalar value. A byte literal is a single, solitary byte. Trying to use it where a char is expected will result in a very polite but firm error from the compiler.
let character: char = b'a'; // ERROR! Mismatched types: expected `char`, found `u8`
Use these when you’re working with raw byte streams, like parsing a file format where you’re looking for specific ASCII delimiters. For everything else textual, use 'a' or "a".
So there you have it. Your full toolkit for saying “number” to the Rust compiler. Use decimal for math, hex for memory and masks, binary for bits, octal for old Unix permissions, and bytes for, well, bytes. Knowing which to use, and why, is half the battle of writing clear, effective systems code. The other half is remembering that 0x is hex and 0o is octal and not mixing them up at 3 AM. I speak from experience.