argentdarae 2025. 2. 22. 21:31
반응형

개요

POCU 아카데미 C언매니지드 학습 4주차 내용을 회고하고 복습하는 글이다

 

 

무엇을 배웠는지

C에서 문자열은 메모리에 어떻게 표현되는지, 어떻게 사용하는지 학습하였다

 

문자열의 표현과 길이

기본 자료형(Primitive Type)의 경우 크기가 고정되어 있다. 하지만 문자열은 길이가 정해져 있지 않으며, 동적으로 변경될 수도 있다

 

또한 C에서 문자열을 다룰 때 가장 큰 특징은 문자열의 길이가 어디에도 저장되지 않는다는 점이다. 따라서 프로그래머가 직접 문자를 관리해야 한다

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

/* 문자열 복사 함수 (size_t를 사용하여 길이 명시) */
void copy_string(char* dest, const char* src, size_t max_length) 
{
    if (max_length == 0) {
        return; /* 최대 길이가 0이면 아무것도 하지 않음 */
    }

    /* strncpy 사용 (안전한 복사) */
    strncpy(dest, src, max_length - 1);
    
    /* 마지막에 널 문자 추가 */
    dest[max_length - 1] = '\0';
}

int main(void) 
{
    char src[] = "Hello, World!";
    char dest[6]; /* 크기 제한 */

    /* 문자열 복사 시 size_t를 활용하여 최대 길이 전달 */
    copy_string(dest, src, sizeof(dest));

    /* 출력 */
    printf("원본 문자열: %s\n", src);
    printf("복사된 문자열: %s\n", dest); /* 5글자까지만 복사되고 널 문자 추가됨 */

    return 0;
}


================ 실행 결과 ================
원본 문자열: Hello, World!
복사된 문자열: Hello

 

C 스타일 문자열

C 스타일 문자열이란 널 문자로 끝나는 char 배열을 의미한다. 문자열을 선언하면 컴파일러가 자동으로 널 문자를 추가해주는데, 자동으로 추가되지 않는 경우도 있으므로 주의해야 한다

char str1[] = "abc";              /* {'a', 'b', 'c', '\0'} '\0' 자동 추가됨  */
char str2[] = { 'a', 'b', 'c' };  /* {'a', 'b', 'c'} (널 문자 없음!)         */

 

위에서 사용된 널 문자란, 문자열이 끝나는 위치를 표시하기 위한 문자다. 아스키 코드 값에서 0이며, '\0' 이다

char null_char1 = 0;
char null_char2 = '\0';
/* 두 코드는 같음 */

 

C의 문자열 함수들

문자열 길이 구하기

size_t strlen(const char* str);

문자열의 길이를 구하는 표준 함수이며, 널 문자가 나타날 때까지 문자의 개수를 세는 함수이다

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

int main(void) 
{
    /* 문자열 선언 */
    char str[] = "Hello";

    /* 문자열 길이 출력 */
    printf("문자열 길이: %zu\n", strlen(str)); /* 널 문자는 포함되지 않음 */

    return 0;
}


================ 실행 결과 ================
문자열 길이: 5

str 배열 내부

 

기억해야 할 것은 널 문자는 문자열의 끝을 나타낼 뿐 문자열의 길이에는 포함되지 않는다

즉, str[] 배열의 크기는 6이지만 strlen으로 찍히는 길이는 5라는 의미이다

 

문자열 비교 함수

C에서는 문자열을 비교할 때 strcmp()와 strncmp()를 사용한다

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

int main(void) 
{
    /* 문자열 선언 */
    char str1[] = "apple";
    char str2[] = "banana";

    /* 문자열 비교 */
    int result = strcmp(str1, str2);

    /* 비교 결과 출력 */
    printf("strcmp 결과: %d\n", result);

    return 0;
}


================ 실행 결과 ================
strcmp 결과: -1

 

strcmp()의 경우 두 문자열을 처음부터 비교하며, 문자마다 아스키 값을 비교하여 첫 번째 차이가 나는 문자의 차이를 반환한다

 

