콘텐츠 대표 이미지 - C언어 문자열 완전 정복: 배열부터 함수까지 싹 다 알려줄게! 🚀

C언어 문자열 완전 정복: 배열부터 함수까지 싹 다 알려줄게! 🚀

코딩의 세계에서 '문자열'은 그냥 글자가 아니야. 데이터를 담는 그릇이자, 프로그램의 얼굴이지!
지금부터 C언어 문자열의 A to Z, 진짜 '찐' 개발자처럼 다루는 법을 마스터해보자. 준비됐어? 😉

안녕, 코딩 친구! 👋 오늘 우리가 함께 탐험할 주제는 바로 C언어의 '문자열'이야. "아, 그거 그냥 글자 나열한 거 아냐?"라고 생각했다면, 오늘 이 글을 끝까지 읽고 나면 생각이 180도 바뀔걸? C언어에서 문자열은 생각보다 훨씬 더 깊고, 때로는 까다롭지만, 한번 제대로 이해하면 정말 강력한 무기가 되거든.

우리가 매일 쓰는 카톡 메시지, 인터넷 검색어, 게임 속 캐릭터 이름까지 전부 문자열로 이루어져 있어. 이처럼 문자열은 프로그램을 살아 숨 쉬게 만드는 핵심 요소 중 하나야. 하지만 C언어는 파이썬이나 자바스크립트처럼 세련된 'string' 타입을 기본으로 제공하지 않아. 대신 좀 더 날것의(?) 형태인 '문자 배열'을 사용하지. 이게 바로 C언어 문자열이 초보자들에게는 넘사벽처럼 느껴지는 이유이기도 해.

하지만 걱정 마! 오늘 내가 C언어 문자열의 정체가 뭔지, 어떻게 선언하고 사용하는지, 그리고 문자열을 자유자재로 요리할 수 있게 해주는 마법 같은 함수들까지, 하나부터 열까지 전부 알려줄게. 마치 친한 친구가 옆에서 과외해주듯, 쉽고 재미있게 말이야. 자, 그럼 이제 C언어 문자열의 세계로 함께 다이빙해볼까? 퐁당! 🌊

1. C언어 문자열, 너 정체가 뭐니? 🤔: 문자 배열과 NULL 종료 문자

가장 먼저, C언어에서 '문자열'이 정확히 무엇인지부터 짚고 넘어가야 해. 이걸 모르면 앞으로 나올 모든 내용이 뜬구름 잡는 소리처럼 들릴 수 있거든. 집중! ✨

결론부터 말하면, C언어의 문자열은 'char' 타입의 배열이야. 그런데 그냥 배열이 아니라 아주 특별한 규칙을 가진 배열이지. 그 규칙은 바로 '문자열의 끝은 항상 NULL 종료 문자(\0)로 끝나야 한다'는 거야.

이게 무슨 말이냐고? 예를 들어 우리가 "hello"라는 문자열을 저장한다고 생각해보자. 우리 눈에는 5글자로 보이지만, 컴퓨터 메모리에는 이렇게 저장돼.

[ 'h' ] [ 'e' ] [ 'l' ] [ 'l' ] [ 'o' ] [ '\0' ]

마지막에 보이는 저 `\0`가 바로 'NULL 종료 문자(Null-terminating character)'야. 아스키코드 값으로는 0이고, 문자는 아니지만 '여기서 문자열이 끝납니다!'라고 알려주는 마침표 같은 역할을 해. C언어의 모든 문자열 관련 함수들(예: `printf`, `strcpy` 등)은 이 `\0`을 만날 때까지 작업을 계속하거든. 만약 이게 없다면? 함수들은 문자열이 어디서 끝나는지 몰라서 메모리의 다른 영역까지 침범하는 끔찍한 대참사 😱(버퍼 오버플로우)가 발생할 수도 있어.

C언어 문자열의 메모리 구조: NULL의 중요성! H str[0] E str[1] L str[2] L str[3] O str[4] \0 str[5] 바로 이 녀석! 문자열의 끝을 알리는 아주 중요한 마침표 역할!

💡 꿀팁! (Pro Tip!)
문자열 "hello"는 5글자지만, 메모리에는 `\0`까지 포함해서 총 6바이트의 공간이 필요해. 앞으로 문자열을 저장할 배열을 만들 땐, 항상 (실제 글자 수 + 1) 만큼의 공간을 확보하는 습관을 들여야 해. 이거 진짜 진짜 중요해! 별 다섯 개! ⭐⭐⭐⭐⭐

문자열 선언과 초기화: 방법이 여러 가지라고?

자, 그럼 이제 코드로 직접 문자열을 만들어보자. C에서는 크게 두 가지 방법으로 문자열을 만들 수 있어.

1. 배열 초기화를 이용하는 방법 (가장 흔하고 쉬운 방법)

이 방법은 큰따옴표("")를 사용해서 아주 간단하게 문자열을 만들 수 있어. 컴파일러가 알아서 글자 수 세고, 맨 뒤에 `\0`까지 붙여주는 아주 착한 녀석이지.

예제 코드 1: 가장 기본적인 문자열 선언


