Coding & Programming/C언어

문자열 포인터를 사용하는 이유 및 문자 배열과의 차이

essdpt 2025. 4. 7. 00:43

1. 문자 배열을 선언하지 않고 문자열 리터럴을 그대로 포인터 변수에 담는 여러 가지 상황들

char *p = "Hello";

위 문장은 '문자열 리터럴'이 저장된 메모리의 시작 주소를 p라는 '문자열 포인터 타입의 변수'에 담는 문장이다. 이러한 문법구조를 사용하는 용도는 아래의 세 가지 상황이 대표적이다.

첫째, 함수의 인자로 문자열을 전달받고자 할 때

printf 함수는 첫번째 매개변수로 const char *format을 정의해두었다. 이 매개변수는 형식지정자가 포함된 형식문자열을 받아서 처리하기 위해 꼭 필요한 것으로, 사용자 입장에서는 이 매개변수 덕분에 문자열 리터럴을 그대로 printf의 인자로 전달해주는 것이 가능해진다. 아래는 printf 함수의 형태이다.

int printf(const char *format, ...) {
    va_list args;
    va_start(args, format);
    int result = vprintf(format, args);
    va_end(args);
    return result;
}

둘째, 명령어를 가상으로 테스트할 때

PC부팅 직전에 나오는 BIOS 설정 창이나 윈도우 설치 시 나오는 명령어 기반 설치 화면, 혹은 임베디드 장비의 초기 부팅화면 등에는 일반적으로 기능을 테스트하는 부분이 존재한다. 이때 기능(명령어 로직)이 제대로 동작하는지 확인하기 위해서는 해당 명령어가 잘 동작하고 있는지를 구체적으로 확인해야 하는데, 보통 사용자가 입력할 명령어를 가상으로 세팅해두고 그걸 기반으로 기능 호출 시의 정상동작 여부를 테스트하는 방식으로 진행된다. 이 과정에서 사용자 입력 명령어는 보통 '하드코딩된 문자열 리터럴의 포인터'를 이용하여 가상으로 세팅하는 게 일반적이다. 간단하게 예시를 보여주자면 아래와 같다.

void shutdown_system() {
    printf("[SYSTEM] Shutting down...\n");
    // 시스템 종료 관련 코드 들어갈 자리
}

int main() {
    char *cmd = "shutdown"; // 가상으로 명령어 문자열을 하드코딩

    if (strcmp(cmd, "shutdown") == 0) {
        shutdown_system();  // 실제 테스트할 기능 호출
    }
    return 0;
}

셋째, 다양한 문자열 중 하나의 문자열만을 선택적으로 사용하고 싶을 때

메시지를 다국어로 처리하는 게 이 경우의 대표적인 예시가 되겠다. 어떤 언어를 선택하느냐에 따라 문자열을 바꿔치기하여 여러 언어로 된 문자열 리터럴 중 하나를 선택해 할당하고자 한다면, 아래와 같이 문자열 리터럴의 시작주소를 포인터에 담는 식으로 작성하는 게 좋다.

#include <stdio.h>

int main() {
    char *lang = "ko";  // "en" or "ko" 가정
    char *msg;

    if (strcmp(lang, "en") == 0) {
        msg = "Welcome!";
    } else if (strcmp(lang, "ko") == 0) {
        msg = "환영합니다!";
    } else {
        msg = "Unsupported language.";
    }

    printf("%s\n", msg);

    return 0;
}

만약 이렇게 쓰지 않고 문자열을 활용하기 위해 일반적으로 쓰는 '문자 배열'을 쓴다면 코드는 아래와 같이 작성할 수 있을 것이다.

char msg[50];

if (strcmp(lang, "en") == 0) {
    strcpy(msg, "Welcome!");
} else if (strcmp(lang, "ko") == 0) {
    strcpy(msg, "환영합니다!");
} else {
    strcpy(msg, "Unsupported language.");
}

하지만 이때에는 배열의 특성 상 문자열 전체를 복사해야 하며, strcpy가 스택에 문자열을 복사하는 과정에서 메모리공간을 더 사용하고 복사하는 데 시간도 소요된다. 데이터의 개수가 매우 증가했을 때를 고려하면 이 차이는 어마어마할 것이다. 데이터의 수정이 필요없고 처리속도가 빨라지기를 바라는 상황에서 간결하게 코드를 작성하기 위해 문자열 리터럴의 포인터는 매우 효과적이다.

