선요약: "Hello" 같은 큰 따옴표를 사용해 표현하는 문자열 리터럴은 char 배열의 초기화와 char 포인터의 초기화에 모두 사용할 수 있지만 그 의미가 다르므로 주의해야 합니다.


아래 내용은 C/C++의 표준에 명시되어있지 않은 내용이고 컴파일러 구현이나 최적화, 기타 환경에 따라 충분히 달라질 수 있는 내용임을 주의하면서 읽어주세요. 다만 컴파일 에러가 발생하는 부분은 대부분 표준에서 허락하지 않는 행위입니다.


이 글은 Linux, gcc 환경에서 테스트 되었으며 Windows, Visual Studio 등 기타 환경과 동작이 다를 수 있습니다. Linux, gcc를 지원하는 온라인 컴파일러는 replit.com 등이 있으니 참고하세요.


사전 지식: 일반적으로 지역 변수는 스택, malloc이나 C++의 new처럼 동적으로 할당한 메모리는 힙, 전역 변수나 static 변수는 별도의 데이터 섹션에 저장됩니다. 본 글에서는 해당 영역들의 배치나 자라는 방향에 대해서는 설명하지 않습니다.


1
2
char str1[] = "Hello";
const char str2[] = "Hello";
cs

위의 두 경우는 문자열 리터럴을 배열의 초기화를 위해 사용합니다. 우리가 선언한건 포인터가 아니라 배열이므로 문자열은 스택에 할당되지만 포인터 변수는 할당되지 않습니다.

이 경우 str1과 str2는 포인터가 아니라 배열 타입을 가지며, 다른 곳을 가리키도록 할 수 없습니다. 대신 str1[1] = 'k'; 처럼 배열 내부의 데이터 수정이 가능합니다. 다만 str2의 경우는 const char의 배열이므로 str2[1] = 'k'; 가 불가능합니다.


1
2
char str[] = "Hello";
char str[6] = { 'H', 'e', 'l', 'l', 'o', '\0' };
cs

문자열 리터럴을 배열 초기화에 사용할 경우, 위 두 문장은 똑같이 해석됩니다



다음은 문자열 리터럴을 포인터의 초기화에 사용하는 경우입니다.

1
2
char *str3 = "Hello";
const char *str4 = "Hello";
cs

위 두 경우는 문자열이 스택에 할당되지 않습니다. 스택에는 포인터 변수만 할당되고, 이 포인터가 데이터 섹션의 특정 영역을 가리키도록 초기화됩니다.

해당 영역은 보통 수정 불가능하고 main이 호출되기 전에(실행 파일을 디스크에서 메모리로 적재할 때) C 런타임에 의해서 초기화됩니다. 따라서 str3는 const가 붙어있지 않지만 수정 불가능한 메모리 영역을 가리킬 가능성이 있고, str3[1] = 'k'; 의 경우 컴파일 에러는 발생하지 않지만 런타임에 수정 금지된 영역에 쓰기를 시도하여 segmentation fault 와 같은 에러가 발생할 수 있습니다.


위 두 변수는 포인터 변수이므로 자유롭게 메모리상의 다른 영역을 가리킬 수 있습니다. (str3 = str1; str4 = str1; 등) 이 경우 수정 가능한 영역을 가리키게되면 배열의 경우와 마찬가지로 데이터를 변경할 수 있습니다.



아래에 예제 replit 링크와 코드를 남겨 놓을테니 컴파일 에러 또는 런타임 에러가 발생하는 곳을 주석처리하며 왜 에러가 발생하는지 생각해보면 좋을 것 같습니다.

https://replit.com/@asd142513/string-pointer-example#main.c


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <stdio.h>
 
int main(void)
{
    char arr[] = "Hello";
    // char arr[6] = {'H', 'e', 'l', 'l', 'o', '\0'}; 과 동일. 문자열이 스택에 할당됨
 
    const char const_arr[] = "Good Bye";
    // const char const_arr[9] = {'G', 'o', 'o', 'd', ' ', 'B', 'y', 'e', '\0'}; 과 동일. 문자열이 스택에 할당됨
 
    char *ptr = "Wow";
    // 스택에 포인터 할당, 데이터 섹션에 존재하는 문자열을 가리키도록 초기화
 
    const char *const_ptr = "Huh";
    // 위와 동일
 
    puts("1");
    puts(arr);
    puts(const_arr);
    puts(ptr);
    puts(const_ptr);
    puts("");
 
    arr[0] = 'A'; // Ok
    const_arr[0] = 'B'; // Compile error
    ptr[0] = 'C'; // Compile ok, 런타임 에러가 발생할 수도 안 할 수도
    const_ptr[0] = 'D'; // Compile error
    puts("2");
    puts(arr);
    puts(const_arr);
    puts(ptr);
    puts(const_ptr);
    puts("");
 
    arr = ptr; // Compile error, arr는 포인터가 아니라 배열이므로 불가능
 
    ptr = arr; // Ok
    ptr[1] = 'X'; // Ok
    puts("3");
    puts(arr);
    puts(ptr);
    puts("");
 
    ptr = const_arr; // Warning, char * 에 const char 배열을 대입
    ptr[1] = 'Y'; // const_arr는 const char 배열이지만 스택에 있는 문자열은 수정 가능하므로 성공
    puts("4");
    puts(const_arr);
    puts(ptr);
    puts("");
 
    ptr = const_ptr; // Warning, char *에 const char *를 대입
    ptr[1] = 'Z'; // Compile ok, 런타임 에러가 발생할 가능성 높음
    puts("5");
    puts(const_ptr);
    puts(ptr);
    puts("");
 
    const_ptr = arr; // Ok, const char * 에 변경 가능한 배열의 주소를 대입하는건 괜찮음
    const_ptr[1] = 'P'; // Compile error, const_ptr는 const char * 이므로 const_ptr를 통한 수정은 불가능
    puts("6");
    puts(arr);
    puts(const_ptr);
    puts("");
 
    arr = "Hi"; // Compile error, 문자열 리터럴을 초기화에 사용할 수 있지만 대입에 사용할 수는 없음
    const_ptr = "Bye"; // Ok, 포인터 const_ptr가 다른 정적 데이터 섹션의 위치를 가리키도록 함
 
    return 0;
}


cs


PS) 본문과 관련은 없지만 const char * p; 와 char *const p; 는 다른 선언입니다. 각각 *p의 수정 가능 여부와 p의 수정 가능 여부가 달라집니다. const char *p와 char const *p 는 동일한 선언이며 const char *const p; 처럼 p와 *p를 모두 const로 선언할 수도 있습니다.