#include <stdio.h>

int main(void) {
    // 컴파일러가 알아서 크기를 6으로 잡아줌 ("hello" 5글자 + '\0' 1글자)
    char str1[] = "hello"; 

    // 공간을 넉넉하게 10으로 지정. 남는 공간은 비어있음.
    char str2[10] = "world";

    printf("str1: %s\n", str1); // 출력: str1: hello
    printf("str2: %s\n", str2); // 출력: str2: world

    return 0;
}

2. 문자 하나하나 직접 넣는 방법 (원리를 이해하기 좋은 방법)

이 방법은 좀 번거롭지만, 문자열이 '문자들의 배열'이라는 사실을 가장 명확하게 보여줘. 여기서 핵심은 마지막에 `\0`을 내 손으로 직접 넣어줘야 한다는 거야. 이걸 빼먹으면 `printf`가 이상한 쓰레기 값을 마구 출력하는 걸 보게 될 거야.

예제 코드 2: 한 땀 한 땀 장인정신으로 선언


#include <stdio.h>

int main(void) {
    // 반드시 마지막에 '\0'을 넣어줘야 함!
    char str3[6] = {'h', 'e', 'l', 'l', 'o', '\0'};

    // 만약 이렇게 '\0'을 빼먹으면? -> 큰일남!
    char str4[5] = {'w', 'o', 'r', 'l', 'd'}; 

    printf("str3: %s\n", str3); // 출력: str3: hello
    printf("str4: %s\n", str4); // 출력: str4: world???? (뒤에 쓰레기 값이 붙어서 나옴)

    return 0;
}

헷갈리면 안 돼! 문자 배열 vs 문자열 리터럴 포인터

초보자들이 정말 많이 헷갈려 하는 부분이 바로 이거야. 아래 두 코드는 비슷해 보이지만 하늘과 땅 차이야.


char str_array[] = "hello";  // 1번: 문자 배열 (수정 가능 O)
char *str_ptr = "hello";     // 2번: 문자열 리터럴을 가리키는 포인터 (수정 불가능 X)

🚨 잠깐, 이건 꼭 알아둬!
1번 `str_array`는 "hello"라는 내용을 복사해서 새로운 메모리 공간(스택 영역)에 만든 '배열'이야. 그래서 우리는 이 배열의 내용을 바꿀 수 있어. 예를 들어 `str_array[0] = 'H';` 라고 하면 "Hello"로 바뀌지. 이건 우리 집이야. 내 마음대로 인테리어를 바꿀 수 있어. 🏠

2번 `str_ptr`은 "hello"라는 문자열이 저장된 *어떤 다른 공간*(주로 읽기 전용 데이터 영역)의 '주소'만 가지고 있는 '포인터'야. 이 공간은 상수 취급을 받아서 절대로 내용을 수정할 수 없어. 만약 `str_ptr[0] = 'H';` 같은 코드를 실행하면 프로그램이 쾅! 하고 죽어버릴 거야(세그멘테이션 오류). 이건 모델하우스 같은 거야. 구경은 할 수 있지만, 벽지 색깔을 바꿀 수는 없지. 🏛️

문자열을 다루면서 내용을 변경할 일이 있다면, 반드시 1번 방식, 즉 배열로 선언해야 한다는 거, 잊지 마! 이 차이점을 이해하는 것만으로도 C언어 문자열의 절반은 마스터한 셈이야. 정말이야! 👍

2. 기본기 다지기 🥋: 문자열 입출력 완전 정복

자, 이제 문자열을 만들었으니 화면에 출력도 해보고, 사용자로부터 입력을 받아보기도 해야겠지? 문자열 입출력은 프로그램과 사용자 사이의 소통 창구와도 같아. 기본이지만 아주 중요하지. 한번 제대로 배워보자!

문자열 출력의 국룰: `printf`와 `puts`

`printf` 함수와 `%s` 서식 지정자

이건 아마 C언어 처음 배울 때부터 써봐서 익숙할 거야. `printf` 함수에서 `%s` 서식 지정자를 사용하면 문자열을 쉽게 출력할 수 있어. `printf`는 `\0`을 만날 때까지 메모리를 쭉 읽어서 화면에 뿌려주는 역할을 해.

예제 코드 3: printf로 문자열 출력하기


#include <stdio.h>

int main(void) {
    char name[] = "Jaenung";
    int age = 10;

    printf("안녕하세요! 제 이름은 %s입니다.\n", name);
    printf("제 나이는 %d살이고, 이름은 %s입니다.\n", age, name);

    return 0;
}

`puts` 함수: 심플 이즈 베스트!

`puts` 함수는 `printf`보다 훨씬 간단해. 오직 문자열 하나만 출력하는 기능에 충실한 함수지. 특징은 문자열을 출력한 뒤에 자동으로 줄바꿈(\n)을 해준다는 거야. 그래서 간단한 문자열만 출력할 땐 `printf("...\n");` 보다 `puts(...)`가 더 편할 수 있어.

예제 코드 4: puts로 깔끔하게 출력하기


#include <stdio.h>

