C언어 - 문자열 배열과 포인터 그리고 안전한 복사
1. C언어 문자열
- C 언어에서
문자열은 Null 문자('\0')로 종료 되는 문자(char)의 연속된 메모리 영역으로 정의할 수 있다.
- Null 문자는 '\0'또는 0x00으로 표현된다.
- 문자열을 표현하기 위한 전용 자료형은 없으며, 문자열 처리는 포인터와 배열을 통해 수행된다.
#include <stdio.h>
#include <string.h>
int main()
{
char str[16];
strcpy(str, "012345678901234");
printf("%s,len=%zu,size=%zu\n", str, strlen(str), sizeof(str));
return 0;
}※초기화 없이 선언된 로컬 변수 char str[32] 메모리 공간에는 미정의 값(garbage value)이 채워진다.
※ %zu는
sizeof()와 strlen()이 반환하는 size_t
타입 값을 정확하게 출력하기 위한 형식 지정자로, 플랫폼 간 이식성을 높이기 위해 %ld 대신 사용한다.
output
012345678901234,len=15,size=16char str[16] 크기는 16이다. 그런데 문자열 맨 끝에 Null 문자가 포함되어야 하므로 15자 길이를 저장할 수 있다.
"012345678901234\0"
←─────15──────→ (strlen)
←─────16───────→ (sizeof)strlen()함수는 문자열의 길이를 구하는 함수로,'\0'가 나타날 때 까지의 문자를 카운트한다.- 반면
sizeof()함수는 문자열의 내용과는 상관없이 변수가 메모리에서 차지하는 크기를 구한다.
※ 해당 예제에는 입력 길이에 대한 검증이 없어, 배열의 크기를 초과하는 데이터를 저장할 수 있다. 이로 인해 메모리 손상이나 프로그램 오동작을 유발하는 버퍼 오버플로우(Buffer Overflow)문제가 발생할 위험이 있다.
1.1. 문자열 초기화
- 문자열 초기화는 문자를 배열이나 포인터에 처음 할당하는 과정이다.
- C 언어에서는 초기화 방법에 따라메모리 위치와 문자열을 수정할 수 있는지 여부가 달라지므로, 각 방식의 차이를 구분해서 이해해야 한다.
#include <stdio.h>
#include <string.h>
int main()
{
char str1[32] = "01234567890123456789";
char str2[] = "01234567890123456789";
char *str3 = "01234567890123456789";
printf("str1: %s,len=%zu,size=%zu\n", str1, strlen(str1), sizeof(str1));
printf("str2: %s,len=%zu,size=%zu\n", str2, strlen(str2), sizeof(str2));
printf("str3: %s,len=%zu,size=%zu\n", str3, strlen(str3), sizeof(str3));
return 0;
}char str1[32] = "..."- 32바이트 크기의 배열을 명시적으로 지정 → 나머지 공간은 '\0'으로 채워짐
char str2[] = "..."- 배열 크기를 생략 → 컴파일러가 문자열 길이 + 1('\0')로 자동 결정
char *str3 = "..."- 포인터로 문자열 변수 선언 → 문자열 리터럴 (읽기 전용 메모리)
output
str1: 01234567890123456789,len=20,size=32
str2: 01234567890123456789,len=20,size=21
str3: 01234567890123456789,len=20,size=8strlen(- 문자열 길이 (Null 문자 이전의 길이) → 세 변수 모두 20
sizeof()str1→ 32 (배열 전체 크기)str2→ 21 (문자 20 + '\0')str3→ 8 (포인터 자체의 크기 - 64비트 시스템)
1.2. 문자열 리터럴
- 문자열
리터럴 (String Literal)은 C 표준에
따라
const로 취급되는 읽기 전용 데이터이다. - 이는같은 리터럴을 여러 곳에서 사용할 때 하나의 복사본만 유지하여 메모리 효율성을 높이고, 프로그래머의 실수로 문자열 리터럴이 수정되는 것을 방지하기 위한 의도적인 보호 때문이다.
const char str2[]- 배열을 const로 명시적 선언 → 컴파일 에러 발생
char *str3- 문자열 포인터로리터럴을 가리킴 → 컴파일은 되지만 런타임 에러 발생(segmentation fault)
1.3. 안전한 문자열 복사
- C 언어에서 문자열 복사는 매우 자주 사용되는 작업이지만, 입력 문자열의 길이를 정확히 검증하지 않으면 버퍼 오버플로우(Buffer Overflow)가 발생할 위험이 있다.
- 앞에서도 언급한 것처럼 strcpy()와 같은 함수는 대상 버퍼의 크기를 고려하지 않고 문자열을 복사하므로, 복사 대상 배열보다 긴 문자열이 전달될 경우 메모리 손상이나 프로그램 오동작을 유발할 수 있다.
- 이를 방지하기 위해 문자열을 복사할 때는 목적지 버퍼의 크기를 반드시 고려해야 하며, 복사 길이를 제한하고 널 종료(Null termination)를 보장하도록 처리해야 한다.
#include <stdio.h>
#include <string.h>
int safe_strcpy(char *dest, size_t dest_size, const char *src)
{
if (dest == NULL || src == NULL) {
return -1; // error: NULL pointer
}
if (dest_size == 0) {
return -1; // error: destination size is 0
}
size_t src_len = strlen(src);
size_t copy_len = 0;
if (src_len < dest_size) {
copy_len = src_len;
} else {
/* source too large: truncate */
copy_len = dest_size - 1;
}
strncpy(dest, src, copy_len);
dest[copy_len] = '\0';
return 0; // Success
}
int main()
{
char str[16] = {0x00, };
strncpy(str, "01234567890123456789", sizeof(str) - 1);
str[sizeof(str) - 1] = '\0';
printf("%s,len=%zu,size=%zu\n", str, strlen(str), sizeof(str));
if (0 != safe_strcpy(str, sizeof(str), "01234567890123456789")) {
printf("string copy failed\n");
}else {
printf("%s,len=%zu,size=%zu\n", str, strlen(str), sizeof(str));
}
return 0;
}※ char
str[16] = {0x00, }는 문자 배열 전체를 0x00으로 초기화하여 빈 문자열 상태로 시작하고 Null 종료를 명확히
보장하기 위한 초기화 방식이다.
output
012345678901234,len=15,size=16
012345678901234,len=15,size=16strncpy()를 사용할 경우에도, 복사 이후 Null 문자를 직접 추가하지 않으면 문자열이 정상적으로 종료되지 않을 수 있으므로 주의가 필요하다.- 이는
strncpy()가 지정한 길이만큼만 문자를 복사할 뿐, 항상 null 문자를 추가해 주는 함수가 아니기 때문이다.
- 이는
- 이러한 문제를 함수 수준에서 안전하게 처리하기 위해
구현한
safe_strcpy()함수는 다음과 같이 안전한 문자열 복사를 수행한다.- 포인터와 버퍼 크기의 유효성을 먼저 검사한다.
- 버퍼 크기를 기준으로 복사 길이를 제한한다.
- 복사 후 Null 문자('\0')를 명시적으로 추가하여 문자열 종료를 보장한다.
※ C언어에서 문자열을 다루는 과정에서 버퍼 크기에 대한 검증과 널(Null) 종료가 보장되지 않으면 언제든지 치명적인 오류로 이어질 수 있다. 안전한 문자열 처리는 프로그램의 안정성과 보안 수준을 좌우하는 핵심 요소이다.