만약 문자열에 데이터를 추가한다면 스택 프레임에서는 capacity와 length만 변동하며, 실제 데이터는 힙 메모리에 추가된다.
str
str은 러스트에서 기본적으로 제공하는 데이터 타입 중 하나이며, ‘문자열 리터럴’이라고도 불린다.
NOTE 리터럴(Literal)이란? 리터럴은 변수에 할당되는 데이터 그 자체를 의미한다. 예를 들어 프로그램에서 a = 100 코드는 a라는 변수에 100이라는 값이 할당된다. 여기서 100은 주소나 특정 의미를 내포한 개념이 아니라 그 자체 숫자로의 100을 의미하며, 이런 데이터 값들을 리터럴이라고 한다. 리터럴의 경우 데이터 세그먼트 영역에 저장되기에 불가변적이다. 예시는 아래 그림과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13
my_name: &str [–––––––––––] +–––+–––+ stack frame │ • │ 6 │ +–│–+–––+ │ +––+ │ preallocated +–V–+–––+–––+–––+–––+–––+ read-only │ P │ a │ s │ c │ a │ l │ memory +–––+–––+–––+–––+–––+–––+
문자열 리터럴이란 문자(char)들의 시퀀스 또는 문자열 슬라이스를 의미한다. 슬라이스란 String의 일부를 참조하는 방식이며, 데이터에 대한 소유권이 없다. (슬라이스에 대해서 더 자세히 알고 싶다면 슬라이스 타입을 참고할 수 있다.) 문자열 리터럴로 변수를 생성하는 방법은 아래와 같다.
1 2 3 4 5 6
// 문자들의 시퀀스 let v_str = "This is str"; let t_str = "This is str"; // 문자열 슬라이스 let string = String::from("This is str"); let slice = &string[0..8];
문자열 리터럴인 변수 ‘v_str’의 타입은 &str으로 데이터 세그먼트에 저장된 값을 참조하는 방식이다. 그렇기 때문에 똑같은 값을 가진 다른 변수 ‘t_str’을 생성하면 동일한 메모리 주소를 가진다는 것을 확인할 수 있다. 반면 String으로 생성된 변수 ‘string’을 문자열 슬라이스로 가져온 ‘slice’는 메모리 값이 다른 것을 확인할 수 있다.
fnmain() { let v_str = "This is str"; println!("{:?}", v_str); println!("{:?}", v_str as *const _); let t_str = "This is str"; println!("{:?}", t_str); println!("{:?}", t_str as *const _); let string = String::from("This is str"); let slice = &string[..]; println!("{:?}", slice); println!("{:?}", slice as *const _); } //동일한 메모리 값을 가져온다. "This is str" 0x55e7e918c141 "This is str" 0x55e7e918c141 //내용은 동일하지만 다른 메모리 값을 가져온다. "This is str" 0x55e7e9b00ad0
그 이유는 문자열(string)의 경우 힙(heap) 메모리에 데이터를 올려두며 이를 참조해가기 때문이다. 이를 그림으로 나타내면 아래와 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
my_name: String last_name: &str [––––––––––––] [–––––––] +–––+––––+––––+–––+–––+–––+ stack frame │ • │ 16 │ 13 │ │ • │ 6 │ +–│–+––––+––––+–––+–│–+–––+ │ │ │ +–––––––––+ │ │ │ │ │ [–│––––––– str –––––––––] +–V–+–––+–––+–––+–––+–––+–––+–V–+–––+–––+–––+–––+–––+–––+–––+–––+ heap │ P │ a │ s │ c │ a │ l │ │ P │ r │ e │ c │ h │ t │ │ │ │ +–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+–––+
함수를 작성하다보면 문자열 참조를 파라미터로 받을 때 두 옵션 중에 헷갈릴 수 있다. 아래 예시 코드를 보자.
1 2 3 4 5 6 7 8 9 10 11
fnmain() { let string = String::from("Understanding the String concept?"); let v_str = "This is str";
print_data(&string); print_data(v_str); }
fnprint_data(data: &str) { println!("printing data {} ", data); }
위 코드는 main()에서 ‘string’이라는 String타입의 데이터를 생성하고 이를 문자열 슬라이스로 참조하여 print_data()로 넘겨준다. 러스트에서 문자열 변수의 참조는 자동으로 문자열 슬라이스로 전환한다. 따라서 print_data()은 ‘string’도 받을 수 있고, ‘v_str’도 인자값으로 받을 수 있다.
1 2 3 4 5 6 7 8 9 10 11
fnmain() { let string = String::from("Understanding the String concept?"); let v_str = "This is str";
print_data(&string); print_data(v_str); }
fnprint_data(data: &String) { println!("printing data {} ", data); }
For more information about this error, try `rustc --explain E0308`. error: could not compile `playground` due to previous error
그 이유는 &String이 &str을 포함하고 있지 않기 때문이다. &String은 힙 메모리에 올라간 데이터를 참조하고자 하기에 문자열 리터럴로 저장된 ‘v_str’은 받지 못한다. 따라서 해당 코드가 작동하려면 .to_string()을 통해 힙 메모리로 올려야 하는데 참조만 할 변수에 사용하기에는 리소스 낭비라고도 볼 수 있다.