int main(void) {
    char greeting[] = "Hello, C World!";
    char farewell[] = "Goodbye!";

    puts(greeting); // "Hello, C World!" 출력 후 자동 줄바꿈
    puts(farewell); // "Goodbye!" 출력 후 자동 줄바꿈

    return 0;
}

문자열 입력: `scanf`의 함정과 안전한 대안

이제부터가 진짜 중요해. 문자열 입력 처리는 C언어에서 가장 많은 버그와 보안 취약점이 발생하는 곳이거든. 정신 바짝 차리고 따라와! 🏃‍♀️

`scanf` 함수: 편리하지만 위험한 친구

`scanf`에 `%s`를 사용하면 사용자로부터 문자열을 입력받을 수 있어. 아주 편리하지. 하지만 `scanf`는 치명적인 단점이 있어. 바로 입력받을 문자열의 길이를 전혀 신경 쓰지 않는다는 거야.

예를 들어, 우리가 `char name[5];` 라고 5칸짜리 배열을 만들어놓고 사용자에게 이름을 입력받는다고 해보자. 만약 사용자가 "John" (4글자)을 입력하면 아무 문제가 없어. 메모리에는 'J', 'o', 'h', 'n', `\0` 이렇게 5칸이 딱 맞게 채워지니까. 하지만 만약 사용자가 "Alexander" (9글자)를 입력하면 어떻게 될까?

🚨🚨🚨 버퍼 오버플로우(Buffer Overflow) 발생! 🚨🚨🚨
`scanf`는 우리가 준비한 5칸의 공간을 넘어서, 뒤에 있는 다른 변수들의 메모리 공간까지 침범하면서 "Alexander"를 꾸역꾸역 다 집어넣으려고 할 거야. 이 과정에서 프로그램의 다른 중요 데이터가 망가져서 오작동하거나, 최악의 경우 프로그램이 다운되고, 해커에게 시스템 제어권을 넘겨주는 심각한 보안 문제로 이어질 수 있어. 절대로, 절대로 `scanf("%s", ...)`를 아무런 보호 장치 없이 사용하면 안 돼!

안전한 입력의 정석: `fgets` 함수

그럼 어떻게 해야 안전하게 문자열을 입력받을 수 있을까? 정답은 바로 `fgets` 함수야. `fgets`는 '파일'에서 문자열을 읽어오는 함수지만, 표준 입력(키보드)을 나타내는 `stdin`을 사용하면 키보드 입력을 안전하게 받을 수 있어.

`fgets`의 가장 큰 장점은 최대 몇 글자까지 받을지 직접 지정할 수 있다는 거야. 그래서 버퍼 오버플로우를 원천적으로 차단할 수 있지.

사용법은 조금 복잡해 보이지만, 금방 익숙해질 거야.


char *fgets(char *buffer, int size, FILE *stream);
  • `buffer`: 입력받은 문자열을 저장할 배열(버퍼)
  • `size`: 버퍼의 전체 크기. 이 크기 이상으로는 절대 입력받지 않아. (안전장치!)
  • `stream`: 어디서부터 읽어올지 지정. 키보드 입력은 `stdin`을 쓰면 돼.

예제 코드 5: fgets로 안전하게 이름 입력받기


#include <stdio.h>

int main(void) {
    char name[10]; // '\0' 포함해서 최대 10글자 저장 가능

    printf("이름을 입력하세요 (최대 9글자): ");
    
    // name 배열에, 최대 10바이트 크기만큼, 표준 입력(stdin)으로부터 읽어온다.
    fgets(name, 10, stdin); 

    printf("안녕하세요, %s님!\n", name);

    return 0;
}

위 코드에서 사용자가 "Christopher"처럼 아주 긴 이름을 입력해도, `fgets`는 똑똑하게 앞에서부터 9글자("Christoph")와 `\0`만 `name` 배열에 저장하고 나머지는 버려. 정말 안전하지? 😎

💡 `fgets` 사용 시 주의할 점!
`fgets`는 사용자가 엔터(Enter) 키를 누른 것까지 문자열의 일부로 취급해서, 줄바꿈 문자(`\n`)까지 함께 저장하는 경우가 많아. 예를 들어 "John"이라고 입력하고 엔터를 치면, `name` 배열에는 "John\n\0"이 저장될 수 있어. 이 `\n`이 원치 않는 결과를 만들 수 있으니, 필요하다면 직접 제거해주는 코드를 추가하는 게 좋아. (이건 나중에 문자열 처리 함수를 배우면 쉽게 할 수 있어!)

이제 문자열의 기본 개념과 입출력 방법을 알았으니, 다음 챕터에서는 문자열을 자유자재로 다룰 수 있게 해주는 강력한 무기들, 바로 `` 헤더 파일의 함수들을 만나볼 거야. 진짜 재밌는 건 지금부터 시작이야! 🚀

3. 스트링 어벤져스 🦸: `` 함수 총출동!

