이번 문서의 목표: 이 파일을 다 읽으면 <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
Hellodest는 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의 버퍼 오버플로 위험을 줄이기 위한 함수입니다.- 주의할 동작 두 가지가 시험에서 자주 나옵니다.
src의 길이가n보다 짧으면, 복사한 뒤 남는 자리를 전부 널 문자로 채웁니다.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] | 0 | n=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/음수)로 해석해야 합니다.
| 비교 결과 | 반환값 | 의미 |
|---|---|---|
s1이 s2보다 사전 순서상 앞선다 | 음수 | s1 < s2 |
s1과 s2가 완전히 같다 | 0 | s1 == s2 |
s1이 s2보다 사전 순서상 뒤진다 | 양수 | 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. 다섯 함수 한눈에 정리
| 함수 | 시그니처 | 반환값 | 주의할 점 |
|---|---|---|---|
strlen | size_t strlen(const char *s) | 널 문자 제외 글자 수 | sizeof와 혼동 금지 |
strcpy | char *strcpy(char *dest, const char *src) | dest | 목적지 크기 검사 없음 |
strncpy | char *strncpy(char *dest, const char *src, size_t n) | dest | n을 채우면 널 문자 자동 미부착 |
strcat | char *strcat(char *dest, const char *src) | dest | 목적지 크기 검사 없음 |
strcmp | int 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가 가리키는 곳은 main의 buf와 같은 메모리이므로 원본이 실제로 바뀝니다.
핵심 정리
strlen은 실행 중 널 문자 앞까지 글자 수를 세는 함수 호출이고,sizeof는 컴파일 타임에 배열 전체 크기를 구하는 연산자다. 포인터에sizeof를 적용하면 포인터 자체의 크기가 나온다.strcpy,strcat은 목적지 버퍼의 크기를 검사하지 않으므로, 호출 전 목적지가 충분히 큰지 프로그래머가 직접 확인해야 한다.strncpy는n을 채우도록 복사한 경우 널 문자를 자동으로 붙이지 않으므로, 문자열로 안전하게 쓰려면 마지막 칸을 직접 널 문자로 채워야 할 수 있다.strcmp의 반환값은 정확한 숫자가 아니라 음수/0/양수의 부호로 해석하며, 같음을 확인할 때는 반드시== 0으로 비교한다.- 포인터를 함수 인자로 넘겨 문자열 함수를 호출하면, 그 포인터가 가리키는 원본 데이터가 함수 밖에서도 바뀐 채로 남는다.