반응형


strtok

문자열을 토큰으로 분리합니다.

Declaration

char *strtok( char *token, const char *delim )

Return value

성공 - 다음 토큰의 시작 위치를 가리키는 포인터 반환
실패 - 토큰이 없으면 NULL 포인터 반환


Parameters

token - 토큰을 포함하고 있는 원본 문자열
delim - 구분 문자로 구성된 문자열


Detail descriptions

strtok()는 원본 문자열을 토큰(token)으로 분리합니다. 이때 토큰으로 해석하기 위해 원본 문자열의 구분 문자 중 첫 번째 구분 문자들을 null 문자로 덮어씁니다. 따라서 strtok()를 사용하면 무조건 원본 문자열에 변경이 가해집니다. 이 사실은 굉장히 중요하니 반드시 기억하기 바랍니다.


토큰은 해석할 수 있는 단위를 말합니다. 모든 프로그램은 저마다 자신만의 해석 단위를 갖고 있기 때문에 프로그램에 따라 토큰의 크기나 내용이 달라집니다. C 컴파일러를 구현한다고 하면 변수나 함수 이름, 자료형과 같은 키워드, 중괄호({})와 같은 특수 문자들이 모두 토큰에 들어갑니다. 이들은 자신의 고유한 방식에 맞게 각기 다르게 해석됩니다. 이들을 구분하는 단위는 보통 공백이 될 것입니다.

프로그램마다 다른 토큰을 사용하려면 서로 다른 토큰을 뽑아낼 수 있는 방법이 있어야 합니다. 이때 사용하는 것이 구분 문자(delimeter, separator)입니다. 구분 문자로는 어느 하나만 사용하는 것이 아니라 여러 개의 구분 문자를 동시에 사용할 수 있어야 하므로, 여기서는 문자열을 사용합니다. 단어를 구분하기 위해 공백(white space)을 사용하는데 공백 문자에는 탭(tab), 개행(newline) 문자도 있습니다. 구분 문자를 한 문자가 아니라 문자열로 구성해야 하는 이유입니다.

이와 같이 프로그램에 따라 토큰이 달라지기 때문에 토큰을 구분하는 문자들 역시 달라집니다. 구분 문자열은 토큰을 분리하는데 사용하는 문자들을 말합니다. 문장이 공백에 의해 구분되어 있다고 하면, 공백을 사용해서 모든 단어를 분리할 수 있으므로, 공백 문자들을 구분 문자라고 할 수 있습니다. 그러나, 이 경우는 공백 문자외에도 구두점과 같은 ".,?!" 등의 문자들도 있기 때문에, 훨씬 많은 개수의 구분 문자를 사용해야 합니다. 구분 문자열은 이와 같이 여러 개의 문자들을 포함하기 때문에 문자열이라고 부르지만, 엄밀히 얘기하면 "구분 문자 집합"으로 생각하는 것이 좋습니다. 문자에 상관없이 공백이거나 구두점이기만 하면 무조건 단어로 분리해야 합니다. 여러 문자들을 중복시켜서 적용하는 것이 아니라 각각의 문자가 토큰을 분리하기 위해 똑같은 역할을 담당합니다. 그러므로 같은 역할을 하는 문자들의 집합으로 보는 것이 좋습니다.

strtok()는 NULL 포인터를 반환할 때까지 반복적인 호출을 필요로 합니다. 원본 문자열에 포함된 모든 토큰을 잘라내기 위한 방법이 NULL 포인터를 반환할 때까지 반복 호출하는 방법입니다. strtok() 호출에서는 원본 문자열의 포인터를 전달하지만, 두 번째 호출부터는 NULL 포인터를 전달하는 점이 특별합니다. 첫 번째 호출에서 첫 번째 토큰을 분리하고 나면 토큰 위치를 식별하는 내부 변수를 이용해서 두 번째 호출부터 자체적으로 토큰을 추적합니다. 유효한 포인터를 전달하면 토큰 위치를 초기화한다는 뜻이고, NULL 포인터를 전달하면 이전 위치부터 검색하겠다는 뜻입니다. 따라서 두 번째 호출부터는 반드시 NULL 포인터를 전달해야 합니다.

strtok()는 실패하지 않습니다. 단지 더 이상의 토큰이 없을 때 끝났다는 것을 알려주기 위해 NULL 포인터를 반환할 뿐입니다. NULL 포인터를 반환할 때까지 반복적으로 호출하면 모든 토큰을 꺼낼 수 있다고 말하는 것과 같습니다. 토큰이 없는 경우는 null 문자의 위치에서 검색을 시작하는 경우와 검색할 문자들이 모두 구분 문자에 포함되는 경우의 두 가지가 있습니다.

토큰을 분리하는데 사용할 구분 문자들은 strtok()를 호출할 때마다 달라질 수도 있고 같을 수도 있습니다. 일반적으로 같은 구분 문자들을 사용하지만, 필요에 따라 얼마든지 다른 구분 문자들을 사용하는 것이 가능합니다. "Example codes" 항목에서 보여주는 코드는 같은 구분 문자열을 사용합니다.

