[C++] 클래스 다루기 : 사칙연산 계산기

amudomolla·2023년 6월 23일
0

C++

목록 보기
12/12
post-custom-banner

참고 사이트


🚗 사칙연산 계산기 클래스 만들기

요구사항

  • 식을 입력하면, num1, num2, sign(+, -, *, /)으로 각각 파싱하여 변수에 저장
  • 프로그램은 break 입력 시에만 종료
  • 잘못된 입력에 대한 예외처리
    [❌]
    1. 입력 문자열이 12자를 초과하는 경우
    2. 정수가 아닌 문자를 입력하는 경우
    3. 정수와 문자를 함께 입력하는 경우
    4. 사칙연산자가 아닌 다른 기호를 입력한 경우
    5. num2의 뒤에 연산자가 있는 경우
      EX) 1-1-
    6. 맨 앞에 연산기호가 붙는 경우 ('-' 제외)
      EX) +1+1
    7. 연산자를 두 개 붙여 입력하는 경우('-' 제외)
      EX) 1++1
    8. 세 개 이상의 정수를 입력하는 경우
      EX) 1+1+1
    9. '/' 연산 때, 분자 혹은 분모에 '0' 입력 시, 다시 입력 요구하기

원격 저장소의 OJT4(기본 틀이 작성되어 있음)를 내려 받은 다음, 작업을 진행해야 함
(작성자의 로컬 저장소 이름 : OJT_3, 원격 저장소 이름 : OJT)

1. 원격 저장소의 커밋을 로컬 저장소에 내려받기


1) 명령어 입력

git pull [원격 저장소 이름] [브랜치 이름]

2) 내려받기 됐는지 로컬 저장소에서 확인

설치된 VSCode 열고, 작업 (설치방법)


2. 부모/자식 클래스 정의


// 부모 클래스
class Operator {
private:
    int num1;
    int num2;
    double result;

protected:
    void setResult(double result) { this->result = result; }
    int getNum1() { return num1; }
    int getNum2() { return num2; }
    virtual void calculate() = 0;  // 순수 가상함수

public:
    void setNumber(int num1, int num2) { this->num1 = num1; this->num2 = num2; }
    double getResult() { calculate(); return result; }
};

// 자식 클래스 : 더하기
class Add : public Operator{
    // write
    virtual void calculate(){
        cout << getNum1() << " + " << getNum2() << " = " << getNum1() + getNum2() << '\n';
    }
};
...  // 빼기, 곱하기, 나누기 모두 똑같음. 연산기호만 바꿔주면 됨.

int main() {
...

case '+':
     cout << '\n' << "[출력]" << '\n';
     a.setNumber(num1, num2);
     a.getResult();
     break;
     
...

}

1) getNum1(), getNum2(), getResult()를 통해서 부모 클래스인 Operator에서 private으로 선언된 변수 num1, num2를 꺼내오고,
2) setNumber()를 통해 값을 넣어준다.


3. 예외처리


stringstream 사용법

stringstream () : 문자열에서 맞는 자료형의 정보를 추출

1) 입력 문자열이 12 초과인 경우

2) 정수가 아닌 실수 혹은 문자를 입력하는 경우

3) 정수와 문자를 함께 입력하는 경우

4) 사칙연산 외의 기호를 입력한 경우

#include <sstream>  // stringstream 라이브러리

...

string input_exp;
int num1, num2;
char sign;

...

cout << "수식을 입력하세요. (break 입력 시 종료)" << endl;
getline(cin, input_exp);

stringstream stream(input_exp);
stream >> num1;  // input_exp에서 연산자 전까지의 정수 추출해 int num1에 저장
stream >> sign;  // input_exp에서 문자만 추출해 sign에 저장
stream >> num2;  // sign 이후의 정수만 추출해 num2에 저장

...

if((len > 12) || (sign != '+') && (sign != '-') && (sign != '*') && (sign != '/')) {
        cout << '\n' << "올바르게 입력했는지 확인하세요." << '\n';
        cout << "===============================" << '\n' << '\n';
        continue;
}

5) 연산자를 붙여서 입력하는 경우 (정수 앞에 붙는 '-' 제외)

6) '맨 앞' 혹은 '맨 뒤'에 연산자가 붙는 경우 (정수 앞에 붙는 '-' 제외)

7) 두 수를 초과해 연산을 입력한 경우(sign이 모두 같은 경우)

  • 문자열 위치 찾기 : find()
문자열.find(검색 문자열)  // 문자열 위치 검색
  • 전달된 문자들 중 첫 번째로 나타나는 문자의 위치