C언어는 기본적으로 문자열을 위한 연산자(+, == 등)를 제공하지 않아. "hello" + "world" 같은 건 꿈도 꿀 수 없지. 😅 대신, 문자열을 복사하고, 붙이고, 비교하고, 검색하는 등 온갖 작업을 할 수 있는 강력한 함수들을 ``라는 헤더 파일에 모아놨어. 이 함수들만 잘 쓸 줄 알아도 C언어 문자열 처리의 90%는 끝난 거야. 자, 그럼 스트링 어벤져스 멤버들을 한 명씩 만나보자!

이 함수들을 사용하려면, 코드 맨 위에 `#include `를 추가하는 거 잊지 마!

① `strlen`: 문자열 길이를 재는 줄자 📏

가장 기본적이면서도 가장 많이 쓰는 함수야. `strlen`은 문자열의 '길이'를 알려줘. 여기서 말하는 길이란, 맨 뒤의 `\0`을 제외한 순수 문자의 개수야.

  • 함수 원형: `size_t strlen(const char *str);`
  • 파라미터: 길이를 알고 싶은 문자열 (`str`)
  • 리턴 값: 문자열의 길이 (`size_t`는 `unsigned int`와 비슷한 자료형이야)

예제 코드 6: strlen으로 문자열 길이 구하기


#include <stdio.h>
#include <string.h>

int main(void) {
    char str[] = "Hello, World!"; // 13글자
    
    int len = strlen(str);
    
    printf("문자열: \"%s\"\n", str);
    printf("문자열의 길이 (strlen): %d\n", len); // 출력: 13
    printf("배열의 크기 (sizeof): %zu\n", sizeof(str)); // 출력: 14 ('\0' 포함)

    return 0;
}

🚨 `strlen` vs `sizeof` 헷갈리지 마!
이거 진짜 단골 질문이야. `strlen`은 문자열의 *내용* 길이를 세는 함수고, `sizeof`는 변수나 배열에 할당된 *메모리 공간*의 크기를 알려주는 연산자야. 위 예제에서 `strlen`은 `\0`을 빼고 13을 반환했지만, `sizeof`는 `\0`을 포함한 배열의 전체 크기인 14를 반환했지? 이 차이를 꼭 기억해둬!

② `strcpy` & `strncpy`: 복사의 달인 👯

C언어에서는 `str2 = str1;` 이런 식으로 문자열을 대입할 수 없어. 배열의 이름은 주소 상수라서 대입 연산이 불가능하거든. 대신, 문자열을 '복사'해주는 `strcpy` 함수를 사용해야 해.

  • 함수 원형: `char *strcpy(char *dest, const char *src);`
  • 파라미터:
    • `dest` (destination): 복사한 내용을 붙여넣을 목적지 배열
    • `src` (source): 복사할 원본 문자열
  • 리턴 값: 목적지 배열의 주소 (`dest`)

🚨 `strcpy`의 치명적인 위험성!
`strcpy`는 `scanf`처럼 아주 위험한 함수야. 목적지 배열(`dest`)의 크기를 전혀 고려하지 않고 원본(`src`)의 내용을 `\0`을 만날 때까지 무작정 복사하거든. 만약 `src`의 길이가 `dest`의 크기보다 크면? 여지없이 버퍼 오버플로우가 발생해! 그래서 요즘은 `strcpy` 대신, 복사할 길이를 지정할 수 있는 안전한 버전인 `strncpy` 사용을 강력하게 권장해.

안전한 복사, `strncpy`

  • 함수 원형: `char *strncpy(char *dest, const char *src, size_t n);`
  • 파라미터: `strcpy`와 같고, 마지막에 `n`이 추가됐어.
    • `n`: 최대 몇 글자까지 복사할지 지정하는 숫자.

예제 코드 7: strcpy의 위험성과 strncpy로 안전하게 복사하기


#include <stdio.h>
#include <string.h>

int main(void) {
    char src[] = "This is a very long string.";
    char dest1[10];
    char dest2[10];

    // 위험한 strcpy 사용 예시 -> 버퍼 오버플로우 발생 가능성 99.9%
    // strcpy(dest1, src); 
    // printf("strcpy 결과: %s\n", dest1); // 아마 프로그램이 비정상적으로 동작할 것

    // 안전한 strncpy 사용 예시
    strncpy(dest2, src, 9); // dest2의 크기가 10이므로, 9글자만 복사 (마지막은 '\0'을 위해)
    dest2[9] = '\0'; // ★★★ strncpy는 '\0'을 자동으로 안 붙여줄 수도 있으니, 수동으로 꼭 붙여주자!

    printf("strncpy 결과: %s\n", dest2); // 출력: This is a

    return 0;
}

💡 `strncpy`의 함정!
`strncpy`는 정말 좋은 함수지만, 한 가지 기억해야 할 게 있어. 만약 복사하려는 원본 문자열의 길이가 지정한 `n`보다 길거나 같으면, `strncpy`는 복사된 문자열 끝에 `\0`을 자동으로 붙여주지 않아! 그래서 위 코드처럼 `dest2[9] = '\0';` 와 같이 **수동으로 NULL 종료 문자를 넣어주는 것이 가장 안전한 습관**이야. 꼭 기억해!

③ `strcat` & `strncat`: 문자열 이어붙이기 장인 🔗

