
간단하면서도 재미있는 주제이다. 우리는 왜 str을 그대로 쓰지 못하고, &str형식으로 써야 하며, String은 단독으로 사용할 수 있을까? 이를 완벽하게 이해하고 설명할 수 있기 위해, 밑바닥부터 차례대로 정리해보는 시간을 가져보겠다.
프로그램이 실행되기 전에, 컴파일러가 소스 코드를 분석하고 최적화하며 기계어(Binary 명령어)로 변환
Source code ->LLVM IR -> Assembly -> Machine Code(CPU가 이해할 수 있는 언어)
컴파일 시 변수의 타입이 결정되는 정적 타이핑(Static Typing)을 사용하므로, 컴파일 타임에 모든 타입 오류를 검출
Syntax Analysis, Type Checking, Ownership & Borrowing Check, Lifetime Analysis, Optimization
이 때, const, static, 문자열 리터럴(&str)과 같은 정적 데이터는 컴파일 타임에 메모리가 할당됨
| 타입 | 저장 위치 | 런타임 변경 가능 여부 | 특이점 |
|---|---|---|---|
const | 🔹 Read-Only 데이터 영역 (임베디드 가능) | ❌ 변경 불가능 | 값이 실행 파일에 직접 포함됨 (컴파일 시 결정됨) |
static | 🔸 Read-Only or Static Data (RW 가능) | ✅ mut 가능 | static mut이면 런타임 변경 가능 (데이터 영역에 위치) |
&str (문자열 리터럴) | 🔹 Read-Only 데이터 영역 | ❌ 변경 불가능 | 실행 파일 내에 저장됨 |
이 때, 메모리 할당 및 해제는 스택(Stack)과 힙(Heap)을 활용합니다.
| 단계 | 역할 | Rust에서의 예제 |
|---|---|---|
| 소스 코드 작성 | Rust 코드 작성 | fn main() { let x = 5; println!("{}", x); } |
| 컴파일 (AOT) | 소스 코드 → 기계어 변환 | rustc main.rs |
| LLVM IR 변환 | 중간 표현(LLVM IR)로 변환 | alloca i32, store, load, add |
| 어셈블리 변환 | CPU 명령어로 변환 | mov eax, 5, add eax, 10 |
| 기계어 변환 | CPU가 실행할 바이너리 생성 | 10111000 00000101 ... |
| OS가 실행 파일 로드 | 실행 파일을 메모리에 적재 | ./main |
| CPU가 명령어 실행 | 기계어를 실행하여 결과 출력 | mov, add, call println |
| 시스템 콜 | OS에 출력 요청 | syscall (write stdout) |
| 프로그램 종료 | 종료 및 메모리 해제 | Rust의 자동 메모리 해제 |
i32, bool) String, Vec<T>) str은 DST(Dynamic Sized Type)이다. 크기가 동적으로 변한다.질문) 아니
let s : str = "hello";하면 되는거 아닌가요?, 해당 문자열 리터럴
hello이미 크기 결정되어 있는데....? 아니str의 설계 자체가 원래 컴파일 타임에서 알 수 없는 것. 해당str의 원래 크기가 동적으로 변할 수 있는 타입으로 설계되었기 때문에
str을 저장한다.str은 Heap에 저장되어 있는데... 근데 해당 str을 직접 다뤄야 할 일이 무조건 생길텐데..String을 예시로 들어보자.String은 str을 힙에 저장하고, 이를 가리키는 포인터를 스택에 유지한다.fn main() {
let s: String = String::from("hello");
}
스택(Stack) 힙(Heap)
-------------------------------------------------
s: String {
ptr: ────────────▶ "hello" (5바이트)
length: 5
capacity: 5 (or more)
}
-------------------------------------------------
이러면 결국 해당 구조체는 런타임때 크기를 알 수 있으므로, 이를 스택에 저장하면 말끔하게 해결, 스택의 구조체를 이용하여 Heap의 str데이터를 접근하면 문제 해결.
런타임때 Heap영역의 str의 크기가 변하더라도, stack안의 String자체의 크기는 항상 일정함. pointer를 썻기 때문.
pub struct Vec<T> {
ptr: *mut T, // ✅ 힙 데이터를 가리키는 포인터
length: usize, // ✅ 현재 저장된 데이터 개수
capacity: usize, // ✅ 할당된 총 메모리 크기
}
결론적으로
str은 설계 자체부터 DST이므로, 크기가 정해져 있지 않아 Heap에 할당을 해야 하며, 이를 사용하기 위해서는 stack에서 포인터 기반 구조체를 통해 가능한 케이스구나.
fn main() {
let s: &str = "hello"; // ✅ `&str`을 통해 문자열 리터럴을 참조
}
Stack Data (Read-Only)
-------------------------------------------------
s: &str ────────────▶ "hello" (5바이트, Read-Only)
-------------------------------------------------
str이 아닌 &str 자체는 크기가 고정되어 있으므로 이는 stack에 할당이 된다.정리
String자체는 컴파일 타임에 고정된 크기를 지닌Sized이다. 그렇기 때문에 해당 포인터 기반 구조체가 Stack에 런타임때 메모리 할당 후 저장이 된다. 하지만String내부적으로 Heap에str를 저장하기 때문에,str는 DST이므로 런타임에 동적으로 변할 수 있다.str은unsized타입(or DST 타입)으로, 컴파일 타임 and 런타임에 크기를 가늠할 수 없는 바이트 스트림이다.