
💡 1분 핵심 요약 (TL;DR)
- ✅ 문자열은 결국 char 배열이며, 끝에 널 문자(\0)가 붙어야 유효한 문자열이 된다
- ✅ char str[N]으로 선언하면 실제 저장 가능한 글자 수는 N-1개다 (\0 자리 하나는 반드시 남겨야 한다)
- ✅ 영어·숫자는 1바이트지만 한글은 인코딩에 따라 2~4바이트를 차지한다
- ✅ scanf("%s", ...)는 공백에서 입력이 끊긴다 — 띄어쓰기가 있는 문장은 fgets를 써야 한다
- ✅ <string.h>에는 strlen, strstr, strcpy, strcmp, strcat, memset 등 문자열 전용 함수가 모여있다
01 문자열이란 무엇인가 — 널 문자(\0)
문자열은 결국 문자들을 담은 배열이다
문자열(string)은 문자를 여러 개 이어붙인 것으로, 사실상 char형 배열입니다. 그런데 문자열에는 다른 배열과 달리 끝을 표시하는 특별한 문자가 붙습니다. 바로 널 문자(\0)입니다.
문자열도 결국 char 배열이다
#include <stdio.h>
int main() {
char str1[] = "Hello"; // 컴파일러가 자동으로 \0을 붙여준다
char str2[] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 직접 \0을 넣은 것
printf("%s\n", str1);
printf("%s\n", str2);
return 0;
}
▶ 실행 결과
Hello
Hello
str1은 문자열 리터럴로 만들었더니 컴파일러가 자동으로 \0을 붙여줬고, str2는 직접 \0을 마지막에 넣어줬습니다. 결과는 똑같지만, 메모리 구조상 둘 다 똑같이 널 문자로 끝나야 유효한 문자열이 됩니다. 이렇게 널 문자로 끝을 표시하는 방식을 널 터미네이티드(null-terminated) 방식이라고 부릅니다.
02 왜 문자열만 끝 표시가 필요할까
int, double과 달리 문자는 경계를 예측할 수 없다
int 배열은 원소 하나가 정확히 4바이트로 고정돼 있어서, 4바이트씩 끊어 읽으면 각 원소의 경계를 알 수 있습니다. 그런데 문자는 사정이 다릅니다. 영어와 숫자는 아스키코드 기준 1바이트지만, 한글은 인코딩 방식에 따라 2바이트에서 4바이트까지 차지합니다.
| 인코딩 방식 | 한글 1글자당 바이트 수 |
| CP949 / EUC-KR (윈도우 한글) | 2바이트 |
| UTF-8 (인터넷 표준) | 보통 3바이트 (자모에 따라 다름) |

같은 세 글자여도 영어면 3바이트, 한글이면 6~9바이트가 될 수 있습니다. 이렇게 문자열은 배열 크기와 타입만으로는 실제 길이를 예측할 수 없기 때문에, 어디까지가 유효한 데이터인지 표시해줄 끝 문자(\0)가 반드시 필요한 것입니다.
03 문자열 배열의 크기, 넉넉하게 잡아야 하는 이유
char str[6]은 6글자가 아니라 5글자까지만 안전하다
⚠️ N칸을 선언하면 실제로는 N-1글자만 안전하다
char str[6]으로 선언하면 6칸이 생기지만, 마지막 한 칸은 \0의 자리로 예약해둬야 합니다. 그래서 실제로 안전하게 저장할 수 있는 글자 수는 5글자뿐입니다. 이 한 칸을 깜빡하고 6글자를 꽉 채워 넣으면(예: "Hello!"), 널 문자가 들어갈 자리가 없어 끝이 표시되지 않은 위험한 문자열이 만들어집니다.