`strcat`은 두 문자열을 하나로 합쳐주는 함수야. 'catenate'가 '연결하다'라는 뜻이거든. 첫 번째 문자열 뒤에 두 번째 문자열을 그대로 가져다 붙여줘.

  • 함수 원형: `char *strcat(char *dest, const char *src);`
  • 파라미터:
    • `dest`: 원본 문자열. 이 문자열의 끝에 `src`가 붙게 됨. (공간이 넉넉해야 함!)
    • `src`: 뒤에 갖다 붙일 문자열.

당연히 예상했겠지만, `strcat`도 `strcpy`처럼 목적지 배열의 크기를 확인하지 않아서 버퍼 오버플로우에 매우 취약해. 그래서 우리는 안전한 `strncat`을 써야겠지? 😉

안전한 이어붙이기, `strncat`

  • 함수 원형: `char *strncat(char *dest, const char *src, size_t n);`
  • 파라미터: `n`이 추가되어, `src`에서 최대 `n`개의 문자만 `dest` 뒤에 붙여.

예제 코드 8: strncat으로 안전하게 문자열 합치기


#include <stdio.h>
#include <string.h>

int main(void) {
    char dest[20] = "Hello, "; // 공간을 넉넉하게!
    char src[] = "World! Welcome to C!";

    printf("합치기 전: %s\n", dest);

    // dest의 남은 공간을 계산해서 안전하게 붙이기
    // 전체 크기 20 - 현재 길이 7 - '\0' 공간 1 = 최대 12글자까지 붙일 수 있음
    strncat(dest, src, 12);

    printf("합치기 후: %s\n", dest); // 출력: Hello, World! Welcom

    return 0;
}

`strncat`은 `strncpy`와는 다르게, 이어붙인 후에 항상 `\0`을 자동으로 붙여줘서 조금 더 편리해. 하지만 목적지 배열의 남은 공간을 잘 계산해서 `n` 값을 정해줘야 한다는 점은 잊지 마!

④ `strcmp` & `strncmp`: 비교의 신 ⚖️

문자열이 같은지 다른지 비교할 때 `if (str1 == str2)` 이렇게 쓰면 될까? 땡! ❌ 이건 두 문자열의 '내용'을 비교하는 게 아니라, 두 배열의 '메모리 주소'를 비교하는 거라 거의 항상 `false`가 나와. 문자열의 내용을 비교하려면 반드시 `strcmp` 함수를 써야 해.

  • 함수 원형: `int strcmp(const char *s1, const char *s2);`
  • 파라미터: 비교할 두 문자열 `s1`, `s2`
  • 리턴 값: 이게 좀 특이해.
    • 두 문자열이 완전히 같으면: 0
    • `s1`이 `s2`보다 사전 순으로 앞서면 (예: "A" vs "B"): 음수
    • `s1`이 `s2`보다 사전 순으로 뒤에 있으면 (예: "B" vs "A"): 양수

예제 코드 9: strcmp로 문자열 비교하기


#include <stdio.h>
#include <string.h>

int main(void) {
    char str1[] = "apple";
    char str2[] = "apple";
    char str3[] = "banana";

    if (strcmp(str1, str2) == 0) {
        printf("%s와 %s는 같습니다.\n", str1, str2);
    } else {
        printf("%s와 %s는 다릅니다.\n", str1, str2);
    }

    if (strcmp(str1, str3) < 0) {
        printf("%s는 %s보다 사전 순으로 앞에 있습니다.\n", str1, str3);
    }

    return 0;
}

`strncmp`는 앞에서부터 `n`개의 문자만 비교하는 버전이야. 비밀번호 앞자리만 확인하는 등 특정 부분만 비교할 때 유용하게 쓸 수 있어.

⑤ `strchr` & `strrchr`: 문자 탐색의 명탐정 🕵️

문자열 안에서 특정 '문자'가 어디에 있는지 찾고 싶을 때 사용하는 함수들이야.

  • `strchr` (character): 문자열의 앞에서부터 문자를 검색해서, 가장 먼저 발견된 문자의 위치(포인터)를 반환해.
  • `strrchr` (reverse character): 문자열의 뒤에서부터 문자를 검색해서, 가장 먼저 발견된 문자의 위치(포인터)를 반환해.
  • 만약 문자를 찾지 못하면 둘 다 `NULL` 포인터를 반환해.

예제 코드 10: strchr로 특정 문자 찾기


#include <stdio.h>
#include <string.h>

int main(void) {
    char str[] = "https://www.jaenung.net";
    char target = '.';
    
    char *ptr = strchr(str, target);

    if (ptr != NULL) {
        // ptr은 '.' 문자가 있는 곳의 주소를 가리킴
        printf("'%c' 문자를 찾았습니다!\n", target);
        printf("찾은 위치 이후의 문자열: %s\n", ptr); // 출력: .jaenung.net
        printf("찾은 문자의 인덱스: %ld\n", ptr - str); // 포인터 연산으로 인덱스 계산! 출력: 11
    } else {
        printf("'%c' 문자를 찾지 못했습니다.\n", target);
    }

    return 0;
}

