Rust Programming Language Flashcards

(31 cards)

1
Q

What happens in memory when you declare let x = 5; in Rust?

A

The integer 5 is stored on the stack frame of the current function. Since i32 (default integer type) is 4 bytes and implements the Copy trait, x is a simple 4-byte value at a known offset from the stack pointer. No heap allocation occurs; the compiler knows the exact size at compile time.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
2
Q

What does let mut x = 5; change compared to let x = 5;?

A

Nothing at the machine level. Mutability is a compile-time concept enforced by the borrow checker. The generated assembly remains identical. mut is a permission flag for the compiler, allowing modification of the variable’s memory location.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
3
Q

What is shadowing and how does it differ from mutation in memory?

A

Shadowing (let x = x + 1;) creates a new variable with the same name, allocating a new stack slot. Mutation (x = x + 1;) modifies the same memory location. Shadowing can change types (e.g., from &str to usize), which mutation cannot.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
4
Q

How are Rust’s integer types stored in memory?

A

Rust’s integer types (i8/u8, i16/u16, i32/u32, i64/u64, i128/u128, isize/usize) are stored as raw binary values in little-endian order. They have no metadata or padding. isize/usize are pointer-sized: 8 bytes on 64-bit, 4 bytes on 32-bit. Signed integers use two’s complement representation. i128/u128 may align to 8 or 16 bytes based on target ABI.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
5
Q

How are floating-point numbers stored in Rust?

A

f32 uses IEEE 754 single precision (4 bytes: 1 sign bit, 8 exponent bits, 23 mantissa bits). f64 uses IEEE 754 double precision (8 bytes: 1 sign bit, 11 exponent bits, 52 mantissa bits). Default is f64. Stored directly on stack with native alignment (4-byte for f32, 8-byte for f64). Rust doesn’t implicitly coerce between f32 and f64.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
6
Q

How is a bool stored in memory in Rust?

A

A bool occupies 1 byte (not 1 bit): 0x00 is false, 0x01 is true. Other values lead to undefined behavior. The compiler may optimize multiple bools in structs/enums to use bitfields, but each bool is guaranteed 1 byte when accessed individually. Alignment is 1 byte.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
7
Q

How is a char stored in memory in Rust?

A

char is 4 bytes (32 bits) representing a single Unicode Scalar Value (valid range: U+0000 to U+D7FF and U+E000 to U+10FFFF). Stored as a u32 internally. Values outside this range cause undefined behavior. Alignment is 4 bytes.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
8
Q

How are tuples laid out in memory?

A

Tuples are stored inline on the stack, with each element at its natural alignment. The compiler may add padding between fields for alignment and may reorder fields, though order is generally preserved. For example, (u8, u32, u8) may take 8 or 12 bytes due to padding. The total size follows struct-like layout rules. The unit tuple () is a zero-sized type (ZST) with size 0.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
9
Q

How are arrays [T; N] stored in memory?

A

Arrays consist of N contiguous elements of type T, with no metadata or padding. For example, [i32; 5] occupies 20 bytes on the stack. Length N is part of the type and known at compile time, not stored at runtime. Arrays are stack-allocated unless boxed. Alignment matches that of T.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
10
Q

What is the difference between a statement and an expression in Rust?

A

Statements perform actions without returning values (e.g., let x = 5;). Expressions evaluate to values. In Rust, most constructs are expressions: blocks {}, if, match, and function bodies. The last expression in a block (without a semicolon) becomes the block’s value. Adding a semicolon turns an expression into a statement that returns (). This is why fn foo() -> i32 { 5 } works, but fn foo() -> i32 { 5; } doesn’t — the semicolon discards the value.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
11
Q

How do functions work at the machine level in Rust?

A

Rust functions follow the platform’s C ABI by default. Arguments pass via registers (e.g., rdi, rsi, rdx on x86-64), spilling to the stack if needed. The return value goes in rax (or rax+rdx for 128-bit). A new stack frame is created with a return address pushed by the call instruction. Rust has no runtime overhead — no garbage collector or vtable lookup (unless using dyn Trait).

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
12
Q

What is ownership in Rust and what problem does it solve?

A

