C/C++에서는 데이터를 관리하기 위해 기본적으로 두 가지 종류의 저장소를 제공한다.
- 변수
- 포인터 : 포인터는 다른 저장소를 가리키는 메모리 주소를 저장한다. 주로 크기가 일정하지 않은 문자열이나 배열 등에 사용한다.
C++은 여기에 참조(Reference)라고 하는 특수한 종류의 변수가 존재한다. 참조는 일반 변수의 저장소를 공유하기 위해 만든 실제 변수가 아닌 컴파일러가 인지하는 가상 변수를 말한다.
C++의 포인터는 메모리 주소에 직접 접근해서 데이터를 읽고 수정할 수 있다. 이를 통해 비교적 크기가 큰 데이터를 빠리게 처리할 수 있다.
일반 객체지향 언어는 포인터를 제공하지 않는다. 대신 가상 포인터와 가상테이블을 사용하여 필요에 따라 서로 다른 데이터 저장소를 가리킨다. int, float같은 기본 타입은 스택 메모리에 저장하지만 클래스의 인스턴스는 항상 힙 메모리에 저장하여 객체를 가리키는 포인터를 저장한다.
C++ 언어는 new
연산자를 사용하지 않으면 스택 메모리를 사용한다. 객체 지향 언어의 클래스와 객체를 도입한 C++은 객체 지향의 원칙을 지원하기 위해 변수와 포인터가 아닌 세 번째 타입으로 참조를 만들게 되었다. 참조에 대해 자세히 알아보기 전에 lvalue
와 rvalue
의 개념에 대해 알아본다.
xxxxxxxxxx
// 대입 연산자를 통해 변수 x에 42란 리터럴을 저장한다. 이 과정에서 변수와 리터럴 사이의 데이터 복사가 발생한다. 이 식 이후 42란 리터럴은 더 이상 사용되지 않는다.
x = 42;
// 숫자를 연산하는 식이다. 하지만 이런 식은 연산자를 기준으로 작업 결과를 보관하는 변수가 없기 떄문에 작업 후 결과 값은 삭제된다.
18 * 4 / 135 + 35;
// 두 개의 문자열 리터럴을 합쳐 str2라는 변수에 저장한다. 이 식의 작업 결과는 대입 연산자를 통해 변수에 입력된다.
str2 = "first string" + "second string";
// 정수 리터럴에 실수 리터럴을 할당하는 작업이다. 왼쪽 정수 리터럴은 별도의 저장소를 가지고 있지 않으므로 에러가 발생한다.
42 = 345.23;
작업의 결과는 선언하고 있는 변수에 저장행야 의미있는 작업이 된다. 이 때 사용하는 연산자는 산술 연산자가 아닌 대입 연산자처럼 저장소에 저장하는 기능을 가지고 있어야 한다. 변수에 작업 결과를 입력시키는 작업은 데이터의 복사 또는 데이터의 이동이 존재한다는 의미를 갖는다.
변수 하나만 단족으로 존재하거나 또는 식의 작업 결과를 변수에 보간하지 안흔다면 작업 자체에 별다른 의미를 부여할 수 없다. 대입 연산자를 기준으로 lvalue와 rvalue로 구분한다.
lvalue
lvalue는 대입 연산자(=)의 왼쪽에 위치한 변수를 말한다. 변수 이외에 포인터를 반환하는 함수 또한 lvalue가 될 수 있다.
이러한 변수나 함수는 모두 공통적으로 식별 가능한 저장소를 가지고 있다. 또한 lvalue는 자체적으로 데이터를 이동하거나 복사하는 작업을 수행하지 않는다. 데이터의 이동이나 데이터의 복사가 발생한다면 해당 변수는 rvalue가 된다. 이 경우를 rvalue 변환이라한다.
xxxxxxxxxx
// str2의 데이터를 복사하여 str1 변수에 대입시킨다. str2는 입력시키는 작업동안 순간적으로 rvalue로 변환된다ㅏ.
str1 = str2;
// std::cout 변수는 이이덴티티가 존재하며 데이터의 이ㅗㄷㅇ이나 복사가 이루어지지 않는다. 따라서 의미상 lvaue가 된다.
std::cout << "hello world!";
// 전위 증감 연산는 하나의 값을 증가/감소시키는 동시에 a 변수에 값을 저장하는 기능을 수행한다. 따라서 연산자를 기준으로 a 변수는 lvalue가 된다.
++a;
--a;
// 모두 대입 연산 작업이므로 a는 lvalue가 된다. b는 연산을 위한 데이터의 복사가 일어나기 때문에 rvalue이다.
a = b;
a += b;
a %= b;
// 포인터가 가리키는 저장소는 일반 변수와 동일하므로 lvalue가 된다. 함수가 포인터를 반환한다면 포인터가 가리키느 저장소 역시 데이터의 저장이 가능하기 때문에 lvalue가 된다.
*p = 13;
// aar[n]처럼 배열 내 원소는 일반 변수와 같은 기능을 하므로 lvalue이다.
int arr[10];
int* p_arr = arr;
p[0] = 100;
array = 100; // error
// 객체의 멤버 변수는 lvalue이다.
struct_inst.val = 10;
p_struct_inst->val = 20;
// 전역 변수를 참조로 반환하는 함수는 lvalue이다.
int& foo(){
return globalvar;
}
foo() = 10; //
// const 상수 역시 아이텐티티가 존재하는 동시에 초기값을 입력할 수 있으며 const_cast연산자를 사용ㅎ해 수정이 가능하므로 lvalue로 분류한다.
const int& x = 8;
rvalue
대입 연산자의 오른쪽 위치에 놓이는 리터럴이나 임시 저장소, 무명 변수(int(10);) 그리고 일반 데이터를 반환 하는 함수를 말한다.
- 데이터의 이동이나 복사가 존재한다.
- 메모리의 주소를 얻을 수 없다.
rvalue는 데이터를 저장하는 방식에 따라 다시 pravlue와 xvalue로 나눈다.
prvalue
prvalue는 순수 rvalue를 의미한다. 따라서 순수한 rvalue는 아이텐티티를 가지지 않으며, 또한 데이터의 이동이나 복사만이 가능하다.
- 문자열 리터럴을 제외한 리터럴은 pravlue가 된다.
- 이런
true
,nullptr
과 같은 키워드는 prvalue이다.
- 참조나 포인터가 아닌 일반 데이터를 반환하는 일반 함수 또는 람다 함수는 prvalue가 된다.
- 연산 작업의 결과(str1 + str2)는 임시 저장소에 보관된다. 따라서 해당 작업 겨로가는 prvalue가 된다.
- a++같은 후위 증감 단항 연산자는 prvalue가 된다.
xxxxxxxxxx
// 전위 증감연산자는 아래와 같이 동작한다.
i = i + 1;
return i;
// 후위 증감연산자의 동작은 다음과 같다. 즉 후위 연산 결과는 임시 저장소 역할을 한 후 사라진다.
const int temp = i;
i = i + 1;
return temp;
- &연산자(주소 반환연산자)와 함께 사용한 변수는 prvalue가 된다.
- 열거형 타입의 멤버나
vector<int>::value_type
처럼 typedef로 선언되어 제공되는 데이터 타입, 일반 데이터를 반환하는 멤버함수는 prvalue이다. - 상수 변환 작업 역시 prvalue이다.
xvalue
xvalue는 lvalue와 rvalue의 중간 개념으로 c++11이후 새롭게 정의된 개념이다. 리터럴이나 일반 연산을 수행하기 위해 만들어진 임시 저장소를 가리킨다. 따라서 rvalue 또는 식으로 계산된 결과를 보관하는 임시 저장소를 참조한다면 이는 xvalue가 된다. xvalue는 소멸되기 전에 사용할 수 있는 lvalue를 뜻한다.
xvalue는 프로그램의 성능을 향상 시키려는 목적에서 출발했다. 식은 작업하는 과정에서 무수히 많은 임시 저장소를 생성하고 소멸시키는 과정을 겪게 된다.
xxxxxxxxxx
int y = func(n) * 100 + (x + 2) % 5;
func(n)
이란 함수를 호출한다. 이 때 함수에 인수로 데이터를 전달하고자 n변수에 복사가 이루어진다.- 함수의 작업이 이루어진 다음 함수가 반환되는 값을 A라고 하는 임시 저장소를 만들어 넣는다.
- 그룹 연산자에 의한
(x + 2)
연산을 수행하고 또 다른 임시 저장소 B에 결과를 보관한다. - A에서 값을 읽어 100을 곱하고 다시 A에 저장한다.
- B에서 값을 읽어 5늘 나눈 나머지를 B에 저장한다.
- A와 B의 저장소에서 값을 읽고 더하여 다시 A에 저장한다.
- A에서 값을 읽어 변수
y
에 넣는다. - 작업 중간마다 결과를 보관하였던 임시 저장소는 모든 작업을 마친 이후에 문장을 종료하는 시점에 모두 삭제된다.
이러한 연산을 더 효율적으로 수행하기 위해 문자으이 종료 시 즉시 삭제되던 임시 저장소의 소멸 시간을 다소 지연시킨 개념이 xvalue이다.