프로그래밍 일반/C++ 프로그래밍

[C++] 문자열 리터럴이 lvalue인 이유

지노윈 2020. 8. 15. 19:31
반응형

C 문자열 리터럴은 Lvalue 입니다.

C의 lvalue와 rvalue를 가집니다. 직관적으로 생각하면 lvalue은 왼쪽에 올 수 있는 표현들을 rvalue는 오른쪽에 올 수 있는 표현들입니다. 예를 들면 다음과 같이 두 항당 문이 있다고 생각해 봅시다.

int x;
x = 1;  // OK
1 = x;  // Error

첫번째 문장은 x가 lvalue이고 1이 rvlaue이며 이상없습니다.

두번째 문장은 왼쪽에 rvalue가 왔기 때문에 에러가 발생합니다.

 

러터럴(literal)은 일반저긍로 7, tue, 1.0f와 같은 rvalue들을 말합니다. 이는 리터럴들은 할당 표현에서 오른쪽에 올 수 있습니다. 그러나, 예외의 경우가 있습니다. 문자열 리터럴(예:"hello")는 실제로는 lvalue입니다.

 

왜인지를 이해하가 위해서, 우리는 C에서의 문자열 리터럴 타입을 이해할 필요가 있습니다. 여러분은 문자열 리터럴은 char* 또는 const char* 타입을 가진다고 생각할지 모릅니다. C에서, 문자열 리터럴은 char[] 타입입니다. 

sizeof 오퍼레이터가 문자열 리터럴에서 어떻게 동작하는지 알아봅시다. C 프로그램에서 다음의 코드는 화면에 무엇이 출력될까요?

#include <stdio.h>

int main() {
  printf("sizeof hello = %zd\n", sizeof "hello");
  return 0;
}

이 프로그램은 sizeof hello = 6을 출력합니다. 왜냐하면 "hello" 문자열의 길이가 6이기 때문입니다. (null 문자 포함)

문자열 리터럴이 포인터 타입이라면 포인터의 사이트를 리턴 했을 것입니다.  문자열 리터럴이 실제로 배열이므로, sizeof 오퍼레이터는 배열의 사이즈를 대신 리턴합니다. 이것은 매우 편리합니다. 때문에 문자열 리터럴의 사이즈가 컴파일 타임에 대체 대는 것이 허용됩니다.

 

문자열 리터럴이 포인터 타입이 아니라 배열 타입이여야 하는 또다른 이유는 이것이 배열 컨텍스트로 사용되어야 하기 때문입니다. 예를 들어 여러분이 문자열 리터럴이 배열일 경우에만 동작하는 다음과 같은 코드를 작성할 수 있습니다. 

void foo(char s[6]) {
    // do something with s
}

int main() {
    foo("hello");
    return 0;
}

일반적으로, C에서 배열 타입은 필요하다면 자동으로 포인터 타입으로 타입 변형 됩니다. 예를 들어, strlen()의 위한 파라미터는 const pointer입니다.

// 배열이 아니라 포인터 타입을 가집니다.
size_t strlen(const char *s);

strlen()으로 "hello"와 같은 문자열 리터럴을 전달할때 그 값은 char[]에서 const char*로 변경됩니다. 이것이 동작하는 이유는 배열이 포인터로 타입 변형되기 때문입니다.  이러해서 "hello"가 실제로는 포인터가 아니라 배열임에도 strlen("hello")와 같은 표현이 완전히 타당합니다.

 

배열은 항상 lvalue입니다.(배열이 메모리의 주소를 갖기때문에), 그래서 C 문자열 리터럴 또한 lvalue입니다.

 

문자열 리터럴 변형(Mutating String Literals)

다음의 프로그램은 C 스펙에서 동작하지만 논리적으로는 맞지 않습니다.

// 문법적으로 맞지만, 부적절합니다.
int main() {
  "foo"[0] = 0;
  return 0;
}

이 프로그램을 컴파일 할때, GCC는 경고를 발생시킵니다(“warning: assignment of read-only location”). 그러나 실행파일을  만듭니다. 프로그램을 실행했을때 segmentation fault를 발생하며 종료합니다. 하지만 이와 같은 프로그램을 실행하면 실제로 어떤 일이 발생합니까? GDB는 프로그램이 크래시 되었을때 다음과 같은 disassembly를 보여줍니다.

(gdb) disas
Dump of assembler code for function main:
   0x0000000000400487 <+0>:	push   %rbp
   0x0000000000400488 <+1>:	mov    %rsp,%rbp
=> 0x000000000040048b <+4>:	movb   $0x0,0x9e(%rip)        # 0x400530
   0x0000000000400492 <+11>:	mov    $0x0,%eax
   0x0000000000400497 <+16>:	pop    %rbp
   0x0000000000400498 <+17>:	retq
End of assembler dump.

이는 컴파일러가 0x400530 위치한 메모리에 1 byte 쓰기를 시도하는 중임을 보여주고 있습니다. 우리는 이 메모리 위치에 무엇이 맵핑되어 있는지 질의할 수 있습니다. 그러나 출력은 유용하지 않습니다.

(gdb) info proc mappings
process 29646
Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
            0x400000           0x401000     0x1000        0x0 /home/evan/a.out
            ... many more lines ...

우리가 0x400530이 매핑된 범위를 볼 수 있지만 해당 범위가 어떠한 권한인지 알 수 없기에 유용하지 않습니다.  권한을 보려면 /proc을 살펴 봐야합니다.

$ cat /proc/29646/maps
00400000-00401000 r-xp 00000000 fd:02 20710675                /home/evan/a.out
... more lines ...

0x400530 메모리 주소는 r-xp 권한으로 매핑된 것을 볼 수 있습니다. 이것은 이 메모리 위치는 읽기와 실행이 가능하다는 의미입니다. 다른 말로하면, 컴파일러는 타당한 코드를 만들었지만 링커는 생성된 코드가 segfault 되도록 메모리 레이아웃을 배열했습니다. 이것이 C의 이상한 특징입니다. 언어 표준은 일반적으로 링커가 없는 것처럼 가장합니다. 실제로 링크는 프로그램의 런타임 동작에 큰 영향을 줍니다.

 

C++ 문자열 리터럴

 사소하지만(그러나 중요하다), C++에서 문자열 리터렁 타입은 C와 조금다릅니다. C에서 문자열 리터럴은 char[ ] 타입이며 C++에서는 문자열 리터럴은 const char[ ] 타입입니다.

 

C++이 문자열 리터럴을 const 값으로 만든다는 사실은 상황을 다소 개선합니다. 예를 들어, 만약에 여러분이 C++ 컴파일러에서 이전의 예제를 컴파일 한다면 const 값의 변형은 엄격히 금지되기 때문에 컴파일 오류가 발생합니다.  항상 그러하듯이 타입 캐스트 또는 앨리어싱 된 포인터를 사용하여 이러한 검사를 쉽게 피할 수 있으므로 여러분은 여전히 주의를 해야합니다.

 

완전성을 위해서, C++에는 lvalue와 rvalue가 있지만 이를 좀더 세분화하여 좀더 복잡한 value category zoo 가 추가 되었습니다. 

 

참고 : https://eklitzke.org/c-string-literals-are-lvalues

 

C String Literals Are Lvalues

C values come in two types: lvalues and rvalues. The intuitive way to think of this is that lvalues are the things allowed on the left side of expressions, and rvalues are the things allowed on the right side. For instance, consider the following two assig

eklitzke.org