← C 강의 목록으로
⚙️
중급 (Intermediate)
char[] · <string.h> · fgets

4단원 — 문자열

C에는 별도의 "문자열" 자료형이 없습니다. 문자열은 **`'\0'`(널 문자)로 끝나는 `char` 배열**입니다. `<string.h>`의 표준 함수를 활용해 길이/복사/비교/연결을 다룹니다.

charstring.hfgets
소요 시간
1~2시간
난이도
📊 초급
선수 조건
🎯 중급 3단원
결과물
문자열을 다루는 표준 함수를 활용한다

이 강의에서 배우는 것

  • 1C 문자열의 **널 종료(null-terminated)** 규칙을 이해한다.
  • 2`<string.h>`의 `strlen`, `strcpy`, `strcat`, `strcmp`를 사용한다.
  • 3`fgets`로 안전하게 한 줄을 입력받는다.
  • 4버퍼 오버플로의 위험을 인지한다.

왜 "문자열"이 별도 타입이 아닐까?

C는 작고 빠른 언어로 설계되어, 문자열도 **단순한 char 배열**로 표현합니다. 배열의 끝을 알릴 방법이 필요해서 도입된 것이 **널 문자(`'\0'`)** 입니다.

c
char s[] = "Hi";

은 메모리에서 다음과 같이 저장됩니다.

text
s[0] s[1] s[2]
┌────┬────┬─────┐
│ H  │ i  │ \0  │   ← '\0'이 "여기서 끝!"이라는 표시
└────┴────┴─────┘

`strlen("Hi")` 가 2를 돌려주는 이유: `'\0'`을 만날 때까지 글자 수를 셈.

핵심 개념

1) 문자열 표현

c
char s1[] = "Hello";          // 6 byte ('H','e','l','l','o','\0')
char s2[10] = "Hi";           // 나머지는 '\0'으로 채워짐
char s3[] = {'H','i','\0'};   // 동치 표현

2) `<string.h>`의 핵심 함수

함수의미주의
`strlen(s)`길이 (널 제외)s가 NULL이면 안 됨
`strcpy(dst, src)`src를 dst에 복사dst 버퍼가 충분해야 함
`strcat(dst, src)`dst 끝에 src 이어 붙임합쳐진 길이 + 1 만큼 공간 필요
`strcmp(a, b)`사전순 비교같으면 0, a<b면 음수, a>b면 양수

3) 한 줄 입력은 `fgets`로

c
char buf[128];
fgets(buf, sizeof(buf), stdin);   // 줄바꿈 포함
buf[strcspn(buf, "\n")] = '\0';   // 줄바꿈 제거

`scanf("%s", ...)` 는 공백을 만나면 끊어지고, 입력 길이도 검사하지 않아 **버퍼 오버플로** 위험이 있습니다.

4) 문자 단위 처리(`<ctype.h>`)

함수설명
`isalpha(c)`알파벳?
`isdigit(c)`숫자?
`tolower(c)` / `toupper(c)`소/대문자 변환

예제로 보기

예제 1 — `ex01_basic.c` : 길이와 한 글자씩 보기

**실행 결과**

text
문자열: Hello, C!
길이:   9
s[0] = H (코드 72)
s[1] = e (코드 101)
s[2] = l (코드 108)
s[3] = l (코드 108)
s[4] = o (코드 111)
s[5] = , (코드 44)
s[6] =   (코드 32)
s[7] = C (코드 67)
s[8] = ! (코드 33)

핵심: 문자열은 결국 `char` 정수의 배열이며, 끝의 `'\0'`은 길이에 포함되지 않습니다.

예제 2 — `ex02_strops.c` : 복사·연결·비교

c
char a[64] = "Hello";
char b[]   = ", World!";
char c[64];
strcpy(c, a);
strcat(a, b);

**실행 결과**

text
a = Hello, World!
c = Hello
strcmp("abc", "abd") = -1
strcmp("abc", "abc") = 0

핵심: `strcat`은 dst의 **널 위치부터** 이어 붙이므로 dst가 유효한 문자열이어야 합니다.

예제 3 — `ex03_fgets.c` : 안전한 입력

**입력**: `hello c world`

text
문장 입력: 입력값: "hello c world"
길이:   13

핵심: `fgets`는 버퍼 크기를 받기 때문에 오버플로가 나지 않습니다.

예제 4 — `ex04_count.c` : 모음/자음 카운트

