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=16

char 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=8
  • strlen(
    • 문자열 길이 (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=16
  • strncpy()를 사용할 경우에도, 복사 이후 Null 문자를 직접 추가하지 않으면 문자열이 정상적으로 종료되지 않을 수 있으므로 주의가 필요하다.
    • 이는strncpy()가 지정한 길이만큼만 문자를 복사할 뿐, 항상 null 문자를 추가해 주는 함수가 아니기 때문이다.
  • 이러한 문제를 함수 수준에서 안전하게 처리하기 위해 구현한 safe_strcpy()함수는 다음과 같이 안전한 문자열 복사를 수행한다.
    • 포인터와 버퍼 크기의 유효성을 먼저 검사한다.
    • 버퍼 크기를 기준으로 복사 길이를 제한한다.
    • 복사 후 Null 문자('\0')를 명시적으로 추가하여 문자열 종료를 보장한다.

※ C언어에서 문자열을 다루는 과정에서 버퍼 크기에 대한 검증과 널(Null) 종료가 보장되지 않으면 언제든지 치명적인 오류로 이어질 수 있다안전한 문자열 처리는 프로그램의 안정성과 보안 수준을 좌우하는 핵심 요소이다.