|

strcpy vs strncpy 차이: C 문자열 복사에서 무엇이 더 안전하고 무엇을 조심해야 할까

strcpy와 strncpy 차이를 설명하는 C 언어 대표 이미지
strncpy는 안전한 strcpy가 아니라 널 종료와 패딩 규칙이 다른 함수다

strcpy vs strncpy 차이는 C 문자열 함수를 배울 때 가장 많이 오해하는 주제 중 하나입니다. 이름만 보면 strncpy가 단순히 더 안전한 버전처럼 느껴지지만, 실제로는 널 종료 보장과 패딩 규칙이 달라서 오히려 다른 종류의 실수를 만들 수 있습니다.

이 글에서는 어떤 함수가 무조건 더 낫다고 단정하지 않고, 두 함수가 각각 무엇을 보장하고 무엇을 보장하지 않는지 차분하게 정리하겠습니다.

strcpy와 strncpy 핵심 비교 카드
복사 범위, 널 종료, 패딩 규칙이 서로 다르다

먼저 결론부터: 둘의 차이는 어디에 있을까

  • strcpy는 src 전체를 널 문자까지 복사한다.
  • strncpy는 최대 count개만 복사한다.
  • strncpy는 잘리면 결과가 널 종료되지 않을 수 있다.
  • strncpy는 src가 짧으면 남는 칸을 널 문자로 패딩한다.

strncpy는 자동으로 안전한 문자열을 만들어 주는 함수가 아닙니다. 이 한 줄을 먼저 잡고 가면 뒤의 설명이 훨씬 쉬워집니다.


strcpy는 정확히 무엇을 할까

char src[] = "abc";
char dest[4];
strcpy(dest, src);   // a, b, c, \0

strcpysrc가 가리키는 널 종료 문자열을 마지막 \0까지 복사합니다. 그래서 정상적으로 끝나면 dest도 문자열이 됩니다.

문제는 함수가 dest 크기를 모른다는 점입니다. 그래서 목적지 버퍼가 충분히 크다는 전제가 없으면 바로 undefined behavior 위험으로 이어집니다.

char src[] = "abcdef";
char dest[4];
strcpy(dest, src);   // 정의되지 않은 동작

Linux strcpy(3) man pagecppreference strcpy 문서도 dest가 충분히 크지 않으면 동작이 정의되지 않는다고 설명합니다.


strncpy는 왜 더 안전해 보이지만 오해를 부를까

strncpy(dest, src, count)는 최대 count개 문자만 복사합니다. 여기까지만 보면 길이 제한이 있으니 더 안전해 보입니다.

하지만 count 안에서 널 문자를 만나지 못하면 결과 배열은 문자열이 아닐 수 있습니다. 반대로 src가 더 짧으면 남는 count 구간을 계속 \0로 채웁니다.

strncpy가 널 종료 없이 끝날 수 있는 상황 그림
count 안에 널 문자가 없으면 결과는 문자열이 아닐 수 있다
char dest[4];
strncpy(dest, "abcdef", sizeof(dest));
printf("%s\n", dest);   // 위험

이 예시의 dest에는 a b c d만 들어가고 널 문자는 없습니다. 이 상태에서 printf("%s")strlen에 넘기면 정의되지 않은 동작이 될 수 있습니다.

CERT STR32-C도 바로 이런 패턴을 경고합니다.


src가 짧을 때는 왜 또 성격이 달라질까

char dest[8];
strncpy(dest, "hi", sizeof(dest));
// h i \0 \0 \0 \0 \0 \0

strncpy는 문자열이 일찍 끝나면 나머지 칸을 모두 널 문자로 패딩합니다. 그래서 단순히 길이를 제한한 복사라고만 이해하면 실제 동작을 놓치게 됩니다.

이 규칙은 고정 길이 필드를 다룰 때는 의미가 있지만, 일반적인 문자열 복사에서는 의도보다 무거울 수 있고 코드 독자도 쉽게 헷갈립니다.


그러면 어느 쪽이 더 안전할까

정답은 함수 이름이 아니라 정책입니다. 버퍼가 충분히 큰 것이 확실하고, 잘림을 허용하지 않으며, 전체 복사가 목적이라면 strcpy의 전제가 더 단순할 수 있습니다.

반대로 길이를 제한하고 싶다면 strncpy를 쓸 수는 있지만, 결과가 문자열인지 직접 보장하는 후처리가 빠지면 안 됩니다.

안전성은 strncpy라는 이름에서 자동으로 오지 않습니다. 널 종료 보장을 코드에서 직접 처리해야 합니다.


truncation을 허용한다면 이런 패턴이 더 명확하다

enum { DEST_SIZE = 8 };

void copy_text(char dest[DEST_SIZE], const char *src)
{
    strncpy(dest, src, DEST_SIZE - 1);
    dest[DEST_SIZE - 1] = '\0';
}

이 패턴의 핵심은 마지막 칸을 직접 \0로 고정하는 것입니다. 그러면 잘렸더라도 최소한 dest는 문자열이 됩니다.

다만 잘림이 발생했는지 별도로 알기 어렵다는 한계는 남습니다. 잘림이 에러여야 하는 상황이라면, 먼저 들어갈 수 있는지 검사한 뒤 복사하는 방식이 더 분명합니다.


실무에서 기억하면 좋은 체크리스트

  1. src가 정말 널 종료 문자열인가
  2. dest가 충분히 큰가
  3. 잘림을 허용하는가
  4. 결과를 문자열 함수에 넘길 것인가
  5. 널 종료를 코드에서 직접 보장했는가

관련해서 문자열과 배열 감각을 먼저 정리하고 싶다면 C 언어 문자열과 문자 배열 차이 글이 도움이 됩니다. 배열과 포인터의 정체성 차이까지 함께 보고 싶다면 C 언어 포인터와 배열 차이 글도 이어서 보면 흐름이 자연스럽습니다.

외부 기준은 cppreference strcpy, cppreference strncpy, Linux strcpy(3) man page, CERT STR32-C를 참고했습니다.

함께보면 좋은 글