**입력**: `Hello World`

text
영문 문장 입력: 모음: 3, 자음: 7

핵심: `tolower`로 대소문자를 통일한 뒤 비교하면 분기가 절반으로 줄어듭니다.

다른 시각으로 보기 — 문자열 연산이 느려 보이는 이유

`strlen`을 호출할 때마다 컴파일러는 처음부터 `'\0'`을 찾습니다.

c
for (int i = 0; i < strlen(s); i++)   // 매 반복마다 길이 재계산!

성능이 중요할 때는 길이를 한 번만 구합니다.

c
size_t n = strlen(s);
for (size_t i = 0; i < n; i++) ...

C 문자열은 **끝을 모르는 배열**이라는 점이 효율과 안전 양쪽에 영향을 줍니다.

자주 하는 실수

  1. **버퍼 부족**: `char dst[3]; strcpy(dst, "Hello");` → 인접 메모리 손상.
  2. **널 종료 누락**: `char s[5]; s[0]='H'; s[1]='i';` 만 하면 끝이 어디인지 모름 → 쓰레기 출력.
  3. **`==`로 문자열 비교**: 두 포인터를 비교하므로 같은 내용이어도 false. 반드시 `strcmp`.
  4. **`fgets` 후 줄바꿈 잔존**: 끝에 `\n`이 남아 출력이 한 줄 더 내려감 → `strcspn`으로 제거.
  5. **상수 문자열 수정**: `char *p = "hi"; p[0] = 'H';` 는 정의되지 않은 동작. 수정하려면 `char p[] = "hi";`.

정리

  • C 문자열은 `'\0'`로 끝나는 `char` 배열이다.
  • 길이/복사/비교/연결은 `<string.h>` 함수로.
  • `scanf("%s")`보다 `fgets`가 안전.
  • `strcat`/`strcpy`는 dst 버퍼가 충분한지 책임은 호출자에게.
  • 문자열 비교는 `strcmp`, 절대 `==`.

직접 해 보기

bash
cd src
gcc -std=c11 -Wall -o ex01 ex01_basic.c   && ./ex01
gcc -std=c11 -Wall -o ex02 ex02_strops.c  && ./ex02
gcc -std=c11 -Wall -o ex03 ex03_fgets.c   && echo "hello c world" | ./ex03
gcc -std=c11 -Wall -o ex04 ex04_count.c   && echo "Hello World"   | ./ex04

응용:

  • `ex01`에서 `s[2] = '\0';` 한 줄을 추가한 뒤 출력해 보세요. 문자열이 어떻게 끊기는지 관찰.
  • `ex02`에서 `c[]` 버퍼를 너무 작게(예: `char c[3]`) 잡고 복사하면 어떤 경고/오류가 뜰까요?

💻 예제 (examples)

실제로 컴파일·실행해 결과를 확인할 수 있는 예제입니다.

ex01_basic.c길이와 한 글자씩 보기
CODE
#include <stdio.h>
#include <string.h>

int main(void) {
    char s[] = "Hello, C!";

    printf("문자열: %s\n", s);
    printf("길이:   %zu\n", strlen(s));

    /* 한 글자씩 출력 */
    for (size_t i = 0; s[i] != '\0'; i++) {
        printf("s[%zu] = %c (코드 %d)\n", i, s[i], s[i]);
    }
    return 0;
}
▶ 실행 결과
문자열: Hello, C!
길이:   9
s[0] = H (코드 72)
s[1] = e (코드 101)
s[2] = l (코드 108)
s[3] = l (코드 108)
s[4] = o (코드 111)
s[5] = , (코드 44)
s[6] =   (코드 32)
s[7] = C (코드 67)
s[8] = ! (코드 33)
ex02_strops.c복사·연결·비교
CODE
#include <stdio.h>
#include <string.h>

int main(void) {
    char a[64] = "Hello";
    char b[]   = ", World!";
    char c[64];

    strcpy(c, a);
    strcat(a, b);

    printf("a = %s\n", a);
    printf("c = %s\n", c);
    printf("strcmp(\"abc\", \"abd\") = %d\n", strcmp("abc", "abd"));
    printf("strcmp(\"abc\", \"abc\") = %d\n", strcmp("abc", "abc"));
    return 0;
}
▶ 실행 결과
a = Hello, World!
c = Hello
strcmp("abc", "abd") = -1
strcmp("abc", "abc") = 0
ex03_fgets.c안전한 입력
CODE
#include <stdio.h>
#include <string.h>