첫 번째 문자열이 두 번째 문자열보다 작으면 음수, 크면 양수, 같으면 0을 반환하는데, 위의 경우 apple의 0번 인덱스, banana의 0번 인덱스를 비교했을 때 a가 더 작기 때문에 -1이 반환되는 것이다

 

하지만 이런 strcmp()에는 안전성 문제가 있다. 두 문자열이 언제 끝나는지 알기 위해 널 문자를 찾을 때까지 계속 탐색한다. 만약 널 문자가 없거나 유효한 메모리 범위를 벗어나면 예측 불가능한 동작이 발생할 수 있다

 

또한 사용하는 상황에 따라 부적절할 수 있다. 예를들어 파일 확장자 비교처럼, 특정 개수의 문자만 비교하고 싶다면 적절하지 않다

 

정리하자면 strcmp()는 전체 문자열이 정확히 같은지, 그리고 비교할 두 문자열의 메모리가 안전하다는 가정 하에 사용하는 것이 바람직하다.

 

만약 안전성 문제가 일어나지 않게 보장하고 싶다면 그리고 특정 개수만 비교하고 싶다면 strncmp()라는 메서드를 사용한다

 

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

int main(void)
{
    /* 문자열 선언 */
    char str1[] = "apple";
    char str2[] = "apricot";

    /* 문자열 비교 (앞 3글자만 비교) */
    int result = strncmp(str1, str2, 3);

    /* 비교 결과 출력 */
    printf("strncmp 결과: %d\n", result);

    return 0;
}


================ 실행 결과 ================
strncmp 결과: 0

 

strncmp(str1, str2, n)은 최대 n개의 문자까지만 비교하고, 문자열이 n개보다 짧으면 널 문자를 만날 때까지 비교한다. 그렇기에 불필요한 비교를 줄이고 더 안전하게 사용할 수 있다

 

위와 같은 특징 때문에 파일 확장자 비교, 접두사 비교, 비교할 문자열을 모를 경우 버퍼 오버플로우를 방지하기 위해 최대 비교 길이를 제한하여 예상치 못한 오류를 방지한다

 

하지만 단 한가지 조심할 점이, 비교 길이 안에 쓰레기 값이 남아있다면 그대로 출력될 여지가 있기 때문에 주의하여야 한다. 따라서 문자열을 안전하게 사용하기 위하여 문자열 관련 데이터를 다룰 때는, 마지막 인덱스에 널 문자를 명시적으로 삽입해주는 것이 좋다

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

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

    strncpy(str2, str1, 5); 
    str2[5] = '\0'; /* 강제로 널 문자 삽입 */

    printf("str2: %s\n", str2); /* 정상 출력 */

    return 0;
}


================ 실행 결과 ================
str2: apple

 

문자열 복사 함수

C에서는 문자열을 복사할 때 strcpy()와 strncpy()를 사용한다

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

int main(void) 
{
    /* 문자열 선언 */
    char source[] = "Hello, World!";
    char destination[20];

    /* 문자열 복사 */
    strcpy(destination, source);

    /* 복사된 문자열 출력 */
    printf("복사된 문자열: %s\n", destination);

    return 0;
}


================ 실행 결과 ================
복사된 문자열: Hello, World!

 

char* strcpy(char* destination, const char* source) 는 문자열을 복사하지만, destination 크기가 source보다 작을 경우 버퍼 크기를 초과하는 문자열이 복사되면서 버퍼 오버플로우가 발생한다는 문제가 있다

 

따라서 보다 안전한 문자열 복사 방법이 필요한데, 그래서 strncpy()를 사용한다

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

int main(void)
{
    /* 문자열 선언 */
    char source[] = "Hello, World!";
    char destination[6];

    /* 문자열 복사 (최대 5개 문자) */
    strncpy(destination, source, 5);
    destination[5] = '\0'; /* 널 문자 추가 */

    /* 복사된 문자열 출력 */
    printf("복사된 문자열: %s\n", destination);

    return 0;
}


