Program Language/C & C++

[C++] 계산기 만들기 (2) input

야곰야곰+책벌레 2022. 1. 6. 09:57
728x90
반응형

Input

입력 읽기는 종종 프로그램에서 가장 곤란한 부분이다. 사람과 의사소통하기 위해 프로그램은 그 사람의 변덕, 관습 및 겉으로 보기에 무작위적인 오류에 대처해야 한다. 사람이 기계에 더 적합한 방식으로 강요하는 것은 공격적으로 간주되기도 한다. 저수준 입력 루틴의 작업은 문자를 읽고 문자에서 더 높은 수준의 토큰을 구성하는 것이다. 이런 토큰은 상위 수준 루틴에 대한 입력 단위다. 여기서 저수준 입력은 ts.get()에 의해 수행된다. 저수준 입력 루틴을 작성하는 것이 일상적인 작업일 필요는 없다. 많은 시스템은 이를 위한 표준 기능을 제공한다.

먼저 Token_stream의 전체 정의를 확인해 보자.

class Token_stream {
public:
	Token_stream(istream& s) : ip{&s}, owns{false} { }
	Token_stream(istream∗ p) : ip{p}, owns{true} { }
    
	˜Token_stream() { close(); }
    
	Token get(); // read and return next token
	Token& current(); // most recently read token
    
	void set_input(istream& s) { close(); ip = &s; owns=false; }
	void set_input(istream∗ p) { close(); ip = p; owns = true; }
    
private:
	void close() { if (owns) delete ip; }
    
	istream∗ ip; // pointer to an input stream
	bool owns; // does the Token_stream own the istream?
	Token ct {Kind::end} ; // current token
};

문자를 가져오는 입력 스트림으로 Token_stream을 초기화한다. Token_stream은 포인터로 전달된 istream을 소유하지만 참조로 전달된 istream이 아닌 규칙을 구현한다. 이것은 프로그램을 약간 복잡하게 할 수 있지만 destruction을 요구하는 리소스에 대한 포인터를 보유하는 클래스에서는 유용하고 일반적인 기술이다.

Token_stream은 입력 스트림(ip)에 대한 포인터, 입력 스트림의 소유권을 나타내는 bool(소유) 및 현재 토큰(ct)의 세 가지 값을 가진다.

ct에 기본값을 주었다. 사람들은 get() 전에 current()를 호출해서는 안되지만 기본값을 해두면 잘 정의된 토큰을 받을 수 있다. current()를 오용하는 프로그램이 입력 스트림에 없는 값을 얻지 못하도록 Kind::end를 ct에 초기 값으로 선택했다.

 

Token_stream::get()을 두 단계로 제시한다. 먼저 간단한 버전을 제공하며 다음으로는 훨씬 사용하기 쉬운 버전으로 수정한다. get()의 아이디어는 문자를 읽고 해당 문자를 사용하여 어떤 종류의 토큰을 구성해야 하는지 결정하고 필요할 때 더 많은 문자를 읽은 다음, 읽은 문자를 나타내는 토큰을 반환하는 것이다.

 

초기 명령문은 *ip(ip가 가리키는 스트림)에서 공백이 아닌 첫 번째 문자를 ch로 읽고 읽기 작업이 성공했는지 확인한다.