Ownership is a compile-time rule replacing garbage collection and manual memory management. Each value has one owner. When the owner goes out of scope, Rust calls drop(), freeing heap memory. This ensures deterministic destruction without GC pauses or manual free(). There’s zero runtime overhead, as the compiler inserts deallocation code during compilation.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
13
Q

What happens in memory during a move?

A

When you write let s2 = s1; for a heap-allocated type like String, Rust performs a shallow copy of the stack data (pointer, length, capacity) to s2’s stack location. The heap data isn’t copied, and s1 is marked as invalid at compile time, rejecting any further use. Unlike C++, Rust moves don’t require a valid “moved-from” state.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
14
Q

How is a String laid out in memory?

A

String is a fat struct on the stack with three usize fields (24 bytes on 64-bit): (1) pointer to a heap-allocated u8 buffer, (2) current length (valid UTF-8 bytes), (3) capacity (total buffer size). The heap buffer contains UTF-8 encoded bytes. String guarantees valid UTF-8 contents and is essentially a Vec<u8> with a UTF-8 invariant.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
15
Q

How does String::from("hello") allocate memory?

A

The allocator (typically jemalloc or the system allocator) is called to allocate at least 5 bytes on the heap. The 5 UTF-8 bytes of “hello” are copied from the read-only data segment (where string literals live) into this heap buffer. On the stack, a 24-byte struct is created: {pointer: <heap>, length: 5, capacity: ≥5}. The capacity may be exactly 5 or rounded up depending on the allocator.</heap>

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
16
Q

What is the difference between String and &str at the memory level?

A

String: owned, heap-allocated, growable buffer (24 bytes on stack: ptr + len + capacity).
&str: fat pointer/slice reference (16 bytes: ptr + len) that borrows a UTF-8 byte sequence stored elsewhere (heap, stack, or static memory). &str has no capacity to grow. String dereferences to &str, allowing cheap borrowing without copying.

17
Q

What does the borrow checker verify at compile time?

A

It enforces three rules: (1) Each value has one owner. (2) You can have either one mutable reference (&mut T) or multiple immutable references (&T), but not both. (3) References must be valid (no dangling pointers). It uses Non-Lexical Lifetimes (NLL) to track when references are last used, not just when they go out of scope. This is erased at runtime — references compile to raw pointers with zero overhead.

18
Q

What is a reference at the machine level?

A

A reference (&T or &mut T) compiles to a raw pointer — just a memory address (8 bytes on 64-bit). No reference counting or metadata (unless referencing a DST like a slice or trait object). The difference between & and &mut exists only at compile time for the borrow checker. Assembly for dereferencing &T, &mut T, *const T, and *mut T is identical.

19
Q

How does the borrow checker manage Non-Lexical Lifetimes (NLL)?

A

Before NLL (Rust 2018), borrow lifetimes extended to the end of the scope. With NLL, the compiler tracks the last use point of each reference using a control-flow graph. A borrow ends at its last use, not at the closing brace. For example, let r = &mut x; println!("{}", r); let r2 = &mut x; is legal because r’s borrow ends after the println, before r2 begins. This analysis uses MIR (Mid-level IR).

20
Q

What is a slice and how is it represented in memory?

A

A slice (&[T]) is a “fat pointer” — a 16-byte value on 64-bit systems with: (1) a pointer to the first element, and (2) the length. It doesn’t own data, store capacity, or know the original collection’s size. A &mut [T] is also 16 bytes, allowing mutation. Slices can reference arrays, Vecs, Strings, or any contiguous memory.

21
Q

What makes a pointer fat in Rust?

A

“Fat pointer” carries extra metadata with the address. Regular references to Sized types are “thin” (8 bytes). Fat pointers are 16 bytes and occur in two cases: (1) Slice references ([T], str) carry a length; (2) Trait object references (dyn Trait) carry a pointer to a vtable. The layout is [data_ptr, metadata]. This allows Rust to handle dynamically sized types (DSTs) through references.

22
Q

What happens at the memory level when taking a slice of a String like &s[0..5]?

A