strtok()는 호출한 상태에 따라 아래와 같이 4가지로 구분할 수 있습니다. 원본 문자열의 가운데에서 strtok()를 호출하는 것은 생각해 보면 앞의 두 가지 상태이거나 뒤의 두 가지 상태 중의 하나입니다. 대부분은 앞쪽에 포함되겠지만, 마지막 호출이 될 수 있는 상황에서는 뒤쪽에 포함될 수도 있겠습니다. 참고로 아래 표를 설명하는 코드가 "Example codes" 항목에 있습니다. 참고하기 바랍니다.

상태 반환 원본
토큰 문자가 처음 첫 번째 토큰 문자 위치 첫 번째 구분 문자를 null로 수정
구분 문자가 처음 구분 문자 이후의 첫 번째 토큰 문자 위치 토큰 이후의 첫 번째 구분 문자를 null로 수정
토큰 문자가 마지막 null 문자 위치 아무 것도 안함
구분 문자가 마지막 NULL 포인터 아무 것도 안함

"토큰 문자가 마지막"인 경우는 처리하지 않아도 괜찮습니다. 이번이 마지막 호출이 아니라 null 문자 위치에서 다시 한번 호출해야 비로소 NULL 포인터를 반환합니다. 따라서 "구분 문자가 마지막"인 경우가 언제나 마지막에 오기 때문에 실제 상황에서는 앞쪽의 두 가지 중 하나에 포함된다고 할 수 있습니다. null 문자는 언제나 훌륭한 구분 문자로써의 역할을 수행합니다. 어찌 됐건 생각 여하에 따라 다르게 볼 수도 있기 때문에 넣었습니다.


Remarks

다양한 검색 함수들(memchr(), strchr(), strrchr(), strstr(), strcspn(), strspn(), strpbrk(), strtok())을 비교한 표가 strchr()에 있습니다. 참고하기 바랍니다.


Header files

<string.h>


Example codes

  1. strtok()를 호출하는 상태에 따른 4가지 코드를 보여줍니다. 여기서 보여주는 4가지 코드는 "Detail descriptions" 항목에서 자세하게 설명했기 때문에 간단하게 증명만 합니다.
  2. "토큰 문자 먼저"는 구분 문자로 "bc"를 사용했으므로 문자열 처음에 있는 "aaa"는 토큰 문자에 해당됩니다. token 변수는 첫 번째 'a'를 가리키고, 첫 번째 구분 문자인 'b'를 null 문자로 덮어씁니다.
  3. "구분 문자 먼저"는 토큰 문자가 중간에 오도록 "ac"를 구분 문자로 사용합니다. token 변수는 구분 문자를 뛰어 넘어서 "bbb"의 첫 번째 'b'를 가리키고, 두 번째 구분 문자 영역의 첫 번째 'c'를 null 문자로 덮어씁니다.
  4. "토큰 문자 마지막"은 토큰 문자만으로 구성되도록 구분 문자열로 "pk"를 주었습니다. 전체가 하나의 토큰이기 때문에 token 변수는 null 문자를 가리키고, 원본은 수정되지 않습니다.
  5. "구분 문자 마지막"은 구분 문자만으로 구성되도록 구분 문자열로 "abc"를 주었습니다. 원본 문자열에 포함된 모든 문자가 구분 문자이므로, 토큰이 전혀 없는 문자열입니다. token 변수는 NULL 포인터를 반환하고, 원본은 수정되지 않습니다.
  6. null 문자로 덮어썼는지 판단하기 위해 "원본" 항목을 출력했습니다. 원본 문자열이 바뀌었다는 것은 null 문자가 추가되었다는 뜻입니다.
  7. 출력 결과는 항목들의 줄을 맞추기 위해 자릿수를 넓게 지정한 결과입니다. 공백 부분에는 진짜 공백이 있는 것이 아니라 단지 자릿수를 맞추기 위한 패딩(padding)입니다. 마지막 출력 결과에서 "(null)"은 NULL 포인터를 출력하려고 할때 나타나는 에러 표시입니다.

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

void main()
{
    char src[64] = "aaabbbccc",
         tmp[64];
    char* token;

    printf( "원본 : %s\n\n", src );
    printf( "원본        토큰          구분\n" );
    printf( "------------------------------\n" );

    strcpy( tmp, src );
    token = strtok( tmp, "bc" );         // 토큰 문자 먼저

    printf( "%-9s - [%-9s] - [bc]\n", tmp, token );

    /////////////////////

    strcpy( tmp, src );
    token = strtok( tmp, "ac" );         // 구분 문자 먼저

    printf( "%-9s - [%-9s] - [ac]\n", tmp, token );

    /////////////////////

    strcpy( tmp, src );
    token = strtok( tmp, "pk" );         // 토큰 문자 마지막

    printf( "%-9s - [%-9s] - [pk]\n", tmp, token );

    /////////////////////

    strcpy( tmp, src );
    token = strtok( tmp, "abc" );         // 구분 문자 마지막

    printf( "%-9s - [%-9s] - [abc]\n", tmp, token );
}

[출력 결과]
원본 : aaabbbccc

