티스토리 뷰
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]
*/