[Rust] String / &str / String slices

suwonyoo·2025년 2월 3일

Rust Study | Special

목록 보기
2/4
post-thumbnail

1. 용어 정리

  • String : 문자열

  • &str : 문자열 리터럴

  • &Str[] :문자열 슬라이스

    구분String&str&str[..] (문자열 슬라이스)
    저장 위치힙(Heap)바이너리의 데이터 영역(프로그램 실행 중 변경되지 않는 읽기 전용(Read-Only) 메모리)원본이 있는 곳과 동일
    가변성✅ 가능 (mut 필요)❌ 불가능❌ 불가능
    소유권(Owning)✅ 소유권 가짐❌ 소유권 없음❌ 소유권 없음
    참조 방식소유권을 가짐바이너리 데이터 영역을 참조문자열의 일부를 참조
    메모리 구조스택에 포인터 + 길이 + 용량, 힙에 데이터스택에 참조만 있음, 데이터는 바이너리에 있음스택에 참조만 있음, 데이터는 원본 위치에 있음
    예제String::from("hello")"hello"&s[0..5]
    TypeString&str&str

2. 짚고 넘아갸아 할 부분들

1. 왜 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)); // 스택에서 차지하는 크기
  }

2. Rust의 자동 역참조(auto deref) 기능 덕분에 &TT처럼 메서드 사용 가능

  • 이 부분을 간과하고 넘어가서 초반에 햇갈렸던 것 같음.,
    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)로 변환하여 Stringlen()을 호출해줌.

3. 문자열 슬라이스(&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
}


4.문자열 슬라이스(&str[..])과 문자열 리터럴(&str) 모두 같은 &str 타입아닌가?

  • 문자열 리터럴("hello")과 문자열 슬라이스(&s[0..5])는 모두 &str 타입
  • 다만, 문자열 리터럴은 데이터 영역(Read-Only), 슬라이스는 원본의 일부를 참조함
  • 또한 문자열 리터럴은 프로그램 종료 시 까지 유지된다.

    &str메모리의 "어디에 저장되어 있는지"가 아니라, 단순히 "문자열 데이터를 참조하는 형식"을 의미. 이 문자열은 힙에 있을수도 있고(String) 아니면 바이너리의 데이터 영역(&Str)에 있을 수 있기 때문.

5.Rust에서는 Deref Coercion(역참조 강제 변환)로 &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 {

&str (문자열 리터럴)

1. Rust에서 문자열 리터럴은 프로그램 실행이 끝날 때까지 유지됨.

  • 스택에서 사라지는 것은 변수(s1, s2), 즉 문자열을 가리키는 포인터(참조)만 제거
    문자열 리터럴 자체는 데이터 영역에 남아 있으므로, 이후에도 동일한 문자열을 사용 가능.

  • 그러면, 같은 문자열 리터럴을 가르키는 서로 다른 변수에 대해서, 문자열을 가리키는 포인터의 주소값을
    보면 같다는 점을 알 수 있음.

  fn main() {
      let s1 = "hello";
      let s2 = "hello";

      println!("{:p}", s1); // 같은 주소 출력
      println!("{:p}", s2); // 같은 주소 출력
  }

2. 불변 참조자이므로, 값을 변경할수 없는 것은 자명


&str[] (문자열 슬라이스)

1. 문자열 슬라이스(&str[..])는 기존 문자열(String 또는 &str)의 일부를 참조하는 &str 타입의 참조자입니다.

2. 문자열 슬라이스는 원본 문자열 데이터를 소유하지 않는다.

  • &str 타입의 참조자이기 때문

3. 원본이 유효한 동안만 슬라이스도 유효

  • Borrowing 규칙 적용하여, 원본의 수명에 맞춰서 문자열 슬라이스의 수명도 결정됨
  • 프로그램이 종료될 때까지 유지되는 문자열 리터럴(&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메모리의 "어디에 저장되어 있는지"가 아니라, 단순히 "문자열 데이터를 참조하는 형식"을 의미하기 때문에, 이 점을 생각하며 진행하면 좋을 것 같다.

0개의 댓글