문자열 리터럴 (String Literal)
문자열 리터럴은 소스 코드에 직접 작성된 문자열입니다. 예: "Hello, World!"
특징
- 읽기 전용 메모리에 저장 (일반적으로 코드 세그먼트)
- 수정 불가능 (수정 시도 시 정의되지 않은 동작)
- 프로그램 실행 중 메모리 주소가 고정됨
- 같은 리터럴은 메모리를 공유할 수 있음
#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)
문자열 배열은 스택에 할당되는 수정 가능한 문자열입니다.
특징
- 스택 메모리에 저장
- 수정 가능
- 함수 종료 시 자동으로 해제됨
- 각 배열은 독립적인 메모리 공간을 가짐
#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) |
| 재할당 | ✅ 가능 (다른 리터럴 가리키기) | ❌ 불가능 |
| 메모리 공유 | 같은 리터럴은 공유 가능 | 각각 독립적 |
| 함수 매개변수 | 포인터로 전달 | 포인터로 변환되어 전달 |
실전 팁
- 수정이 필요하면 배열 사용:
char arr[] = "Hello" - 읽기만 하면 포인터 사용:
const char *str = "Hello" - 함수 매개변수는 포인터 사용: 배열도 자동으로 포인터로 변환됨
- 동적 할당은 필요할 때만:
malloc()사용 후 반드시free() - const 키워드 적극 활용: 의도를 명확히 하고 실수 방지