[C/C++] strcpy vs strncpy: 문자열 복사 완벽 마스터하기

안녕하세요! 오늘은 C언어에서 문자열을 다룰 때 빼놓을 수 없는 두 가지 함수, strcpystrncpy에 대해 깊이 있게 알아보려고 합니다. 이 함수들은 문자열을 복사하는 기본적인 기능을 제공하지만, 정확히 알고 사용하지 않으면 예기치 않은 문제를 일으킬 수도 있답니다.

1. 기본 다지기: strcpy와 strncpy 알아보기

C언어에서 문자열은 기본적으로 char형 배열이나 포인터로 다루며, 문자열의 끝은 널(NULL) 문자 \0로 표시됩니다. strcpystrncpy는 이러한 C 스타일 문자열을 복사하는 데 사용되는 표준 라이브러리 함수입니다.

1.1. strcpy: 전체를 복사하는 해결사

  • 헤더 파일: <string.h> (C++에서는 <cstring>)
  • 함수 원형: char* strcpy(char* destination, const char* source);
  • 기능: source가 가리키는 문자열 (널 문자 \0 포함)을 destination이 가리키는 메모리 공간으로 복사합니다.
  • 반환 값: destination 포인터를 반환합니다.

strcpy는 이름에서 알 수 있듯(STRing CoPY), source 문자열 전체를 destination으로 옮겨 담습니다. 이때, 문자열의 끝을 알리는 \0까지 함께 복사된다는 점이 중요합니다.

간단 사용 예:

char source_str[] = "Hello C!";
char dest_str[20];

strcpy(dest_str, source_str);
// 이제 dest_str에는 "Hello C!\0"가 복사되어 있습니다.

1.2. strncpy: 원하는 만큼만, 안전하게?

  • 헤더 파일: <string.h> (C++에서는 <cstring>)
  • 함수 원형: char* strncpy(char* destination, const char* source, size_t num);
  • 기능: source가 가리키는 문자열에서 최대 num개의 문자를 destination으로 복사합니다.
  • 반환 값: destination 포인터를 반환합니다.

strncpystrcpyn (number)이 추가된 형태로, 복사할 최대 문자 수를 num 매개변수로 지정할 수 있습니다. 이것만 보면 strcpy보다 안전해 보이지만, 몇 가지 까다로운 특징이 있습니다. (이 부분은 '주의사항'에서 자세히 다룰게요!)

간단 사용 예:

char source_data[] = "BlockDMask";
char dest_data[100];

strncpy(dest_data, source_data, 5); // source_data에서 처음 5개 문자만 복사
// dest_data에는 "Block"이 복사되지만, \0의 존재는 보장되지 않습니다!
// 따라서 수동으로 추가해줘야 할 수 있습니다: dest_data[5] = '\0';

2. 실제 사용법: 예제로 이해하기

백문이 불여일견! 예제를 통해 두 함수가 어떻게 동작하는지 살펴보겠습니다.

2.1. strcpy 사용 예제

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

int main() {
    char origin[] = "TestString"; // 길이 10 (널 문자 제외)
    char dest1[20];
    char dest2_small[5]; // 복사될 문자열보다 작은 버퍼
    char dest3_overwrite[15] = "Old Value+++++";

    // Case 1: 충분한 공간으로 복사
    strcpy(dest1, origin);
    printf("dest1: %s\n", dest1); // 출력: TestString

    // Case 2: 작은 버퍼로 복사 (위험! 주석 처리)
    // strcpy(dest2_small, origin); // 버퍼 오버플로우 발생 가능성!
    // printf("dest2_small: %s\n", dest2_small);

    // Case 3: 기존 내용이 있는 더 큰 버퍼로 복사
    strcpy(dest3_overwrite, origin);
    printf("dest3_overwrite: %s\n", dest3_overwrite); // 출력: TestString (Old Value+++++는 덮어쓰임)
    // "TestString\0+++++" 와 같이 메모리에 저장되지만, printf는 \0까지만 읽음.

    return 0;
}

결과 분석:

  • dest1: origin 문자열이 \0까지 성공적으로 복사됩니다.
  • dest2_small: origin("TestString", 10글자)을 5칸짜리 dest2_small에 복사하려 하면 버퍼 오버플로우가 발생하여 프로그램이 비정상 종료되거나 예상치 못한 동작을 할 수 있습니다. 절대 금물!
  • dest3_overwrite: origin이 복사되면서 기존 "Old Value" 부분을 덮어씁니다. origin\0 문자까지 복사되므로, printf는 "TestString"까지만 출력합니다.