================ 실행 결과 ================
복사된 문자열: Hello

 

char* strncpy(char* destination, const char* source, size_t num) 는 num 만큼의 문자만 복사한다

 

하지만 널 문자를 자동으로 추가하지 않기 때문에, 명시적으로 널 문자를 마지막 인덱스에 삽입해야 한다

즉, destination이 source보다 작을 경우 널 문자를 추가하지 않으면, destination을 문자열로 안전하게 사용할 수 없게 된다

 

만약 제대로 널 문자를 추가하지 않을 경우 쓰레기 값이 찍힐 수 있다

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

int main(void)
{
    /* 문자열 선언 */
    char source[] = "Hello, World!";
    char destination[6]; /* destination 크기가 작음 */

    /* 문자열 복사 (최대 5개 문자) */
    strncpy(destination, source, 5);
    
    /* 널 문자 추가하지 않음 */

    /* 복사된 문자열 출력 */
    printf("복사된 문자열: %s\n", destination); /* 예상치 못한 동작 발생 가능 */

    return 0;
}

Hello 뒤에 k가 찍힌 모습

 

그리고, strncpy()는 복사할 문자열이 num보다 짧으면 남은 공간을 널 문자로 채운다

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

int main(void) 
{
    /* 충분한 크기의 destination */
    char source[] = "Hello";
    char destination[10];

    /* destination을 0xAA로 초기화 (눈에 보이도록) */
    memset(destination, 0xAA, sizeof(destination));

    /* 문자열 복사 (num이 source보다 큼) */
    strncpy(destination, source, 10);

    /* 복사된 문자열 출력 */
    printf("복사된 문자열: %s\n", destination);

    /* 메모리 상태 출력 */
    printf("메모리 상태: ");
    for (size_t i = 0; i < sizeof(destination); i++) {
        printf("%02X ", (unsigned char)destination[i]);
    }
    printf("\n");

    return 0;
}


================ 실행 결과 ================
복사된 문자열: Hello
메모리 상태: 48 65 6C 6C 6F 00 00 00 00 00

 

 

문자열 포매팅

C에서는 문자열을 연결할 때 strcat()와 strncat()을 사용한다

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

int main(void) 
{
    /* 문자열 선언 */
    char str1[20] = "Hello, ";
    char str2[] = "World!";

    /* 문자열 연결 */
    strcat(str1, str2);

    /* 연결된 문자열 출력 */
    printf("연결된 문자열: %s\n", str1);

    return 0;
}


================ 실행 결과 ================
연결된 문자열: Hello, World!

 

strcat()은 destination의 기존 문자열 끝에 source를 이어붙이지만, 버퍼 크기를 확인하지 않으면 버퍼 오버플로우 발생 위험이 있다

따라서 위 함수도 마찬가지로 안전성을 보장하기 위해 strncat()을 사용할 수 있다

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

int main(void)
{
    /* 문자열 선언 */
    char str1[20] = "Hello, ";
    char str2[] = "World!";

    /* 문자열 연결 (최대 3글자만) */
    strncat(str1, str2, 3);

    /* 연결된 문자열 출력 */
    printf("연결된 문자열: %s\n", str1);

    return 0;
}


================ 실행 결과 ================
연결된 문자열: Hello, Wor

 

strncat()은 num 개수만큼 destination에 추가하므로, destination의 크기를 초과하는 문제를 방지할 수 있다. 하지만 여전히 주의해야 할 점이 있다

 

strncat()은 destination의 남은 공간을 확인하지 않고 문자열을 추가하기 때문에, 안전하게 사용하려면 남은 공간을 확인하는 것이 필수적이다

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