find_first_of("찾을 문자", 찾기 시작할 인덱스)
  • 전달된 문자들 중 가장 마지막에 나타나는 문자의 위치
find_last_of("찾을 문자", 찾기 마지막 인덱스)

📌 npos : 검색 함수가 실패할 때 "찾을 수 없음" 또는 "나머지 모든 문자"를 나타내는 -1로 초기화된 부호 없는 정수 값

#include <iomanip>  // fixed 라이브러리
#include <cmath>  // setprecision 라이브러리

...

int len = input_exp.length();  // 입력받은 문자열 길이 구하기
int sign_index = input_exp.find(sign);  // sign 인덱스
int sign_subtract = input_exp.find('-',sign_index+2);  // 예외 상황 중 '-'제외에 활용할 '-' 인덱스

char ch[] = {'+', '*', '/'};  // 입력된 연산자와 비교하기 위한 연산자 배열 선언
char sign_arr;

// '-'를 제외한 기호들을 for문을 활용해 하나씩 추출해 3, 4번에 적용
for(int i = 0; i < 3; i++) {

   sign_arr = ch[i];
	
   int sign_temp = input_exp.find(sign_arr,sign_index+1);  // num2 이후에 또 sign이 입력되면, 그 sign의 인덱스를 저장
   int first = input_exp.find_first_of(sign_arr,0);  // 입력된 연산자 중 가장 앞 쪽에 놓인 sign의 인덱스
   int last = input_exp.find_last_of(sign_arr,len-1);  // 입력된 연산자 중 가장 뒤 쪽에 놓인 sign의 인덱스
   
   
   // 3. 앞에서부터 찾은 연산자 인덱스,뒤에서부터 찾은 연산자 인덱스 비교
   if(first == last){
     // 4. '-'를 맨 앞 혹은 num2 앞에 입력했는지 판단
     if(input_exp.find('-') == 0 || input_exp.find('-') == sign_index+1) {
        break;
     }
   }
   else{
        cout << '\n' << "계산식을 올바르게 입력했는지 확인하세요." << '\n';
        cout << "========================================" << '\n' << '\n';
        sign = {};  // sign 초기화
        break;
   }
   // 5. 입력 정수가 3개 이상인지 판단
   if(sign_temp != string::npos || sign_subtract != string::npos) {
       cout << '\n' << "서로 다른 연산자를 2개 이상 입력했는지 확인하세요.('-'제외)" << '\n';
       cout << "===========================================================" << '\n' << '\n';
       sign = {};  // sign 초기화
       break;
   }
}

...

first == last 에서 두 수 초과 입력 예외처리 가능하지 않나?

first에 입력된 sign과 last에 입력된 sign이 같은 경우에만 가능
그래서 예외처리 7번(코드에서는 주석 5번)을 구현해 서로 달라도 가능하도록 함.


7) 두 수를 초과해 연산을 입력한 경우 (sign이 서로 다른 경우)

sign이 2번 이상 오면 X (음수를 위한 ‘-’는 제외)

sign_index+1 = ‘-’ 혹은 정수 가능
sign_index+2 부터 = 정수만 가능
이를 초과하면, 세 자리 이상 입력된 것

그러므로,
1) [sign_index]+1에서 ‘+, *, /’ 이 발견되는지
2) [sign_index]+2에서 ‘-’가 발견되는지
비교하고, 발견된다면, 오류 메시지 출력.


8) 분자 혹은 분모에 '0' 을 입력하는 경우

...

case '/':
    if(num1 != 0 && num2 != 0) {
        cout << '\n' << "[출력]" << '\n';
        d.setNumber(num1, num2);
        d.getResult();
    }
    else {
        cout << '\n' << "분자 혹은 분모에 '0'을 입력했는지 확인하세요." << '\n';
        cout << "=============================================" << '\n' << '\n';
    }
    break;
    
...

4. 3번까지 구현 후 추가 문제점 확인 -> 디버깅


참고 사이트

프로그래밍 언어에서는 수를 표현하기 위해 크게 두 가지 타입을 제공하는데, 바로 정수 타입과 부동소수점 타입이다.

- 부동소수점 이란?

실수를 표현할 때, 소수점의 위치를 고정하지 않는 것

- 부동소수점 자료형

- 부동소수점 구조

