C 문자열 리터럴과 문자열

문자열 리터럴 (String Literal)

문자열 리터럴은 소스 코드에 직접 작성된 문자열입니다. 예: "Hello, World!"

특징

  1. 읽기 전용 메모리에 저장 (일반적으로 코드 세그먼트)
  2. 수정 불가능 (수정 시도 시 정의되지 않은 동작)
  3. 프로그램 실행 중 메모리 주소가 고정
  4. 같은 리터럴은 메모리를 공유할 수 있음
#include <stdio.h>

int main() {
    // 문자열 리터럴을 포인터에 할당
    char *str1 = "Hello";
    char *str2 = "Hello";

    // 같은 리터럴이면 같은 주소를 가리킬 수 있음
    printf("%p\n", str1);  // 예: 0x4005f4
    printf("%p\n", str2);  // 예: 0x4005f4 (같은 주소)

    // 수정 시도 - 위험! (정의되지 않은 동작)
    // str1[0] = 'h';  // 런타임 에러 가능

    return 0;
}

문자열 배열 (String Array)

문자열 배열은 스택에 할당되는 수정 가능한 문자열입니다.

특징

  1. 스택 메모리에 저장
  2. 수정 가능
  3. 함수 종료 시 자동으로 해제
  4. 각 배열은 독립적인 메모리 공간을 가짐
#include <stdio.h>
#include <string.h>

int main() {
    // 문자열 배열 선언 방법들

    // 방법 1: 크기 명시
    char str1[20] = "Hello";

    // 방법 2: 자동 크기 결정
    char str2[] = "Hello";  // 크기는 6 (NULL 문자 포함)

    // 방법 3: 초기화 후 나중에 할당
    char str3[20];
    strcpy(str3, "Hello");

    // 수정 가능
    str1[0] = 'h';
    printf("%s\n", str1);  // "hello"

    // 각 배열은 독립적인 메모리
    printf("%p\n", str1);  // 서로 다른 주소
    printf("%p\n", str2);  // 서로 다른 주소

    return 0;
}

문자열 포인터 vs 문자열 배열

문자열 포인터 (읽기 전용)

char *str = "Hello";
  • 포인터 변수는 스택에 저장
  • 포인터가 가리키는 문자열은 읽기 전용 메모리에 저장
  • 수정 불가능

문자열 배열 (수정 가능)

char str[] = "Hello";
  • 배열 전체가 스택에 저장
  • 수정 가능
#include <stdio.h>

int main() {
    // 문자열 포인터
    char *ptr = "Hello";
    // ptr[0] = 'h';  // 위험! (정의되지 않은 동작)

    // 문자열 배열
    char arr[] = "Hello";
    arr[0] = 'h';  // 안전하게 수정 가능
    printf("%s\n", arr);  // "hello"

    return 0;
}

메모리 저장 위치 비교

#include <stdio.h>

int main() {
    // 문자열 리터럴 (읽기 전용 메모리, 보통 코드 세그먼트)
    char *literal1 = "Hello";
    char *literal2 = "Hello";

    // 문자열 배열 (스택 메모리)
    char arr1[] = "Hello";
    char arr2[] = "Hello";

    printf("리터럴 주소:\n");
    printf("literal1: %p\n", literal1);
    printf("literal2: %p\n", literal2);
    // 같은 리터럴이면 같은 주소일 수 있음

    printf("\n배열 주소:\n");
    printf("arr1: %p\n", arr1);
    printf("arr2: %p\n", arr2);
    // 각 배열은 독립적인 메모리 (다른 주소)

    return 0;
}

sizeof 연산자 차이

#include <stdio.h>

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

    // 포인터의 크기 (포인터 변수 자체의 크기)
    printf("sizeof(ptr): %zu\n", sizeof(ptr));  // 8 (64비트) 또는 4 (32비트)

    // 배열의 크기 (배열 전체의 크기)
    printf("sizeof(arr): %zu\n", sizeof(arr));  // 6 (NULL 문자 포함)

    // 문자열 길이 (NULL 문자 제외)
    printf("strlen(ptr): %zu\n", strlen(ptr));  // 5
    printf("strlen(arr): %zu\n", strlen(arr));  // 5

    return 0;
}

수정 가능 여부 비교

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

int main() {
    // 문자열 포인터 (읽기 전용)
    char *ptr = "Hello";

    // 문자열 배열 (수정 가능)
    char arr[] = "Hello";

    // 배열은 수정 가능
    arr[0] = 'h';
    strcpy(arr, "World");  // 가능
    printf("%s\n", arr);  // "World"

    // 포인터는 수정 불가
    // ptr[0] = 'h';  // 위험! (정의되지 않은 동작)
    // strcpy(ptr, "World");  // 위험! (세그먼트 폴트 가능)

    // 하지만 포인터 자체는 재할당 가능
    ptr = "World";  // 가능 (다른 리터럴을 가리키도록 변경)
    printf("%s\n", ptr);  // "World"

    // 배열은 재할당 불가
    // arr = "World";  // 컴파일 에러

    return 0;
}

함수 매개변수로 전달

포인터로 전달

#include <stdio.h>