int main(void)
{
    /* 문자열 선언 */
    char str1[10] = "Hi";
    char str2[] = " there!";

    /* strncat을 사용할 때 남은 공간 확인 */
    size_t remaining_space = sizeof(str1) - strlen(str1) - 1;
    
    strncat(str1, str2, remaining_space); /* 남은 공간만큼만 추가 */

    /* 연결된 문자열 출력 */
    printf("연결된 문자열: %s\n", str1);

    return 0;
}

================ 실행 결과 ================
연결된 문자열: Hi there

 

위와 같이 남은 공간을 sizeof(destination) - strlen(destination) - 1로 계산하여 안전하게 문자열을 추가하는 것이 중요하다

 

 

문자열 검색

문자열에서 특정 문자열을 찾으려면 strstr()을 사용한다

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

int main(void) 
{
    /* 문자열 선언 */
    char str[] = "Hello, World!";
    char *substr = strstr(str, "World");

    /* 검색 결과 출력 */
    if (substr) {
        printf("찾은 문자열: %s\n", substr);
    } else {
        printf("문자열을 찾을 수 없음\n");
    }

    return 0;
}


================ 실행 결과 ================
찾은 문자열: World!

 

찾은 문자열의 시작 주소를 반환하고, 찾을 수 없다면 널 포인터를 반환한다

널 포인터를 반환하기 때문에, 널이 반드시 발생 안하는지 확인하고 발생한다면 어떻게 처리해야할 지 생각해두어야 한다. Segmentation Fault가 발생할 수 있기 때문이다

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

int main(void) 
{
    /* 문자열 선언 */
    char str[] = "Hello, World!";
    char *substr = strstr(str, "Moon"); /* 존재하지 않는 문자열 */

    /* 검색 결과 출력 */
    printf("찾은 문자열: %s\n", substr); /* substr이 NULL이면 오류 발생 가능 */

    return 0;
}

================ 실행 결과 ================
찾은 문자열: (null)

 

 

문자열 토큰화

strtok()는 문자열의 특정 구분자를 나누는 함수다

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

int main(void) 
{
    /* 문자열 선언 */
    char str[] = "apple,banana,grape";
    char *token = strtok(str, ",");

    /* 문자열 토큰화 */
    while (token != NULL) {
        printf("토큰: %s\n", token);
        token = strtok(NULL, ",");
    }

    return 0;
}


================ 실행 결과 ================
토큰: apple
토큰: banana
토큰: grape

 

첫 번째 호출시 문자열을 전달하고, 이후 호출부터는 NULL을 전달해야 한다. 또, 원본 문자열이 변경되므로 주의해야 한다

또한 내부적으로 정적 변수를 사용하여 문자열 상태를 유지한다. 그렇기 때문에 멀티스레드 환경에서는 데이터 충돌이 발생할 수 있다

 

 

서식 지정 출력 (Formatted Output)

C에서는 다양한 방식으로 데이터를 출력할 수 있다. 대표적으로 printf(), fprintf(), sprintf() 등이 있으며, 각각의 차이를 이해하고 적절하게 활용하는 것이 중요하다

 

printf()

printf()는 서식 지정자를 사용하여 다양한 형식으로 출력할 수 있다

#include <stdio.h>

int main(void) 
{
    /* 다양한 자료형 출력 */
    int num = 42;
    float pi = 3.14159;
    char ch = 'A';
    char str[] = "Hello, C!";

    printf("정수: %d\n", num);
    printf("실수: %.2f\n", pi);
    printf("문자: %c\n", ch);
    printf("문자열: %s\n", str);

    return 0;
}


================ 실행 결과 ================
정수: 42
실수: 3.14
문자: A
문자열: Hello, C!

 

fprintf()

printf()와 유사하지만, 출력 대상을 파일로 지정할 수 있다

#include <stdio.h>

int main(void) 
{
    /* 파일 포인터 선언 */
    FILE *file = fopen("output.txt", "w");

    if (file == NULL) {
        printf("파일 열기 실패\n");
        return 1;
    }

    /* 파일에 출력 */
    fprintf(file, "이것은 파일 출력입니다.\n");

    /* 파일 닫기 */
    fclose(file);

    return 0;
}


