Skip to Content
독학사독학사 2단계C프로그래밍18. 문자열 라이브러리 함수와 시험 단골 패턴

이번 문서의 목표: 이 파일을 다 읽으면 <string.h>의 핵심 문자열 함수 다섯 개를 시그니처와 반환값까지 정확히 쓸 수 있고, 버퍼 크기나 널 문자를 빠뜨려 생기는 시험 단골 오류를 코드만 보고 짚어낼 수 있으며, 포인터 인자로 넘어온 문자열이 함수 안에서 어떻게 다뤄지는지 추적할 수 있다.

왜 이 함수들이 시험 단골인가

11편에서 문자열은 “널 문자(\0)로 끝나는 char 배열”이라고 배웠습니다. 그런데 C에는 다른 언어처럼 문자열을 통째로 대입하거나(str1 = str2), +로 이어 붙이거나, ==로 비교하는 문법이 없습니다. 이 모든 작업을 함수 호출로 대신해야 하는데, 그 함수들이 바로 <string.h>에 있는 strlen, strcpy, strncpy, strcat, strcmp 등입니다. 이 함수들은 내부적으로 포인터 연산과 널 문자 검사를 반복하는 형태로 구현되어 있어서, 12~13편에서 배운 포인터 지식과 정확히 맞물립니다. 독학사 시험은 이 함수들의 반환값버퍼 크기 계산을 특히 자주 묻습니다.

쉽게 말하면: C의 문자열 함수들은 “복사기”, “이어 붙이는 테이프”, “길이를 재는 자”처럼, 문자열에 대해 하고 싶은 동작 하나하나를 대신해 주는 도구 상자입니다. 도구를 잘못 쓰면(버퍼 크기를 안 맞추면) 종이가 찢어지듯 메모리가 깨집니다.

1. strlen — 문자열 길이 재기

size_t strlen(const char *s);
  • s가 가리키는 문자열에서 널 문자를 제외한 글자 수를 셉니다.
  • 반환 타입은 size_t(부호 없는 정수)입니다.
  • const char *로 받는다는 것은, 이 함수가 문자열의 내용을 읽기만 하고 바꾸지 않는다는 약속을 매개변수 타입으로 드러낸 것입니다(13편의 const 포인터 개념).
#include <stdio.h> #include <string.h> int main(void) { char s[] = "Hello"; printf("%zu\n", strlen(s)); printf("%zu\n", sizeof(s)); return 0; }
5 6

자주 틀리는 점: strlen(s)sizeof(s)를 혼동하는 것이 가장 흔한 실수입니다. strlen실행 중에 문자를 하나씩 세어 널 문자를 만나면 멈추는 함수 호출이고, sizeof컴파일 타임에 배열 전체의 바이트 크기(널 문자까지 포함)를 계산하는 연산자입니다. char s[] = "Hello";처럼 배열로 선언했을 때만 sizeof(s)가 6(글자 5개 + 널 문자 1개)이 되고, char *s = "Hello";처럼 포인터로 선언했다면 sizeof(s)는 포인터 자체의 크기(보통 8바이트, 64비트 환경 기준)가 되어 버립니다. 이 배열-포인터 sizeof 차이는 12편에서 배운 “배열 이름은 대부분 포인터로 변환되지만, sizeof를 적용할 때만은 예외로 배열 전체 크기를 돌려준다”는 규칙과 정확히 일치합니다.

2. strcpy — 문자열 복사

char *strcpy(char *dest, const char *src);
  • src가 가리키는 문자열을(널 문자까지 포함해서) dest가 가리키는 공간으로 복사합니다.
  • **반환값은 dest**입니다. 복사한 결과의 길이나 성공 여부를 알려 주는 것이 아니라, 그냥 첫 번째 인자로 받은 주소를 그대로 돌려줍니다. 이 반환값은 printf(strcpy(buf, "Hi"))처럼 함수 호출을 연달아 이어 쓸 때(체이닝) 쓰입니다.
  • dest가 가리키는 공간은 반드시 src 문자열의 길이(널 문자 포함)만큼 이상 커야 합니다. strcpy는 자체적으로 크기를 검사하지 않습니다. 이것이 시험에서 가장 자주 나오는 함정입니다.
#include <stdio.h> #include <string.h> int main(void) { char dest[10]; char *result = strcpy(dest, "Hello"); printf("%s\n", dest); printf("%s\n", result); return 0; }
Hello Hello

dest는 10칸이고 "Hello"는 널 문자 포함 6바이트이므로 안전합니다. 그러나 다음처럼 dest가 너무 작으면 문제가 됩니다.

char dest[3]; strcpy(dest, "Hello"); // 위험: dest는 3칸인데 6바이트를 복사하려 함

이 코드는 컴파일은 되지만, 실행하면 dest 배열의 범위를 넘어서는 메모리까지 덮어써 버립니다(버퍼 오버플로). 이 문제는 17편에서 다룬 “문자열을 위한 동적 할당은 strlen(src) + 1만큼”이라는 규칙과 같은 맥락으로, strcpy를 쓸 때는 항상 목적지 버퍼의 크기가 충분한지 미리 확인해야 합니다.