파일 경로에서 확장자를 분리하거나, 이메일 주소에서 '@' 기호를 찾을 때 아주 유용하게 쓰이는 함수들이니 꼭 알아둬!

⑥ `strstr`: 부분 문자열 찾기 전문가 🔎

`strchr`가 '문자' 하나를 찾는 함수였다면, `strstr`는 '문자열' 속에서 특정 '문자열'을 찾는 함수야.

  • 함수 원형: `char *strstr(const char *haystack, const char *needle);`
  • 파라미터:
    • `haystack` (건초더미): 검색 대상이 되는 전체 문자열
    • `needle` (바늘): 찾고 싶은 부분 문자열
  • 리턴 값: `haystack` 내에서 `needle`이 처음으로 나타나는 위치의 포인터. 못 찾으면 `NULL`.

예제 코드 11: strstr로 '재능넷' 키워드 찾기


#include <stdio.h>
#include <string.h>

int main(void) {
    char content[] = "다양한 전문가를 만날 수 있는 곳, 바로 재능넷(Jaenung.net)입니다!";
    char keyword[] = "재능넷";

    char *found = strstr(content, keyword);

    if (found) { // NULL이 아니면 true
        printf("'%s' 키워드를 찾았습니다!\n", keyword);
        printf("찾은 위치: %s\n", found);
    } else {
        printf("키워드를 찾지 못했습니다.\n");
    }

    return 0;
}

⑦ `strtok`: 문자열을 싹둑! 분리의 마법사 ✂️

이 함수는 조금 특별하고 까다롭지만, 한번 익혀두면 정말 강력한 도구가 돼. `strtok`은 특정 구분자(delimiter)를 기준으로 문자열을 여러 조각(토큰)으로 잘라주는 역할을 해.

예를 들어 "apple,banana,orange"라는 문자열을 ','를 기준으로 "apple", "banana", "orange" 세 조각으로 나누고 싶을 때 사용하지.

  • 함수 원형: `char *strtok(char *str, const char *delim);`
  • 특이점:
    • 원본 문자열을 수정해! `strtok`은 구분자를 `\0`으로 바꿔버리기 때문에 원본이 훼손돼. 원본 보존이 필요하면 복사본을 만들어서 사용해야 해.
    • 두 번째 호출부터는 첫 번째 인자에 `NULL`을 넣어줘야 해. `strtok`은 내부에 마지막으로 작업했던 위치를 기억하고 있거든(정적 변수 사용). 그래서 다음 토큰을 얻으려면 "아까 작업하던 거기서부터 계속해줘"라는 의미로 `NULL`을 전달해야 해.

예제 코드 12: strtok으로 쉼표로 구분된 과일 이름 분리하기


#include <stdio.h>
#include <string.h>

int main(void) {
    char str[] = "apple,banana,orange,grape";
    const char *delim = ","; // 구분자

    // 첫 번째 토큰 분리
    char *token = strtok(str, delim);

    while (token != NULL) {
        printf("분리된 토큰: %s\n", token);
        // 다음 토큰을 계속해서 분리 (첫 번째 인자로 NULL을 전달!)
        token = strtok(NULL, delim);
    }

    return 0;
}

와, 정말 많은 함수들을 살펴봤지? 이 함수들만 자유자재로 쓸 수 있어도 웬만한 문자열 처리는 문제없을 거야. 처음에는 좀 헷갈릴 수 있지만, 각 함수의 역할과 특징, 특히 `strncpy`나 `strncat`처럼 안전한 함수를 쓰는 습관을 들이는 게 중요해. 직접 코드를 짜면서 연습해보는 게 최고의 공부법이라는 거, 잊지 마! 💪

4. 실전! 문자열 처리, 이것만은 조심하자 💣

지금까지 C언어 문자열의 기본기와 강력한 함수들을 배웠어. 이제 너는 문자열을 꽤 잘 다루는 것처럼 보일 거야. 하지만 실전은 언제나 예상치 못한 함정으로 가득 차 있지. 💥 이 챕터에서는 C언어 문자열을 다룰 때 초보자는 물론이고 경력자들도 흔히 저지르는 실수와 반드시 피해야 할 위험 요소들을 정리해줄게. 이것만 잘 숙지해도 디버깅 시간을 절반으로 줄일 수 있을 거야!

① 버퍼 오버플로우: 시한폭탄을 다루는 방법

앞에서도 여러 번 강조했지만, C언어 문자열 처리에서 가장 중요하고 가장 위험한 문제이기 때문에 다시 한번 짚고 넘어갈게. 버퍼 오버플로우는 C언어의 가장 고질적인 보안 취약점이야.

🚨 버퍼 오버플로우, 왜 이렇게 위험할까?
단순히 프로그램이 죽는 것에서 그치지 않아. 해커는 버퍼 오버플로우를 이용해서 프로그램의 실행 흐름을 조작하고, 악성 코드를 심어서 시스템을 장악할 수 있어. 실제로 역사상 수많은 해킹 사건들이 `strcpy`, `gets`, `scanf` 같은 위험한 함수들을 부주의하게 사용해서 발생했어. 내가 만든 작은 프로그램의 실수가 시스템 전체를 위협하는 좀비가 될 수 있다는 거야.

