티스토리 뷰

반응형

Ownership

  • Rust 프로그램이 메모리를 어떻게 관리할지 결정하는 규칙들
  • 소유권 규칙에 따라 컴파일 시점에 메모리 할당 및 해제가 결정된다.
  • 변수의 범위 scope 가 끝나면 메모리 해제 가능
fn main() {
    {
        let s = "헬로";
    }
    // s 를 호출할 수 없다 => s의 메모리가 해제된다. 
}

Scalar 데이터 타입은 Stack 에서 관리하기 때문에 소유권 개념이 없고,

그 외 타입은 Heap 에서 관리하기 때문에 소유권에 따라 메모리 할당 및 해제가 일어난다.

소유권 규칙

  • Rust 에서 모든 값은 소유자가 있다.
  • 한 시점에 딱 하나의 소유자만 있을 수 있다.
  • 소유자의 범위가 끝나면 값도 제거된다.

일반적인 스칼라 타입 데이터는 스택에서 값 자체가 복사 되기 때문에 아래와 같은 사용이 가능하다.

let x = 3; 
let y = x; // y는 x의 값을 복사해서 갖게 된다. (scalar type 이라)
println!("x = {x} , y = {y}");

그러나 힙 메모리에서 관리하는 String 타입 변수의 경우, s1의 소유권이 s2로 넘어가게 되면 그 이후부터 s1은 사용할 수 없다.

let s1 = String::from("헬로");
let s2 = s1; // s1의 소유권인 s2로 넘어간다.
println!("s2={s2}"); 
println!("s1={s1}"); // s1의 소유권이 이미 없기때문에 여기서 에러가 난다. 


(Rust 는 utf-8 방식으로 String을 저장하기 때문에 한글자당 3byte를 차지한다)

s1과 s2 둘 다 사용하고 싶다면 아래와 같이 clone() 을 이용해서 복사해서 사용해야 한다.

fn main() {
    let s1 = String::from("헬로");
    let s2 = s1.clone(); // s1 을 복사해서 s2 를 만든다.
    println!("s2={s2}"); 
    println!("s1={s1}"); 
}

함수 호출 시 소유권 이동

아래와 같이 String 타입 변수(s1)를 함수에 넘겨 사용하게 되면 소유권이 이전되어 main 함수에서 더이상 s1을 사용할 수 없다.

fn main() {
  let s1 = String::from("헬로");
  string_length(s1); // s1의 소유권이 string_length() 함수로 넘어가버림
  println!("s={s1}"); // ERROR!
}

fn string_length(s: String) {
    println!("문자열 s의 길이는: {}", s.len()); // 문자열 s의 길이는: 6 -> utf-8은 3byte 라서.
}

스택에서 관리하는 정수타입 x 의 경우 복사 되서 관리되기 때문에 소유권 이동이 없어 아래와 같이 사용 가능 하다.

fn main() {
    let s1 = String::from("헬로");

    let x = 3;
    double(x);
    println!("X={x}");
}

fn double(x: i32) {
    println!("x={x} 입니다.");
}

소유권을 잃지 않는 방법

⇒ 소유권을 받은후 리턴으로 다시 준다.

fn main() {
    let s = String::from("헬로");
    let (len, s) = string_length(s);

    println!("문자열 {s} 의 길이는 {len}");
}

fn string_length(s: String) -> (usize, String) {
    (s.len(), s)
}

이렇게 하면 함수를 써서 len 도 받고, 이전 처럼 s 를 이용해서 문자열을 조작할 수 도 있다.

하지만 len 만 받으려고 만든 함수 string_length 에서 소유권 이전 문제를 회피하기 위해 s를 같이 반환해줘야 한다. 매번 이렇게 하는 것은 굉장히 불편한 일이다. 이 문제는 소유권 임대로 해결할 수 있다.

소유권 임대

&를 이용해서 소유권을 임대해 줄 수 있다. 이를 통해 코드를 깔끔하게 작성할 수 있게 된다.

fn main() {
    let s = String::from("헬로");
    let len = string_length(&s);
    println!("문자열 {s} 의 길이는 {len}");
}

fn string_length(s: &String) -> usize {
   s.len()
}

변수 s의 소유권을 string_length() 함수에 임대해 줬기 때문에 함수에서 빠져나올 때 소유권 해지 활동이 일어나지 않고, main() 함수에서 s를 계속 사용 할 수 있게 되었다.

  • &s : 참조 값
    • 특정 데이터가 위치한, 접근할 수 있는 주소
    • 해당 데이터는 누군가 다른 소유자가 소유하고 있는 데이터에 접근한다.
    • 그럼 참조값은 여러 개 존재할 수 있나? → 그렇다!

