포인터(pointer)와 문자배열(Char Array), 문자열상수(String Constant)의 관계

여러분은 아래 두줄 선언의 차이를 정확하게 알고 계시나요 ?

  • char * p = “abcd”;  —  (1)
  • char n[] = “abcd”;  —  (2)

위 두 라인의 차이점에 대해 저도 종종 혼동하기도 하고 정확하고 Detail 하게 설명할 수 없었던 적도 있습니다.
사실 짧은 지식으로 간단히 얘기하면 할말이 많지 않지만, 깊숙하게 파고들기 시작하면 위의 두 라인을 가지고도 강의 한시간은 채울 수 있을 것입니다.
위의 첫번째 라인에서 p 는 문자열상수 “abcd” 를 가리키는 stack 포인터 변수이고, 두번째 라인에서 n 은 “abcd”라는 문자열이 저장된 stack 영역을 대표하는 stack array 이름, 즉 포인터 상수를 나타냅니다.
포인터 변수는 말 그대로 포인터이므로 p = “xyz” 와 같이 다른 문자열 상수를 가리키도록 변경할 수 있지만, 포인터 상수는 변경불가한 고정된 주소값을 가지기 때문에 n = “xyz” 와 같이 변경할 수 없습니다.
(1) 번에서 p 자체는 stack 에 잡히는 포인터 변수입니다. 그리고, “abcd”는 컴파일러에서 컴파일 시에 실행파일 또는 so library 에 써놓는 문자열상수, 즉 고정된 문자열입니다.
컴파일러는 컴파일 시에 상수들을 모두 모아서 실행파일 또는 so library 의 특정 부분에 써놓습니다.
그리고, 실행파일을 로드 시 이러한 상수들을 프로그램의 data 영역 중 읽기 전용 static 메모리 공간에 올려놓습니다.
올려 놓은 후 위의 코드를 실행 시 stack 변수 p 가 할당되고, 해당 p 변수에 읽기 전용 메모리 공간에 저장된 문자열상수의 주소값을 할당하는 것이죠.

(2) 번은 사실 n 이라는 array 공간을 잡고 초기화를 편하게 해주기 위해 사용하는 방식일 뿐입니다.
“abcd” 문자열이 저장될 만큼의 stack 영역을 할당하고 곧 바로 초기화를 한번에 수행해주기 위한 구문입니다.
마치 의미상으로만 따지면 아래의 두 줄을 한번에 수행하는 것이라고 할까요 ?
char n[4];
strcpy(n, "abcd");
자, 그럼 p 를 이용해서 또는 n 을 이용해서 각각 자신이 가리키는 문자열을 변경할 수 있을까요 ?
int main()
{
    char   n[] = "abcd";

    n[0] = 'n';
    printf("[%s]n", n);
}

위의 소스는 문자열 array 를 가리키는 n 을 이용하여 첫번째 문자를 변경해본 것입니다.
아래의 결과처럼 변경이 아주 잘되는군요.

sh> ./a.out
[nbcd]

그렇다면, 아래의 결과는 어떨까요 ?

int main()
{
    char * p   = "abcd";

    p[0] = 'p';
    printf("[%s]n", p);
}

실행해보면 세그멘테이션 오류 (Segmentation Fault)가 발생합니다.
읽기 전용 문자열상수가 저장된 영역에 포인터 변수 p 를 이용하여 접근하여 값을 수정하려고 했기 때문입니다.

gdb 를 사용해서 차이점을 좀더 살펴볼까요 ?
아래는 포인터 변수 p 를 사용하는 소스를 -g 옵션으로 컴파일한 다음 gdb 로 수행해본 것입니다.

(gdb) b main
Breakpoint 1 at 0x4004fc: file t.c, line 3.
(gdb) r
Starting program: /home/dplee/tmp/a.out
Breakpoint 1, main () at t.c:3
3    char * p   = "abcd";

(gdb) p p
$1 = 0x0

(gdb) n
5    p[0] = 'p';
(gdb) print p
$2 = 0x40061c "abcd"     # 주소와 주소가 가리키는 공간에 저장된 값이 같이 보임.

(gdb) p p[0]
$3 = 97 'a'

(gdb) x/4c 0x40061c
0x40061c:97 'a'98 'b'99 'c'100 'd'

(gdb) n
Program received signal SIGSEGV, Segmentation fault.
0x0000000000400508 in main () at t.c:5
5    p[0] = 'p';

위의 comment 를 보면 p 라는 변수를 print 해본 결과 주소와 값을 같이 보여줌을 알 수 있습니다.
그러면, array 이름인 n 일 경우에 어떤지 비교해보죠.

(gdb) b main
Breakpoint 1 at 0x40056c: file t.c, line 2.

(gdb) r
Starting program: /home/dplee/tmp/a.out
Breakpoint 1, main () at t.c:2
2{

(gdb) n
3    char  n[]   = "abcd";

(gdb) n
5    n[0] = 'n';

(gdb) print n
$1 = "abcd"              # 주소는 나오지 않고 문자열 자체만 나온다.

주소는 안나오고 문자열만 나오는군요. 당연합니다. n 이라는 것은 포인터변수가 아니기 때문이죠.

Array 공간에 저장된 값들이 “abcd” 이니 그냥 그 값들을 보여줄 뿐 포인터가 아니니 주소값이 나오지 않는 것입니다.
p 와 n 은 둘다 p[i], n[i] 과 같이 문자열 중 각각의 문자 character 를 index 를 이용하여 접근할 수 있습니다.
하지만, 이를 이용하여 변경을 할수 있냐없냐의 차이가 있습니다.
이번에는 objdump 라는 tool 을 사용하여 컴파일 후 만들어지는 object file 을 Disassemble 하여 위의 내용을 확인하는 차원에서 간단히 분석해 보겠습니다.

int main()
{
    char * p = "abcd";
}

위의 코드를 x.c 로 저장한 후 컴파일하여 objdump -D x.o 명령을 수행하면 다음과 같은 결과를 볼 수 있습니다.
결과를 보면 object binary 의 내용을 section 별로 잘 분류하여 보여주는 것을 알 수 있습니다.
“abcd” 는 각각 ascii code 로 변환하면 0x61, 0x62, 0x63, 0x64 입니다.
이를 통해 확인해보니 해당 문자열이 rodata (Read-Only Data) 영역에 저장되어 있는 것을 확인할 수 있습니다.

그렇다면, 아래와 같이 array 초기화를 수행하는 코드에 대해서는 어떤 결과가 나오는지 볼까요 ?

int main()
{
    char p[] = "abcd";
}

아래 결과를 보니 rodata section 에 대한 내용은 사라지고 text section, 즉 코드 영역에 0x64636261 값을 직접 할당하는 것을 볼 수 있네요.
즉, 위의 코드에서 “abcd” 의 값은 읽기 전용 메모리 공간에 있는 것이 아니라 실행 시에 직접 초기화로 사용하는 코드 영역 상의 rvalue 값일 뿐이네요.

어때요 ? 이제 차이점들이 확실히 보이나요 ? ^^
여러분에게 보여드릴 글을 쓰면서 저 또한 흐지부지 흩어져있던 개념들을 머리속에 잘 정리할 수 있는 계기가 되는 것 같아서 좋습니다. ^^;;;
그럼~

You may also like...