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 ------------------------|  (소유권 이전, 복사 없음)
}