back

비좌변값 배열(non-lvalue array) #2

11년 전 작성

이 글은 시리즈 글의 일부입니다.


구조체 안에 포함된 배열과 관련된 이야기를 꺼내기 전에 우선 글제목에 나온 "좌변값(lvalue)"에 대한 썰을 좀 풀어볼까 합니다. 이제는 전국민(?)의 상식이지만"좌변값"은 이름 그대로 우리말 이름에서 "좌변"과 영어 이름(lvalue)에서 "l"은 왼쪽(left)을 의미해 대입 연산자의 왼쪽(좌변, lhs, left-hand side)에 나올 수 있는 값임을 의미합니다.

C 언어에서도 대부분의 경우에는 이 좌변값의 의미가 그대로 적용됩니다. 예를 들어,

0 = 1;

과 같은 수식에서 정수 상수 0은 대입 연산자의 좌측에 나올 수 없으므로 좌변값이 아니고

int a;
a = 0;

에서 변수 a는 가능하므로 좌변값이 됩니다.

컴파일러는 수식 안에서 완전히 같은 모양으로 쓰인 형태라 해도 좌변값으로 쓰였는지, 아니면 (좌변값이 아닌 형태인) 우변값(rvalue)으로 쓰였는지에 따라 전혀 다른 코드를 생성해야 합니다. 예를 들어,

a = 0;    /* 1번 수식 */
b = a;    /* 2번 수식 */

두 수식에서 동일한 형태로 a가 쓰였지만 1번 수식에서는 좌변값으로 쓰였기 때문에 실제로는 a라는 변수의 메모리 상 위치를 의미하며, 2번 수식에서는 a라는 변수의 메모리 위치에 저장되어 있는 값을 의미하게 됩니다. 즉, 굳이 표현하자면 어떤 변수의 메모리 위치를 가져오는 연산자를 ADDR()이라고 하고 값을 가져오는 연산자를 VALUE()라고 하면 위 예는

ADDR(a) = 0;
ADDR(b) = VALUE(a);

를 의미하는 것입니다. 그리고 실제 컴파일러가 생성하는 코드도 이와 동일한 형태로 수식을 해석해 결과로 내게 됩니다.

하지만, C 언어에서 좌변값은 대입 연산자의 좌변에 나올 수 있는지 여부로 정의되어있지 않습니다. 물론, 원래 "좌변값"이라는 용어가 갖는 의미 그대로의 정의를 사용할 수도 있었겠지만, 그럴 경우 언어를 기술하는 과정에서 복잡도가 올라가기 때문에 가장 자연스러운 의미로 좌변값(lvalue)을 정의해 사용합니다 - 그 복잡도가 올라가는 이유가 바로 이 글의 또 다른 축에 해당하는 "배열"때문입니다.

C90(공식 명칭 ISO/IEC 9899:1990)에서 정의되어 있는 좌변값은 이렇습니다.

이미지

(화면 캡쳐가 왜 이렇게 허접하냐고 물으신다면... 원본이 그렇습니다. C90 표준은 인쇄본을 하드 스캔하여 만든 것이 공식 PDF라서 화질도 이따구고 검색도 잘 안 됩니다.)

모바일 배려...

An lvalue is an expression (with an object type or an incomplete type other than void) that designates an object.

친절한 번역...

_좌변값(lvalue)_은 대상체(엄밀하게는 다르지만 변수와 비슷한 의미입니다)를 지정하는 대상체형(object type) 혹은 void가 아닌 불완전형(incomplete type)을 갖는 수식이다.

핵심을 설명하면, 메모리 상의 값을 저장하기 위해 공간을 차지하는 대상이 좌변값(lvalue)이 된다고 정의하고 있습니다. 물론, 컴퓨터의 밑바닥 이야기를 아시는 분은 "잉? 상수도 프로그램 이미지가 로드될 때 메모리에 저장되지, 그러면 어디에 저장되나?"라고 물으실 수 있지만, 추상적 관점에서 c 프로그램의 상수는 메모리에 저장되는 것이 아닌 것으로 약속합니다. 그러니 저장된 메모리의 주소를 묻는 & 연산자를 상수에는 적용할 수 없지요.

