선요약: "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로 선언할 수도 있습니다.