본문 바로가기
Language & FrameWork/C++

[C++] strtok() 함수를 활용하여 문자열 토크나이징하기

by Anthropologist 2020. 10. 2.

문자열 토크나이징이란 문자열을 원하는 문장 혹을 단어를 기준으로 나누는 작업을 뜻합니다.

그 예로 "I am a boy"를 " "(빈칸)을 기준으로 나누어보면, "I", "am", "a", "boy"로 나눌 수 있습니다.

 

문자열 토크나이징은 그 활용도가 높아 여러 언어에서 해당 기능을 기본 유틸 함수로 제공해주고 있습니다.

C++ 또한 strtok() 메서드를 통해 해당 기능을 제공하지만 다른 언어에 비해 사용하기 조금 복잡한 면이 있습니다.

사용이 어려운 가장 큰 이유는 string 타입이 stl이 아닌 단순한 container이기에 C에서 제공하는 방식을 사용하기 때문입니다.

 

사용 과정

 

사용 과정은 다음과 같다

  1. string을 char [] 자료형으로 변형시켜 준다. (c_str() 사용)
  2. char []을 strtok()으로 잘라낸다.
  3. 잘라낸 char []을 string으로 변환시킨다. (2 - 3 과정을 반복)

예제 코드로 더 자세히 살펴보겠습니다.

#include <iostream>
#include <string>
#include <cstring>
#include <vector>

using namespace std;

int main()
{
  string s = "I am a boy"; //자르고자 하는 문자열
  char str_buffer[1000];
  strcpy(str_buffer, s.c_str()); //char[]로 자료형 변경 후 배열에 저장
  vector<string> result; //토크나이징 된 결과가 담길 vector
  
  char *tok = strtok(str_buffer, " "); // 첫번째 인자는 자를 문자열, 두번째 인자는 기준이 되는 문자열을 넣어준다.
  //자르는 기준으로 문자(' ')가 아닌 문자열(" ")을 넣어주어야한다.
  
  while (tok != nullptr)
  {
    result.push_back(string(tok));
    tok = strtok(nullptr, " "); //이어서 문자열을 자르도록 nullptr을 넣어준다.
  }
  
  for(int i = 0; i < result.size(); i++)
  {
  	cout << result[i] << endl;
  }
  // 출력결과:
  // I
  // am
  // a
  // boy
  return 0;
}

 

strtok에 문자열이 아닌 nullptr을 사용할 경우 문자열을 이어서 자를 수 있습니다.

이는 해당 유틸 함수 내 정적으로 할당되어 있는 lastToken 변수를 통해 내부적으로 문자열을 관리하고 있기 때문에 가능합니다.

 

만약 한 개의 문자열을 토크나이징 하던 도중에 strtok 함수에 다른 문자열을 넣을 경우 lastToken에 기존의 문자열이 대체됩니다.

따라서 문자열 토크나이징을 병렬적으로 실행한다면 예상과 다른 결과물 나올 수 있습니다.

 

strtok의 작동 원리

strtok의 작동원리를 살펴보기 위해 내부 코드를 열어보면 아래와 같습니다.

/*
이 소스는
http://research.microsoft.com/en-us/um/redmond/projects/invisible/src/crt/strtok.c.htm
에서 가져왔습니다.
*/

char *__cdecl strtok(char *s1, const char *delimit) {
  static char *lastToken = NULL; /* UNSAFE SHARED STATE! */
  char *tmp;                     /* Skip leading delimiters if new string. */
  if (s1 == NULL) {
    s1 = lastToken;
    if (s1 == NULL) /* End of story? */
      return NULL;
  } else {
    s1 += strspn(s1, delimit);
  } /* Find end of segment */
  tmp = strpbrk(s1, delimit);
  if (tmp) { /* Found another delimiter, split string and save state. */
    *tmp = '\0';
    lastToken = tmp + 1;
  } else { /* Last segment, remember that. */
    lastToken = NULL;
  }
  return s1;
}

 

strtok() 메서드의 작동원리를 이해하기 앞서 내부에서 사용되는 strspn(), strpbrk() 함수의 기능을 먼저 살펴보겠습니다.

 

strspn() 메서드는 문자열에서 다른 문자열에 포함되어 있는 문자들을 검색합니다.

이때 첫 번째로 일치하는 문자에 도달하기까지 읽어 들인 문자의 개수를 리턴합니다.


strpbrk() 메서드는 기준 문자(delimit)들 중 문자열(s1)의 문자들과 첫 번째로 일치하는 문자를 가리킵니다.

만약 str1의 널 문자 이전까지 일치하는 것이 없다면 NULL을 리턴합니다.

 

strtok()에서는 strcspn()을 통해 문자열 조각의 시작 위치를 찾고 strpbrk()을 통해 이번 문자열 조각의 마지막 위치를 찾습니다.

이 과정에서 마지막 위치에 문자를 '\n'로 변경하므로 기존 문자열의 값이 변경됩니다.

 

주의사항

strtok() 메서드 내부에서 사용되는 strspn(), strpbrk() 모두 delimiter를 판단할 때 전부와 일치하는지 확인하지 않기 때문에

strtok() 함수가 의도한 것과 다른 결과물을 보여줄 가능성이 있습니다.

 

의도와 다른 결과물이 나오는 사례를 코드를 통해 살펴보겠습니다.

#include <iostream>
#include <string>
#include <cstring>
#include <vector>

using namespace std;

int main()
{
  string s = "I am a boy, I am a girl";
  char str_buffer[1000];
  strcpy(str_buffer, s.c_str());  
  vector<string> result; 
  
  char *tok = strtok(str_buffer, "am");
  while (tok != nullptr)
  {
    result.push_back(string(tok));
    tok = strtok(nullptr, "am");
  }
  
  for(int i = 0; i < result.size(); i++)
  {
  	cout << result[i] << endl;
  }
  
  return 0;
}

 

위 코드는 "am"을 기준으로 문자열을 잘라내고자 합니다.

하지만 실행 결과는 다음과 같이 의도와는 다른 형태를 출력합니다.

I

boy, I

girl

 

 

아래와 같이 문자열을 자르기 위해 "am"만을 이용하지 않고 부분적으로 일치하는 "a"를 사용했지 때문입니다.

(am)
(a)
boy, I (am)
(a)
girl

strtok() 메서드는 간편하게 문자열을 원하는 단위를 기준으로 잘라낼수 있습니다.

하지만 delimiter를 판단하는 과정에서 문자열 자체를 사용하지 않기에 delimiter를 단어 단위로 사용하는 경우 주의가 필요합니다.

'Language & FrameWork > C++' 카테고리의 다른 글

[C++] std::advance(),std::distance() 임의 접근 생성자  (0) 2020.05.04
& 참조형 변수  (0) 2020.03.22
[C++11]emplace 함수  (0) 2020.03.17
[C++] 메모리 관리 방법 정리  (0) 2020.03.17