그렇다면 문자열의 크기에는 제한이 있을까요? C언어 문법 자체에는 배열의 크기 제한이 없습니다. 배열의 크기는 컴퓨터에서 사용 가능한 메모리(스택 또는 힙 영역) 용량에 의해서만 결정되며, 수십 메가바이트(MB) 이상도 선언할 수 있습니다. 다만 너무 큰 배열을 스택 영역에 선언하면 스택 오버플로우(Stack Overflow) 에러가 날 수 있으므로, 대용량 문자열을 다룰 때는 메모리 상황에 맞게 공간을 설계해야 합니다.
04 배열로 접근하기 vs 포인터로 접근하기
문자열도 결국 배열이라, 배열에서 배운 규칙이 그대로 적용된다
문자열은 char 배열이기 때문에, 앞서 배운 배열-포인터 관계가 똑같이 적용됩니다. 인덱스로 접근해서 수정할 수도, 포인터로 접근해서 수정할 수도 있습니다.
배열 인덱스와 포인터, 둘 다 문자열을 수정한다
#include <stdio.h>
int main() {
char str4[] = "World";
char *ptr_str4 = str4; // 배열 이름으로 포인터 초기화 (관례적으로 흔히 쓰는 방식)
printf("%s\n", str4);
str4[2] = 'a'; // 배열 인덱스로 수정
*(ptr_str4 + 1) = 'E'; // 포인터 연산으로 수정
printf("%s\n", str4);
return 0;
}
▶ 실행 결과
World
WEald
"World"에서 배열 인덱스로 str4[2](r 자리)를 'a'로 바꾸고, 포인터 연산으로 *(ptr_str4 + 1)(o 자리)을 'E'로 바꿨습니다. 그 결과 W-E-a-l-d, 즉 WEald가 됩니다. 배열 인덱스 방식과 포인터 연산 방식이 같은 메모리를 가리키고 있어서 둘 다 원본 문자열을 실제로 수정합니다.
📖 문자열 포인터는 &를 잘 안 쓴다
일반 배열에서는 &arr[0] 처럼 명시적으로 주소를 넘기는 방식이 권장된다고 했었는데, 문자열을 다룰 때는 관례적으로 char *ptr = str; 처럼 배열 이름을 그대로 대입하는 코드를 더 자주 보게 됩니다. 두 방식 모두 정상 동작하니, 다른 사람의 코드를 읽을 때 당황하지 않으면 됩니다.
05 scanf의 한계와 fgets
띄어쓰기가 있는 문장은 scanf로 온전히 못 받는다
scanf("%s", ...)는 공백이나 엔터를 만나면 입력을 끊습니다. 그래서 띄어쓰기가 포함된 문장을 통째로 입력받고 싶을 때는 문제가 생깁니다.
❌ scanf는 공백에서 입력이 끊긴다
#include <stdio.h>
int main() {
char str[100];
printf("입력: ");
scanf("%s", str);
printf("결과: %s\n", str);
return 0;
}
▶ 실행 결과 (Hello World를 입력했지만 잘린다)
입력: Hello World
결과: Hello
이럴 때는 fgets를 사용합니다. fgets는 엔터를 만나기 전까지 공백을 포함해서 한 줄 전체를 읽어옵니다.
✅ fgets는 공백까지 포함해서 한 줄을 통째로 읽는다
#include <stdio.h>
int main() {
char str[100];
printf("입력: ");
fgets(str, sizeof(str), stdin); // 첫 번째: 저장할 곳, 두 번째: 최대 길이, 세 번째: 표준입력
printf("결과: %s", str);
return 0;
}
▶ 실행 결과
입력: Hello World
결과: Hello World
📖 fgets의 세 번째 인자, sizeof(str)을 쓰는 이유
두 번째 인자는 입력받을 최대 길이입니다. sizeof(str)로 배열의 전체 크기를 넘겨주면, 버퍼가 넘치지 않도록 fgets가 알아서 길이를 제한해줍니다. 세 번째 인자 stdin은 표준 입력(터미널)에서 읽어온다는 뜻입니다. 참고로 fgets는 입력한 엔터(\n)까지 문자열에 포함시킨다는 점도 함께 기억해두면 좋습니다.
scanf("%s", ...)와 scanf("%d", ...)를 번갈아 쓸 때는 이전 강의에서 배웠던 getchar() 버퍼 비우기가 다시 등장합니다. 이름과 나이를 순서대로 입력받는 코드로 확인해보겠습니다.
✅ 직접 실습해본 코드 — scanf 사이사이 버퍼 비우기
#include <stdio.h>
int main() {
char name[20];
int age;
printf("이름을 입력하세요 : ");
scanf("%s", name);
while (getchar() != '\n'); // 입력 버퍼 비우기
printf("나이를 입력하세요 : ");
scanf("%d", &age);
while (getchar() != '\n'); // 입력 버퍼 비우기
printf("입력된 이름은 %s, 나이는 %d살 입니다.\n", name, age);
return 0;
}
▶ 실행 결과
이름을 입력하세요 : 홍길동
나이를 입력하세요 : 10
입력된 이름은 홍길동, 나이는 10살 입니다.
scanf("%s", ...)는 엔터(\n)를 입력 버퍼에 그대로 남겨둡니다. 이 상태에서 바로 다음 scanf를 실행하면, 남아있던 엔터를 먼저 읽어버려서 원하는 입력을 못 받는 문제가 생깁니다. while (getchar() != '\n');로 엔터를 만날 때까지 버퍼를 비워주면 이 문제를 예방할 수 있습니다.
06 strlen — 문자열 길이 구하기
직접 만들어보면 왜 헷갈리는지 이해된다
strlen을 쓰기 전에, 문자열의 길이를 직접 계산해보는 코드를 짜보면 어디서 실수가 나기 쉬운지 감을 잡을 수 있습니다. \0을 만날 때까지 한 칸씩 세어나가는 방식입니다.
❌ 흔히 하는 실수 — \0까지 세어버리기
char str[] = "ABC";
int len = 0;
while (str[len] != '\0') {
len++;
}
len++; // 실수로 \0 자리까지 세어버렸다
printf("길이: %d\n", len);
▶ 실행 결과 (틀렸다 — ABC는 3글자인데 4가 나온다)
길이: 4
이런 실수를 방지하기 위해서라도, 길이 계산은 직접 만들기보다 표준 함수 strlen을 쓰는 것이 안전합니다.
✅ strlen은 \0을 세지 않는다
#include <stdio.h>
#include <string.h>
int main() {
char str1[] = "ABC";
char str2[] = "가나";
printf("str1 길이: %zu\n", strlen(str1));
printf("str2 길이: %zu\n", strlen(str2));
return 0;
}
▶ 실행 결과 (인코딩에 따라 str2 값은 달라질 수 있다)
str1 길이: 3
str2 길이: 4 (UTF-8 기준, 한글 한 글자당 대략 2~3바이트)

