[C언어 50강] 24강. 문자열 라이브러리: strlen/strcpy/strncpy/strcat/strcmp
문자열 라이브러리는 C에서 생산성을 올려주는 도구이지만, 원리를 모른 채 쓰면 버그를 더 빨리 만드는 도구가 되기도 합니다. 오늘은 strlen, strcpy, strncpy, strcat, strcmp를 함수 목록으로 암기하지 않고, 각 함수가 무엇을 전제로 동작하는지를 중심으로 이해합니다. 핵심은 단 하나입니다. C 문자열은 길이 정보를 따로 저장하지 않으며, \0을 만날 때까지 메모리를 읽는 규칙 위에 모든 함수가 서 있다는 점입니다.
핵심 개념
- C 문자열 함수는 대부분
\0종단 규칙을 전제로 하므로, 종단 보장이 깨지면 연쇄 버그가 발생한다. strcpy/strcat은 대상 버퍼 크기를 모르므로, 호출 전에 개발자가 용량을 계산해야 한다.strncpy는 “항상 안전한 복사 함수”가 아니며, 상황에 따라\0을 보장하지 않는다.strcmp의 반환값은 “같다/다르다”만이 아니라 사전순 관계를 나타내는 음수/0/양수다.- 문자열 처리는 함수 선택보다 버퍼 정책(길이, 경계, 종단, 검증) 설계가 먼저다.
개념 먼저 이해하기
문자열 라이브러리를 처음 배우면 보통 함수별 용도만 외웁니다. strlen은 길이, strcpy는 복사, strcat은 이어붙이기처럼요. 그런데 실무에서는 이 수준의 지식으로는 금방 한계에 부딪힙니다. 이유는 간단합니다. 이 함수들은 공통적으로 “입력 문자열이 정상적으로 \0으로 끝난다”는 강한 가정 위에서 동작하기 때문입니다. 즉 함수 자체가 메모리 안전성을 보장하는 것이 아니라, 호출자가 이미 안전한 상태를 만들었다고 가정하고 일을 수행합니다.
예를 들어 strlen(s)는 문자열 길이를 저장된 메타데이터에서 읽어오는 함수가 아닙니다. 시작 주소 s부터 한 바이트씩 전진하면서 \0을 만날 때까지 세는 루프에 가깝습니다. 이 말은 곧, s가 종단되지 않았거나 잘못된 주소를 가리키면 함수가 의도 범위를 넘어 메모리를 읽을 수 있다는 뜻입니다. strcpy와 strcat도 본질은 비슷합니다. 둘 다 대상 버퍼의 크기를 인자로 받지 않기 때문에, 함수는 “여기 충분한 공간이 있겠지”라고 믿고 복사/결합을 진행합니다. 안전성 책임은 100% 호출자에게 있습니다.
많은 입문자가 strncpy를 만능 안전 함수로 오해합니다. n 길이를 주니 자동으로 안전하다고 느끼기 때문입니다. 하지만 strncpy(dest, src, n)는 src 길이가 n 이상이면 dest 끝에 \0을 넣지 않습니다. 즉 결과가 문자열이 아닐 수 있습니다. 반대로 src가 짧으면 남는 구간을 \0으로 패딩해 불필요한 쓰기 비용이 생길 수도 있습니다. 그래서 현대 코드에서는 목적을 분리해 사용합니다. “정확히 n바이트 필드 복사” 같은 용도에는 적합하지만, 일반적인 문자열 복사에는 후처리(dest[n-1]='\0') 같은 방어 코드가 필요합니다.
strcmp도 단순한 불리언 함수가 아닙니다. 반환값이 0이면 동일, 0이 아니면 다름이라는 수준에서 멈추면 정렬/검색 같은 문제에서 힘을 잃습니다. strcmp(a,b) < 0이면 사전순으로 a가 앞서고, > 0이면 b가 앞섭니다. 이 성질은 문자열 정렬, 범위 조건, 접두어 기반 분기 설계에 직접 연결됩니다. 중요한 건 반환값 “정확한 숫자”가 아니라 “부호(sign)”입니다. 구현마다 구체 숫자는 달라도 부호 의미는 동일합니다.
결국 문자열 라이브러리 활용의 본질은 이렇습니다. 함수 이름을 고르는 문제가 아니라, “내가 지금 다루는 버퍼의 최대 길이, 현재 길이, 종단 상태, 입력 출처 신뢰도”를 먼저 정의하고 그 정책에 맞는 함수를 선택하는 문제입니다. C에서 안정적인 문자열 코드는 우연히 나오지 않습니다. 경계값을 먼저 계산하고, 실패 조건을 먼저 생각하고, 마지막에 함수를 호출해야 합니다. 이 순서를 몸에 익히면 문자열 버그의 80%는 사전에 제거할 수 있습니다.
기본 사용
예제 1) strlen과 strcmp로 상태 확인하기
#include <stdio.h>
#include <string.h>
int main(void) {
const char *cmd = "start";
printf("길이: %zu\n", strlen(cmd));
if (strcmp(cmd, "start") == 0) {
puts("시작 명령 인식");
} else if (strcmp(cmd, "stop") == 0) {
puts("정지 명령 인식");
} else {
puts("알 수 없는 명령");
}
return 0;
}
설명:
strlen은 바이트 단위 길이를 반환하므로 타입은size_t로 받는 습관이 좋습니다.strcmp는 문자열 내용 비교이며, 포인터 주소 비교(==)와 완전히 다른 동작입니다.- 조건 분기에서
strcmp(...) == 0패턴을 명확히 쓰면 오독을 줄일 수 있습니다.
예제 2) strcpy/strcat 사용 전 용량 계산
#include <stdio.h>
#include <string.h>
int main(void) {
const char *first = "Dev";
const char *second = "Lab";
// "Dev"(3) + "Lab"(3) + '\0'(1) = 7
char name[7];
strcpy(name, first);
strcat(name, second);
printf("결과: %s\n", name);
printf("최종 길이: %zu\n", strlen(name));
return 0;
}
설명:
strcat호출 시 대상 배열에는 기존 문자열 끝(\0)부터 이어 붙일 공간이 있어야 합니다.- 합친 뒤 길이를 확인하는 습관은 테스트 단계에서 경계 계산 오류를 빨리 발견하게 해줍니다.
- 실무에서는 상수 문자열 결합이라면 컴파일 타임 결합/포맷 함수 대체를 먼저 검토합니다.
예제 3) strncpy의 함정과 방어 패턴
#include <stdio.h>
#include <string.h>
int main(void) {
char dst[8];
const char *src = "ABCDEFGHIJK"; // 11글자
strncpy(dst, src, sizeof(dst));
// 방어 코드: 종단 보장
dst[sizeof(dst) - 1] = '\0';
printf("dst: %s\n", dst);
printf("len: %zu\n", strlen(dst));
return 0;
}
설명:
src가 더 길면strncpy는dst를 종단하지 않을 수 있습니다.- 따라서 문자열 용도로 쓸 때는 마지막 칸
\0강제 할당이 사실상 필수입니다. - 이 예제는 “복사 길이 제한”과 “문자열 유효성 보장”이 별개 문제임을 보여줍니다.
자주 하는 실수
실수 1) strcmp(a, b)를 불리언처럼 오해
- 원인: 0/1만 반환한다고 착각해
if (strcmp(a, b))를 동등 비교로 사용하는 실수. - 해결: “같다 = 0”을 명시적으로 기억하고, 동등 비교는
== 0, 순서 비교는< 0,> 0으로 작성합니다.
실수 2) strcpy 호출 전에 버퍼 크기 검증 생략
- 원인: 대상 배열 크기를 알지 못한 채 복사.
- 해결:
strlen(src) + 1 <= sizeof(dest)같은 사전 검증 또는 더 안전한 설계(동적 할당/포맷 제한) 적용.
실수 3) strncpy가 자동 종단한다고 믿음
- 원인: 길이 제한 인자가 있으니 문자열 안전성도 보장된다고 오해.
- 해결: 문자열 목적이면 복사 후
dest[n-1] = '\0'을 강제하고, 가능하면 목적에 맞는 대체 패턴 사용.
실수 4) strcat 누적 호출 시 남은 공간 계산 누락
- 원인: 반복 결합에서 총 길이 추적 없이 계속 덧붙임.
- 해결: 현재 길이(
strlen(dest))와 남은 공간(cap - curr - 1)을 매번 계산하거나 누적 빌더 함수를 둡니다.
실무 패턴
- 버퍼 계약 명시: 함수 인자로
char *buf, size_t cap를 함께 받고, 반환값으로 실제 사용 길이 또는 에러 코드를 돌려줍니다. - 문자열 유틸 래핑: 프로젝트 공통
safe_copy,safe_concat유틸을 두어 종단/길이 검증 정책을 통일합니다. - 검증 순서 고정: (1) 입력 신뢰도 확인 → (2) 길이 검증 → (3) 복사/결합 → (4) 종단 보장 → (5) 결과 검사.
- 리뷰 체크리스트 적용: PR 리뷰에서
strcpy/strcat사용 시 “버퍼 크기 근거” 코멘트를 의무화하면 사고율이 크게 줄어듭니다. - 테스트 경계 자동화: 빈 문자열, 정확히 cap-1 길이, cap 길이, cap+1 길이를 반복 테스트 케이스로 고정합니다.
오늘의 결론
한 줄 요약: C 문자열 함수는 편리하지만, 안전성은 함수가 아니라 버퍼 정책을 설계한 개발자가 책임집니다.
연습문제
safe_strcpy(char *dst, size_t cap, const char *src)를 작성하세요.- 복사 성공/잘림/실패를 반환값으로 구분하고, 항상
\0종단을 보장하세요.
- 복사 성공/잘림/실패를 반환값으로 구분하고, 항상
first,last이름을 받아"last, first"형식으로 합치는 프로그램을 작성하세요.- 결합 전 필요한 총 길이를 계산하고, 공간이 부족하면 에러 메시지를 출력하세요.
- 문자열 배열을 사전순으로 정렬하는 간단한 버블 정렬을 구현하세요.
- 비교는
strcmp를 사용하고, 반환 부호 기준으로 swap 여부를 결정하세요.
- 비교는
이전 강의 정답
지난 23강은 fgets 중심의 안전한 입력 흐름을 만드는 것이 핵심이었습니다.
정답 1) read_line 구현 예시
#include <stdio.h>
#include <string.h>
enum ReadResult { READ_OK, READ_TRUNCATED, READ_FAIL };
enum ReadResult read_line(char *buf, size_t n) {
int ch;
if (n == 0) return READ_FAIL;
if (fgets(buf, n, stdin) == NULL) {
return READ_FAIL;
}
size_t len = strcspn(buf, "\n");
if (buf[len] == '\n') {
buf[len] = '\0';
return READ_OK;
}
while ((ch = getchar()) != '\n' && ch != EOF) {}
return READ_TRUNCATED;
}
핵심 포인트:
- 입력 실패/정상/잘림을 분리해 호출자가 정책적으로 대응할 수 있게 만듭니다.
- 잘림 발생 시 남은 입력을 비워 다음 입력 안정성을 확보합니다.
정답 2) 제목/작성자 입력 후 출력
#include <stdio.h>
#include <string.h>
int main(void) {
char title[64], author[32];
printf("제목: ");
if (!fgets(title, sizeof(title), stdin)) return 1;
title[strcspn(title, "\n")] = '\0';
printf("작성자: ");
if (!fgets(author, sizeof(author), stdin)) return 1;
author[strcspn(author, "\n")] = '\0';
puts("--- 입력 결과 ---");
printf("제목: %s\n", title);
printf("작성자: %s\n", author);
return 0;
}
핵심 포인트:
- 프롬프트는
printf(줄바꿈 없음), 출력 요약은puts/printf조합으로 역할 분리.
정답 3) 버퍼 12바이트 상태 설명
- 5글자 입력(예:
ABCDE+ Enter): 버퍼에는ABCDE\n\0가 들어갈 수 있어 개행 포함 가능. - 11글자 입력(예:
ABCDEFGHIJK+ Enter): 버퍼가ABCDEFGHIJK\0로 꽉 차 개행은 스트림에 남음. - 20글자 입력: 첫 호출에는 앞 11글자+
\0만 저장되고, 나머지 글자와 개행이 스트림에 남아 후속 입력에 영향.
핵심 포인트:
- “한 줄 입력”과 “한 번의
fgets호출”은 항상 동일하지 않습니다. 버퍼 크기가 작으면 여러 번에 나뉘어 소비됩니다.
실습 환경/재현 정보
- 컴파일러: clang 17+ 또는 gcc 13+
- 컴파일 옵션:
-std=c11 -Wall -Wextra -O2 - 실행 환경: macOS(Apple Silicon) / Linux x86_64
- 재현 체크:
- 예제 2에서
name크기를 의도적으로 줄여 경계 오류를 관찰 - 예제 3에서 마지막 종단 코드를 제거해 출력 이상 가능성 확인
strcmp분기에서== 0을 빼먹었을 때 논리 오류가 어떻게 발생하는지 테스트
- 예제 2에서