저 정의에 따르면 배열도 분명 메모리에 공간을 차지하므로 좌변값이 됩니다. 그렇다면 배열도 대입 연산자의 좌변에 나올 수 있나요?

int a[10], b[10];
a = b;    /* 오류 */

당연히 그럴 수 없죠! 그래서 C 언어에서 좌변값은 대입 연산자의 좌변에 나오는지 여부가 기준이 되지 않습니다. 이런 내용은 좌변값을 정의하는 부분의 주석에도 나와 있습니다.

이미지

모바일 배려...

The name "lvalue" comes originally from the assignment expression E1 = E2, in which the left operand E1 must be a (modifiable) lvalue. It is perhaps better considered as representing an object "locator value." What is sometimes called "rvalue" is in this International Standard described as the "value of an expression." An obvious example of an lvalue is an identifier of an object. As a further example, if E is a unary expression that is a pointer to an object, *E is an lvalue that designates the object to which E points.

그닥 친절하지 않은 번역...

"좌변값"이라는 이름은 원래 대입식 E1 = E2에서 좌측 피연산자 E1이 (수정 가능한) 좌변값이어야 한다는 데서 기원하였습니다. (하지만, C 표준에서는) 대상체 "위치값(locator value)"을 의미하는 것으로 생각하는 것이 나을 것입니다. 종종 "우변값"이라고 부르는 것은 이 표준에서는 "수식의 값"이라고 기술합니다. 좌변값의 명확한 예로는 대상체로 선언된 명칭이 있으며, 다른 예로는 E가 대상체를 가리키는 포인터 수식일 때 *EE가 가리키는 대상체를 지정하는 좌변값이 됩니다.

("lvalue"의 "l"을 "locator"로 치환하는 저 센스!)

이처럼 좌변값임에도 대입 연산자의 좌측에 나올 수 없는 존재가 있다면, 표준은 대입 연산자의 좌측에 배열이 나오지 못하도록 어떻게 기술하고 있을까요? "배열이 아닌 좌변값이 나올 수 있다"라고 기술할까요? 요렇게 간단히 되어 있습니다.

이미지

(이건 이미지가 충분히 커서 모바일 배려는 안 할랍니다.)

간단한 번역...

대입 연산자(assignment operator)의 좌측 피연산자로는 수정 가능한 좌변값(modifiable lvalue)가 주어져야 한다.

실제 (배열 요소가 아닌) 배열 자체는 좌변값이기는 하지만 수정 가능한 좌변값은 아닙니다. 아하! 그렇다면 배열은 수정 가능한 좌변값이 아니라서 대입의 좌측 피연산자로 나올 수 없는 것이군요...라고 생각하면 함정입니다. 저 "수정 가능한"은 const로 한정된 애들을 배제하기 위한 것입니다.

그보다는 배열은 아래 2가지 경우(표준에서는 3가지 경우를 설명하고 있으나 하나는 초기치에 사용된 특수한 경우이므로 2가지만...)를 제외하면 첫 요소(element)를 가리키는 포인터로 변환(decay)된다고 정의하고 있습니다.

  • sizeof 연산자의 피연산자로 나온 경우, 포인터가 아닌 배열의 크기를 계산함
  • & 연산자의 피연산자로 나온 경우, 배열 자체의 주소를 구함 (따라서, 결과 데이터형이 "pointer to array ..."가 됨)

그리고 포인터로 변환된 후에는 더 이상 좌변값이 아니라고 규정합니다. (관련 표준은 다음 이야기에 인용할 예정입니다.)

대입 연산자의 좌측 피연산자로 나온 경우는 위의 두 경우에 해당하지 않으므로 배열은 곧 포인터가 되고 좌변값이 아니기에 좌측 피연산자의 후보 근처에도 가지 못합니다.

헥헥... 이제 진짜로 제가 하고 싶은 이야기로 가보겠습니다.