strlen(str1)은 정확히 3이 나옵니다 — \0은 세지 않기 때문입니다. str2("가나")는 눈에 보이는 글자 수는 2개지만, 한글이 여러 바이트를 차지하기 때문에 바이트 기준 길이가 그보다 훨씬 크게 나옵니다. 실행 환경(OS, 인코딩 설정)에 따라 정확한 숫자는 달라질 수 있습니다.
⚠️ fgets로 입력받은 문자열의 strlen이 1 더 큰 이유
앞서 배운 fgets로 문자열을 입력받은 뒤 strlen을 재보면, 눈에 보이는 글자 수보다 1 더 큰 값이 나옵니다. 예를 들어 "abc"를 fgets로 입력하면 strlen 결과는 3이 아니라 4입니다. 이건 \0이 세어져서가 아니라(strlen은 절대 \0을 세지 않습니다), fgets가 엔터를 누를 때 딸려오는 개행 문자(\n)까지 문자열에 포함시켜 저장하기 때문입니다. 즉 "abc\n\0"이 저장되고, strlen은 \n까지 포함해서 4를 반환하는 것입니다. fgets를 쓸 때 이 \n 하나를 감안하지 않으면 이후 strcmp 등에서 예상과 다른 결과를 만날 수 있습니다.
07 strstr — 문자열 안에서 찾기
주소값의 뺄셈으로 인덱스를 구하는 원리
strstr(원본, 찾을 문자열)은 원본 문자열 안에서 특정 단어가 어디서 시작하는지 가리키는 포인터를 반환합니다. 원본의 시작 주소와 찾은 위치의 주소를 빼면 인덱스를 계산할 수 있습니다.
✅ 직접 실습해본 코드 — 내가 그린 기린 그림
#include <stdio.h>
#include <string.h>
int main() {
char str[100] = "내가 그린 기린 그림은 고슴도치 그림이다.";
char substr[100] = "고슴도치";
char *result = strstr(str, substr);
if (result) {
printf("Substr Index : %ld\n", result - str); // 주소끼리 빼면 인덱스가 나온다
} else {
printf("Not found.\n");
}
return 0;
}
▶ 실행 결과 (UTF-8 환경 기준 — 인코딩에 따라 달라질 수 있다)
Substr Index : 31