버퍼 오버플로우를 피하는 철칙:

  1. 경계 검사를 하지 않는 함수는 사용 금지!
    • `gets()`: 이 함수는 이제 C 표준에서도 퇴출됐어. 그냥 존재 자체를 잊어버려. 🗑️
    • `scanf("%s", ...)`: 버퍼 크기를 지정할 수 없으므로 절대 사용하지 마. (굳이 쓴다면 `scanf("%9s", ...)`처럼 폭을 지정할 수는 있지만, 다른 방법이 더 좋아.)
    • `strcpy()`, `strcat()`: 버퍼 크기를 확인하지 않아. 대신 `strncpy()`, `strncat()`을 사용하자.
  2. 언제나 입력 데이터의 길이를 의심하라!

    사용자는 언제나 우리가 예상한 것보다 길거나 이상한 데이터를 입력할 수 있다고 가정해야 해. "에이, 설마 여기에 1000글자를 입력하겠어?" 하는 안일한 생각이 보안 사고로 이어지는 거야.

  3. `fgets()`와 'n'이 붙은 안전한 함수를 생활화하라!

    문자열을 입력받을 땐 `fgets()`, 복사나 이어붙이기를 할 땐 `strncpy()`, `strncat()`을 쓰는 것을 숨 쉬는 것처럼 당연하게 만들어야 해. 안전은 아무리 강조해도 지나치지 않아.

② NULL 종료 문자(`\0`)를 잊지 말자!

C언어 문자열의 알파이자 오메가, 바로 `\0`이야. 이걸 제대로 처리하지 않으면 문자열 함수들은 절대 제대로 동작하지 않아.

예제 코드 13: NULL 문자가 없을 때 벌어지는 일


#include <stdio.h>
#include <string.h>

int main(void) {
    // 일부러 '\0'을 빼먹은 문자 배열
    char bad_str[5] = {'o', 'o', 'p', 's', '!'}; 
    
    // strlen은 '\0'을 찾을 때까지 메모리를 계속 헤맬 것이다.
    // 운이 좋으면 근처에 0인 메모리가 있어서 멈추겠지만,
    // 대부분 엄청나게 큰 값이 나오거나 프로그램이 죽는다.
    printf("잘못된 문자열의 길이: %zu\n", strlen(bad_str)); 
    
    // printf도 마찬가지. "oops!" 뒤에 있는 알 수 없는 쓰레기 값들을
    // '\0'을 만날 때까지 계속 출력하려고 시도한다.
    printf("잘못된 문자열 출력: %s\n", bad_str);

    return 0;
}

특히 `strncpy`처럼 `\0`을 자동으로 붙여주지 않을 수도 있는 함수를 사용할 때는, 작업이 끝난 후 배열의 마지막에 수동으로 `\0`을 넣어주는 습관이 정말 중요해. "내 코드는 항상 `\0`으로 끝난다"는 확신을 가질 수 있어야 해.

③ 문자열 리터럴은 '상수'다! (수정 불가)

이것도 앞에서 잠깐 언급했지만, 너무 중요해서 다시 한번 강조할게. 아래 두 코드의 차이점을 완벽하게 이해해야 해.


char str_array[] = "hello"; // 스택에 생성된 '변수' 배열. 수정 가능.
char *str_ptr = "hello";    // 읽기 전용 메모리에 있는 '상수' 문자열의 주소. 수정 절대 불가.

🤔 왜 `char *`로 선언하면 수정이 안 될까?
컴파일러는 코드에 있는 `"hello"` 같은 문자열 리터럴들을 모아서 프로그램의 읽기 전용(Read-only) 데이터 영역에 저장해. 그리고 `str_ptr`에는 그 주소 값만 쏙 넣어주는 거지. 운영체제는 이 읽기 전용 메모리 영역을 철저하게 보호하기 때문에, 누군가 이 영역에 데이터를 쓰려고 시도하면 "어딜 감히! 😠" 하면서 프로그램을 강제로 종료시켜 버려. 이게 바로 세그멘테이션 오류(Segmentation Fault)의 정체야.

함수에 문자열을 인자로 넘길 때, 그 함수가 문자열의 내용을 수정할 가능성이 있다면 절대로 문자열 리터럴의 포인터를 넘기면 안 돼. 예를 들어 `strtok( "a,b,c" , "," )` 같은 코드는 100% 에러를 발생시켜. `strtok`은 원본 문자열을 수정하는 함수니까!

④ 함수 리턴 값 확인은 필수!

문자열을 다루다 보면 포인터를 리턴하는 함수들을 많이 만나게 돼. (`strchr`, `strstr`, `strtok` 등) 이 함수들은 작업을 성공하면 유효한 메모리 주소를, 실패하면 `NULL`을 리턴하도록 약속되어 있어.

많은 초보자들이 이 `NULL` 체크를 빼먹고, 함수가 리턴한 포인터를 무작정 사용하려다가 에러를 만나곤 해.

