Programming🧑💻/Cpp
L-value와 R-value
생각 깎는 아이
2025. 2. 6. 21:43
표현식 ( Expression )
표현식이란, 하나의 값으로 평가될 수 있는 코드의 단위입니다.
아래 코드에서 표현식은 계산되어 하나의 값이됩니다.
int a = 5; // 5는 표현식
int b = a + 3; // a + 3은 표현식
int c = b * (a-1); // b * (a-1)은 표현식, (a-1)도 표현식
L-Value와 R-Value 개념
L-value와 R-value는 표현식(expression)의 값 범주를 구분하는 방법입니다.
L-Value의 'L'은 'Left'에서 왔습니다. 이는 할당 연산자(=)의 왼쪽에 올 수 있는 표현식을 의미합니다.
L-Value의 핵심적인 특징은 메모리상에서 이름과 주소를 가지고 있어 여러번 참조 할 수 있다는 점입니다.
int x = 10; // x는 L-value
위의 코드에서 x는 L-value입니다. x라는 변수는 메모리상에 특정 위치를 가리키는 이름이 있고, 프로그램이 실행되는 동안 그 위치에 계속 존재하기 때문에 같은 범위내에서 활용이 가능합니다.
R-Value의 'R'은 'Right'에서 왔습니다. 이는 할당 연산자(=) 오른쪽에 올 수 있는 표현식을 의미합니다.
일반적으로 임시적인 값을 의미하며 표현식이 예산된 후 곧바로 사라지는 임시값이기 때문에 다음에 사용이 불가능합니다.
int x = 10; // 10은 R-value
int y = x + 5; // x + 5는 R-value
세 가지 매개변수 전달 방식
값에 의한 전달 ( Pass by Value )
- 함수 호출 시 매개변수의 완전한 복사본이 생성됩니다
- 함수 내부에서 다시 한 번 복사가 일어나므로, 총 두 번의 복사가 발생합니다
- 원본 데이터는 안전하게 보호되지만, 메모리와 성능 측면에서는 비효율적입니다
int main() {
// 1단계: normalStr 생성
std::string normalStr = "Hello World";
/* 이때 일어나는 일:
- "Hello World" 문자열 리터럴은 실제로 프로그램의 상수 영역에 저장됩니다
- std::string 객체는 스택에 생성됩니다 (normalStr)
- 실제 문자열 데이터의 복사본이 힙에 할당됩니다
- normalStr은 이 힙 메모리를 가리키게 됩니다 */
storeByValue(normalStr);
// 함수 호출이 끝나면 s와 b는 소멸되며, 그들이 가리키던 힙 메모리도 해제됩니다
}
void storeByValue(std::string s) {
/* 2단계: 매개변수 s 생성
- s는 새로운 std::string 객체로 스택에 생성됩니다
- normalStr이 가리키는 문자열의 새로운 복사본이 힙에 생성됩니다
- s는 이 새로운 힙 메모리를 가리킵니다 */
std::string b = s;
/* 3단계: 지역변수 b 생성
- b는 새로운 std::string 객체로 스택에 생성됩니다
- s가 가리키는 문자열의 또 다른 복사본이 힙에 생성됩니다
- b는 이 새로운 힙 메모리를 가리킵니다 */
} // 함수가 끝나면 b와 s가 소멸되며, 각각이 가리키던 힙 메모리도 해제됩니다
[프로그램 시작 시점]
상수 영역: "Hello World" (문자열 리터럴)
스택 힙
main() {
normalStr --------------> ["Hello World"] (첫 번째 복사)
}
[storeByValue 함수 호출 시점]
상수 영역: "Hello World"
스택 힙
main() {
normalStr --------------> ["Hello World"] (그대로 유지)
}
storeByValue() {
s ----------------------> ["Hello World"] (두 번째 복사)
}
[함수 내 b 생성 시점]
상수 영역: "Hello World"
스택 힙
main() {
normalStr --------------> ["Hello World"] (그대로 유지)
}
storeByValue() {
s ----------------------> ["Hello World"] (두 번째 복사)
b ----------------------> ["Hello World"] (세 번째 복사)
}
L-Value Reference
- 함수 호출 시 복사가 발생하지 않고 원본의 메모리 주소만 전달됩니다
- 함수 내부에서 한 번의 복사만 발생합니다
- 원본 데이터를 수정할 수 있으므로 주의가 필요합니다
void storeByLRef(std::string &s) {
std::string b = s; // 여기서만 새로운 복사가 일어납니다
}
int main() {
std::string normalStr = "Hello World";
storeByLRef(normalStr);
}
// 1단계: main 함수에서 normalStr 생성
std::string normalStr = "Hello World";
/* 이때 일어나는 일:
- "Hello World" 문자열 리터럴은 프로그램의 상수 영역에 저장
- normalStr 객체가 스택에 생성됨
- 문자열 데이터가 힙에 복사됨
- normalStr이 이 힙 메모리를 가리킴 */
// 2단계: std::move(normalStr) 실행
storeByRRef(std::move(normalStr));
/* 이때 일어나는 일:
- normalStr의 리소스 소유권이 s로 이동됨
- normalStr은 유효하지만 비어있는 상태가 됨
- 힙 메모리의 실제 복사는 일어나지 않음 */
void storeByRRef(std::string &&s) {
std::string b = std::move(s);
/* 이때 일어나는 일:
- b는 새로운 std::string 객체로 스택에 생성됨
- s가 가리키던 힙 메모리의 소유권이 b로 이동됨
- s는 비어있는 상태가 됨
- 힙 메모리의 실제 복사는 일어나지 않음 */
}
[프로그램 시작 시점]
상수 영역: "Hello World"
스택 힙
main() {
normalStr --------------> ["Hello World"] (첫 번째 복사)
}
[storeByLRef 함수 호출 시점]
상수 영역: "Hello World"
스택 힙
main() {
normalStr ----------┐---> ["Hello World"] (복사 없음)
} │
storeByLRef() { │
s (참조) ----------┘
}
[함수 내 b 생성 시점]
상수 영역: "Hello World"
스택 힙
main() {
normalStr ----------┐---> ["Hello World"] (원본 유지)
} │
storeByLRef() { │
s (참조) ----------┘
b -----------------------> ["Hello World"] (한 번의 복사)
}
R-Value Reference
- 임시 객체나 std::move된 객체를 받을 수 있습니다
- 메모리 복사 없이 리소스 소유권만 이전됩니다
- 가장 효율적이지만, 원본 데이터는 이동 후 유효하지 않게 됩니다
void storeByRRef(std::string &&s) {
std::string b = std::move(s); // 이동 연산: 힙 메모리의 소유권만 이전됨
}
int main() {
std::string normalStr = "Hello World";
storeByRRef(std::move(normalStr));
}
// 1단계: main 함수에서 normalStr 생성
std::string normalStr = "Hello World";
/* 이때 일어나는 일:
- "Hello World" 문자열 리터럴은 프로그램의 상수 영역에 저장
- normalStr 객체가 스택에 생성됨
- 문자열 데이터가 힙에 복사됨
- normalStr이 이 힙 메모리를 가리킴 */
// 2단계: std::move(normalStr) 실행
storeByRRef(std::move(normalStr));
/* 이때 일어나는 일:
- normalStr의 리소스 소유권이 s로 이동됨
- normalStr은 유효하지만 비어있는 상태가 됨
- 힙 메모리의 실제 복사는 일어나지 않음 */
void storeByRRef(std::string &&s) {
std::string b = std::move(s);
/* 이때 일어나는 일:
- b는 새로운 std::string 객체로 스택에 생성됨
- s가 가리키던 힙 메모리의 소유권이 b로 이동됨
- s는 비어있는 상태가 됨
- 힙 메모리의 실제 복사는 일어나지 않음 */
}
[프로그램 시작 시점]
상수 영역: "Hello World"
스택 힙
main() {
normalStr --------------> ["Hello World"] (첫 번째 복사)
}
[std::move 호출 후]
상수 영역: "Hello World"
스택 힙
main() {
normalStr (비어있음) ["Hello World"]
} ↑
storeByRRef() { │
s ------------------------| (소유권 이전, 복사 없음)
}
[함수 내 b = std::move(s) 실행 시]
상수 영역: "Hello World"
스택 힙
main() {
normalStr (비어있음) ["Hello World"]
} ↑
storeByRRef() { │
s (비어있음) │
b ------------------------| (소유권 이전, 복사 없음)
}