부동소수점 형식은 정해진 비드(32Bit 혹은 64Bit)를 적절히 분배해 부호부, 지수부, 가수부를 할당하는 일종의 규칙을 가짐
EX] -9.6875 → -1001.1011(2) → -1.0011011×2³ → [부호부 음수, 지수부 3, 가수부 0011011]
.
부호부(sign) : 1비트. 숫자의 부호를 나타내며, 양수일 때는 0, 음수일 때는 1
지수부(Exponent) : 8비트. 지수를 의미
가수부(Mantissa) : 23비트. 가수 또는 유효숫자를 의미

- 부동소수점의 정밀도

부동소수점의 정밀도란 정보 손실 없이 얼마나 많은 유의한 자릿수를 나타낼 수 있는지를 의미

❗문제점 #1

하기 문자열 입력 시, 9999800001라는 값이 출력되어야 정상

99999*99999

그러나 1409865409 라는 값이 출력되는 상황

  • 99999*9999 입력 시, 999890001 정상 출력
  • 9999*9999 입력 시, 99980001 정상 출력
    => 여기까지는 정상출력이 됐지만, '9'를 더 붙이면 제대로 된 값이 출력되지X

❗문제점 #2

하기 문자열 입력 시, 0.00000001라는 값이 출력되어야 정상

1 / 100000000

그러나 1e-08 이라는 값이 출력되는 상황

  • 1 / 10000 입력 시, 0.0001 정상 출력되었지만 '0'을 하나 더 붙이면, 지수로 출력

🛸 문제 발생 이유

위 설명의 구조에서 알 수 있듯, float이나 double의 경우 가수부의 크키는 일정하기 때문에 지수가 충분히 클 경우에는 소수점 이하를 표현하기 어려움
즉, 부동소수점의 정밀도 때문에 문제가 발생한 것

💡 해결 방법

setprecision() 함수를 사용해 기본 정밀도를 재정의

  • int는 표현 가능한 범위가 작으므로 두 수를 double로 변환

  • fixed : 기본이 소수점 아래 6자리까지 출력

  • setprecision(8) : 정밀도를 8로 재정의

#include <iomanip>  // fixed 라이브러리
#include <cmath>  // setprecision 라이브러리

// 자식 클래스 : 곱하기
class Multiply : public Operator{
    // write
    virtual void calculate(){
        cout << getNum1() << " * " << getNum2() << " = " << fixed << setprecision(0) << (double)getNum1() * (double)getNum2() << '\n';
    }
};

// 자식 클래스 : 나누기
class Divide : public Operator{
    // write
    virtual void calculate(){
        cout << getNum1() << " / " << getNum2() << " = " << fixed << setprecision(8) << (double)getNum1() / (double)getNum2() << '\n';
    }
};

5. 결과


🔗 정상 / 예외상황 실행결과


👩‍💻 최종 소스코드


#include <iostream>
#include <string>
#include <sstream>  // stringstream 라이브러리
#include <iomanip>  // fixed 라이브러리
#include <cmath>  // setprecision 라이브러리

using namespace std;

// 부모 클래스
class Operator {
private:
    int num1;
    int num2;
    double result;

protected:
    void setResult(double result) { this->result = result; }
    int getNum1() { return num1; }
    int getNum2() { return num2; }
    virtual void calculate() = 0;  // 순수 가상함수

public:
    void setNumber(int num1, int num2) { this->num1 = num1; this->num2 = num2; }
    double getResult() { calculate(); return result; }
};

// 자식 클래스 : 더하기
class Add : public Operator{
    // write
    virtual void calculate(){
        cout << getNum1() << " + " << getNum2() << " = " << getNum1() + getNum2() << '\n' << '\n';
    }
};

// 자식 클래스 : 빼기
class Subtract : public Operator{
    // write
    virtual void calculate(){
        cout << getNum1() << " - " << getNum2() << " = " << getNum1() - getNum2() << '\n' << '\n';
    }
};

// 자식 클래스 : 곱하기
class Multiply : public Operator{
    // write
    virtual void calculate(){
        cout << getNum1() << " * " << getNum2() << " = " << fixed << setprecision(0) << (double)getNum1() * (double)getNum2() << '\n' << '\n';
    }
};

// 자식 클래스 : 나누기
class Divide : public Operator{
    // write

    virtual void calculate(){
        cout << getNum1() << " / " << getNum2() << " = " << fixed << setprecision(8) << (double)getNum1() / (double)getNum2() << '\n' << '\n';
    }
};

