C++로 만든 계산기
회사 스터디 커리큘럼 중 만들어 본 C++로 구현된 사칙연산 계산기이다.
사실은 TCP 서버/클라이언트를 공부하는 와중에 책에 나온 예제가 너무 쉽다하여 계산기 기능을 가진 서버와 클라이언트로 바꾸기로 해서 만들게 되었다. 책에는 클라이언트에서 입력값 3개를 받도록 한다. 첫번째로는 상수, 두번째는 연산자, 세번째로는 상수를 받아서 서버로 보내면 서버는 두 상수를 연산자에 따라 계산하고 클라이언트로 다시 전송하는 방식이다. 이거야 뭐….;;
이 방법도 너무 쉽기 때문에 아예 수식으로 입력 받도록 만들었다.
최소한 인터넷에 떠도는 대부분의 사칙연산 계산기보다는 내가 만든게 훨씬 더 정교하고 정확하다고 말할 수 있을 것 같다.
만드는 과정에서 어려웠던 점과 해결하는 알고리즘 몇가지에 대해 기억해두려고 여기에 적어둔다. 혹시 누가 이 글을 보게 된다면, 내가 생각했던 부분이나 내 코드에 문제가 있다면 지적해주면 좋겠다.
일단 수식을 입력 받은 후 제일 처음 할일은 수식의 빈칸을 모두 제거하는 일이다. 수식의 처음부터 끝까지 검색하며 빈칸을 모두 제거하는 함수를 하나 만들어 적용한다.
괄호 문제
사칙연산에서 괄호가 들어가면 계산의 우선순위가 바뀐다는 것이 문제인데 이에 대한 해결책은 후위 연산과 스택을 응용하는 방법이다. 여기에 대해서는 인터넷에 이미 많은 글들이 있어서 따로 여기에 적을 필요는 없는 것 같다. 내 경우에는 vector와 stack을 둘다 응용하며 썼다.
보통 인터넷에서 찾은 사칙연산 계산기는 딱 여기까지만 해법이 나와있는 경우가 대부분이다. 난 여기에 보완하여 아래의 문제사항들을 대응할 수 있도록 바꾼다.
음수 연산자 문제
위에 괄호 문제를 해결하고 나니 음수가 문제가 되었다. 예를 들면, -2+3 같은 식은 2 앞에 붙은 -를 연산자가 아니라 해당 상수와 같이 묶어서 봐야한다는 것. 그리고 여기에는 괄호에 마이너스가 붙는 경우까지 생겨난다. 예를 들면, -(2+3) 같은 수식이다. 위에 1번에서 괄호문제를 풀기 위해 후위 연산으로 변경하고 상수 스택과 연산자 스택으로 만들었다면 - 기호를 하나의 연산자로 취급하기 때문에 같이 계산할 대상 상수가 없어서 오류가 나게 된다. 이 문제를 해결하기 위해서는 음수를 0-x와 같은 식으로 바꿔줘야한다. 예를 들면 -2+3은 0-2+3으로, -(2+3)과 같은 경우는 0-(2+3)과 같이 바꿔줘야한다. 이렇게 하면 후위연산으로 전환 후에 스택으로 계산할 때 같이 비교할 대상이 있기 때문에 정상적으로 계산이 된다.
하나 주의할 것은, 만약 음수 기호 앞에 0이 아닌 다른 상수가 왔다면 이 경우에는 정상적으로 계산이 되므로 더이상 어떤 처리를 하면 안된다. 아래에 나오는 이중연산자 문제를 미리 처리하고 이 음수 연산자 문제를 처리하도록 만들었기 때문에 아래와 같은 정리할 수 있다.
- 괄호 앞에 -가 있는 경우 [ex : -(3+4) 라면]
- 0 - x로 변경한다. [ex : 0-(3+4)]
- 하지만 해당 부분 앞에 0이 아닌 상수가 있는 경우에는 [ex : …3-(3+4)] 변경하지 않는다.
- 그러므로 n번째에 ‘(‘가 오고 n-1번째에 -가 왔다면 n-2번째에는 반드시 0이 아닌 상수이어야하며 그렇지 않은 경우에는 0-x로 변경한다.
- 그런데 이미 이중연산자는 이전에 걸러져서 올 것이므로 위와 같은 상황이 오는 경우는 n-2번째가 ‘)’이거나 ‘(‘인 경우 밖에 없다.
- 따라서 이러한 경우는 해당 부분이 ‘)-(‘이거나 혹은 ‘(-(‘인 경우이므로, 이런 경우에는 각각 ‘)+0-(‘와 ‘(0-(‘으로 변경한다.
위의 6가지 규칙을 차례대로 따라 수식을 처리하면 된다. string의 find와 replace 메서드를 가지고 위 처리를 할 수 있다.
정말 사람 애먹인 작업이었다. 인터넷으로 찾아봐도 이 문제를 해결하는 방법에 대해서는 아무 도움을 받을 수 없어서 혼자 생각했다. 한참 골치아팠던 문제.
소수점 문제
위에 문제들을 해결하고나니 이번엔 정수형 상수만 처리가 되었다. 소수점 처리도 하기 위해 후위연산 변환 후에는 double 형으로 처리할 수 있도록 변경한다. double 형으로 하면 소수점 이하 6자리까지는 나온다. 만약 이보다 더 강력한 정밀도를 원한다면 char 배열을 응용하여 계산하면 가능하다. (…지만 너무 귀찮아서 내가 해보진 않았다.) 소수점이 들어가게 되면 처리하고 가공할 때 숫자, 사칙연산자 뿐만 아니라 . 까지도 처리한다는 것에 유의해야한다.
잘못된 수식에 대한 문제 (연산자)
수식의 맨마지막에 연산자가 오는 경우
수식계산 중 처음부터 아예 잘못된 수식이 입력될 가능성이 있다. 예를 들면 수식의 맨마지막에 연산자가 붙는 경우, 예를 들면, ‘2+3-‘와 같은 경우. 이 역시도 후위연산 전환후 계산과정에서 계산할 대상이 없어지게 되므로 프로그램이 죽는 문제가 발생한다. 수식의 맨마지막에 숫자가 아닌 다른 기호가 왔는지 찾아서 처리한다.
잘못된 수식에 대한 문제 (괄호)
수식의 괄호의 갯수가 맞지 않거나 짝이 맞지 않는 경우
정상적인 수식이라면 여는 괄호와 닫는 괄호의 갯수가 맞아야하며 서로 짝이 맞아야한다. 이 상황을 만족시키기 위해서 난 다음과 같이 생각했다.
- 수식에 맨처음부터 검색했을 시 나오는 괄호가 닫는 괄호이거나, 수식의 맨뒤에서부터 검색했을 시 나오는 괄호가 여는 괄호라면 이 수식은 잘못되었다. 따라서 find나 find_first_of 등의 메서드를 이용해서 여는 괄호와 닫는 괄호가 처음 나오는 인덱스를 구한 다음 두 인덱스를 검사해보면 된다.
- 수식의 괄호의 갯수를 맞춰보기 위해서는 flag를 하나 설정하고 초기값을 0으로 둔다. 앞에서부터 검색해가며 여는 괄호 ‘(‘가 나왔을 시 flag를 1 더하고, 닫는 괄호가 ‘)’가 나왔을 시 flag를 1 뺀다. 수식의 끝까지 다 돌았을 때 여는 괄호와 닫는 괄호의 갯수가 같았다면 flag는 0이 되어야한다. 만약 0보다 크다면 여는 괄호가 더 많은 것이고 0보다 작다면 닫는 괄호가 더 많은 수식이다.
잘못된 수식에 대한 문제 (잘못된 수식)
수식에 문자가 들어온 경우
간단하게 하나씩 검색해가며 문자가 들어있는지 판단한다. 문자열 검색은 너무 간단해서 적어둘 필요가 없을 것 같다. C/C++을 해본 사람이라면 누구나 다 간단하게 할 수 있다.
잘못된 수식에 대한 문제 (이중연산자)
이중연산자가 있는 경우
한줄로 수식을 입력받는 경우 연산자가 이중으로 입력될 수 없다. 예를 들면 3+-2, 3*-2 등의 경우에는 처리할 수 없다. 한줄로 입력받는 사칙연산의 경우 연산자가 겹칠 경우에는 항상 괄호로 묶어야하며 그렇지 않고 연산자가 이중으로 온 경우에는 계산할 수 없다. 수식이 입력되었을 때 현재 글자와 이전 글자를 비교함으로써 이중연산자가 있는지 검색하고 있다면 오류로 판단한다.
위 프로그램을 클라이언트와 서버로 나누었다. TCP로 통신하게 되며 여기에 스레드를 통한 코드는 없다. 그냥 한번 1:1 대응 통신이다.
서버 프로그램 다운로드 :: CalcServer
클라이언트 프로그램 다운로드 :: CalcClient
여기까지 하면 대부분의 계산을 다 처리할 수 있다. 남은건 서버에서 계산한 결과를 클라이언트로 보내는 것. 나같은 경우는 클라이언트로 패킷을 보낼 때 처음 한바이트를 에러코드로 넣었다. 때문에 클라이언트에서 잘못된 수식을 입력하면 서버가 무슨 이유로 계산이 안됐는지 알려주고 클라이언트는 그 이유를 화면에 보여준다.
여기까지 하고 테스트 해보니 한줄로 입력가능한 사칙연산은 대부분 다 처리할 수 있었다. 거의 한 98%? 후…
간단한 사칙연산 계산기 하나 만드는게 얼마나 어려운지 몸소 체험했다. 이 문제의 핵심은 첫째로 중위연산을 후위연산으로 전환하고 스택을 이용하여 계산해가는 것 그리고 둘째로 후위연산으로 전환할 때 나오는 문제점을 해결해나가는 것 이다. 후위연산과 스택을 응용한 계산을 처리하지 못한다면 아무 진행도 할 수가 없다. 이 원리를 꼭 이해해야만 다음 과정들을 해결해나갈 수 있다.
네트워크 공부하다가 어쩌다보니 문자열과 수식연산에 대해 많이 공부하게 된것 같다.
위에 98%는 해결했다고 한 이유… 그 2%. 아직 해결하지 못한 문제가 남아있어서 이것도 적어둔다.
나누기 0
수식에 0/x가 입력되거나 x/0이 입력되는 경우 혹은 계산과정 중 이런 경우가 나오는 경우
일단 수학에서는 0 나누기 x는 0이고 x 나누기 0은 무한대다. 컴퓨터에서는 0 나누기 x를 0, x 나누기 0은 오류로 판단한다.
입력된 수식 중 0 / x가 있는지 검색하거나 x / 0이 있는지 검색하는건 그리 어렵지 않다. 문자열에서 해당하는 부분을 검색하면 되니까.
문제는 입력된 수식을 계산하는 과정중 이러한 부분이 나오면 문제가 된다. 예를 들면,
100+3/(3-3)*(3-3)
이런 수식의 경우에는 처음부터 0 / x나 x / 0이라는 부분은 없기 때문에 단순히 문자열검색으로는 문제가 없다. 하지만 계산하는 과정 중에 이런 부분이 나오기 때문에 계산 과정 중 오류가 나고 프로그램이 죽게 된다. 이 문제를 해결하려면 후위연산으로 전환한 후에 스택을 처리하며 연산자 스택에 나누기가 있을 때 계산 스택에서 0이 있는지 확인해보는 수 밖에 없다.
이 문제는 스택 구조만 좀더 생각해보면 충분히 해결할 수 있을 것으로 보이는데 현재는 지쳐서 중지.
괄호 앞 상수 입력
이건 만들다가 깜빡 잊고 안 넣은건데 나중에서야 생각났다. 수학의 규칙에서는 괄호 앞에 상수와 상수끼리는 연산자를 생략할 수 없다. 예를 들면, 2*3을 23이라고 쓸 수 없는 것과 마찬가지다. 연산자를 생략가능한 경우는 문자 즉, 변수와 같이 쓸 경우네는 2*x를 2x라고 쓸 수 있다. 지금 내가 만든 것은 상수항만 입력 받은 수식이므로 당연히 연산자는 생략될 수 없다.
중요한건 괄호가 있는 경우에도 연산자는 생략할 수 없다. 따라서 2(3+4)라는 수식은 실제로는 이렇게 써서는 안되는 수식이다.
이 문제에 대해서는 예전에 이게 되는거다 안되는거다 한번 인터넷에서 논란이 되었었는데, 난 이 경우에 대해서는 상수끼리는 연산자를 생략할 수 없다.라고 생각하고 그렇게 규정하기 때문에 잘못된 수식으로 간주한다. 그리고 C++ 컴파일러도 이런식의 수식은 잘못되었다고 판단하기 때문이다. 2(3+4)가 2*(3+4)라는 것은 인간이 쓰기 편하게 하려고 그렇게 약속한 것일뿐 엄밀히 기계적으로 따졌을 때는 틀렸다고 생각한다.
어찌되었던 괄호앞에 상수가 나오는 경우에는 잘못된 수식으로 봐야하는데 이 경우에 대한 처리를 넣지 못했다. 이것도 스택 구조만 조금 생각하면 충분히 해결할 수 있을 것 같다.
불완전한 소수 입력
소수를 입력할 때 0.1, 3.0처럼 입력하지 않고 .1, 3. 처럼 불완전하게 입력하는 경우에 대한 문제다. 테스트 해본 결과 double 형으로 변경하는 과정에서 이러한 숫자들은 전부 완전한 소수형태로 자동전환이 되었다. 하지만 수학에서 이렇게 쓰는 경우는 없으므로 체크하는게 좋을 것 같다. 이건 구현하기 쉬운 부분이라 일단 패쓰.