✓ result - str이 인덱스가 되는 이유
str은 문자열의 시작 주소이고, result는 strstr이 찾아낸 "고슴도치"가 시작하는 위치의 주소입니다. 이 둘을 빼면 "시작점에서 몇 바이트를 이동했는지"가 그대로 인덱스 값이 됩니다. "고슴도치" 앞에 한글 9글자와 공백 4개가 있는데, UTF-8에서 한글이 한 글자당 3바이트를 차지하다 보니 9×3+4 = 31이 나온 것입니다. 같은 코드라도 EUC-KR/CP949 환경(한글 2바이트)에서 실행하면 훨씬 작은 숫자가 나옵니다 — 인코딩에 따라 결과가 달라진다는 걸 직접 확인할 수 있는 좋은 예제입니다.
⚠️ 자주 하는 실수 — 포인터를 %d로 출력하기
str이나 result 같은 포인터(주소)를 확인해보고 싶을 때 printf("%d", str)처럼 %d를 쓰기 쉽습니다. 하지만 주소는 %d가 아니라 %p로 출력해야 합니다. %d로 출력하면 값이 잘리거나 이상한 숫자가 나오는 정의되지 않은 동작이 됩니다.
08 strcpy와 strcat — 복사와 결합
strcat은 원본을 파괴한다는 점을 꼭 기억하자

strcpy(목적지, 원본)은 원본 문자열을 목적지에 그대로 복사합니다.
strcpy로 문자열 복사하기
#include <stdio.h>
#include <string.h>
int main() {
char source[] = "Hello World";
char copy[20];
strcpy(copy, source); // 목적지가 먼저, 원본이 나중
printf("원본: %s\n", source);
printf("복사본: %s\n", copy);
return 0;
}
▶ 실행 결과
원본: Hello World
복사본: Hello World
이번엔 두 문자열을 이어붙이는 strcat(목적지, 붙일 문자열)을 사용해보겠습니다.
strcat으로 문자열 결합하기
#include <stdio.h>
#include <string.h>
int main() {
char str1[20] = "Hello "; // 뒤에 붙을 공간까지 넉넉하게 잡는다
char str2[] = "World";
strcat(str1, str2); // str1 뒤에 str2를 붙인다
printf("str1: %s\n", str1);
printf("str2: %s\n", str2);
return 0;
}
▶ 실행 결과
str1: Hello World
str2: World
⚠️ strcat은 첫 번째 인자(원본)를 직접 수정한다
strcat을 실행하면 str1 자체가 변형됩니다. "Hello "라는 원본 데이터는 사라지고 "Hello World"로 덮어씌워집니다. 원본을 보존하고 싶다면, 결합 전에 별도의 문자열 변수를 하나 더 만들어서 복사해둔 뒤 그 사본에 strcat을 적용해야 합니다. 그리고 str1처럼 결합된 결과가 들어갈 공간을 미리 넉넉하게 잡아둬야 한다는 것도 잊지 마세요.
09 strcmp — 문자열 비교하기
길이 비교가 아니라 아스키코드 비교다
strcmp(문자열1, 문자열2)는 길이가 아니라 문자를 앞에서부터 하나씩 아스키코드로 비교합니다. 두 문자열이 완전히 같으면 0을, 다르면 0이 아닌 값을 반환합니다.
strcmp의 세 가지 결과
#include <stdio.h>
#include <string.h>
int main() {
printf("%d\n", strcmp("apple", "banana")); // a(97) < b(98)
printf("%d\n", strcmp("apple", "apple")); // 완전히 동일
printf("%d\n", strcmp("banana", "apple")); // b(98) > a(97)
return 0;
}
▶ 실행 결과 (정확한 숫자는 컴파일러마다 다를 수 있다)
-1
0
1