3. strncpy — 크기를 제한하는 복사

char *strncpy(char *dest, const char *src, size_t n);
  • src에서 최대 n바이트까지만 dest로 복사합니다. strcpy의 버퍼 오버플로 위험을 줄이기 위한 함수입니다.
  • 주의할 동작 두 가지가 시험에서 자주 나옵니다.
    1. src의 길이가 n보다 짧으면, 복사한 뒤 남는 자리를 전부 널 문자로 채웁니다.
    2. src의 길이가 n보다 같거나 길면, dest널 문자를 자동으로 붙여 주지 않습니다.n바이트를 꽉 채워 복사한 경우, 그 결과가 문자열로서 끝을 알리는 널 문자가 없을 수 있습니다.
#include <stdio.h> #include <string.h> int main(void) { char dest[10]; strncpy(dest, "Hi", 5); for (int i = 0; i < 10; i++) { printf("%d ", dest[i]); } printf("\n"); return 0; }
72 105 0 0 0 -52 -52 -52 -52 -52

한 줄씩 실행 추적

인덱스내용설명
dest[0]72문자 'H'의 아스키(ASCII) 코드 값
dest[1]105문자 'i'의 아스키 코드 값
dest[2]–dest[4]0n=5이고 “Hi”는 2글자이므로, 남는 3칸(인덱스 2, 3, 4)을 strncpy가 널 문자(0)로 채움
dest[5]–dest[9]초기화되지 않은 쓰레기 값(예시로 -52)strncpy가 손대지 않은 영역이라 배열 선언 당시의 쓰레기 값이 그대로 남음

이 예시에서 실제 쓰레기 값은 실행 환경마다 다르게 나타나므로, “정확히 몇 번째 값이 나온다”고 단정하는 문제는 출제되지 않습니다. 시험이 확인하려는 것은 “n보다 짧게 복사되면 남는 자리가 0으로 채워진다”와 “strncpy가 손대는 범위는 딱 n바이트까지다”라는 규칙입니다.

char dest[5]; strncpy(dest, "Hello", 5); // "Hello"는 5글자 + 널 문자 = 6바이트인데 n=5 printf("%s\n", dest); // 위험: dest에 널 문자가 없어 문자열의 끝을 알 수 없음

자주 틀리는 점: strncpy(dest, src, n)을 쓰기만 하면 항상 안전하다고 오해하기 쉽지만, src의 길이가 n과 같거나 길면 널 문자가 붙지 않는다는 점을 놓치면 printf("%s", dest)에서 dest 배열의 끝을 넘어서까지 계속 문자를 읽어(오버런, overrun) 예측할 수 없는 문자가 출력될 수 있습니다. 이런 경우를 대비해 dest[n-1] = '\0';을 직접 덧붙여 안전을 보장하는 관례를 시험 문제 해설에서 함께 봐 두는 것이 좋습니다.

4. strcat — 문자열 이어 붙이기

char *strcat(char *dest, const char *src);
  • dest 문자열의 (널 문자 위치)부터 src 문자열을 이어 붙입니다. 결과는 다시 dest에 저장되고, 반환값도 dest입니다.
  • dest가 가리키는 공간은 원래 있던 문자열 길이 + src 문자열 길이 + 널 문자 1개를 모두 담을 수 있을 만큼 충분히 커야 합니다. strcpy와 마찬가지로 크기 검사를 하지 않습니다.
#include <stdio.h> #include <string.h> int main(void) { char dest[20] = "Hello, "; strcat(dest, "World!"); printf("%s\n", dest); return 0; }
Hello, World!

dest는 처음에 "Hello, "(7글자 + 널 문자 = 8바이트)를 담고 있었고, 여기에 "World!"(6글자)를 이어 붙이면 최종적으로 13글자 + 널 문자 = 14바이트가 필요합니다. dest를 20칸으로 선언했으므로 안전합니다. 만약 dest를 10칸으로만 선언했다면 strcpy와 똑같이 버퍼 오버플로가 발생합니다.

5. strcmp — 문자열 비교

int strcmp(const char *s1, const char *s2);
  • 두 문자열을 사전식(lexicographic) 순서로 비교합니다. == 연산자로는 문자열의 내용을 비교할 수 없다는 것이 08편에서 다룬 원칙이었는데, strcmp가 바로 그 대안입니다.
  • 반환값은 세 가지 경우로만 나뉘며, 정확한 숫자값이 아니라 부호(양수/0/음수)로 해석해야 합니다.
비교 결과반환값의미
s1s2보다 사전 순서상 앞선다음수s1 < s2
s1s2가 완전히 같다0s1 == s2
s1s2보다 사전 순서상 뒤진다양수s1 > s2
#include <stdio.h> #include <string.h> int main(void) { printf("%d\n", strcmp("apple", "apple")); printf("%d\n", strcmp("apple", "banana") < 0); printf("%d\n", strcmp("banana", "apple") > 0); return 0; }
0 1 1

