|

C 언어 문자열과 문자 배열 차이: char 포인터까지 나오면 왜 더 헷갈릴까

C 언어 문자열 리터럴, 문자 배열, char 포인터 차이를 설명하는 대표 이미지
문자 배열은 데이터를 직접 가지고, char 포인터는 문자열의 시작 주소를 가리킨다

C 언어 문자열과 문자 배열 차이는 포인터가 끼어드는 순간 갑자기 더 헷갈려집니다. 겉으로는 모두 글자 여러 개를 다루는 코드처럼 보이지만, 실제로는 문자열 리터럴, 문자 배열, char 포인터가 서로 다른 정체성을 가집니다.

이 글은 일반 배열과 포인터 차이의 반복보다, 문자열 관점에서 왜 char name[] = “Alice” 와 char *name = “Alice” 가 갈리는지에 집중합니다. 문자열 리터럴, 수정 가능성, 함수 인자 동작까지 한 흐름으로 보겠습니다.


C 언어 문자열과 문자 배열 차이: 먼저 결론부터

  • C에는 다른 언어처럼 독립된 string 기본 타입이 없다.
  • 문자열은 보통 널 문자로 끝나는 char 배열 관례로 다룬다.
  • char 배열은 데이터를 직접 가진다.
  • char 포인터는 어떤 문자의 시작 주소를 가진다.
  • 문자열 리터럴을 수정하려 하면 정의되지 않은 동작이다.
char arr[] = "abc";
char *ptr = "abc";
const char *msg = "abc";

세 선언은 모두 비슷해 보이지만, 배열 복사본을 만드는지, 문자열 리터럴을 가리키는지, 읽기 전용 의도를 드러내는지에서 의미가 달라집니다.

문자 배열은 데이터를 직접 가진 객체이고, char 포인터는 어떤 문자를 가리키는 주소를 가진 객체입니다.


문자열 리터럴은 무엇일까

문자열 리터럴은 눈에 보이는 글자만이 아니라 마지막 널 문자까지 포함합니다. 그래서 abc 세 글자를 저장하려고 해도 실제 배열 크기는 4가 됩니다.

"abc"   // 'a', 'b', 'c', '\0'

strlen은 널 문자 전까지의 글자 수를 세고, 배열 크기는 널 문자까지 생각해야 한다는 점이 여기서 시작됩니다.


char 배열로 받으면 실제 복사본이 생긴다

#include <stdio.h>

int main(void)
{
    char arr[] = "abc";

    printf("%s\n", arr);
    printf("sizeof(arr) = %zu\n", sizeof(arr));

    return 0;
}

이 코드는 길이 4인 문자 배열을 만듭니다. 배열 안에는 a, b, c, 널 문자가 들어가며, 문자열 리터럴 내용을 배열 안으로 복사해 초기화한 것으로 이해하면 됩니다.

+-----+-----+-----+-----+
| 'a' | 'b' | 'c' | '\0'|
+-----+-----+-----+-----+
char arr[] = "abc";
arr[0] = 'A';   // 가능

여기서 수정 대상은 문자열 리터럴이 아니라 내 손에 있는 배열 복사본이므로 정상적인 코드입니다.


char 포인터로 받으면 가리키는 관계가 된다

#include <stdio.h>

int main(void)
{
    char *ptr = "abc";

    printf("%s\n", ptr);
    printf("sizeof(ptr) = %zu\n", sizeof(ptr));

    return 0;
}

여기서 ptr은 배열이 아니라 포인터 변수 하나입니다. ptr은 문자열 리터럴의 첫 글자를 가리키고, sizeof(ptr)는 문자열 길이가 아니라 포인터 변수 크기를 보여 줍니다.

문자열 리터럴 쪽
+-----+-----+-----+-----+
| 'a' | 'b' | 'c' | '\0'|
+-----+-----+-----+-----+
  ^
  |
 ptr

포인터 변수 ptr 자체
+------------------+
| 첫 글자의 주소값 |
+------------------+

왜 char 포인터 쪽은 수정이 위험할까

char *ptr = "abc";
ptr[0] = 'A';   // 정의되지 않은 동작

문자열 리터럴이 만드는 저장 영역을 수정하려 하기 때문입니다. 환경에 따라 바로 오류가 날 수도 있고, 겉보기에는 돌아가는 것처럼 보여도 신뢰하면 안 됩니다.

char arr[] = "abc";
arr[0] = 'A';   // 정상적인 수정

같은 문자열로 시작해도, 수정 대상이 배열 복사본인지 문자열 리터럴인지에 따라 안전성이 완전히 달라집니다.