2. printf의 첫번째 매개변수에 전달 가능한 인자들

이외에도 다른 여러 상황들이 있겠지만, 앞서 살펴본 용도만으로도 char *p = "Hello";와 같은 문법의 필요성은 인정된다. 이러한 상황에서 덩달아 작성 가능해지는 것이 바로 아래와 같은 코드이다.

#include <stdio.h>

int main() {
    char str1[] = "Hello";
    printf(str1);
    char *p = "World";
    printf(p);
    return 0;
}

이것은 printf 함수의 첫번째 매개변수가 const char *format으로 정의되었기 때문에 가능해진 것인데, 문자 배열의 이름을 그대로 첫번째 인자로 전달하거나 문자열 리터럴의 시작주소를 담고 있는 포인터 변수를 그대로 첫번째 인자로 전달하는 것이다.

변수이름을 그대로 첫번째 인자로 넣는 것이 파이썬과 유사해보이기도 하는데, C언어와 파이썬의 차이점에 대해 간단히 알고 있던 사용자라면 특이하다고 여길 수 있다. 물론 C언어에 대해 깊이 파헤치지 않을 예정이라면 이것이 가능함을 알아두는 것만으로 충분하다고 생각한다.

3. 문자배열과 문자열 포인터의 차이

다음으로, 문자배열을 선언해서 사용하는 것과 문자열리터럴과 포인터를 사용하는 것이 어떤 점에서 다른지에 대해 알아보겠다.

문자열 리터럴은 컴파일 시 프로그램의 메모리 상으로 텍스트 영역(.rodata)(.rodata, Read-Only Data Segment — 읽기 전용 데이터 구역)에 저장된다. 이후 이걸 바로 포인터 변수로 저장하면 그 포인터 변수에는 .rodata 영역에 존재하는 해당 문자열 리터럴의 시작주소를 그대로 담게 된다. 만약 이 문자열 리터럴을 포인터 변수가 아닌 문자들의 배열로 선언하여 사용하고자 한다면, 프로그램의 스택 영역에 배열을 위한 메모리공간을 할당하고 거기에다가 .rodata영역에 있는 문자열 리터럴을 복사해서 넣어야 한다. 이렇게 되면 배열을 통해 접근하여 문자열을 수정할 수 있게 되지만 메모리를 많이 차지하게 된다. 결과적으로 해당 문자열 데이터는 .rodata 영역과 스택 영역에 중복저장되는 것이다. 물론 요즘의 발전된 컴파일러는 최적화에 따라 초기화에 사용한 이후 .rodata영역에 있는 문자열 리터럴을 바로 제거해주기도 하지만, 복사를 따로 해야 한다는 점에서 메모리 사용량이 더 많다는 점은 부정할 수 없다. 이는 문자열 개수가 매우 많아지거나 텍스트 자체의 길이가 매우 길 경우 차이가 명확히 드러날 것이다. 포인터로 간결하게 사용하는 방식이 엄청난 메모리 절약 효과를 가져오게 된다. 컴파일러의 최적화로 인해 메모리 상의 이점이 줄어들게 되었다는 의견에 대하여는, 임베디드 환경이나 리소스가 제한된 환경이라면 복사 자체가 부담일 수 있다는 점을 명심해야 한다고 생각한다. 또한 동일한 리터럴을 두 번 사용해야 할 때, 문자 배열이라면 두 문자열을 별도로 복사했겠지만 char*는 공유 가능한 문자열에 대해 같은 메모리 주소를 재사용할 수 있다는 점에서도 효과적이다.

구분 포인터 방식 배열 방식
코드 char *msg = "Hello"; char msg[] = "Hello";
저장 위치 .rodata 스택 + .rodata
수정 가능 여부 불가 가능
메모리 복사 없음 있음 (strcpy 등 필요)
메모리 사용량 적음 많음

정리하자면,

"현대 C 컴파일러는 문자열 상수를 최적화해서 복사 없이 사용하거나, 필요 없으면 제거하기까지 하므로 char *p = "Hello"; 방식의 메모리 절약 효과는 예전만큼 두드러지지 않는다. 다만, 여전히 char* 방식은 문자열을 가볍게 전달하거나 공유할 때, 그리고 printf 같은 함수와 함께 사용할 때 매우 유용하다."

라고 마무리할 수 있겠다.