자주 틀리는 점: if (strcmp(a, b) == 1)처럼 “두 문자열이 다르면 반환값이 정확히 1이다”라고 오해하는 것이 대표적인 오답입니다. 표준은 반환값이 “음수/0/양수 중 어느 쪽인가”만 보장하지, 그 정확한 절댓값(1, 2, 47 등)은 컴파일러 구현에 따라 달라질 수 있습니다. 그래서 두 문자열이 같은지 확인할 때는 반드시 strcmp(a, b) == 0으로 비교해야 하고, “다르다”는 것을 확인하려면 strcmp(a, b) != 0을 씁니다.

6. 다섯 함수 한눈에 정리

함수시그니처반환값주의할 점
strlensize_t strlen(const char *s)널 문자 제외 글자 수sizeof와 혼동 금지
strcpychar *strcpy(char *dest, const char *src)dest목적지 크기 검사 없음
strncpychar *strncpy(char *dest, const char *src, size_t n)destn을 채우면 널 문자 자동 미부착
strcatchar *strcat(char *dest, const char *src)dest목적지 크기 검사 없음
strcmpint strcmp(const char *s1, const char *s2)음수/0/양수정확한 값이 아니라 부호로 판단

7. 포인터 인자로 호출을 분석하는 연습

시험형 문제는 흔히 이 함수들에 포인터 변수를 인자로 넘기고 결과를 묻습니다.

#include <stdio.h> #include <string.h> void appendMark(char *s) { strcat(s, "!"); } int main(void) { char buf[10] = "Hi"; char *p = buf; appendMark(p); printf("%s\n", buf); return 0; }
Hi!

한 줄씩 실행 추적

실행 내용
char buf[10] = "Hi";buf 배열에 “Hi”와 널 문자, 나머지는 초기화 안 됨
char *p = buf;p는 buf 배열의 시작 주소를 가리킴(12편의 배열-포인터 관계)
appendMark(p);p에 담긴 주소값이 함수의 매개변수 s로 복사되어 전달(13편의 값 전달 원칙)
함수 안 strcat(s, "!")s가 가리키는 곳(결국 buf와 같은 곳)의 끝에 ”!”를 이어 붙임
함수 종료 후 printf("%s\n", buf)buf 자체가 함수 안에서 변경된 내용을 그대로 반영해 출력

이 문제가 시험에서 중요한 이유는, 13편에서 배운 “배열은 함수에 넘기면 주소가 복사되므로, 함수 안에서 그 주소를 통해 원본 배열의 내용을 바꿀 수 있다”는 원칙이 문자열 함수 호출에도 그대로 적용된다는 것을 보여주기 때문입니다. appendMark 함수는 s라는 지역 변수(포인터)를 받았을 뿐이지만, s가 가리키는 곳은 mainbuf와 같은 메모리이므로 원본이 실제로 바뀝니다.

핵심 정리

  • strlen은 실행 중 널 문자 앞까지 글자 수를 세는 함수 호출이고, sizeof는 컴파일 타임에 배열 전체 크기를 구하는 연산자다. 포인터에 sizeof를 적용하면 포인터 자체의 크기가 나온다.
  • strcpy, strcat은 목적지 버퍼의 크기를 검사하지 않으므로, 호출 전 목적지가 충분히 큰지 프로그래머가 직접 확인해야 한다.
  • strncpyn을 채우도록 복사한 경우 널 문자를 자동으로 붙이지 않으므로, 문자열로 안전하게 쓰려면 마지막 칸을 직접 널 문자로 채워야 할 수 있다.
  • strcmp의 반환값은 정확한 숫자가 아니라 음수/0/양수의 부호로 해석하며, 같음을 확인할 때는 반드시 == 0으로 비교한다.
  • 포인터를 함수 인자로 넘겨 문자열 함수를 호출하면, 그 포인터가 가리키는 원본 데이터가 함수 밖에서도 바뀐 채로 남는다.

마무리 복습

문제 14지선다
char s[] = Hello 로 선언했을 때 strlen(s)와 sizeof(s)의 값에 대한 설명으로 옳은 것은?
문제 24지선다
strcpy 함수에 대한 설명으로 옳지 않은 것은?
문제 34지선다
strncpy(dest, src, n)에서 src의 길이가 n과 같거나 길 때 일어나는 일로 옳은 것은?
문제 44지선다
strcmp(apple, banana)의 반환값을 해석하는 방법으로 가장 적절한 것은?
문제 54지선다
다음 함수 호출에서 원본 배열 buf의 내용이 함수 밖에서도 실제로 바뀌는 이유로 가장 적절한 것은? (appendMark 함수는 매개변수로 받은 포인터에 strcat을 호출한다)
문제 64지선다
strcat(dest, src) 사용 시 주의할 점으로 가장 적절한 것은?

참고 자료

Last updated on