원본        토큰          구분
------------------------------
aaa       - [aaa      ] - [bc]
aaabbb    - [bbb      ] - [ac]
aaabbbccc - [aaabbbccc] - [pk]
aaabbbccc - [(null)   ] - [abc]


  1. strtok()를 사용하는 가장 일반적인 코드를 보여줍니다. 숫자와, 대문자, 소문자 중에서 선택할 수 있고, 이들 외의 숫자를 입력할 때까지 반복합니다.
  2. 토큰으로 분리할 문자열은 CreateString()를 사용해서 숫자와 대문자, 소문자로 채웁니다. 구분 문자의 종류가 세 가지이기 때문에, 어떤 것을 선택하느냐에 따라 토큰이 달라집니다.
  3. 숫자를 구분 문자로 사용하면 숫자를 이용해서 토큰을 분리하고, 대문자나 소문자가 구분 문자라면 이들 문자를 이용해서 토큰을 분리합니다. FillSelect()를 사용해서 구분 문자열을 구성했고, 구분 문자의 개수는 각가 10개와 26개씩으로 조금 많은 편입니다.
  4. 먼저 원본 문자열을 매개 변수로 전달하면서 strtok()를 호출합니다. 두 번째 호출부터는 첫 번째 호출 결과를 기반으로 동작하기 때문에 첫 번째 매개 변수에 NULL 포인터를 전달합니다.
  5. strtok()는 토큰으로 분리할 문자들이 없으면 NULL 포인터를 반환합니다. 지금처럼 NULL 포인터를 반환할 때까지 호출하는 방법이 strtok()를 사용하는 올바른 방법입니다.
  6. strtok()에서 사용하는 구분 문자는 모든 토큰을 분리할 때까지 변경하지 않았습니다.
  7. 출력 결과에 토큰 문자와 구분 문자를 분명하게 구분지었습니다. 코드를 따라가기 어려우면 결과를 해석한 다음에 코드를 보는 것도 좋은 방법입니다.

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

void CreateString( char* str, int size );
void FillSelect( char* delim, int select );

void main()
{
    char str[32], delim[32], * token;
    int  i, select;

    while( 1 )
    {
        CreateString( str, sizeof(str) );

        printf( "원본 : %s\n", str );

        printf( "선택 : 0. 숫자  1. 대문자  2. 소문자 - " );
        scanf( "%d", &select );

        if( select < 0 || select > 2 )
            break;

        FillSelect( delim, select );
        printf( "구분 : %s\n", delim );

        token = strtok( str, delim );

        for( i = 0; token != NULL; i++ )
        {
            printf( "%4d : %s\n", i, token );

            token = strtok( NULL, delim );
        }
        printf( "\n" );
    }
}

void CreateString( char* str, int size )
{
    int i, value;

    for( i = 0; i < size-1; i++ )
    {
        value = rand();

        switch( value%3 )
        {
        case  0 : str[i] = '0' + value%10;   break;
        case  1 : str[i] = 'A' + value%26;   break;
        default : str[i] = 'a' + value%26;   break;
        }
    }
    str[i] = '\0';
}

void FillSelect( char* delim, int select )
{

    int  i, count;
    char ch;

    switch( select )
    {
    case  0 : ch = '0';   count = 10;   break;
    case  1 : ch = 'A';   count = 26;   break;
    default : ch = 'a';   count = 26;   break;
    }

    for( i = 0; i < count; i++ )
        delim[i] = ch+i;

    delim[i] = '\0';
}

[출력 결과]
원본 : phQGhU88AylnL7DxFi7614c3GG1wkfn
선택 : 0. 숫자  1. 대문자  2. 소문자 - 0
구분 : 0123456789
   0 : phQGhU
   1 : AylnL
   2 : DxFi
   3 : c
   4 : GG
   5 : wkfn

원본 : 6d89WfNF4z1s33k1PrEpgG77Pn1VYSt
선택 : 0. 숫자  1. 대문자  2. 소문자 - 1
구분 : ABCDEFGHIJKLMNOPQRSTUVWXYZ
   0 : 6d89
   1 : f
   2 : 4z1s33k1
   3 : r
   4 : pg
   5 : 77
   6 : n1
   7 : t

원본 : m28YSyYCq5e9Ik613m9n4M6kAsvW49e
선택 : 0. 숫자  1. 대문자  2. 소문자 - 2
구분 : abcdefghijklmnopqrstuvwxyz
   0 : 28YS
   1 : YC
   2 : 5
   3 : 9I
   4 : 613
   5 : 9
   6 : 4M6
   7 : A
   8 : W49

원본 : NzKy29f3tL8407sf6dpoO09x372O0j6
선택 : 0. 숫자 1. 대문자 2. 소문자 - 3

반응형

'프로그래밍' 카테고리의 다른 글

EOF  (0) 2008.04.25
제어문자  (0) 2008.04.25
strlen 함수에 대해  (0) 2008.04.25
ssize_t, size_t 그리고 pid_t  (0) 2008.04.25
화살표(->) 연산자  (0) 2008.04.25

+ Recent posts