🚨 NULL 포인터 역참조(Dereferencing)
`NULL`은 '아무것도 가리키지 않는다'는 의미의 특별한 포인터야. 그런데 `NULL`이 가리키는 곳의 데이터를 읽거나 쓰려고 하면 어떻게 될까? 당연히 운영체제는 "거긴 아무것도 없는데 뭘 하려는 거야!"라며 프로그램을 즉시 중단시켜. 이것도 세그멘테이션 오류의 아주 흔한 원인 중 하나야.


// 나쁜 예: NULL 체크를 하지 않음
char *ptr = strchr("hello", 'z'); // 'z'는 없으므로 ptr은 NULL이 됨
printf("%c\n", *ptr); // CRASH! NULL 포인터를 역참조하려고 시도함

// 좋은 예: 항상 NULL 체크를 함
char *ptr = strchr("hello", 'z');
if (ptr != NULL) {
    printf("%c\n", *ptr);
} else {
    printf("문자를 찾지 못했습니다.\n");
}

포인터를 리턴하는 함수를 썼다면, 그 포인터를 사용하기 전에 `if (ptr != NULL)`로 확인하는 습관을 들이자. 이건 선택이 아니라 필수야!

자, 여기까지 C언어 문자열 처리의 위험한 지뢰밭을 함께 살펴봤어. 버퍼 오버플로우, NULL 종료 문자, 문자열 리터럴, NULL 포인터 체크. 이 네 가지만 확실히 기억하고 코딩해도, 훨씬 더 안정적이고 튼튼한 프로그램을 만들 수 있을 거야. 코딩은 단순히 기능을 구현하는 것뿐만 아니라, 이런 예외 상황과 위험을 미리 예측하고 방어하는 것까지 포함된다는 사실을 꼭 기억해줘! 🛡️

5. 마무리하며: 문자열, 이제 자신 있지? 😎

와, 정말 긴 여정이었어! C언어에서 문자열이 단순한 글자 나열이 아니라 `\0`으로 끝나는 '문자 배열'이라는 사실부터 시작해서, 안전한 입출력 방법, 그리고 문자열을 자유자재로 요리하게 해주는 ``의 어벤져스 함수들, 마지막으로 실전에서 마주칠 수 있는 위험한 함정들까지. 정말 많은 내용을 함께 살펴봤네. 👏

처음에는 C언어의 문자열 처리가 복잡하고 불편하게 느껴졌을지도 몰라. 하지만 오늘 배운 내용들을 되짚어보면, 사실은 메모리를 직접 다루는 C언어의 특성을 가장 잘 보여주는 부분이 바로 이 문자열 처리라는 걸 알 수 있을 거야. 버퍼의 크기를 직접 관리하고, NULL 종료 문자를 신경 써야 하는 번거로움은 우리에게 메모리를 더 효율적이고 세밀하게 제어할 수 있는 강력한 힘을 주지.

이제 여러분은 더 이상 `char str[20];`을 보고 그냥 '문자열 변수'라고 생각하지 않을 거야. '20바이트의 메모리 공간이 할당되었고, 여기에 문자들이 채워지며, 마지막은 반드시 `\0`으로 끝나야 하는구나'라고 머릿속에 그릴 수 있게 되었을 테니까. 바로 이게 진짜 C 프로그래머로 성장하고 있다는 증거야!

🚀 앞으로의 학습을 위한 제언
오늘 배운 지식은 시작일 뿐이야. 이 지식을 바탕으로 더 넓은 세상으로 나아가 봐!

  • 직접 함수 만들어보기: `strlen`, `strcpy` 같은 함수들이 내부적으로 어떻게 동작할지 상상하면서 직접 한번 만들어봐. 포인터와 배열에 대한 이해도가 비약적으로 상승할 거야.
  • 다양한 문제 풀어보기: 백준이나 프로그래머스 같은 코딩 테스트 사이트에서 문자열 관련 문제들을 풀어봐. 문자열 뒤집기, 특정 패턴 찾기, 단어 개수 세기 등 오늘 배운 함수들을 응용하며 실전 감각을 키울 수 있어.
  • 전문가의 도움이 필요할 때: 혼자 공부하다가 막히는 부분이 생기거나, 실제 프로젝트에 문자열 처리 기술을 어떻게 적용할지 막막하다면, 재능넷 같은 재능 공유 플랫폼에서 C언어 전문가들의 도움을 받아보는 것도 아주 좋은 방법이야. 숙련된 멘토의 코드 리뷰 한 번이 혼자 며칠을 고민하는 것보다 더 큰 성장을 가져다줄 수 있거든.

문자열을 정복했다는 것은 C언어의 가장 큰 산 하나를 넘었다는 뜻이야. 이제 여러분은 파일 처리, 네트워크 프로그래밍, 데이터 구조 등 더 흥미진진한 C언어의 세계를 탐험할 준비가 되었어. 오늘 배운 내용들을 꼭 여러 번 복습하고, 직접 코드를 짜보면서 완전히 여러분의 것으로 만들길 바라. 너의 코딩 여정을 항상 응원할게! 파이팅! 🔥

댓글 작성

이 글에 대한 여러분의 생각을 들려주세요

댓글 0
댓글을 불러오는 중입니다.

아직 댓글이 없습니다.