
String : 문자열
&str : 문자열 리터럴
&Str[] :문자열 슬라이스
| 구분 | String | &str | &str[..] (문자열 슬라이스) |
|---|---|---|---|
| 저장 위치 | 힙(Heap) | 바이너리의 데이터 영역(프로그램 실행 중 변경되지 않는 읽기 전용(Read-Only) 메모리) | 원본이 있는 곳과 동일 |
| 가변성 | ✅ 가능 (mut 필요) | ❌ 불가능 | ❌ 불가능 |
| 소유권(Owning) | ✅ 소유권 가짐 | ❌ 소유권 없음 | ❌ 소유권 없음 |
| 참조 방식 | 소유권을 가짐 | 바이너리 데이터 영역을 참조 | 문자열의 일부를 참조 |
| 메모리 구조 | 스택에 포인터 + 길이 + 용량, 힙에 데이터 | 스택에 참조만 있음, 데이터는 바이너리에 있음 | 스택에 참조만 있음, 데이터는 원본 위치에 있음 |
| 예제 | String::from("hello") | "hello" | &s[0..5] |
| Type | String | &str | &str |
String은 Primitive type이 아닐까?String은 구조체(struct) 기반의 복합 타입이기 때문String 요소임을 명심.
use std::mem;
fn main() {
let s1 = String::from("hello");
println!("s1: {:?}", s1); // String 출력
println!("s1 포인터: {:p}", s1.as_ptr()); // 힙에 저장된 문자열의 주소
println!("s1 길이: {}", s1.len()); // 문자열 길이
println!("s1 용량: {}", s1.capacity()); // 할당된 힙 메모리 용량
println!("s1의 스택 크기: {} 바이트", mem::size_of_val(&s1)); // 스택에서 차지하는 크기
}
&T도 T처럼 메서드 사용 가능 fn main() {
let s = String::from("hello");
let ref_s: &String = &s; // `s`의 참조
println!("{}", ref_s.len()); // ✅ 자동 역참조 덕분에 String의 메서드 사용 가능
println!("{}", (*ref_s).len()); // 위아래 두가지가 같음.
}
ref_s는 &String이지만, Rust가 자동으로 (*ref_s)로 변환하여 String의 len()을 호출해줌.&str[..])는 String과 문자열 리터럴(&str) 모두에서 사용할 수 있는가?String 의 일부 슬라이스 가능 → String은 힙(Heap)에 저장되므로 그 일부를 참조할 수 있음.&str)의 일부도 슬라이스 가능 → &str은 데이터 영역(Read-Only)에 저장되므로 그 일부를 참조할 수 있음. fn main() {
let s1 = String::from("hello world");
let s2 = "hello world"; // 문자열 리터럴
let slice1 = &s1[0..5]; // ✅ `String`에서 슬라이스
let slice2 = &s2[0..5]; // ✅ 문자열 리터럴에서 슬라이스
println!("{}", slice1); // hello
println!("{}", slice2); // hello
}
&str[..])과 문자열 리터럴(&str) 모두 같은 &str 타입아닌가?"hello")과 문자열 슬라이스(&s[0..5])는 모두 &str 타입
&str은 메모리의 "어디에 저장되어 있는지"가 아니라, 단순히 "문자열 데이터를 참조하는 형식"을 의미. 이 문자열은 힙에 있을수도 있고(String) 아니면 바이너리의 데이터 영역(&Str)에 있을 수 있기 때문.
fn print_str(s: &str) {
println!("{}", s);
}
fn main() {
let s = String::from("hello");
print_str(&s); // ✅ `&String`이 `&str`로 자동 변환됨
print_str(&s[..]); // ✅ `&s[..]`도 `&str`
}
&str을 사용하면 &String과 &str 둘 다 받을 수 있어 유연한 코드 작성이 가능합니다. fn first_word(s: &str) -> &str {
스택에서 사라지는 것은 변수(s1, s2), 즉 문자열을 가리키는 포인터(참조)만 제거
문자열 리터럴 자체는 데이터 영역에 남아 있으므로, 이후에도 동일한 문자열을 사용 가능.
그러면, 같은 문자열 리터럴을 가르키는 서로 다른 변수에 대해서, 문자열을 가리키는 포인터의 주소값을
보면 같다는 점을 알 수 있음.
fn main() {
let s1 = "hello";
let s2 = "hello";
println!("{:p}", s1); // 같은 주소 출력
println!("{:p}", s2); // 같은 주소 출력
}
&str[..])는 기존 문자열(String 또는 &str)의 일부를 참조하는 &str 타입의 참조자입니다.&str 타입의 참조자이기 때문 &str)과 크게 다른 점 중 하나.fn main() {
let s = String::from("hello world");
let slice = &s[0..5]; // ✅ "hello" 부분만 참조
println!("{}", slice);
}
먼저 Rust의 자동 역참조 기능을 강조하지 않았던 점이, 초반 이해에서 뭔가 부족하다고 느꼈던 이유 중 하나였던 것 같다. 그리고 결국 (1) String, (2) 문자열 리터럴 그리고 (3) 문자열 슬라이스 3가지 비슷비슷한 무언가가 나오는데, 3가지를 엄격하게 구분하기 보다는, 연결성을 생각하여 진행해야 한다. 특히 (2), (3)은 같은 타입이지만, 데이터가 어디에 저장되어 있는지(바이너리의 데이터 타입 vs 참조된 데이터가 저장되어 있는 곳), 수명 등등을 고려해야 한다. 그리고 중요한 점은,
&str은 메모리의 "어디에 저장되어 있는지"가 아니라, 단순히 "문자열 데이터를 참조하는 형식"을 의미하기 때문에, 이 점을 생각하며 진행하면 좋을 것 같다.