C에서 문자열은 특별한 타입이 아니라 char(문자) 여러 개를 늘어놓은 배열입니다. 파이썬 같은 언어와 달리 C는 "여기까지가 문자열"이라는 끝 표시를 사람이 직접 챙겨야 하고, 그 표시가 널 종단 문자입니다. 이 한 글자만 이해하면 문자열 함수들이 왜 그렇게 동작하는지 대부분 풀립니다. 이번 편은 코드를 직접 따라 치고 출력을 확인하면서 익히는 것을 목표로 합니다.
16.1 문자열은 char 배열입니다
문자 하나는 char에 담고 작은따옴표로 씁니다. 문자 여러 개가 이어진 문자열은 char 배열에 담고 큰따옴표로 씁니다.
#include <stdio.h>
int main(void) {
char word[] = "cat"; // c a t 다음에 \0 이 자동으로 붙음
printf("%s\n", word);
return 0;
}
실행하면 이렇게 나옵니다.
cat
"cat"은 사실 char 네 칸입니다. c, a, t, 그리고 눈에 안 보이는 종료 문자 '\0'. 배열 크기를 대괄호 안을 비워 두면 컴파일러가 이 종료 문자까지 세어 4칸을 잡아 줍니다.
함정. 'A'(작은따옴표)는 문자 하나, "A"(큰따옴표)는 문자열입니다. "A"는 A와 \0 두 칸을 차지합니다. 둘은 타입이 달라 섞어 쓰면 경고가 납니다.
16.2 널 종단 문자가 끝을 알립니다
널 종단 문자 '\0'은 값이 0인 바이트입니다. C의 문자열 함수들은 배열이 어디서 끝나는지 모르기 때문에, 이 0을 만나는 자리를 "여기가 끝"으로 판단합니다. 직접 한 칸씩 채워 보면 정체가 보입니다.
char s[4] = {'h', 'i', '!', '\0'};
printf("%s\n", s);
hi!
함정. 널 종단을 빠뜨리면 %s는 0을 만날 때까지 배열 밖 메모리를 계속 읽어 엉뚱한 값을 쏟아냅니다. char s[3] = {'h','i','!'};처럼 \0 자리가 없으면 정의되지 않은 동작(undefined behavior)입니다. 문자열을 담을 배열은 항상 글자 수보다 최소 한 칸 크게 잡습니다.
![[C 16] 문자열, char 배열과 널 종단](https://img.thenullpage.com/posts/6551/6551_1_f8aaaa.webp)
16.3 문자열 길이는 strlen으로
글자 수를 세려면 string.h의 strlen을 씁니다. 상황은 흔합니다. 입력받은 이름이 몇 글자인지 확인하는 경우입니다.
#include <stdio.h>
#include <string.h> // strlen
int main(void) {
char name[] = "jiwoo";
printf("%zu글자\n", strlen(name));
return 0;
}
5글자
strlen은 \0 앞까지의 글자 수를 셉니다. 이는 배열이 잡은 전체 칸 수(sizeof)와 다릅니다.
char buf[20] = "abc";
printf("%zu\n", strlen(buf)); // 3, 실제 글자
printf("%zu\n", sizeof buf); // 20, 배열 크기
3
20
주의. strlen의 결과 타입은 size_t라서 %zu로 출력합니다. %d로 찍으면 타입이 어긋나 정의되지 않은 동작입니다.
16.4 한 글자씩 훑기
문자열도 배열이라 대괄호 인덱스로 각 글자에 접근합니다. 끝까지 도는 조건은 "\0을 만나기 전까지"입니다. 예를 들어 특정 글자가 몇 번 나오는지 셀 때 이 틀을 씁니다.
char s[] = "banana";
int count = 0;
for (int i = 0; s[i] != '\0'; i++) {
if (s[i] == 'a') count++;
}
printf("a는 %d번\n", count);
a는 3번
s[i] != '\0'을 조건으로 두면 strlen을 따로 부르지 않아도 종료 문자를 만나는 순간 반복이 멈춥니다. 문자열을 다루는 반복문의 가장 기본 형태입니다.
16.5 문자열 복사는 strcpy로
초보자가 가장 먼저 부딪히는 벽이 복사입니다. 숫자처럼 등호로 옮기려 하면 막힙니다.
함정. char a[10]; a = "hi";는 컴파일 오류입니다. 배열 이름에는 통째로 대입할 수 없습니다. 문자열 복사는 strcpy로 합니다.
char dest[20];
strcpy(dest, "hello"); // hello 를 dest 로 복사
printf("%s\n", dest);
hello
strcpy는 원본의 \0까지 함께 복사해 dest를 온전한 문자열로 만듭니다.
주의. 받는 배열이 원본 글자 수와 널 종단을 합친 것보다 작으면 넘쳐서 정의되지 않은 동작입니다. dest 크기를 넉넉히 잡거나, 복사 한도를 받는 snprintf(dest, sizeof dest, "%s", src)를 쓰면 안전합니다.
16.6 문자열 이어 붙이기 strcat
성과 이름을 합쳐 전체 이름을 만드는 상황을 봅시다.
char full[50] = "kim";
strcat(full, "jiwoo"); // full 뒤에 이어 붙임
printf("%s\n", full);
kimjiwoo
strcat은 앞 문자열의 \0을 찾아 그 자리부터 뒤 문자열을 덮어쓰고, 끝에 새 \0을 답니다.
함정. 첫 배열이 이미 유효한 문자열, 즉 널 종단을 가진 상태여야 합니다. 초기화하지 않은 char full[50];에 바로 strcat을 하면 \0을 못 찾아 엉뚱한 메모리에 씁니다. full[0] = '\0';으로 빈 문자열을 만든 뒤 붙이면 안전합니다.
16.7 문자열 비교는 strcmp로
로그인처럼 두 문자열이 같은지 검사하는 상황입니다. 여기서 초보자가 거의 반드시 한 번은 틀립니다.
함정. == 은 내용이 아니라 배열의 주소를 비교합니다. 내용 비교는 strcmp를 씁니다.
char input[] = "open123";
if (strcmp(input, "open123") == 0) { // 0이면 같음
printf("로그인 성공\n");
} else {
printf("비밀번호 틀림\n");
}
로그인 성공
strcmp는 두 문자열이 같으면 0, 다르면 사전 순서에 따라 음수나 양수를 돌려줍니다.
주의. "0이면 같다"가 헷갈려 if (strcmp(a, b))라고만 쓰면, 0이 아닐 때 곧 서로 다를 때가 참이 되어 로직이 뒤집힙니다. 같은지 보려면 == 0을 반드시 붙이세요.
![[C 16] 문자열, char 배열과 널 종단 (2)](https://img.thenullpage.com/posts/6551/6551_2_1efc25.webp)
16.8 입력은 fgets로, gets는 금지
사용자에게 문자열을 입력받을 때 쓰면 안 되는 함수가 있습니다.
함정. gets는 입력이 배열보다 길어도 막지 않아 버퍼를 넘칩니다. 이 위험 때문에 C11 표준에서 아예 삭제됐습니다. 절대 쓰지 마세요. 대신 버퍼 크기를 받는 fgets를 씁니다.
char name[20];
printf("이름: ");
fgets(name, sizeof name, stdin);
name[strcspn(name, "\n")] = '\0'; // 끝의 개행 제거
printf("%s님 환영합니다\n", name);
홍길동을 입력하면 이렇게 됩니다.
이름: 홍길동
홍길동님 환영합니다
fgets는 넘긴 크기만큼만 읽으므로 넘치지 않습니다. 다만 엔터('\n')까지 배열에 담기니, strcspn으로 그 자리를 찾아 \0으로 잘라 줍니다.
16.9 종합 예제, 문자열 뒤집기
배운 것을 모아 "hello"를 "olleh"로 뒤집어 봅니다. 문자열이 메모리 위의 배열이라는 사실을 알면, 자리를 직접 맞바꿀 수 있습니다.
char s[] = "hello";
int len = (int)strlen(s);
for (int i = 0; i < len / 2; i++) {
char tmp = s[i];
s[i] = s[len - 1 - i];
s[len - 1 - i] = tmp;
}
printf("%s\n", s);
olleh
양 끝 글자를 하나씩 맞바꾸며 가운데로 좁히면 뒤집힙니다. len / 2까지만 도는 이유는 절반을 넘어가면 이미 바꾼 것을 다시 되돌리기 때문입니다.
문자열의 거의 모든 함정은 널 종단 한 글자로 통합니다. 끝 표식이 없으면 길이도, 복사도, 비교도 어긋납니다. 배열은 널 종단까지 담을 만큼 넉넉히 잡고, 비교는 strcmp로, 입력은 fgets로 하는 습관을 들이면 초보 시절 문자열에서 겪는 사고 대부분을 피할 수 있습니다.