4.2) StringBuffer 클래스 개선
모듈을 합치는 건 성공했고 모두 잘 동작한다. 하지만 이것만으로 끝이 난 건 아니다. 컴파일러라면 사칙 연산 이외에 더 많은 연산이 가능해야 하고, 변수를 읽어내고 변수로 연산할 수 있어야 한다. 자료형은 바꾸기만 하면 된다고 생각할 수도 있으니 일단 int형이라고 가정해보자. 연산은 사칙 연산에서 연산자를 추가하고 확장하면 어떻게 될 것 같다. 우선 변수를 읽어야 하므로 수식 해석 모듈을 수정하자. 문자열을 인자로 넘기던 함수에 StringBuffer클래스를 적용한다.
그런데 생각해보자. rdx와 dcl 모듈은 모두 정수를 읽을 수 있어야 한다. 사실 정수뿐만이 아니라 식별자 획득, 공백 제거 등의 기능은 모두 필요하며 아주 자주 사용된다. 우리는 이러한 기능을 함수로 묶어내야 함을 알고 있는데, 이를 수행하는 함수를 StringBuffer 클래스의 메서드로 넣으면 어떨까? 다음은 StringBuffer 클래스에 새롭게 추가할 메서드이다.
- std::string get_number(); // 수를 획득한다.
- std::string get_identifier(); // 식별자를 획득한다. 키워드 획득 시에도 사용할 수 있다.
- std::string get_operator(); // C 연산자를 획득한다.
- void trim(); // 현재 위치에서 공백이 아닌 문자가 나올 때까지 포인터를 옮긴다.
- std::string get_token(); // 현재 위치 다음에 존재하는 공백이 아닌 기호(token)를 획득한다.
이전에 설명하지 않은 중요한 단어 중에 토큰이 있는데, 토큰(token)은 의미를 지닌 기호, 수, 문자 또는 문자열을 의미한다. 위에 제시한 함수들은 모두 다음과 같이 가정하고 있으므로 사용 시에 반드시 알고 있어야 한다.
- get_XXX() 함수는 모두 알아서 공백을 무시하고 가장 근접한 토큰을 반환한다.
- get_XXX() 함수는 토큰 획득에 실패하면 Exception 형식의 예외를 던진다.
- get_XXX() 함수는 해석 가능한 문자까지만 해석한다.
> get_number() 함수는 버퍼에 123x456이 남은 경우 123을 반환하고 포인터가 x456으로 이동한다.
> get_identifier() 함수는 버퍼에 v1_2+x8이 남은 경우 v1_2를 반환하고 포인터가 +x8로 이동한다.
> get_operator() 함수는 발견된 연산자 중 가장 긴 것을 반환한다. ++a++와 같은 입력이 들어오면 ++ 연산자를 뜻하는 정수를 반환하고 포인터가 a++로 이동한다.
그럼 이제 StringBuffer 클래스에 이를 구현해보자. 여기서 헤더와 소스가 몇 가지 바뀐다.
- 위에서 제시한 메서드를 StringBuffer의 멤버로 추가
- common 헤더 파일을 StringBuffer의 헤더 파일에 추가
- Exception 형식을 나타내는 StringBufferException 정의
- 소스 파일에 정의했던 Exception 정의를 삭제하고 포함한 string 헤더 파일을 제외
먼저 가장 간단한 trim 메서드부터 구현해보자.
StringBuffer.cpp |
void StringBuffer::trim() { while (is_empty() == false) { // 버퍼에 문자가 남아있는 동안 if (is_space(str[idx]) == false) // 공백이 아닌 문자를 발견하면 break; // 반복문을 탈출한다 ++idx; // 공백이면 다음 문자로 포인터를 넘긴다 } } |
코드의 흐름을 주석을 통해 모두 적었으니, 쉽게 이해할 수 있을 것이다. 다음은 정수를 획득하는 get_number 메서드를 구현하자.
StringBuffer.cpp |
std::string StringBuffer::get_number() { trim(); // 공백 제거 if (is_empty()) // 버퍼에 남은 문자가 없다면 예외 throw StringBufferException("Buffer is empty"); else if (is_digit(str[idx]) == false) // 첫 문자가 숫자가 아니면 예외 throw StringBufferException("invalid number"); std::string value; while (is_empty() == false) { if (is_digit(str[idx]) == false) break; value += str[idx]; ++idx; } return value; } |
우리는 이미 이 부분을 연습했으므로 역시 어렵지 않다. 다음은 식별자를 획득하는 get_identifier 메서드의 구현이다.
StringBuffer.cpp |
std::string StringBuffer::get_identifier() { trim(); // 공백 제거 if (is_empty()) // 버퍼에 남은 문자가 없다면 예외 throw StringBufferException("Buffer is empty"); else if (is_fnamch(str[idx]) == false) throw StringBufferException("invalid identifier"); std::string identifier; while (is_empty() == false) { if (is_namch(str[idx]) == false) // 식별자 문자가 아니라면 탈출 break; identifier += str[idx]; ++idx; } return identifier; } |
is_digit 메서드가 is_namch 메서드로 바뀐 것을 제외하고는 크게 바뀌지 않았으므로 분석하는 것이 어렵지 않다.다음은 get_operator의 구현인데 이것도 크게 복잡하지 않다.
StringBuffer.cpp |
std::string StringBuffer::get_operator() { trim(); if (is_empty()) throw StringBufferException("Buffer is empty"); char ch = str[idx++]; // 현재 문자를 획득하고 포인터를 이동한다 std::string op; switch (ch) { case '+': op = ch; break; case '-': op = ch; break; case '*': op = ch; break; case '/': op = ch; break; default: throw StringBufferException("invalid operator"); } return op; } |
위 switch 구문이 이상하다고 생각할 수도 있는데, 이에 대해서는 후에 자세히 다룰 것이다. 마지막으로 get_token의 구현이다.
StringBuffer.cpp |
std::string StringBuffer::get_token() { trim(); if (is_empty()) throw StringBufferException("Buffer is empty"); char ch = str[idx]; std::stringstream ss; // 문자열 스트림 생성 if (is_digit(ch)) { // 정수를 발견했다면 정수 획득 ss << get_number(); // cout 출력 스트림처럼 사용하면 된다 } else if (is_fnamch(ch)) { // 식별자 문자를 발견했다면 식별자 획득 ss << get_identifier(); } else { // 이외의 경우 일단 연산자로 획득 ss << get_operator(); } return ss.str(); // 스트림에 담긴 문자열을 std::string 객체로 반환한다 } |
이제 이것이 정상적으로 동작하는지 확인하는 프로그램을 만들자. 정상 동작을 확인하려면 모든 토큰이 제대로 읽혀지는지를 봐야 한다. 테스트의 편의를 위해 무한히 입력받다가, 적법하지 않은 문장(세미콜론) 등이 들어오면 종료하도록 하자. 입력에 대해 다음 출력이 나오면 성공이라고 하겠다.
입력 |
출력 |
123 *456+var1/ var2 test test object HELLOWORLD ; |
[123][*][456][+][var1][/][var2] [test] [test][object][HELLOWORLD] Program ended |
다음은 필자의 구현이다.
StringBufferV2Main.cpp |
#include <iostream> #include "common.h" #include "StringBuffer.h" int main(void) { try { const int MAX_INPUT_SIZ = 256; char input[MAX_INPUT_SIZ]; while (true) { clear_input_buffer(); std::cin.getline(input, MAX_INPUT_SIZ); StringBuffer buffer(input); while (buffer.is_empty() == false) { std::string token = buffer.get_token(); // 토큰 획득 std::cout << '['<< token.c_str() << ']'; // 감싸서 출력 } std::cout << std::endl; } return 0; } catch (Exception &ex) { // 정수 획득 실패, 식별자 획득 실패 후 // 연산자 획득 메서드인 get_operator에서 던진 예외를 // main 함수에서 받는다. // 따라서 ex는 invalid operator가 된다. std::cerr << "Program ended" << std::endl; return 1; } } |
이제 제법 쓸 만하게 StringBuffer 클래스를 개선했으니, 본격적으로 rdx 모듈을 수정해보자.