================ 실행 결과 ================
output.txt 파일에 "이것은 파일 출력입니다." 내용이 저장됨

 

fprintf()는 파일에 데이터를 출력한다

 

sprintf()

sprintf()는 출력 결과를 문자열에 저장하는 함수다

#include <stdio.h>

int main(void) 
{
    /* 문자열 버퍼 선언 */
    char buffer[50];

    /* 문자열에 저장 */
    sprintf(buffer, "정수: %d, 실수: %.2f", 42, 3.14);

    /* 저장된 문자열 출력 */
    printf("저장된 문자열: %s\n", buffer);

    return 0;
}


================ 실행 결과 ================
저장된 문자열: 정수: 42, 실수: 3.14

 

sprintf()는 printf()와 동일하지만, 출력 결과를 buffer에 저장한다. 즉, 문자열 변수에 결과를 저장하는 용도로 사용한다

그리고 위의 문자열 메서드들과 마찬가지로, 버퍼 크기 초과를 방지하기 위하여 snprintf() 사용을 권장한다

 

snprintf()와 sprintf() 차이

#include <stdio.h>

int main(void) 
{
    char small_buffer[5];

    snprintf(small_buffer, sizeof(small_buffer), "Hello, C!");
    printf("버퍼 내용: %s\n", small_buffer);

    return 0;
}


================ 실행 결과 ================
버퍼 내용: Hell

 

버퍼 크기만큼만 저장되고, 버퍼 오버플로우를 방지할 수 있음이 보인다

 

서식 문자열

서식 문자열은 추가적인 메모리 할당 없이 데이터를 사람이 읽기 좋은 형태로 변환하여 출력 스트림(콘솔, 파일, 문자열 등)에 출력하는 역할을 한다

 

이를 통해 출력 형식을 세밀하게 제어할 수 있으며, 특히 수치 데이터나 문자열을 원하는 형태로 정렬하고 가공할 수 있다

 

서식 지정자 (Format Specifiers)

C에서 printf() 및 관련 함수들은 다양한 서식 지정자를 제공한다

서식 지정자 설명
%d, %i 정수 (int)
%u 부호 없는 정수 (unsigned int)
%f 실수 (float, double)
%c 문자 (char)
%s 문자열 (char 배열)
%p 포인터 (메모리 주소)
%e, %E 지수 표기법 (Exponential)
%g, %G 자동 형식 (실수 or 지수 표기법 중 적절한 것 선택)
#include <stdio.h>

int main(void) 
{
    /* 다양한 자료형 선언 */
    int num = -42;
    unsigned int unum = 42;
    float fnum = 3.14159;
    double dnum = 2.71828;
    char ch = 'A';
    char str[] = "Hello, C!";
    void *ptr = &num; /* 포인터 */

    printf("정수 (%%d, %%i): %d, %i\n", num, num);
    printf("부호 없는 정수 (%%u): %u\n", unum);
    printf("실수 (%%f): %f\n", fnum);
    printf("실수 (%%lf, %%f 사용 가능): %lf\n", dnum);
    printf("문자 (%%c): %c\n", ch);
    printf("문자열 (%%s): %s\n", str);
    printf("포인터 (%%p): %p\n", ptr);
    printf("지수 표기법 (%%e, %%E): %e, %E\n", dnum, dnum);
    printf("자동 형식 (%%g, %%G): %g, %G\n", dnum, dnum);

    return 0;
}


================ 실행 결과 ================
정수 (%d, %i): -42, -42
부호 없는 정수 (%u): 42
실수 (%f): 3.141590
실수 (%lf, %f 사용 가능): 2.718280
문자 (%c): A
문자열 (%s): Hello, C!
포인터 (%p): 0x16f6a7220
지수 표기법 (%e, %E): 2.718280e+00, 2.718280E+00
자동 형식 (%g, %G): 2.71828, 2.71828

 

8진수, 16진수 출력

