계산기 만들기

Drakk·2021년 7월 13일
0
post-thumbnail

⬛개요

🟥개발환경

계산기를 간단하게 만들어 볼겁니다.

🟣개발언어c++14, 17, 20에서 정상작동 확인되었습니다.
🟣운영체제Windows10 home입니다.
🟣통합개발환경Devcpp랑 Visual Studio 섞어가면서 만들었습니다.

그럼 군말 안하고, 바로 시작하겠습니다.

🟥참고자료

우선은 저는 이 계산기에 대한 기본적인 알고리즘공부는 아래 사이트에서 공부했습니다.

계산기의 계산과정에 대한 정확한 이해를 하고싶으시다면 아래 링크로 들어가주세요.
문자열 계산기에 대한 정확한 설명

⬛구현

🟥헤더파일 선언

헤더파일및 선언부분 종류는 밑에 사진처럼 설정했습니다.

우선, SourceMain.cpp가 메인함수가 있는부분입니다.
Tokenizer.h와 Tokenizer.cpp
만약 문자열에 "1+ (2 *1)"과 같은 문자열을 받아왔을때,
{1,(,2,*,1,)}같은 형태로 쪼개주는 역할을 할겁니다.

마지막으로 StringCalculator.h와 StringCalculator.cppTokenizer.h와 Tokenizer.cpp에서 쪼개진 토큰을 바탕으로 값을 계산하는 부분이 되겠습니다.

🟥Tokenizer

🟣Tokenizer.h의 내용입니다.

#pragma once
#include <string>
#include <vector>
#include <queue>


/// <summary>
/// https://velog.io/@dpmawile
/// </summary>
namespace mawile {
	/// <summary>
	/// char을 std::string으로 변환하는 함수
	/// </summary>
	/// <param name="">변환할 char</param>
	/// <returns>반환될 std::string</returns>
	std::string ctos(char);
	
	/// <summary>
	/// 문자열계산식에서 토큰을 쪼개는 함수
	/// </summary>
	/// <param name="">문자열 계산식</param>
	/// <returns>공백이 포함된 토큰컨테이너</returns>
	std::vector<std::string> Tokenizing(std::string);

	/// <summary>
	/// 공백이 포함된 토큰컨테이너를 최적화하는 함수
	/// </summary>
	/// <param name="">공백이 포함된 토큰컨테이너</param>
	/// <returns>최적화된 토큰컨테이너</returns>
	std::queue<std::string> Assembly(std::vector<std::string>&);
}

우선 Tokenizing함수는 첫번째 파라미터로 받을 문자열 계산식을 단일토큰으로 쪼갤겁니다.
하지만, 그냥 이 함수만 실행하면, 중간에 공백(' ')과 같은 토큰이 존재할지도 모릅니다.

따라서, 이러한 공백토큰을 제거시켜주는 역할이 필요합니다.
이 역할은 Assembly함수가 실행합니다.

다음은 🟣Tokenizer.cpp의 내용입니다.

#include "Tokenizer.h"

std::string mawile::ctos(char c) {
	std::string str = "";
	str += c;
	return str;
}

std::vector<std::string> mawile::Tokenizing(std::string str) {
	std::vector<std::string> ans;
	std::string tmp = "";
	bool number = false;

	for (std::size_t i = 0; i <= str.size(); ++i) {
		if (str[i] == '\0') { //문자열의 끝에 도달했을시
			ans.push_back(tmp);
			break;
		}
		else if (str[i] == ' ') { //공백일시
			if (!tmp.empty()) ans.push_back(tmp);
			tmp = "";
			continue;
		}
		else if (str[i] == '*' && str[i + 1] == '*') { //제곱연산자일시
			if (number) {
				number = false;
				ans.push_back(tmp);
			}
			ans.push_back("**");
			++i;
			tmp = "";
		}
		else if (str[i] == '+' || str[i] == '-' || str[i] == '*' ||
			str[i] == '/' || str[i] == '(' || str[i] == ')') { //일반 연산자일시
			if (number) {
				number = false;
				ans.push_back(tmp);
			}
			ans.push_back(ctos(str[i]));
			tmp = "";
		}
		else { //숫자일시
			tmp += str[i];
			number = true;
		}
	}

	return ans;
}

