문자 여럿을 저장하기
C에는 이름 같은 문자열을 저장하는 자료형이 따로 없습니다.
char 변수에는 문자 하나를 저장합니다.
그런데 이름이나 단어는 문자가 여럿입니다.
자료형이 같은 값이 여럿이라면 배열에 저장하면 됩니다.
문자열(string)은 새 자료형이 아닙니다
C에서 문자열은 char 배열에 저장된 문자의 나열입니다.
char 배열의 첫 요소부터 문자를 차례로 저장하고, 끝을 알리는 문자 하나를 붙인 것이 전부입니다.
그래서 배열의 규칙이 그대로 적용됩니다 —
인덱스는 0부터 시작하고, 배열의 크기는 선언할 때 정해지며, 범위를 벗어나지 않게 하는 일은 프로그래머가 맡습니다.
char a[6] = {'h', 'e', 'l', 'l', 'o', '\0'}; // 요소를 하나씩 초기화한다
char b[6] = "hello"; // 큰따옴표로 한 번에 초기화한다
char c[] = "hello"; // 크기를 쓰지 않으면 '\0'까지 들어가게 정해진다
printf("%s\n", a);
printf("%s\n", b);
printf("%s\n", c);| 초기화 방법 | 특징 |
|---|---|
{'h', 'e', ...} |
요소를 하나씩 초기화합니다. 끝을 알리는 문자 '\0'까지 직접 써야 합니다. |
"hello" |
큰따옴표로 한 번에 초기화합니다. '\0'은 자동으로 붙습니다. |
char c[] = "hello" |
배열의 크기를 쓰지 않으면 '\0'까지 들어가도록 크기가 정해집니다. |
따옴표 두 종류를 구분합니다
작은따옴표 'A'는 문자 하나, 큰따옴표 "A"는 문자열입니다.
글자 수가 같아도 다른 것입니다. "A"는 문자가 하나뿐이어도
끝을 알리는 문자가 뒤에 붙어 char 두 개만큼의 공간을 차지합니다.
그래서 char grade = "A";는 문자 하나를 저장하는 변수에 문자열을 넣으려는 것이라
맞지 않습니다. 작은따옴표와 큰따옴표를 바꿔 쓸 수 없는 이유가 이것입니다.
스스로 확인
char b[6] = "hello";에서 배열의 크기를 [5]로 줄이면 무엇이 들어갈 공간이 모자랄까요?
끝을 알리는 널 문자
배열은 자기가 어디까지 채워졌는지 모릅니다.
크기가 10인 배열을 "hello"로 초기화하면 문자가 들어가지 않은 요소가 남습니다(이렇게 초기화하면 남은 요소는 모두 0이 됩니다).
그런데 화면에 출력하면 hello까지만 나오고 나머지 요소는 나오지 않습니다.
어디까지가 문자열인지를 무엇이 알려 주는 걸까요?
char word[10] = "hello";
for (int i = 0; i < 10; i = i + 1) {
printf("%d번 요소에 저장된 수: %d\n", i, word[i]); // 문자는 사실 「정수」로 저장되어 있다
}| 인덱스 | 0 | 1 | 2 | 3 | 4 | 5 | 6~9 |
|---|---|---|---|---|---|---|---|
| 문자 | h | e | l | l | o | 널 문자 | — |
| 저장된 정수 | 104 | 101 | 108 | 108 | 111 | 0 | 0 |
word[5]의 0이 「여기서 끝」이라는 신호입니다
문자열의 끝에는 값이 0인 문자가 하나 붙습니다.
코드에서는 '\0'이라고 쓰고 널 문자(null character)라고 부릅니다.
printf는 %s를 만나면 첫 요소부터 문자를 하나씩 출력하다가 널 문자를 만나면 멈춥니다.
이것이 배열의 크기를 문자 수보다 적어도 하나 더 크게 잡아야 하는 이유입니다.
문자가 다섯 개면 요소는 여섯 개가 있어야 합니다.
크기를 딱 맞게 char b[5] = "hello";라고 쓰면 널 문자가 들어갈 공간이 없어,
출력할 때 멈출 곳을 찾지 못하고 배열 밖까지 읽어 나갈 수 있습니다. 범위 밖에 접근하는 것과 같아서 무엇이 출력될지 정해지지 않습니다.
문자가 char 요소에 정수로 저장되어 있다는 것도 인덱스 표의 「저장된 정수」 줄에서 보입니다.
'h'는 104, 'e'는 101입니다.
문자마다 정해진 번호가 있고, char에는 그 번호가 저장됩니다.
그러면 문자열에 문자가 몇 개 들어 있는지는 어떻게 알까요?
널 문자 앞까지 문자 수를 세어 주는 strlen이 있습니다.
printf처럼 이름을 불러 쓰는 함수이며, string.h를 포함해야 사용할 수 있습니다.
printf("배열의 크기: %d\n", (int) (sizeof(word) / sizeof(word[0]))); // 10 (배열의 크기)
printf("문자열의 길이: %d\n", (int) strlen(word)); // 5 (문자열의 길이)배열의 크기와 문자열의 길이는 다릅니다
sizeof로 구한 10은 배열의 크기이고,
strlen이 알려 주는 5는 널 문자 앞까지의 문자 수, 곧 문자열의 길이입니다.
앞의 것은 선언할 때 정해져 변하지 않고, 뒤의 것은 저장한 문자열에 따라 달라집니다.
여기서 한 가지를 확인하고 가야 합니다.
strlen이 세는 것은 char 요소의 개수이지 사람이 보는 글자 수가 아닙니다.
영어는 한 글자가 char 하나라 둘이 같아 보이지만, 한글은 그렇지 않습니다.
#include <stdio.h>
#include <string.h>
int main(void) {
char eng[] = "hi";
char kor[] = "안녕";
printf("hi의 길이: %d\n", (int) strlen(eng)); // 2
printf("안녕의 길이: %d\n", (int) strlen(kor)); // 6
return 0;
}한글 한 글자는 char 하나가 아닙니다
"안녕"은 두 글자인데 6이 나옵니다.
소스 파일을 UTF-8 방식으로 저장했다면 한글 한 글자가 char 세 개를 차지하기 때문입니다.
char 하나로 나타낼 수 있는 번호의 가짓수가 한글을 다 나타내기에는 모자라서,
char 여러 개를 이어 한 글자를 나타냅니다.
그래서 한글이 포함된 문자열을 요소 하나씩 나누어 다루면 글자가 깨집니다.
kor[0]을 출력해도 「안」이 나오지 않습니다.
이 차시에서 문자를 하나씩 다루는 예시를 영어로만 드는 이유가 이것입니다.
스스로 확인
「programming」을 저장하려면 배열의 크기가 최소 얼마여야 할까요? 문자 수를 세고 하나를 더해 보세요.
입력과 출력
여기서 &가 사라집니다.
#include <stdio.h>
int main(void) {
char name[20];
printf("이름을 입력하세요: ");
scanf("%19s", name); // 배열 이름에는 &를 붙이지 않는다
printf("%s님, 안녕하세요.\n", name);
return 0;
}배열 이름에는 &를 붙이지 않습니다
변수에 입력받을 때는 &score처럼 붙였고,
배열의 요소 하나에 입력받을 때도 &score[i]처럼 붙였습니다.
그런데 scanf에 배열 이름을 쓰면 배열이 메모리의 어디에 있는지(주소)가 그대로 전달되므로
앞에 &를 더 붙이지 않습니다.
요소 하나(name[0])는 변수처럼 다루므로 &를 붙여 위치를 알려 줍니다.
배열 이름(name)은 이미 위치를 나타내므로 붙이지 않습니다.
%19s의 숫자가 배열의 범위를 지킵니다
숫자 없이 %s로만 입력받으면 char name[20]에 열아홉 문자보다 긴 문자열이 입력될 때 널 문자까지 저장할 공간이 모자라 배열 밖까지 저장됩니다.
범위 밖에 접근할 때와 마찬가지로 C는 이것을 막아 주지 않습니다.
%와 s 사이에 숫자를 써서 「최대 몇 문자까지만 입력받아라」라고 정해 둘 수 있습니다.
19를 쓴 것은 널 문자가 들어갈 요소 하나를 남겨 두기 위해서입니다.
배열의 크기가 n이면 %와 s 사이에 n − 1을 씁니다.
다만 scanf로 문자열을 입력받으면 공백에서 끊깁니다.
「hello there」를 입력하면 hello까지만 저장됩니다.
공백이 포함된 한 줄 전체를 입력받으려면 다른 함수를 씁니다.
#include <stdio.h>
int main(void) {
char line[50];
printf("좋아하는 문장을 입력하세요: ");
fgets(line, 50, stdin); // 공백이 있어도 한 줄을 입력받는다(배열에 들어가는 만큼)
printf("입력한 문장: %s", line); // 줄바꿈 문자까지 저장되어 있어 \n을 더 출력하지 않는다
return 0;
}fgets는 공백이 있어도 한 줄을 입력받습니다
괄호 안에 어디에 저장할지, 배열의 크기가 얼마인지, 어디서 읽을지 세 가지를 씁니다.
stdin은 입력이 들어오는 곳으로, 보통은 키보드입니다.
배열의 크기를 쓰는 곳이 따로 있어서 줄이 길면 배열에 들어갈 만큼만 저장하고 배열 밖까지 저장하지 않습니다.
대신 한 줄이 배열 안에 다 들어가면 엔터를 누를 때 입력된 줄바꿈 문자까지 함께 저장됩니다.
그래서 이 코드는 마지막에 \n을 따로 출력하지 않았습니다.
두 번 출력하면 빈 줄이 하나 더 생깁니다.
스스로 확인
주소나 책 제목처럼 공백이 포함된 문자열을 입력받아야 한다면 scanf와 fgets 가운데 무엇을 골라야 할까요?
문자열 함수
먼저 되지 않는 것부터 봅니다.
두 배열에 같은 문자열이 저장되어 있습니다. ==로 비교하면 무엇이 나올까요?
if (a == b) { // 저장된 「내용」을 비교하는 것이 아니다
printf("같습니다\n");
} else {
printf("다릅니다\n"); // 내용이 같은데도 이쪽이 출력된다
}다릅니다가 출력됩니다
저장된 문자가 똑같은데도 그렇습니다. ==는 값 하나끼리 비교하는 연산자인데,
문자열은 값 하나가 아니라 여러 요소로 이루어진 배열이기 때문입니다.
a == b는 두 배열이 메모리의 어디에 있는지(주소)를 비교합니다.
두 배열은 서로 다른 위치에 만들어졌으니 결과는 「다르다」입니다.
컴파일러도 이 코드를 이상하게 여깁니다. 경고를 켜 두면 「배열 둘을 비교하고 있다」는 경고 문구가 나타납니다. 경고가 나타났는데도 결과가 그럴듯하게 나오면 특히 위험합니다. 실행은 되지만 확인하고 싶었던 것을 비교하고 있지 않기 때문입니다.
내용을 비교하려면 문자를 하나씩 차례로 비교하는 함수가 필요합니다.
string.h에 자주 쓰는 문자열 함수가 들어 있으니 가져다 쓰면 됩니다.
#include <stdio.h>
#include <string.h>
int main(void) {
char a[20] = "banana";
char b[20] = "apple";
char copy[20];
printf("문자열의 길이: %d\n", (int) strlen(a)); // 6
strcpy(copy, a); // a에 저장된 문자열을 copy에 복사한다
printf("복사한 문자열: %s\n", copy); // banana
if (strcmp(a, b) == 0) { // 두 문자열이 같으면 결과가 0이다
printf("같습니다\n");
} else {
printf("다릅니다\n"); // 다릅니다
}
return 0;
}| 함수 | 하는 일 | 결과 |
|---|---|---|
strlen(a) |
문자열의 길이를 구한다 | 널 문자 앞까지의 문자 수 |
strcpy(b, a) |
a의 내용을 b에 복사한다 |
— (b가 바뀐다) |
strcmp(a, b) |
내용을 첫 문자부터 차례로 비교한다 | 같으면 0, 다르면 0이 아닌 수 |
「같다」가 0입니다
strcmp의 결과는 두 문자열이 같을 때 0입니다.
조건식에서 0은 거짓으로 처리되므로, if (strcmp(a, b))라고 쓰면
다를 때 참이 되어 의미가 뒤집힙니다.
같은지 확인하려면 == 0을 반드시 함께 씁니다.
strcpy는 복사받을 배열의 크기가 모자라도 그대로 복사합니다.
복사받을 배열의 크기가 충분한지 프로그래머가 먼저 확인해야 합니다.
여기까지를 한 프로그램으로
단어를 입력받아 길이를 구하고, 마지막 요소부터 거꾸로 출력하고, 찾는 문자가 몇 번 나오는지 셉니다.
#include <stdio.h>
#include <string.h>
int main(void) {
char word[50];
char target;
printf("영어 단어 하나: ");
scanf("%49s", word);
printf("찾을 문자: ");
scanf(" %c", &target); // %c 앞의 공백이 남은 줄바꿈 문자를 건너뛴다
int len = (int) strlen(word);
int count = 0;
for (int i = 0; i < len; i = i + 1) {
if (word[i] == target) {
count = count + 1;
}
}
printf("\n문자열의 길이는 %d입니다.\n", len);
printf("거꾸로: ");
for (int i = len - 1; i >= 0; i = i - 1) {
printf("%c", word[i]);
}
printf("\n");
printf("'%c'는 %d번 나옵니다.\n", target, count);
return 0;
}
거꾸로 출력하는 반복문이 len - 1에서 시작해 0까지 내려가는 것을 보세요.
마지막 문자의 인덱스가 len - 1이라는 것을 그대로 쓴 것입니다.
배열에서 익힌 규칙이 문자열에서도 그대로 통합니다.
스스로 확인
입력받은 단어가 앞뒤로 읽어도 같은지 알아보려면 어떤 요소끼리 비교해야 할까요? 몇 번까지 비교하면 될지도 생각해 보세요.
핵심 정리
문자열은 char 배열이다
새 자료형이 아니다. 배열의 규칙이 그대로 통한다 — 인덱스는 0부터, 배열의 크기는 선언할 때 정해지고, 범위는 프로그래머가 지킨다.
널 문자에 요소 하나
끝에 붙는 널 문자('\0')가 문자열이 어디서 끝나는지 알린다.
그래서 배열의 크기가 문자 수보다 적어도 하나 더 커야 한다.
크기 ≠ 길이 ≠ 글자 수
sizeof로는 배열의 크기를, strlen으로는 널 문자 앞까지의 char 개수를 구한다.
소스 파일을 UTF-8로 저장했다면 한글 한 글자는 char 세 개라 길이가 글자 수와 달라진다.
==로 비교하지 않는다
==는 두 배열이 메모리의 어디에 있는지를 비교한다.
내용을 비교하려면 strcmp를 쓰고,
같으면 0이므로 == 0을 함께 쓴다.
확인 퀴즈
배운 내용을 확인해 봅시다.
1. "school"을 저장하려면 char 배열의 요소가 최소 몇 개 있어야 하는가?
2. 두 문자열의 내용이 같은지 알아보는 올바른 방법은?
3. char s[20] = "안녕";일 때 strlen(s)의 값은? (소스 파일은 UTF-8로 저장했다)