✓ 부호만 보면 된다
strcmp는 두 번째 문자열이 더 크면 음수, 첫 번째가 더 크면 양수, 완전히 같으면 0을 돌려줍니다. 반환되는 정확한 숫자(예: -1, -25 등)는 컴파일러 구현마다 다를 수 있으므로, 실무에서는 항상 0인지 아닌지, 부호가 양수인지 음수인지만 확인하면 충분합니다.
10 memset — 메모리를 한 번에 초기화하기
0으로 초기화할 땐 완벽하지만, 다른 값은 조심해야 한다
memset(대상, 값, 크기)은 지정한 메모리 영역을 특정 값으로 통째로 채워주는 함수입니다. 배열 전체를 0으로 초기화할 때 특히 자주 사용됩니다.
✅ memset으로 배열을 0으로 초기화하기
#include <stdio.h>
#include <string.h>
int main() {
int arr[5];
memset(arr, 0, sizeof(arr));
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
▶ 실행 결과
0 0 0 0 0
⚠️ memset(arr, 1, ...)은 원하는 결과가 나오지 않는다
"그럼 1로 초기화하고 싶으면 memset(arr, 1, ...)을 쓰면 되지 않을까?"라고 생각하기 쉽지만, 이렇게 하면 엉망인 값이 나옵니다. memset의 두 번째 인자는 1바이트 단위로 채워지기 때문입니다. int는 4바이트인데 1바이트짜리 1이 4번 연속으로 채워지다 보니, 우리가 기대하는 "4바이트 단위의 1"과는 전혀 다른 값이 만들어집니다. 0으로 초기화할 때만 안전하게 쓰고, 0이 아닌 특정 값으로 초기화하고 싶다면 for문을 사용하는 것이 일반적입니다.
0이 아닌 값으로 초기화하려면 for문을 쓰자
int arr[5];
int len = sizeof(arr) / sizeof(int);
for (int i = 0; i < len; i++) {
arr[i] = 1;
}
📖 memset의 버그가 눈에 안 보일 수도 있다
memset으로 초기화하려고 해놓고, 바로 뒤에 코드를 덧붙여 덮어써버리면 앞서 저지른 실수를 알아채기 힘듭니다. 예를 들어 memset(arr, 1, sizeof(arr));을 실행한 바로 다음 줄에서 이 for문으로 다시 값을 채운다면, 출력 결과는 1 1 1 1 1로 완벽하게 정상처럼 보입니다. 하지만 이건 memset이 제대로 동작해서가 아니라, for문이 memset의 잘못된 결과를 덮어썼기 때문입니다. memset 직후에 바로 출력해보지 않으면 이 버그를 놓치기 쉬우니, 의심이 되면 중간중간 출력해서 확인하는 습관을 들이는 것이 좋습니다.
📌 핵심 정리
· 문자열 : char 배열이며, 끝에 널 문자(\0)가 있어야 유효하다 (널 터미네이티드 방식)
· 배열 크기 : char str[N]은 실제로 N-1글자까지만 안전하게 저장할 수 있다
· 인코딩 : 영어·숫자는 1바이트, 한글은 인코딩 방식에 따라 2~4바이트를 차지한다
· scanf vs fgets : 공백 포함 문장은 scanf가 아니라 fgets로 받아야 한다
· strlen : \0을 세지 않고 실제 글자(바이트) 길이만 반환한다
· strstr : 찾은 위치의 포인터를 반환하며, 시작 주소와의 차이로 인덱스를 구할 수 있다
· strcpy / strcat : strcat은 첫 번째 인자(목적지) 원본을 직접 수정하니 주의한다
· strcmp : 길이가 아니라 아스키코드 순서를 비교하며, 0/음수/양수 세 가지로 결과를 판단한다
· memset : 0으로 초기화할 때만 안전하다 — 0이 아닌 값은 for문으로 채우는 것이 일반적이다
※ 이 글은 아래 동영상으로 학습한 내용을 요약한 글입니다.
출처 동영상: https://youtu.be/O3fjri0yTJM?si=iz2qfhM5smbFq-wY
태그 : C언어 문자열string.h 사용법널문자 개념strlen strcpy strcmpstrcat 사용법strstr 문자열 찾기fgets scanf 차이memset 사용법C언어 한글 인코딩C언어 코딩 기초 강의
'학과 수업 노트 > C언어 기초 배우기' 카테고리의 다른 글
| 12. 포인터 (0) | 2026.07.14 |
|---|---|
| 11. 배열 (0) | 2026.07.12 |
| 10. 함수 (0) | 2026.07.05 |
| 9. Switch Case문 (0) | 2026.07.03 |
| 8. 반복문 while문과 무한반복 (0) | 2026.07.02 |