서식 지정사 설명
%x, %X 16진수 (Hexadecimal)
%o 8진수 (Octal)

 

여기에 #을 사용하면 접두사를 붙일 수 있다

#include <stdio.h>

int main(void)
{
    printf("%o\n", 10);
    printf("%x\n", 10);
    printf("%X\n", 10);
    printf("%#o\n", 10);
    printf("%#x\n", 10);
    printf("%#X\n", 10);

    return 0;
}


================ 실행 결과 ================
12
a
A
012
0xa
0XA

 

출력 너비 지정

서식 지정자 설명
정수 출력 너비 (너비보다 작으면 공백)
- 왼쪽 정렬
0 빈 공간을 0으로 채움
+ 항상 부호를 표시
공백 양수인 경우에도 부호란을 비워둠
#include <stdio.h>

int main(void)
{
    int num = 10;

    /* 기본 출력 */
    printf("기본 출력: %d\n", num);

    /* 출력 너비 지정 (5자리) */
    printf("출력 너비 5: |%5d|\n", num);

    /* 왼쪽 정렬 */
    printf("왼쪽 정렬: |%-5d|\n", num);

    /* 빈 공간을 0으로 채움 */
    printf("0으로 채움: |%05d|\n", num);

    /* 항상 부호를 표시 */
    printf("항상 부호 표시: |%+5d|\n", num);

    /* 공백을 이용하여 양수도 부호란 확보 */
    printf("공백 부호란 확보: |% 5d|\n", num);

    return 0;
}


================ 실행 결과 ================
기본 출력: 10
출력 너비 5: |   10|
왼쪽 정렬: |10   |
0으로 채움: |00010|
항상 부호 표시: |  +10|
공백 부호란 확보: |   10|

 

 

소수점 자리수 설정

서식 지정자 설명
%f 기본 실수 출력 (소수점 아래 6자리 까지 출력)
%.nf 소수점 이하 n자리까지 출력
%m.nf 최소 너비 m, 소수점 이하 n자리 출력
#include <stdio.h>

int main(void) 
{
    float num = 3.14159;

    printf("기본 출력: %f\n", num);
    printf("소수점 3자리: %.3f\n", num);
    printf("최소 너비 6, 소수점 2자리: %6.2f\n", num);
    printf("자동 형식 선택: %g\n", num);

    return 0;
}


================ 실행 결과 ================
기본 출력: 3.141590
소수점 3자리: 3.142
최소 너비 6, 소수점 2자리:   3.14
자동 형식 선택: 3.14159

 

출력 스트림과 버퍼링

출력 스트림(stdout, stderr, 파일 등)은 버퍼링(Buffering)을 사용하여 성능을 최적화한다. 버퍼링은 데이터를 일정 크기로 모아서 한 번에 출력하는 방식으로, 크게 3가지 종류가 있다

 

Full Buffering

버퍼가 가득 차면 데이터를 한 번에 출력한다. 파일과 같은 출력 스트림에서 일반적으로 사용된다

강제로 버퍼를 비우려면 fflush(stdout)을 사용한다

 

Line Buffering

개행 문자가 입력되거나 버퍼가 가득 차면 출력된다

 

No Buffering

stderr은 기본적으로 버퍼링이 적용되지 않는다. 즉, 즉시 에러 메시지가 출력 된다는 것

setbuf(stdout, NULL);을 사용하면 stdout도 버퍼링 없이 즉시 출력되도록 설정 가능하다

 

예제 코드

#include <stdio.h>
#include <unistd.h>  /* sleep() 함수 사용 */