Token Token_stream::get()
{
	char ch = 0;
	∗ip>>ch;
	switch (ch) {
	case 0:
		return ct={Kind::end}; // assign and return

기본적으로 >>연산자는 공백(즉, 공백, 탭, 줄 바꿈)을 건너뛰고 입력 작업이 실패한 경우 ch값을 변경하지 않은 채로 둔다. 결과적으로 ch==0은 입력의 끝을 나타낸다.

할당은 연산자고 할당의 결과는 할당된 변수의 값이다. 이를 통해 Kind:end 값을 curr_tok에 할당하고 동일한 명령문에서 반환할 수 있다. 두 개보다 하나의 명령문이 유지 관리에 유리하다. 코드에서 할당과 반환이 분리된 경우 프로그래머는 하나를 업데이터 하고 다른 하나를 업데이트하는 것을 잊어버릴 수 있다.

또한 할당의 오른쪽에 {}-목록 표기법이 사용되는 방식에 유의하자. 즉, 표현이다. 그 return 명령어를 다음과 같이 작성할 수 있다.

ct.kind = Kind::end; // assign
return ct; // return

그러나 ct의 개별 구성원을 처리하는 것보다 완전한 개체 {Kind::end}를 할당하는 것이 더 명확하다고 생각한다. {Kind::end}는 {Kind::end,0,0}과 동일하다. Token의 마지막 두 멤버만 신경 쓰면 좋고, 성능만 신경 쓰면 별로다. 여기서도 해당되지 않지만 일반적으로 완전한 객체를 처리하는 것이 데이터 멤버를 개별적으로 조작하는 것보다 더 명확하고 오류가 발생하기는 쉽다. 아래 예는 다른 방법이다.

case ';': // end of expression; print
case '∗':
case '/':
case '+':
case '−':
case '(':
case ')':
case '=':
	return ct={static_cast<Kind>(ch)};

char에서 Kind로의 암시적 변환이 없기 때문에 static_cast가 필요하다. 일부 문자만 Kind 값에 해당하므로 이 경우 ch가 'certify'해야 한다.  숫자는 다음과 같이 처리된다.

case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9':
case '.':
	ip−>putback(ch); // put the first digit (or .) back into the input stream
	∗ip >> ct.number_value; // read the number into ct
	ct.kind=Kind::number;
	return ct;

case를 수직이 아닌 수평으로 나열하는 것은 일반적으로 읽기 어렵기 때문에 좋은 생각은 아니다. 그러나 각 숫자에 대해 수직으로 늘어놓는 것은 지루한 면이 있다. >>연산자는 부동 소수점 값을 double로 읽기 위해 이미 정의되어 있으므로 코드는 간단하다. 먼저 초기 문자(숫자 또는 점)를 cin에 다시 넣는다. 그런 다음 부동 소수점 값을 ct.number_value로 읽을 수 있다.

토큰이 입력의 끝, 연산자, 구두점 문자 또는 숫자가 아닌 경우 이름이어야 한다. 이름은 숫자와 유사하게 처리된다.

default: // name, name =, or error
	if (isalpha(ch)) {
		ip−>putback(ch); // put the first character back into the input stream
		∗ip>>ct.string_value; // read the string into ct
		ct.kind=Kind::name;
		return ct;
	}

마지막으로 단순히 오류가 있을 수 있다. 단순하지만 오류를 처리하는 합리적이고 효과적인 방법은 error() 함수를 호출한 다음 error()가 반환하는 경우 print 토큰을 반환하는 것이다.

error("bad token");
return ct={Kind::print}

표준 라이브러리 함수 isalpha()는 모든 문자를 별도의 케이스 레이블로 나열하는 것을 방지하는 데 사용된다. 문자열에 적용된 >>연산자(이 경우 string_value)는 공백에 도달할 때까지 읽는다. 따라서 사용자는 이름을 피연산자로 사용하는 연산자 앞에 공백으로 이름을 종료해야 한다. 이것은 이상적이지 않으므로 이 문제를 다뤄 본다.

마지막으로 완전한 입력 기능은 다음과 같다.

Token Token_stream::get()
{
	char ch = 0;
	∗ip>>ch;
	switch (ch) {
	case 0:
		return ct={Kind::end}; // assign and return
        
	case ';': // end of expression; print
	case '∗':
	case '/':
	case '+':
	case '−':
	case '(':
	case ')':
	case '=':
		return ct={static_cast<Kind>(ch)};
        
	case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9':
	case '.':
		ip−>putback(ch); // put the first digit (or .) back into the input stream
		∗ip >> ct.number_value; // read number into ct
		ct.kind=Kind::number;
		return ct;
        
	default: // name, name =, or error
		if (isalpha(ch)) {
			ip−>putback(ch); // put the first character back into the input stream
			∗ip>>ct.string_value; // read string into ct
			ct.kind=Kind::name;
			return ct;
		}

		error("bad token");
		return ct={Kind::print};
	}
}

연산자의 종류가 연산자의 정수 값으로 정의되었기 때문에 연산자를 토큰 값으로 변환하는 것은 간단하다.

728x90
반응형

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

[C++] 계산기 만들기 (4) error handling  (0) 2022.01.06
[C++] 계산기 만들기 (3) low-level input  (0) 2022.01.06
[C++] 계산기 만들기 (1) parser  (4) 2022.01.05
[C++] range-for 문  (0) 2022.01.05
[C++] 선언 명령문  (0) 2022.01.05