int main()
{
    Add a;
    Subtract s;
    Multiply m;
    Divide d;

    string input_exp;
    int num1, num2;
    char sign;

    while (1) {
        cout << "수식을 입력하세요. (break 입력 시 종료)" << endl;
        getline(cin, input_exp);

        // write

        // stringstream : 문자열에서 필요한 정보만 추출
        stringstream stream(input_exp);
        stream >> num1;
        stream >> sign;
        stream >> num2;

        int len = input_exp.length();  // 입력받은 문자열 길이 구하기
        int sign_index = input_exp.find(sign);  // sign 인덱스
        int sign_subtract = input_exp.find('-',sign_index+2);  // 예외 상황 중 '-'제외에 활용할 '-' 인덱스

        // 1. "break" 입력 시, 프로그램 종료
        if(input_exp == "break"){
            break;
        }

        // 2. 입력한 문자열이 길이가 12 이하이면서 연산자는 +,-,*,/ 인지 판단
        if((len > 12) || (sign != '+') && (sign != '-') && (sign != '*') && (sign != '/')){
            cout << '\n' << "올바르게 입력했는지 확인하세요." << '\n';
            cout << "===============================" << '\n' << '\n';
            continue;
        }

        char ch[] = {'+', '*', '/'};  // 입력된 연산자와 비교하기 위한 연산자 배열 선언
        char sign_arr;

        // '-'를 제외한 기호들을 for문을 활용해 하나씩 추출해 3, 4번에 적용
        for(int i = 0; i < 3; i++) {

            sign_arr = ch[i];

            int sign_temp = input_exp.find(sign_arr,sign_index+1);  // num2 이후에 또 sign이 입력되면, 그 sign의 인덱스를 저장
            int first = input_exp.find_first_of(sign_arr,0);  // 입력된 연산자 중 가장 앞 쪽에 놓인 sign의 인덱스
            int last = input_exp.find_last_of(sign_arr,len-1);  // 입력된 연산자 중 가장 뒤 쪽에 놓인 sign의 인덱스

            // 3. 앞에서부터 찾은 연산자 인덱스,뒤에서부터 찾은 연산자 인덱스 비교
            if(first == last){

                // 4. '-'를 맨 앞 혹은 num2 앞에 입력했는지 판단
                if(input_exp.find('-') == 0 || input_exp.find('-') == sign_index+1) {
                    break;
                }
            }
            else{
                cout << '\n' << "계산식을 올바르게 입력했는지 확인하세요." << '\n';
                cout << "========================================" << '\n' << '\n';
                sign = {};  // sign 초기화
                break;

            }

            // 5. 입력 정수가 3개 이상인지 판단
            if(sign_temp != string::npos || sign_subtract != string::npos) {
                cout << '\n' << "서로 다른 연산자를 2개 이상 입력했는지 확인하세요.('-'제외)" << '\n';
                cout << "===========================================================" << '\n' << '\n';
                sign = {};  // sign 초기화
                break;
            }
        }

        switch(sign) {
            case '+':
                cout << '\n' << "[출력]" << '\n';
                a.setNumber(num1, num2);
                a.getResult();
                break;
            case '-':
                cout << '\n' << "[출력]" << '\n';
                s.setNumber(num1, num2);
                s.getResult();
                break;
            case '*':
                cout << '\n' << "[출력]" << '\n';
                m.setNumber(num1, num2);
                m.getResult();
                break;
            case '/':
                if(num1 != 0 && num2 != 0) {
                    cout << '\n' << "[출력]" << '\n';
                    d.setNumber(num1, num2);
                    d.getResult();
                }
                else {
                    cout << '\n' << "분자 혹은 분모에 '0'을 입력했는지 확인하세요." << '\n';
                    cout << "=============================================" << '\n' << '\n';
                }
                break;
            default :
                cout << "다시 시도하세요." << '\n' << '\n';
                break;
        }
        num1, num2 = 0;  // num1, num2 초기화
        sign = {};  // 연산기호 초기화
    }
    return 0;
}


6. 빌드 및 실행


1) 우측 상단에서 Run 버튼 클릭

2) [C/C++:g++ build and debug active file] 클릭

그러면, 아래와 같이 tasks.json 파일과 main 실행 파일이 생성됨.

3) Run 버튼 클릭하면, 실행


7. GitHub에 수정한 파일 업데이트


1) 로컬 저장소에서 터미널 열기

git status  // 빨간 색

git add .
git status  // 초록색으로 변한 것 확인

git commit -m "커밋 메시지"

git pull origin main

만약 여기서 'git pull ...' 이후, 하기와 같은 에러가 뜨는 경우

github you have not concluded your merge

아래 명령어를 입력한 후 다시 pull -> push 해주면 됨.(참고)

git merge --abort
git pull origin main
git push origin main
profile
👩‍💻 기록 및 복습을 위함
post-custom-banner

0개의 댓글