2.2. strncpy 사용 예제

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

int main() {
    char source[] = "ExampleStr"; // 길이 10 (널 문자 제외)
    char dest_a[15];
    char dest_b[15] = "Original Data";
    char dest_c[5]; // 복사할 일부 문자열을 담을 버퍼

    // Case 1: 원본 문자열 길이만큼 정확히 복사 (널 문자 포함)
    strncpy(dest_a, source, strlen(source) + 1); // strlen(source) + 1은 널 문자까지의 길이
    // 또는 strncpy(dest_a, source, sizeof(source)); 도 가능
    printf("dest_a: %s\n", dest_a); // 출력: ExampleStr

    // Case 2: 원본 문자열보다 적은 수의 문자만 복사 (널 문자 미포함 가능성)
    strncpy(dest_b, source, 4); // "Exam"만 복사
    // dest_b는 "Examle Data\0" 와 같이 앞부분만 바뀜.
    // 안전하게 하려면: dest_b[4] = '\0';
    printf("dest_b (처음 4개 복사 후): %s\n", dest_b); // 출력: Examle Data

    // Case 3: 널 문자를 수동으로 추가해야 하는 경우
    strncpy(dest_c, source, 4); // "Exam" 복사
    // dest_c[4] = '\0'; // 이 줄이 없다면? -> 쓰레기 값 출력 가능성
    printf("dest_c (널 문자 추가 전): %s\n", dest_c); // 쓰레기 값 포함될 수 있음
    dest_c[4] = '\0';
    printf("dest_c (널 문자 추가 후): %s\n", dest_c); // 출력: Exam

    // Case 4: 복사할 개수(num)가 원본 문자열 길이(널 포함)보다 클 때
    char dest_d[15];
    strncpy(dest_d, "Hi", 10); // "Hi\0" 복사 후 나머지 공간을 \0으로 채움 (총 10개)
    printf("dest_d: %s (문자 H 위치: %p)\n", dest_d, &dest_d[0]);
    printf("dest_d[5] (널문자): %d (문자 H로부터 5칸 뒤: %p)\n", dest_d[5], &dest_d[5]); // 0 (널 문자)

    return 0;
}

결과 분석:

  • dest_a: strlen(source) + 1 만큼 복사하여 \0까지 안전하게 복사합니다.
  • dest_b: source의 처음 4글자("Exam")만 dest_b의 앞부분에 덮어씁니다. strncpy는 지정된 num만큼만 처리하므로, dest_b의 나머지 "ple Data" 부분과 \0은 그대로 남아있습니다.
  • dest_c: source의 처음 4글자("Exam")를 복사합니다. 만약 dest_c[4] = '\0'; 라인이 없다면, dest_c는 널로 끝나지 않아 printf가 쓰레기 값을 출력할 수 있습니다. strncpy 사용 시 가장 주의해야 할 부분입니다!
  • dest_d: num(10)이 복사할 문자열("Hi", \0 포함 3)보다 크면, "Hi\0"을 복사한 후 destination의 나머지 공간을 num에 도달할 때까지 \0으로 채웁니다(padding).

3. 핵심 주의사항: 이것만은 꼭! (strcpy & strncpy 함정 피하기)

이제 두 함수의 가장 중요한 차이점과 사용 시 주의사항을 정리해 보겠습니다.

3.1. strcpy의 최대 함정: "버퍼 오버플로우"

strcpydestination 버퍼의 크기를 전혀 고려하지 않습니다. 만약 source 문자열의 길이가 destination 버퍼보다 크면, 버퍼의 경계를 넘어 데이터를 쓰게 되어 버퍼 오버플로우(Buffer Overflow)가 발생합니다. 이는 심각한 보안 취약점으로 이어질 수 있으며, 프로그램 오작동의 주범이 됩니다.

→ 해결책: strcpy를 사용하기 전, destination 버퍼가 source 문자열을 담기에 충분한지 반드시 확인해야 합니다. (예: if (strlen(source) < sizeof(dest_buffer))) 하지만 이런 확인은 번거롭고 실수하기 쉽습니다.


3.2. strncpy의 복잡성: \0 (널 문자)와 길이(num)의 딜레마