참조는 기본적으로 불변이다. 함수 내에서 참조 변수의 값을 변경하고 싶다면 가변참조를 만들어 사용해야 한다. 가변참조는 &mut 를 사용한다.

fn main() {
    let mut s1 = String::from("hello");
    addString(&mut s1);

    println!("{s1}");
}

fn addString(s: &mut String) {
    s.push_str(", pott!")
}
  • &mut : mutable reference
    • s1 대해 가변참조는 1개보다 많이 존재할 수 없다. 그냥 참조는 여러개 사용 가능
    • 불변참조가 살아있는데 가변참조가 생길 수 없다. 불변참조는 자신의 값이 바뀔거라고 예상할 수 없기 때문.
fn main() {
    let mut s = String::from("헬로");

    let r1 = &mut s;
    let r2 = &mut s; // 이건 안됨.

    println!("r1 = {}, r2 = {}", &r1, &r2); 
}
fn main() {
    let mut s = String::from("헬로");

    let r1 = &s;
    let r2 = &s;
    let r3 = &mut s; // 이것도 안됨. mut를 쓰게 되는 순간부터 배타참조가 됨. 

    println!("r1 = {}, r2 = {}, r3 = {}", &r1, &r2, &r3); 
}
fn main() {
    let mut s = String::from("헬로");

    let r1 = &s;
    let r2 = &s;
    println!("r1 = {}, r2 = {}", &r1, &r2); 

    let r3 = &mut s;  
    println!("r3 = {}", &r3); // 이건 가능. 이후에 r1, r2 를 호출하지 않는다면 이후에는 r3 의 소유권만 활성화된것으로 판단하기 때문.
    // 즉, 선언은 별 문제 없음. 사용시점이 중요함. 
}

슬라이스 Slice 타입

  • 어떤 모음에 있는 (일부) 연속된 요소들을 참조하는 방법
  • 슬라이스 변수는 일종의 참조 변수라서 소유권을 갖지 않는다.
fn main() {
    let s = String::from("헬로 월드");

    let word: &str= &s[0..6]; // 슬라이스 참조를 통해 문자열의 일부만 참조해서 가져올 수 있다.
    println!("word = {}", word);

    let word = &s[7..]; // 7번부터 마지막까지 참조한다.
    println!("word = {}", word);

    let word = &s[..6]; // 처음부터 6바이트까지 참조한다.
    let word = &s[..]; // 전체 문자열 참조.
}

/*
word = 헬로
word = 월드
*/
  • 참고: 문자열 리터럴은 사실 슬라이스다!
    • 문자열 리터럴은 &str 타입이다. 이것은 이진 파일의 특정 지점을 가리키는 슬라이스이다. 이것이 문자열 리터럴이 변경할 수 없는 이유이다.
    • 문자열 리터럴은 스택도 아니고 힙도 아니고 별도의 공간에 고정적으로 확보된다.
    • String : 힙메모리에 있는 변경가능한 값 / 문자열 리터럴: 어딘가에 저장된 문자열을 참조하고 있는 string slice
fn main() {
        let hello: &str = "헬로";
    let s = String::from("헬로 월드");
    let word = first_word(&s);
    println!("word = {}", word);
}

fn first_word(s: &String) -> &str { // 여기에 hello 는 넘길 수 없다. 왜? hello 는 문자열 참조가 아니라 문자열 슬라이스라서. 타입이 다름. 
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i]; //공백을 찾았으면 공백까지만 반환
        }
    }

    &s[..] // 공백을 못찾았으면 전체 반환
}
fn main() {
        let hello: &str = "헬로";
    let s = String::from("헬로 월드");
    let word = first_word(hello); // first_word(&s) 도 물론 가능!
    println!("word = {}", word);
}

fn first_word(s: &str) -> &str { // 이렇게 하면 hello 쓸 수 있음.
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i]; //공백을 찾았으면 공백까지만 반환
        }
    }

    &s[..] // 공백을 못찾았으면 전체 반환
}

이런 이유 때문에 함수를 만들때 문자열 인자는 &str 로 받는게 좋다.

슬라이스는 문자열 이외의 타입에도 사용할 수 있다.

fn main() {
    let a = [1,2,3,4,5];
    let slice = &a[1..3];

    println!("a = {:?}, slice = {:?}", a, slice); 
}

/*
a = [1, 2, 3, 4, 5], slice = [2, 3]
*/
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/02   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
글 보관함