int main(void) {
    char line[128];

    printf("문장 입력: ");
    if (fgets(line, sizeof(line), stdin) == NULL) return 1;

    line[strcspn(line, "\n")] = '\0';   /* 끝의 \n 제거 */

    printf("입력값: \"%s\"\n", line);
    printf("길이:   %zu\n", strlen(line));
    return 0;
}
ex04_count.c모음/자음 카운트
CODE
#include <stdio.h>
#include <ctype.h>
#include <string.h>

int main(void) {
    char buf[256];
    printf("영문 문장 입력: ");
    if (fgets(buf, sizeof(buf), stdin) == NULL) return 1;
    buf[strcspn(buf, "\n")] = '\0';

    int vowels = 0, consonants = 0;
    for (size_t i = 0; buf[i] != '\0'; i++) {
        char ch = (char)tolower((unsigned char)buf[i]);
        if (ch >= 'a' && ch <= 'z') {
            if (ch == 'a' || ch == 'e' || ch == 'i' ||
                ch == 'o' || ch == 'u')
                vowels++;
            else
                consonants++;
        }
    }

    printf("모음: %d, 자음: %d\n", vowels, consonants);
    return 0;
}

📝 과제 (exercises)

직접 풀어보고, 막힐 때 정답을 펼쳐 비교해보세요.

과제 1

문제 1 (hw01.c)

목표: 영문 문자열을 입력받아 **모두 대문자로** 변환해 출력하세요. (`<ctype.h>`의 `toupper` 사용)

요구사항
  • 파일명: hw01.c
정답 코드 펼치기 / 접기
SOLUTION
#include <stdio.h>
#include <ctype.h>
#include <string.h>

int main(void) {
    char buf[256];
    printf("문장 입력: ");
    if (fgets(buf, sizeof(buf), stdin) == NULL) return 1;
    buf[strcspn(buf, "\n")] = '\0';

    for (size_t i = 0; buf[i] != '\0'; i++)
        buf[i] = (char)toupper((unsigned char)buf[i]);

    printf("%s\n", buf);
    return 0;
}
과제 2

문제 2 (hw02.c)

목표: 문자열을 입력받아 **회문(palindrome)** 인지 판정하세요. 대소문자 구분 없이.

요구사항
  • 파일명: hw02.c
입출력 예시
입력: Level
회문 입니다.
정답 코드 펼치기 / 접기
SOLUTION
#include <stdio.h>
#include <ctype.h>
#include <string.h>

int main(void) {
    char buf[256];
    printf("문장 입력: ");
    if (fgets(buf, sizeof(buf), stdin) == NULL) return 1;
    buf[strcspn(buf, "\n")] = '\0';

    int len = (int)strlen(buf);
    int palindrome = 1;
    for (int i = 0, j = len - 1; i < j; i++, j--) {
        char a = (char)tolower((unsigned char)buf[i]);
        char b = (char)tolower((unsigned char)buf[j]);
        if (a != b) { palindrome = 0; break; }
    }

    printf("%s\n", palindrome ? "회문 입니다." : "회문이 아닙니다.");
    return 0;
}
▶ 실행 결과
입력: Level
회문 입니다.
과제 3

문제 3 (hw03.c)

목표: 문자열에서 특정 문자 하나(`char`)의 등장 횟수를 출력하세요.

요구사항
  • 파일명: hw03.c
입출력 예시
문장: hello world
찾을 문자: l
'l'는 3번 나타납니다.
정답 코드 펼치기 / 접기
SOLUTION
#include <stdio.h>
#include <string.h>

int main(void) {
    char buf[256];
    char target;

    printf("문장: ");
    if (fgets(buf, sizeof(buf), stdin) == NULL) return 1;
    buf[strcspn(buf, "\n")] = '\0';

    printf("찾을 문자: ");
    scanf(" %c", &target);

    int count = 0;
    for (size_t i = 0; buf[i] != '\0'; i++)
        if (buf[i] == target) count++;

    printf("'%c'는 %d번 나타납니다.\n", target, count);
    return 0;
}
▶ 실행 결과
문장: hello world
찾을 문자: l
'l'는 3번 나타납니다.
예제 코드 / 강의 자료

전체 강의 자료와 예제 코드는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