int main(void) 
{
    /* 버퍼링 테스트 시작 */
    printf("=== 출력 버퍼링 테스트 ===\n\n");

    /* 1. 풀 버퍼링 (Full Buffering) */
    FILE *file = fopen("buffer_test.txt", "w");
    if (file == NULL) {
        perror("파일 열기 실패");
        return 1;
    }
    setvbuf(file, NULL, _IOFBF, 1024); /* 풀 버퍼링 설정 */
    fprintf(file, "파일에 데이터 기록 중...\n");
    sleep(3);  // 파일을 확인하면 아직 데이터가 기록되지 않음 (버퍼링 때문)
    fflush(file); // 강제 플러시
    fclose(file);
    printf("[풀 버퍼링] 파일 'buffer_test.txt'을 확인하세요.\n\n");

    /* 2. 라인 버퍼링 (Line Buffering) */
    setvbuf(stdout, NULL, _IOLBF, 1024); /* 라인 버퍼링 설정 */
    printf("[라인 버퍼링] 이 메시지는 즉시 출력되지 않을 수도 있습니다");
    sleep(3);  // 개행 문자가 없으므로 출력되지 않을 수 있음
    printf("\n");  // 개행 문자 입력 -> 즉시 출력됨

    /* 3. 버퍼링 없음 (No Buffering) */
    setvbuf(stdout, NULL, _IONBF, 0); /* 버퍼링 없음 설정 */
    printf("[버퍼링 없음] 즉시 출력됩니다.\n");
    sleep(2);

    /* 4. 표준 오류(stderr)는 기본적으로 버퍼링 없음 */
    fprintf(stderr, "[stderr] 표준 오류는 즉시 출력됩니다.\n");
    sleep(2);

    return 0;
}


================ 실행 결과 ================
=== 출력 버퍼링 테스트 ===

[풀 버퍼링] 파일 'buffer_test.txt'을 확인하세요.

[라인 버퍼링] 이 메시지는 즉시 출력되지 않을 수도 있습니다
[버퍼링 없음] 즉시 출력됩니다.
[stderr] 표준 오류는 즉시 출력됩니다.

 

실제 테스트도 코드대로 지연되면서 출력됨을 확인할 수 있었다

 

인상 깊었던 내용

C스타일 문자열의 장단점

장점은 최소한의 메모리를 사용한다는 점이다. 그리고 문자열과 길이를 한 가지 데이터 구조로 표현할 수 있다

 

단점은 문자열의 길이를 구할 때 O(n) 연산이 필요하다

그리고 안전하지 않다. 널 문자를 찾을 때까지 무조건 순회하기 때문에, 소유하지 않은 메모리에 접근할 위험이 있다

 

 

strtok() 실험

strtok() 함수는 내부적으로 정적 변수를 사용하여 문자열 상태를 유지하기 때문에, 호출 중간에 다른 문자열을 넣으면 예상치 못한 동작이 발생할 수 있다. 이를 확인하기 위해 다양한 케이스를 실험해보았다

 

1. NULL이 아닌 새로운 문자열을 전달하는 경우

strtok()가 호출 중간에 다른 문자열을 전달하면 어떻게 동작할까?

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

int main(void) 
{
    /* 문자열 선언 */
    char str1[] = "apple,banana,grape";
    char str2[] = "cat,dog,elephant";

    /* 첫 번째 문자열 토큰화 (apple까지만) */
    printf("첫 번째 문자열 토큰화 결과:\n");

    char *token = strtok(str1, ",");
    if (token != NULL) {
        printf("토큰: %s\n", token);
    }

    /* 즉시 두 번째 문자열 토큰화 시작 */
    printf("\n두 번째 문자열 토큰화 결과:\n");
    token = strtok(str2, ",");
    while (token != NULL) {
        printf("토큰: %s\n", token);
        token = strtok(NULL, ",");
    }

    /* 첫 번째 문자열의 상태 출력 */
    printf("\n첫 번째 문자열(str1)의 상태 출력:\n");
    printf("str1: %s\n", str1);

    /* str1의 원래 길이만큼 문자 상태 출력 */
    printf("\n첫 번째 문자열(str1)의 메모리 상태 (문자 출력):\n");
    for (size_t i = 0; i < sizeof(str1); i++) {
        if (str1[i] == '\0') {
            printf("\\0");  /* 널 문자는 가시적으로 표시 */
        } else {
            printf("%c", str1[i]);
        }
    }
    printf("\n");

    return 0;
}


