C언어 문자열 포인터 차이점

조회수 943회
char str[] = "abcd";
char *p_str = "abcd";

책의 설명 중에 문자열 상수의 경우는 주소를 반환한다고 명시되어있더라구요.
위의 경우는 "abcd" = 0x1000 ( 예시 주소 ) 처럼 반환이 된다고 하더라구요.

그 원리를 생각하여 2번째 코드를 해석하게되면 논리가 맞게 됩니다. 
좌변은 포인터형이고 우변은 주소니깐 자연스럽게 논리가 적용이 됩니다.

그러나 첫번째 코드의 경우는 책의 내용을 보면 포인터와 달리 배열의 이름의 경우는 상수형 포인터라고 하더라구요.
배열이름 = 주소 식으로 문법상 옳지 않다고 들었습니다. 그런데 위 코드를 위와 같은 논리로 해석하게 되면
배열의 이름에 주소가 들어가는 꼴 아닌가요 ? 물론 직접적으로 str = "abcd" 는 아닙니다만,

그러나 가능한 이유는 단순한 배열이름이 아니라 초기화라서 허용이 되는건가요 ? 

큰 논리로 한번에 이해를 할 순 없을까여 ? 단순히 문자열 배열이라서 되고, 포인터 배열이라서 안되고
각각의 특성상 안되는걸로 따로 받아들이는 수 밖에 없을까여 ?

포인터의 사용 이유는 간접적으로 데이터를 접근할 수 있어서라고 생각하는데, 2번째 코드의 경우는
포인터로 접근하여 값을 바꾸지 못하더라구요. ( p_str[0] = 'X'; )

그러면 포인터의 의미가 있나여 ?
  • (•́ ✖ •̀)
    알 수 없는 사용자

