1. What does the str slice type (&str) represent in Rust?
Answer: An immutable view/slice over UTF-8 encoded string data — a borrowed reference to string bytes, not an owner.
&str (a string slice) is a (&[u8], len) pair: a pointer to UTF-8 bytes plus a length. It borrows data that lives elsewhere — in a String’s heap buffer, in static binary memory (string literals), or in a &[u8]:
let owned: String = String::from("hello");
let view: &str = &owned; // borrows owned's buffer
let lit: &str = "hello"; // points into the binary
Properties:
- Immutable — you can’t modify string data through
&str(use&mut stronly for ASCII-heavy cases, rarely). - No ownership — the backing storage must outlive the slice (lifetime-checked).
- UTF-8 — bytes are guaranteed valid UTF-8.
Contrast: String is the owned, growable heap buffer; &str is the cheap borrowed view. The interview answer: &str is an immutable, borrowed view of UTF-8 string bytes living elsewhere.
2. What is the size of a char in Rust?
Answer: 4 bytes (32 bits) — it holds a Unicode Scalar Value.
A Rust char is always a fixed 4 bytes, representing a single Unicode Scalar Value (any code point except surrogates):
let a: char = 'a'; // 4 bytes
let z: char = 'ℤ'; // 4 bytes
let crab: char = '🦀'; // 4 bytes
Key distinction: char (4 bytes) vs UTF-8 encoding (1–4 bytes per character). The same character encodes to 1–4 bytes in a String/&str, but as a char value it always occupies 4 bytes regardless of the character. This makes char arrays/vectors uniform and indexable by scalar value.
This differs from C (char = 1 byte), Java (char = 2 bytes, UTF-16 code unit). The interview answer: 4 bytes — a Unicode scalar value, independent of its UTF-8 encoded length.
3. What does String::from(“hello”) allocate?
Answer: A growable, UTF-8 buffer on the heap, plus a stack-side handle holding pointer, length, and capacity.
String is the owned, resizable string type. Its layout:
- Heap: the actual UTF-8 bytes in a growable buffer (can reallocate to grow).
- Stack: the
Stringvalue itself — three words: a pointer to the heap buffer, the current length (bytes), and the capacity (allocated bytes).
let s = String::from("hello"); // heap: h,e,l,l,o stack: (ptr, 5, 5)
This three-word structure is why a String is cheap to move and resize: moving copies the handle, growing reallocates the heap buffer and updates the metadata. When the String is dropped, the heap buffer is freed (RAII). The interview answer: a heap-allocated UTF-8 buffer with a stack handle (pointer, length, capacity).
4. What is the output of the following slice operation?
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
println!("{}", hello);
}
Output: hello.
&s[0..5] slices byte indices 0 through 4 (inclusive of start, exclusive of end). The first five bytes of "hello world" are h,e,l,l,o, so the slice is "hello". Output: hello.
(These indices are byte offsets, and &s[a..b] will panic if they don’t land on UTF-8 character boundaries — here the string is ASCII so any split is fine.) The interview answer: hello — &s[0..5] takes bytes 0–4.
5. What happens if a string slice index falls in the middle of a multi-byte UTF-8 character?
Answer: A runtime panic — Rust won’t create a slice on a non-character boundary.
&s[a..b] is checked at runtime: the byte index must lie on a UTF-8 character boundary. If a or b splits a multi-byte character, slicing panics rather than producing invalid UTF-8.
let s = "héllo"; // 'é' is 2 bytes
let bad = &s[1..3]; // panics: byte 1 is inside 'é'
Why panic instead of returning a result: the Index operator has no way to signal failure gracefully, and silently producing a broken slice would violate the UTF-8 invariant. For safe boundary handling, use char_indices() to find real boundaries or the get()/get_mut() methods which return Option:
let ok = s.get(2..3); // Option<&str> — None if not a boundary
The interview answer: it panics at runtime because indices must fall on valid UTF-8 character boundaries.
6. What is the difference between panic! and returning Result::Err?
Answer: panic! is for unrecoverable errors (unwinds/aborts the program); Result::Err is for recoverable errors the caller should handle.
panic!— signals a bug or impossible state the program can’t recover from (e.g., index out of bounds, violated invariant). It unwinds the current thread’s stack (running destructors) or aborts, depending on config. Callers don’t handle it — the program fails.Result<T, E>— the expected failure mode: file missing, parse failed, I/O error. The function returnsOk/Err, and the caller decides what to do (propagate with?, match, unwrap).
Rust’s guidance is explicit: use Result for errors you anticipate and want handled; reserve panic! for programmer errors and unrecoverable states. Libraries overwhelmingly return Result rather than panicking, so callers keep control. The interview answer: panic! = unrecoverable program failure (unwind/abort); Result::Err = recoverable error the caller must handle.
7. What is the default mechanism for handling a panic! in Rust binaries?
Answer: Stack unwinding — Rust walks up the stack, running destructors, until it aborts the program (or the panic is caught).
By default, a panic causes the runtime to unwind the stack of the panicking thread:
- Each frame’s local variables are dropped (their
Dropimplementations run), releasing resources. - Unwinding continues until the thread’s top, where the program aborts (or
catch_unwindintercepts it).
This is configurable in Cargo.toml — setting panic = "abort" skips unwinding entirely and aborts immediately (smaller binaries, faster, but no destructors run):
[profile.release]
panic = "abort"
Default is unwind, giving a chance to clean up. The interview answer: by default Rust unwinds the stack, calling destructors, before aborting; panic = "abort" can change that to immediate abort.
8. Which keyword is used to enter an unchecked environment where Rust’s safety guarantees are relaxed?
Answer: unsafe.
unsafe is Rust’s escape hatch — it opens a block, function, or trait impl where you take on responsibility the compiler normally enforces. It grants five superpowers:
- Dereference a raw pointer (
*const T,*mut T). - Call an unsafe function or method.
- Access/modify a mutable static.
- Implement an unsafe trait.
- Access fields of a union.
Crucially, unsafe does not disable all checks or make code “untyped” — it only relaxes specific guarantees, and the programmer must uphold the safety invariants manually (which is why it should be wrapped in safe APIs and heavily commented). The interview answer: unsafe — a scoped opt-out from some of Rust’s safety guarantees.
9. Which of the following operations is allowed exclusively inside an unsafe block or function?
Answer: Dereferencing a raw pointer (*const T / *mut T).
Raw pointer dereference is one of the operations reserved for unsafe. Safe Rust forbids it because a raw pointer could be null, unaligned, dangling, or aliased, and the borrow checker can’t validate it:
let mut x = 5;
let p: *mut i32 = &mut x; // create raw pointer (safe)
unsafe { *p = 10; } // dereference requires unsafe
The other options are safe Rust: Box::new() allocates normally, returning Result from main is supported (the standard pattern), and calling trait-object methods is fully safe. The interview answer: dereferencing raw pointers requires unsafe; everything else listed is safe.
10. What is the difference between raw pointers (*const T, *mut T) and standard references (&T, &mut T)?
Answer: References are guaranteed valid, non-null, borrow-checked; raw pointers bypass lifetimes, may be null, and ignore aliasing rules.
- References (
&T,&mut T): safe pointers the borrow checker validates — always non-null, always point to valid data while in scope, and aliasing rules are enforced (one&mutXOR many&). Created from values, used freely in safe code. - Raw pointers (
*const T,*mut T): the compiler imposes no guarantees — they can be null, dangling, unaligned, or aliased. Creating them is safe; dereferencing requiresunsafe(you manually promise they’re valid).
let mut x = 5;
let r: &mut i32 = &mut x; // safe, compiler-checked
let p: *mut i32 = &mut x; // raw pointer
unsafe { *p += 1; } // manual safety promise
Raw pointers are for FFI, low-level data structures, and escape hatches. The interview answer: references are borrow-checked, guaranteed-valid pointers; raw pointers carry no compiler guarantees and need unsafe to dereference.
Premium Content
Unlock Top 25 - Part 2 and all premium lessons with a subscription.
From ₹199.99/year — See plans