std::queue<std::string> mawile::Assembly(std::vector<std::string>& vec) {
	std::queue<std::string> ans;

	for (std::size_t i = 0; i < vec.size(); ++i) {
		if (!vec[i].empty()) ans.push(vec[i]);
	}

	return ans;
}

보시다시피, ctos함수는 매우 간단하구요.

mawile::Tokenizing함수내에 존재하는 반복문의 조건분기들을 설명드리기전에,
for문의 종료조건이 str.size()로 했습니다.
그러면 여기서 의문점이 드는분이 계실겁니다.
"어? 그러면 문자열의 범위를 벗어나지 않을까?"

흠... 일단 조건분기의 첫번째가
'\0'을 만났을시 입니다. 문자열에서 '\0'는 종료지점을 말합니다.
따라서 이렇게 문자열의 크기보다 큰 조건을 for문에 적용해도 상관없게됩니다.

우선은 저는 제곱 연산자도 넣어봤습니다.
"**"입니다. 예를들어서 "a ** b"를 하게되면, a의 b제곱인 수를 의미하게 됩니다.

🟥StringCalculator

다음은 🟣StringCalculator.h의 내용입니다.

/*
* string calculator
* blog >> https://velog.io/@dpmawile
* dev by drakk
*/

#pragma once
#include <stack>
#include <string>
#include <sstream>

#include "Tokenizer.h"


/// <summary>
/// https://velog.io/@dpmawile
/// </summary>
namespace mawile {

	/// <summary>
	/// 연산자 우선순위와 정보를 저장하는 구조체
	/// </summary>
	struct _Operator {
		/// <summary>
		/// 연산자 우선순위
		/// </summary>
		int _priority;

		/// <summary>
		/// 연산자 정보
		/// </summary>
		std::string _operator;
	};

	class Calculator {
	public:
		/// <summary>
		/// 문자열 계산식을 실행하는 함수
		/// </summary>
		/// <param name="">문자열 계산식</param>
		/// <returns>계산 성공여부</returns>
		bool Execute(std::string);

		/// <summary>
		/// 계산된 결과를 반환하는 함수
		/// </summary>
		/// <returns>계산된 결과</returns>
		int Value();

		/// <summary>
		/// 일반 생성자
		/// </summary>
		Calculator();

		/// <summary>
		/// 문자열 계산식을 계산하는 생성자
		/// </summary>
		/// <param name="">문자열 계산식</param>
		Calculator(std::string);
	private:
		void Calculer();

		/// <summary>
		/// 연산자 스택
		/// </summary>
		std::stack<_Operator> __Operator;

		/// <summary>
		/// 숫자 스택
		/// </summary>
		std::stack<int> __Number;

		/// <summary>
		/// 계산 결과
		/// </summary>
		int _Value;
	};
}

이부분은 진짜 설명할게없습니다.
주석으로 대신하겠습니다.

다음은 🟣StringCalculator.cpp의 내용입니다.

#include "StringCalculator.h"

void mawile::Calculator::Calculer() {
	int b = __Number.top();
	__Number.pop();
	int a = __Number.top();
	__Number.pop();
	std::string _Oper = __Operator.top()._operator;
	__Operator.pop();

	if (_Oper == "+") __Number.push(a + b);
	else if (_Oper == "-") __Number.push(a - b);
	else if (_Oper == "*") __Number.push(a * b);
	else if (_Oper == "/") __Number.push(a / b);
	else if (_Oper == "**") __Number.push((int)std::pow(a, b));
}

mawile::Calculator::Calculator() {

}

mawile::Calculator::Calculator(std::string _Input) {
	Execute(_Input);
}