No data is copied. A new fat pointer is created: data pointer is s.as_ptr() + 0 (pointing to s’s heap buffer), and length is 5. This 16-byte (&str) value resides on the stack. The original String’s heap buffer is shared, not duplicated. Rust verifies that byte indices (0 and 5) are valid UTF-8 boundaries, panicking if not.

23
Q

What is the Copy trait and its machine-level implications?

A

The Copy trait allows types to be duplicated by copying bytes (bitwise/memcpy). When a Copy type is assigned or passed to a function, the compiler performs a memcpy of the stack bytes, keeping the original valid. Primitive types (integers, floats, bool, char), arrays, and tuples of Copy types implement this trait. A type can only implement Copy if all its fields are Copy and it doesn’t implement Drop, as needing a destructor would cause double-free.

24
Q

What is the Drop trait and when does the compiler insert drop calls?

A

Drop is Rust’s destructor trait. When a value goes out of scope, the compiler calls drop() to free resources (heap memory, file handles, etc.). Drop order is: local variables in reverse declaration order, struct fields in declaration order. The compiler inserts drop calls at every scope exit, including early returns and panics (via stack unwinding). You can’t call drop() explicitly on a value — use std::mem::drop(val) to take ownership and let the value drop at the end of its body. Types with Drop cannot be Copy.

25
How are **structs** laid out in memory?
Rust makes no guarantees about struct field ordering or padding. The compiler can reorder fields to minimize padding. For example, struct { a: u8, b: u32, c: u8 } might be rearranged to { b: u32, a: u8, c: u8 } to reduce size from 12 to 8 bytes. Use `#[repr(C)]` for C-compatible layout and `#[repr(packed)]` to remove padding. Default repr is `repr(Rust)`.
26
What is **struct padding** and **alignment**?
Each type has an alignment requirement (address must be divisible by its alignment). For example, **u32** has 4-byte alignment, **u64** has 8-byte alignment. The compiler adds invisible padding bytes between struct fields to meet alignment. The struct's overall alignment is the maximum of its fields. Its size is rounded up to a multiple of its alignment. For instance, struct { a: u8, b: u32 } in repr(C) = 8 bytes: 1 byte a + 3 padding + 4 bytes b.
27
What are **zero-sized types (ZSTs)** and why do they matter?
ZSTs have size 0 and alignment 1. Examples: `()`, `struct Marker;`, `PhantomData`, `[u8; 0]`. They occupy no space in structs or arrays. `Vec<()>` allocates no heap memory — it tracks the count. The compiler optimizes all reads/writes to ZSTs. They're used for type-level programming: **PhantomData** expresses type relationships (variance, drop behavior) without runtime cost. **HashMap** uses a ZST as the value type for **HashSet**.
28
How are **tuple structs** and **unit structs** stored?
* **Tuple structs** (e.g., `struct Meters(f64)`) share the same memory layout as their inner types — `Meters(3.0)` is 8 bytes. * **Unit structs** (e.g., `struct MyUnit;`) are zero-sized, occupying 0 bytes. The compiler generates no storage or load/store instructions for them; they exist solely for type-system purposes.
29
How does the `impl` block work at the compiler level?
`impl` blocks don't create runtime structures. Methods compile as functions with an implicit first parameter (self, &self, or &mut self). For example, `rect.area()` compiles to `Rect::area(&rect)` — a direct function call, not a vtable lookup. Method syntax is syntactic sugar. Associated functions (no self) are namespaced free functions, with zero runtime overhead compared to free functions.
30
How are **enums** represented in memory?
**Enums** use a tagged union representation. The compiler allocates space for: (1) a discriminant tag (usually u8) to identify the active variant, plus (2) enough space for the largest variant's data. For `enum Foo { A(u32), B(u64) }`, size = 8 (tag) + 8 (largest payload, aligned) = 16 bytes typically. The compiler may optimize tag sizes or apply niche optimizations to reduce size.
31
What is **enum niche optimization**?
Compiler uses "impossible" bit patterns to remove the discriminant tag. For example, `Option<&T>` is 8 bytes, same as `&T`. Null pointer (0x0) represents `None`, while non-zero values indicate `Some`. This applies to NonZero types, references, Box, and function pointers. `Option` is 1 byte since bool uses only 0 and 1, so 2 means `None`.