1 답변

  • 좋은 질문입니다.

    한가지 외워야 합니다.

    문자열은 상수입니다. 아시다시피 상수이므로 변경불가입니다.

    char *p_str = "abcd";
    

    pefile(.exe 파일) 이라는 가정하에 상기코드는 "abcd" 문자열을 rdata 섹션에 저장하고 그 메모리 주소를 p_str에 저장합니다.

    rdata 섹션은 읽기전용의 상수들이 저장되는데 문자열은 이곳에 저장을 합니다. 그러므로 문자열 "abcd" 는 읽기전용이 되고 p_str[0] = 'X'; 이런 값을 변경하는 코드는 메모리 오류가 발생합니다.

    char str[] = "abcd";
    

    상기의 경우는 왜 문제가 없을까...저 경우는 문자의 배열 공간을 스택에 만들고 (문자 4개와 더불어 널문자까지 5개 공간)각각의 문자 a,b,c,d 를 대입하는 것이므로 변경이 가능한겁니다.

    즉 핵심은 문자열은 읽기전용공간에 저장이 되므로 수정 불가입니다.

    =====================

    어셈블리를 읽을 수 있다면 디버거를 통해 직접 확인해 볼 수 있습니다.

    이왕 적은김에...한번 확인해봅시다.

    저는 현재 리눅스 이므로 gdb를 사용하여 디버깅을 해보겠습니다.

    source: c.c

    #include <stdio.h>
    
    int main()
    {
        char str[] = "abcd";
        char *p_str = "abcd";
        printf("%s\n", str);
        printf("%s\n", p_str);
    }
    
    

    컴파일 후 gdb 디버거로 바이너리를 디버깅 합니다.

    $ gcc -g3 -o c c.c
    $ gdb c
    (gdb) set disassembly-flavor intel                        => 인텔문법으로 셋팅
    (gdb) disas main                                                     => main 함수를 디스어셈블
    Dump of assembler code for function main:
       0x00000000000006aa <+0>: push   rbp
       0x00000000000006ab <+1>: mov    rbp,rsp
       0x00000000000006ae <+4>: sub    rsp,0x20
       0x00000000000006b2 <+8>: mov    rax,QWORD PTR fs:0x28
       0x00000000000006bb <+17>:    mov    QWORD PTR [rbp-0x8],rax
       0x00000000000006bf <+21>:    xor    eax,eax
       0x00000000000006c1 <+23>:    mov    DWORD PTR [rbp-0xd],0x64636261
       0x00000000000006c8 <+30>:    mov    BYTE PTR [rbp-0x9],0x0
       0x00000000000006cc <+34>:    lea    rax,[rip+0xc1]        # 0x794
       0x00000000000006d3 <+41>:    mov    QWORD PTR [rbp-0x18],rax
       0x00000000000006d7 <+45>:    lea    rax,[rbp-0xd]
       0x00000000000006db <+49>:    mov    rdi,rax
       0x00000000000006de <+52>:    call   0x570 <puts@plt>
       0x00000000000006e3 <+57>:    mov    rax,QWORD PTR [rbp-0x18]
       0x00000000000006e7 <+61>:    mov    rdi,rax
       0x00000000000006ea <+64>:    call   0x570 <puts@plt>
       0x00000000000006ef <+69>:    mov    eax,0x0
       0x00000000000006f4 <+74>:    mov    rdx,QWORD PTR [rbp-0x8]
       0x00000000000006f8 <+78>:    xor    rdx,QWORD PTR fs:0x28
       0x0000000000000701 <+87>:    je     0x708 <main+94>
       0x0000000000000703 <+89>:    call   0x580 <__stack_chk_fail@plt>
       0x0000000000000708 <+94>:    leave  
       0x0000000000000709 <+95>:    ret    
    End of assembler dump.
    (gdb) b *main                    => main 함수**에 브레이크 포인트 설정
    Breakpoint 1 at 0x6aa: file c.c, line 4.            => 4번째 라인에 브레이크 포인터 설정
    (gdb) l                                => c 코드 보기
    1   #include <stdio.h>
    2   
    3   int main()
    4   {
    5       char str[] = "abcd";
    6       char *p_str = "abcd";
    7       printf("%s\n", str);
    8       printf("%s\n", p_str);
    9   }
    
    (gdb) info files
    .......
        0x00005555555545a0 - 0x0000555555554782 is .text
    .......
    
    

    위의 => 부분은 제가 주석을 단 부분이니 참고하시면 됩니다.

    0x00000000000006c1 <+23>: mov DWORD PTR [rbp-0xd],0x64636261

    상기 부분이 har str[] = "abcd"; 부분입니다. 보시다시피 0x64636261 <- dcba 입니다. 16진수 61은 10진수 97이며 이는 아스키코드에서 a 입니다. 즉 dcba 값을 스택(rbp-0xd)에 저장하고 있습니다. 반대로 저장하는 이유는 제 cpu가 인텔 계열이므로 little endian 으로 저장하기 때문입니다.

    0x00005555555546cc <+34>:   lea    rax,[rip+0xc1]        # 0x555555554794
    
    (gdb) x/s 0x555555554794
    0x555555554794: "abcd"
    

    부분이 char *p_str = "abcd"; 연관된 부분입니다. 0x555555554794 주소는 0x00005555555545a0 - 0x0000555555554782 is .text 에서 보듯 .text 세그먼트에 속합니다.

    리눅스에서 .text 는 읽기전용입니다.

    이런 이유로 문자열을 가리키는 포인터는 값을 변경할 수 없습니다.

    • 결국 문법적 특성이군요. 답변 감사합니다. 알 수 없는 사용자 2019.4.22 19:38
    • 시스템적인 특성에 가깝지 문법적인 특성이 아닙니다. 훗날 pe file 에 대해서 학습하실 기회가 있다면 더 확실히 알 수 있을 겁니다. 정영훈 2019.4.22 19:43
    • 시스템적인 특성도 이해가됩니다만, 애초에 그렇게 설계한 컴파일러, 즉 언어의 문법적 특성 아니예여 ? 알 수 없는 사용자 2019.4.22 19:50
    • 변태같은 컴파일러라면 모를까 어떤 언어가 되었든지 컴파일시 문자열은 상수이므로 상수가 저장되는 영역(세그먼트)에 저장을 합니다. 정영훈 2019.4.22 20:37
    • 답변 정말 감사드립니다. 알 수 없는 사용자 2019.4.23 20:28

답변을 하려면 로그인이 필요합니다.

프로그래머스 커뮤니티는 개발자들을 위한 Q&A 서비스입니다. 로그인해야 답변을 작성하실 수 있습니다.

(ಠ_ಠ)
(ಠ‿ಠ)