bool mawile::Calculator::Execute(std::string _Input) {
	if (_Input == "") return false;

	try {
		std::string _Token;

		std::vector<std::string> Token1 = Tokenizing(_Input);
		std::queue<std::string> Token2 = Assembly(Token1);

		while (!Token2.empty()) {
			_Token = Token2.front();
			Token2.pop();

			if (_Token == "(") {
				__Operator.push({ 0 , _Token });
			}
			else if (_Token == ")") {
				while (__Operator.top()._operator != "(") Calculer();
				__Operator.pop();
			}
			else if (_Token == "+" || _Token == "-" || _Token == "*" || _Token == "/" || _Token == "**") {
				int Priority = 0;
				if (_Token == "+" || _Token == "-") Priority = 1;
				else if (_Token == "*" || _Token == "/") Priority = 2;
				else if (_Token == "**") Priority = 3;

				while (!__Operator.empty() && Priority <= __Operator.top()._priority) Calculer();
				__Operator.push({ Priority , _Token });
			}
			else __Number.push(std::stoi(_Token));
		}

		while (!__Operator.empty()) Calculer();
		_Value = __Number.top();
		while (!__Number.empty()) __Number.pop();
	}
	catch (std::exception&) {
		return false;
	}

	return true;
}

int mawile::Calculator::Value() {
	return _Value;
}


이 부분은 사실상 mawile::Calculator::Execute함수부분 빼고도 설명할게없습니다.

이에 대한 자세한 설명은 위에 🟥참고자료부분에서 확인해주세요!
그리고, 위 링크에서 달라진 점이 있다면 제곱연산자를 추가해봤습니다.
제곱연산자는 우선순위를 제일 높게 주었습니다.

🟥Main

마지막으로 🟣메인함수부분입니다.

#include "StringCalculator.h"
#include <conio.h>
#include <iostream>

int main() {
	mawile::Calculator calc;
	std::string str;

	while (true) {
		std::getline(std::cin, str);
		if (calc.Execute(str)) {
			std::cerr << calc.Value() << '\n';
		}
		else break;
	}
}

우선 문자열계산기와 문자열 계산식을 선언한뒤,
문자열 계산식을 "str"에 저장하여 계산했습니다.
계산이 성공했을때, 그 값을 출력합니다.
실패했을때는 반복문을 종료합니다.

또는, 이렇게 하셔도됩니다.

#include "StringCalculator.h"
#include <conio.h>
#include <iostream>
#include <Windows.h>

int main() {
	mawile::Calculator calc;
	std::string str;

	while (true) {
		std::getline(std::cin, str);
		std::system("cls");
		if (calc.Execute(str)) {
			std::cerr << str <<  " = " << calc.Value() << '\n';
		}
		else break;
	}
}

⬛마무리

🟥테스트

우선은 몇가지 입력을 통한 테스트를 해보았습니다.

후.. 떨리는 마음으로 테스트를 해보았는데요.
아주 잘됩니다!
괄호와 연산자 우선순위도 잘 되는것같네요!


위와같이 왜 이렇게 만들었는지 모를법한 계산식도, 프로그램이 잘 알아듣고 계산하는군요.

🟥다운로드

🟣다운로드는 일단 임시로 구글드라이브로 해놓겠습니다.
요즘 피곤해서, github올리기 귀찮네요.
언젠가 귀차니즘이 사라질시기가 오면 깃허브링크로 바꿔놓겠습니다ㅋㅋ
그때는 프로젝트파일 통째로 올려놓겠습니다!

실행파일 다운로드(google drive)
프로젝트 다운로드(github)

🟥마치며...

위 글에서는
문자열 계산기를 객체화해서 사용하기 편하게 만들어봤습니다.
궁금한부분 있으시거나 이해가 되지않는 부분은 댓글로 질문주세요.

profile
C++ / Assembly / Python 언어를 다루고 있습니다!

0개의 댓글