strncpynum이라는 길이 제한 덕분에 strcpy의 무분별한 버퍼 오버플로우를 막아주는 듯 보입니다. 하지만 다음과 같은 특징 때문에 사용이 까다롭습니다.

  • 널 문자 자동 추가의 부재: 만약 복사할 문자열(source)의 실제 길이(널 문자 제외)가 num보다 크거나 같으면, strncpydestination널 문자(\0)를 자동으로 추가하지 않습니다.

    char src[] = "longstring";
    char dest[5];
    strncpy(dest, src, 5); // dest에는 "longs"가 복사됨. \0 없음!
    // printf("%s", dest); // 위험! dest는 널로 끝나지 않은 문자열
    dest[4] = '\0'; // 이렇게 수동으로 널 문자를 추가해야 안전 (버퍼 크기가 5라면 dest[4]가 마지막)
    // 또는 strncpy(dest, src, sizeof(dest)-1); dest[sizeof(dest)-1] = '\0';
    

    이것이 strncpy 사용 시 가장 흔히 저지르는 실수이며, 예상치 못한 결과(쓰레기 값 출력, 다른 메모리 침범 등)를 초래합니다.

  • 널 문자로 채우기(Padding): 만약 source 문자열의 길이(널 문자 포함)가 num보다 작으면, strncpysource를 복사한 후 destination의 나머지 공간을 num개의 문자가 채워질 때까지 \0으로 채웁니다.

    char src[] = "Hi"; // "Hi\0" (3바이트)
    char dest[10];
    strncpy(dest, src, 7); // dest에는 "Hi\0\0\0\0\0" 가 복사됨 (총 7바이트 처리)
    

    이는 때로 유용할 수 있지만, 불필요한 널 문자 채우기로 성능 저하를 일으킬 수도 있습니다.

  • num 값 설정의 중요성:

    • numdestination 버퍼의 전체 크기를 넘지 않도록 주의해야 합니다. (num <= sizeof(destination))
    • destination에 안전하게 널 문자를 추가하려면, numsizeof(destination) - 1로 하고, 복사 후 destination[sizeof(destination)-1] = '\0';를 명시적으로 해주는 것이 일반적인 안전한 사용법입니다.

4. 어떤 함수를 선택해야 할까? (strcpy vs. strncpy)

  • strcpy:

    • 장점: 사용이 간단명료합니다.
    • 단점: 버퍼 오버플로우에 매우 취약합니다.
    • 언제 사용?: 복사될 문자열의 최대 길이를 명확히 알고 있고, 목적지 버퍼가 항상 충분히 크다는 것이 100% 보장될 때만 제한적으로 사용합니다. (예: 컴파일 타임에 크기가 결정되는 문자열 리터럴 복사)
  • strncpy:

    • 장점: 복사할 최대 길이를 제한하여 strcpy보다는 버퍼 오버플로우에 덜 취약합니다.
    • 단점: 널 문자를 항상 보장하지 않아 수동 관리가 필요하며, 사용이 다소 복잡합니다. 널 패딩으로 인한 비효율이 있을 수 있습니다.
    • 언제 사용?: 외부 입력이나 동적으로 길이가 변하는 문자열을 다룰 때, 버퍼 크기를 지정하여 복사해야 할 경우 사용합니다. 단, 널 문자 처리에 각별히 주의해야 합니다.

현대적인 대안: 사실, C11 표준부터는 strcpy_sstrncpy_s와 같이 좀 더 안전한 함수들이 제공됩니다. (MSVC 컴파일러 등에서는 이전부터 지원) 이 함수들은 목적지 버퍼의 크기를 명시적으로 인자로 받아 좀 더 안전한 작업을 수행합니다. 가능하다면 이런 보안 강화 함수들을 사용하는 것이 좋습니다. C++ 환경이라면 std::string 클래스를 사용하는 것이 훨씬 안전하고 편리합니다.

5. 마무리하며

오늘은 C언어의 대표적인 문자열 복사 함수 strcpystrncpy에 대해 알아보았습니다. strcpy는 간편하지만 위험하고, strncpy는 조금 더 안전하지만 널 문자 처리라는 숨겨진 복잡성이 있다는 것을 알게 되셨을 겁니다.

가장 중요한 것은 목적지 버퍼의 크기를 항상 인지하고, 문자열의 끝을 알리는 \0을 올바르게 처리하는 것입니다. 이 두 가지만 명심한다면 C언어에서도 문자열을 좀 더 안전하게 다룰 수 있을 거예요.