void print_string(char *str) {
    printf("%s\n", str);
    // str[0] = 'X';  // 위험! (리터럴이면 수정 불가)
}

int main() {
    char *ptr = "Hello";
    char arr[] = "World";

    print_string(ptr);  // 가능
    print_string(arr);  // 가능 (배열이 포인터로 변환됨)

    return 0;
}

배열로 전달 (실제로는 포인터로 변환됨)

#include <stdio.h>

void print_string(char str[]) {  // 실제로는 char *str과 동일
    printf("%s\n", str);
    printf("sizeof(str): %zu\n", sizeof(str));  // 포인터 크기 (배열 크기가 아님!)
}

int main() {
    char arr[] = "Hello";
    print_string(arr);

    return 0;
}

동적 할당 문자열

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

int main() {
    // 힙 메모리에 동적 할당
    char *dynamic = (char *)malloc(20 * sizeof(char));
    strcpy(dynamic, "Hello");

    // 수정 가능
    dynamic[0] = 'h';
    printf("%s\n", dynamic);  // "hello"

    // 메모리 해제 필요
    free(dynamic);

    return 0;
}

실전 예제

안전한 문자열 수정

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

void modify_string(char *str) {
    // str이 리터럴인지 배열인지 확인 불가
    // 안전하게 수정하려면 배열을 전달해야 함

    // 방법 1: 새로운 배열 생성
    char buffer[100];
    strcpy(buffer, str);
    buffer[0] = 'X';  // 안전
    printf("%s\n", buffer);
}

int main() {
    char *literal = "Hello";
    char arr[] = "World";

    // 리터럴을 수정하려고 하면 위험
    modify_string(literal);  // 내부에서 복사하므로 안전

    // 배열은 안전
    modify_string(arr);

    return 0;
}

const 키워드 사용

#include <stdio.h>

int main() {
    // const를 사용하여 수정 불가능함을 명시
    const char *str1 = "Hello";  // 문자열은 수정 불가
    char *const str2 = "Hello";   // 포인터 자체는 수정 불가
    const char *const str3 = "Hello";  // 둘 다 수정 불가

    // str1[0] = 'h';  // 컴파일 에러
    // str2 = "World";  // 컴파일 에러
    // str3[0] = 'h';    // 컴파일 에러
    // str3 = "World";   // 컴파일 에러

    return 0;
}

문자열 복사 비교

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

int main() {
    char *literal = "Hello";

    // 방법 1: 배열로 복사 (스택)
    char arr[20];
    strcpy(arr, literal);
    arr[0] = 'h';  // 안전하게 수정 가능
    printf("%s\n", arr);

    // 방법 2: 동적 할당으로 복사 (힙)
    char *dynamic = (char *)malloc(strlen(literal) + 1);
    strcpy(dynamic, literal);
    dynamic[0] = 'h';  // 안전하게 수정 가능
    printf("%s\n", dynamic);
    free(dynamic);

    return 0;
}

주의사항

1. 리터럴 수정 시도

char *str = "Hello";
str[0] = 'h';  // 위험! 세그먼트 폴트 가능

2. 함수에서 지역 배열 반환

char *get_string() {
    char arr[] = "Hello";
    return arr;  // 위험! 스택 메모리는 함수 종료 시 해제됨
}

올바른 방법:

char *get_string() {
    char *str = "Hello";  // 리터럴은 프로그램 종료까지 유지
    return str;  // 안전
}

// 또는
char *get_string() {
    static char arr[] = "Hello";  // 정적 변수
    return arr;  // 안전
}

// 또는
char *get_string() {
    char *str = malloc(20);
    strcpy(str, "Hello");
    return str;  // 호출자가 free() 해야 함
}

3. sizeof와 strlen 혼동

char arr[] = "Hello";
printf("%zu\n", sizeof(arr));  // 6 (NULL 문자 포함)
printf("%zu\n", strlen(arr));  // 5 (NULL 문자 제외)

char *ptr = "Hello";
printf("%zu\n", sizeof(ptr));  // 8 (포인터 크기, 문자열 크기가 아님!)
printf("%zu\n", strlen(ptr));  // 5 (문자열 길이)

요약 표

구분문자열 리터럴 (포인터)문자열 배열
선언char *str = "Hello"char str[] = "Hello"
메모리 위치읽기 전용 메모리 (코드 세그먼트)스택 메모리
수정 가능❌ 불가능✅ 가능
sizeof 결과포인터 크기 (8 또는 4)배열 크기 (문자열 길이 + 1)
재할당✅ 가능 (다른 리터럴 가리키기)❌ 불가능
메모리 공유같은 리터럴은 공유 가능각각 독립적
함수 매개변수포인터로 전달포인터로 변환되어 전달

실전 팁

  1. 수정이 필요하면 배열 사용: char arr[] = "Hello"
  2. 읽기만 하면 포인터 사용: const char *str = "Hello"
  3. 함수 매개변수는 포인터 사용: 배열도 자동으로 포인터로 변환됨
  4. 동적 할당은 필요할 때만: malloc() 사용 후 반드시 free()
  5. const 키워드 적극 활용: 의도를 명확히 하고 실수 방지