실무에서 const char *가 자주 보이는 이유

const char *msg = "abc";

읽기만 할 문자열이라면 이 형태가 더 자연스럽습니다. 포인터가 가리키는 문자를 이 경로로 바꾸지 않겠다는 의도를 분명하게 보여 주기 때문입니다.

문자열 상수를 함수에 넘기거나, 고정 메시지를 가리키거나, 수정이 없어야 하는 API를 만들 때 특히 유용합니다.


왜 char 배열과 char 포인터가 같은 것처럼 보일까

char arr[] = "abc";
char *ptr = arr;

printf("%c\n", arr[1]);
printf("%c\n", ptr[1]);
  • arr[1]은 배열의 두 번째 원소를 읽는다.
  • ptr[1]은 포인터가 가리키는 위치에서 한 칸 이동해 읽는다.
  • 결과가 같을 수는 있어도 출발점과 정체성은 다르다.

이 때문에 입문자는 둘을 같은 물건으로 외우기 쉽지만, 문자열 리터럴과 수정 가능성까지 엮이는 순간 차이를 모르면 바로 사고가 납니다.


sizeof와 strlen은 무엇이 다를까

#include <stdio.h>
#include <string.h>

int main(void)
{
    char arr[] = "abc";
    char *ptr = arr;

    printf("sizeof(arr) = %zu\n", sizeof(arr));
    printf("sizeof(ptr) = %zu\n", sizeof(ptr));
    printf("strlen(arr) = %zu\n", strlen(arr));

    return 0;
}
  • sizeof(arr) 는 배열 전체 바이트 수
  • sizeof(ptr) 는 포인터 변수 크기
  • strlen(arr) 는 널 문자 전까지의 글자 수

문자열 길이와 배열 크기를 같은 개념으로 섞어 생각하면 버그가 생기기 쉽습니다. 널 문자까지 포함하는지 아닌지를 항상 구분해야 합니다.


함수 매개변수에서는 왜 다시 char 포인터처럼 보일까

void print_text(char text[])
{
    printf("%s\n", text);
}

void print_text2(char *text)
{
    printf("%s\n", text);
}

함수 매개변수의 char text[]는 실제로 char *text로 조정됩니다. 그래서 함수 안에서는 원래 배열 전체 길이를 자동으로 알 수 없습니다.

#include <stdio.h>

void show_size(char text[])
{
    printf("함수 안 sizeof(text) = %zu\n", sizeof(text));
}

int main(void)
{
    char arr[] = "hello";
    printf("함수 밖 sizeof(arr) = %zu\n", sizeof(arr));
    show_size(arr);
    return 0;
}

함수는 문자 배열 전체를 통째로 받는 것이 아니라, 대부분 첫 글자를 가리키는 포인터를 받는다고 이해하면 실수가 크게 줄어듭니다.


읽기 전용 함수라면 이렇게 쓰는 편이 낫다

#include <stdio.h>

void print_text(const char *text)
{
    printf("%s\n", text);
}
  • 문자열 리터럴을 자연스럽게 넘길 수 있다.
  • 함수 안에서 입력 문자열을 바꾸지 않겠다는 의도를 분명하게 보여 준다.
  • 호출하는 쪽에서도 읽기 전용 함수라는 점을 더 쉽게 이해할 수 있다.

자주 하는 오해

  • C에는 string 기본 타입이 있다.
  • char arr[] = "abc" 와 char *ptr = "abc" 는 완전히 같다.
  • 문자열 리터럴도 문자 배열이니 수정해도 된다.
  • 함수가 char text[]를 받으면 배열 길이도 자동으로 안다.
  • sizeof 와 strlen은 비슷한 정보를 준다.

정리

  1. C 문자열은 보통 널 문자로 끝나는 char 배열 관례다.
  2. 문자열 리터럴은 읽기 전용처럼 다루는 것이 안전하다.
  3. char 배열은 데이터를 직접 가진다.
  4. char 포인터는 주소를 가진다.
  5. 함수 매개변수에서 배열 표기는 대부분 포인터처럼 조정된다.
  6. 읽기만 할 문자열 인자는 const char *가 의도를 잘 보여 준다.

관련해서 일반적인 배열과 포인터 차이를 먼저 넓게 잡고 싶다면 C 언어 포인터와 배열 차이 글이 도움이 됩니다. 포인터에 const가 붙을 때 의미가 다시 헷갈린다면 C 언어 const 포인터 정리 글까지 이어서 보면 흐름이 자연스럽습니다.

개념 기준은 cppreference string literal 문서, array 문서, pointer 문서를 참고했습니다.

함께보면 좋은 글