================ 실행 결과 ================
첫 번째 문자열 토큰화 결과:
토큰: apple

두 번째 문자열 토큰화 결과:
토큰: cat
토큰: dog
토큰: elephant

첫 번째 문자열(str1)의 상태 출력:
str1: apple

첫 번째 문자열(str1)의 메모리 상태 (문자 출력):
apple\0banana,grape\0

 

strtok()는 내부적으로 정적 변수를 사용하므로, 다른 문자열을 전달하면 기존 진행 상태가 사라진다! 즉, 기존에 진행 중이던 str1에 대한 토큰화는 중단되고, str2에 대한 토큰화만 진행되는 것이다

 

그리고 원본 문자열이 변경되었기 때문에 apple\0banana,grape 상태가 되었다. 혹시라도, str1을 재사용 할 경우 조심해야겠다

 

2. 중간에 NULL 대신 다른 구분자를 전달하는 경우

strtok()는 기존 상태를 유지하면서 새로운 구분자를 적용할 수 있을까?

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

int main(void) 
{
    /* 문자열 선언 */
    char str[] = "apple,banana-grape;orange-kiwi,melon";
    
    /* 첫 번째 호출: ,(콤마)로 구분 */
    char *token = strtok(str, ",");
    printf("첫 번째 토큰: %s\n", token);

    /* 두 번째 호출 - 다른 구분자 사용 */
    token = strtok(NULL, "-"); 
    printf("두 번째 토큰: %s\n", token);

    /* 계속 진행 */
    while (token != NULL) {
        token = strtok(NULL, ",");
        if (token != NULL) {
            printf("다음 토큰: %s\n", token);
        }
    }

    return 0;
}


================ 실행 결과 ================
첫 번째 토큰: apple
두 번째 토큰: banana
다음 토큰: grape;orange-kiwi
다음 토큰: melon

 

 

strtok(NULL, 새로운_구분자)를 전달해도, 기존 상태를 초기화하는 것이 아니라 남은 문자열에서 새로운 구분자를 기준으로 탐색하는 것을 확인할 수 있었다

 

따라서 이미 기존 구분자로 잘려 있던 문자열이 있다면, 이후 새 구분자로 자르더라도 영향을 받지 않고 남은 부분에서만 작동한다. 

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

int main(void) 
{
    /* 문자열 선언 */
    char str[] = "apple,banana-grape;orange-kiwi,melon";

    /* 여러 개의 구분자를 한 번에 적용 */
    char *token = strtok(str, ",-;");
    while (token != NULL) {
        printf("토큰: %s\n", token);
        token = strtok(NULL, ",-;");
    }

    return 0;
}


================ 실행 결과 ================
토큰: apple
토큰: banana
토큰: grape
토큰: orange
토큰: kiwi
토큰: melon

 

혹시라도, 여러 개의 구분자로 문자열을 나누려면 처음부터 strtok(str, "구분자들") 형식으로 사용해야 한다

 

 

 

마무리

4주차 학습을 통해 문자열 처리의 핵심 개념들과 관련된 표준 함수들의 특징, 주의점 등을 이해할 수 있었다

특히, C에서 문자열이 메모리에 어떻게 표현되는지, 어떤 방시긍로 다루어야 안전한지 직접 실험하고 검증하면서 체감할 수 있었다

 

앞으로는 문자열을 다룰 때 버퍼 오버플로우를 방지할 수 있는 함수를 사용한다던지, 의도적으로 안전하다 가정하고 함수를 쓰고 주석을 남긴다던지 코드를 취사 선택해서 작성할 수 있을 것 같다

 

또한 앞으로 나올 표준 함수들도 단순히 암기하는 것이 아니라 실험을 통해 동작을 검증하고 활용 방안을 깊이 고민하는 학습태도를 유지해보겠다