본문 바로가기

C++로 만든 프로그램 - Gym Manager

Gym Manager 구현 영상(gif)

Gym Manager는 손예진 교수님의 <C++ 프로그래밍 II> 수업에서 기말 대체 팀 프로젝트로 내주신 프로그램이다. 트레이너와 회원 모두 이용할 수 있는 일종의 운동 관리 프로그램이었다. 팀 프로젝트는 나 포함 총 2명이서 진행했다. 

 

0123456
기말 대체 팀프로젝트 설명 ppt

당시에는 구현 영상없이 이 피피티 하나만 주셨다. 수업시간에 구현한 프로그램을 보여주시긴 했지만 따로 응용 프로그램이나 영상을 주시진 않아서 만들어 보았다. 아래는 같은 구현 영상인데 혹시 gif가 아닌 영상으로 보고싶은 사람들을 위해 첨부한다.

 

사실 이 포스팅을 계속해서 미뤄왔다. 왜냐면 C++ 만진게 작년이라 다 까먹었기 때문^_^! 게다가 이 프로젝트는 2년 전(2018년)에 하던 거라 기억이.. 하ㅏ하하하하 지금 보니까 내가 어떻게 이렇게 만들었지 싶기도 하다.. 당시 어떤 과정에서 이런 식으로 짰는지 남겨놓은 메모 같은 것도 없어서 아예 처음부터 다시 만들어보기로 했다!

윤은영 교수님의 <컴퓨터공학 입문Ⅰ> 수업자료 일부

 

구현하기 전에 알고리즘을 짜야한다. 알고리즘을 설계하는 방법에는 두가지가 있는데, 하나는 의사 코드(pseudo code)이고 하나는 순서도(flow chart)이다. 의사 코드는 코드를 언어로 표현한 것이고, 순서도는 말 그대로 프로그램 진행 순서를 도식화한 것이다. 위의 이미지에 있는 예시를 보면 이해하기 쉬울 것이다. 

 

012
Gym Manager 구현하기 전 작성한 의사코드

우선 main.cpp에서 메인 메뉴를 구성하고 trainer와 member는 클래스로 선언하라는 교수님의 말씀이 기억나서 이를 토대로 알고리즘을 대충 구성해봤다. 손예진 교수님의 설명 ppt가 일종의 순서도여서 나는 의사 코드를 작성해보았다. 

 

보다시피 생각보다 간단하다. 예전에는 의사코드 작성하는 것이 어떤 정해진 양식을 따라야 하는 것 같아 어렵기만 했는데 "코드를 언어로만 표시"한다면 의사코드가 될 수 있다. 굳이 어떤 양식을 따라야 하는 것이 아니라, 내가 구현하기 쉽게 언어로 풀면 되는 것이다! 물론 협업해야 할 땐 남도 알아볼 수 있으면 좋겠지만 이건 나 혼자 해보는 것이니 내가 알아보기 쉽게 썼다.

 

1. Main.cpp


 

(왼) 구현 전 vs. 구현 후 (오)

사실 구현이 꼭 의사코드대로 되진 않는다. 구현하는 과정에서 바뀌는 경우가 더 많다. 그래서 바뀐 부분을 주황색 글씨로 작성했다. 그리고 의사코드는 추상적인 면이 있어 좀 더 구현 과정에서 구체적으로 명시할 필요가 있는 경우 연두색 글씨로 덧붙였다. 이제 알고리즘을 설계하고 구현하면서 생긴 issue들과 새롭게 알게 된 점들을 정리해보겠다. 

switch ~ case문? while문, if ~ else문!

#include "Trainer.h"
#include "Member.h"
#include <iostream>
using namespace std;

int main()
{
    int mainNum{};
    int trainerNum{};
    int memberNum{};

    Trainer trainer[2];
    Member member[5];
    string name{};
    int id{};
    int trainerID{};
    int trainerCnt{};
    int memberCnt{};
    string rsvCondition{ "Not yet" };

    // Main Menu(1. trainer    2. member    3. exit)
    while (true)
    {
    mainMenu:
        cout << "---------GymManager is running---------" << endl;
        cout << "1. trainer\t2. member\t3. exit" << endl;
        cout << "---------------------------------------" << endl;
        cin >> mainNum;

        switch (mainNum)
        {
        // Trainer Menu(1. 정보입력    2. 전체 트레이너 정보확인    3. 예약확인    4. exit)
        case 1:
            while (true)
            {
                cout << "-----------------------------Trainer Menu-----------------------------" << endl;
                cout << "1. 정보입력\t2. 전체 트레이너 정보확인\t3. 예약확인\t4. exit" << endl;
                cin >> trainerNum;

                switch (trainerNum)
                {
                // 1. 정보입력
                case 1:
                    if (trainerCnt < 2)
                    {
                        cout << "이름: ";
                        cin >> name;
                        trainer[trainerCnt].setName(name);
                        trainer[trainerCnt].setID(100 + trainerCnt);
                        trainerCnt += 1;
                    }
                    else
                    {
                        cout << "Trainer는 2명까지 입력할 수 있습니다." << endl;
                    }
                    break;

                // 2. 전체 트레이너 정보확인
                case 2:
                    for (int i = 0; i < trainerCnt; i++)
                    {
                        cout << trainer[i].getID() << '\t' << trainer[i].getName() << endl;
                    }
                    break;

                // 3. 예약확인
                case 3:
                    cout << "트레이너 ID: ";
                    cin >> trainerID;
                    for (int i = 0; i < memberCnt; i++)
                    {
                        if (member[i].getTrainerID() == trainerID)
                        {
                            cout << member[i].getID() << '\t' << member[i].getName() << '\t' << member[i].getTrainerID() << '\t' << member[i].getRsvCondition() << endl;
                            cout << "y(yes) / n(no)? ";
                            cin >> rsvCondition;
                            if (rsvCondition == "y")
                            {
                                member[i].setRsvCondition(rsvCondition);
                                cout << member[i].getID() << '\t' << member[i].getName() << '\t' << member[i].getTrainerID() << '\t' << member[i].getRsvCondition() << endl;

                            }
                            else if (rsvCondition == "n")
                            {
                                cout << "예약을 승인하지 않았습니다." << endl;
                            }
                            else
                            {
                                cout << "y 또는 n만 입력해주세요." << endl;
                            }
                        }
                    }
                    break;

                // 4. exit
                case 4:
                    goto mainMenu;

                // Exception
                default:
                    cout << "1, 2, 3, 4 중에 입력해주세요." << endl;
                    break;

                    break;
                }
            }
            break;

        // Member Menu(1. 정보입력    2. 전체 회원 정보확인    3. 예약    4. exit)
        case 2:
            while (true)
            {
                cout << "----------------------Member Menu----------------------" << endl;
                cout << "1. 정보입력\t2.전체 회원 정보확인\t3.예약\t4.exit" << endl;
                cin >> memberNum;

                switch (memberNum)
                {
                // 1. 정보입력
                case 1:
                    if (memberCnt < 5)
                    {
                        cout << "이름: ";
                        cin >> name;
                        member[memberCnt].setName(name);
                        member[memberCnt].setID(200 + memberCnt);
                        memberCnt += 1;
                    }
                    else
                    {
                        cout << "Member는 5명까지 입력할 수 있습니다." << endl;
                    }
                    break;

                // 2. 전체 회원 정보확인
                case 2:

                    for (int i = 0; i < memberCnt; i++)
                    {
                        if (member[i].getTrainerID()) {
                            cout << member[i].getID() << '\t' << member[i].getName() << '\t' << member[i].getTrainerID() << '\t' << member[i].getRsvCondition() << endl;
                        }
                        else
                        {
                            cout << member[i].getID() << '\t' << member[i].getName() << '\t' << member[i].getTrainerID() << '\t' << "Not yet" << endl;
                        }
                    }
                    break;

                // 3. 예약
                case 3:
                    cout << "회원 ID: ";
                    cin >> id;
                    cout << "[예약 가능한 트레이너]" << endl;
                    for (int i = 0; i < trainerCnt; i++)
                    {
                        cout << trainer[i].getID() << '\t' << trainer[i].getName() << endl;
                    }
                    cout << "예약하고 싶은 트레이너 ID: ";
                    cin >> trainerID;
                    member[id - 200].setTrainerID(trainerID);
                    break;

                // 4. exit
                case 4:
                    goto mainMenu;

                // Exception
                default:
                    cout << "1, 2, 3, 4 중에 입력해주세요." << endl;
                    break;
                }
            }
            break;

       // Exit
        case 3:
            exit(0);

       // Exception
        default:
            cout << "1, 2, 3 중에 입력해주세요." << endl;
            break;
        }

    }

    return 0;
}

 

메인 메뉴를 구성하는 과정에서 처음에는 switch ~ case문을 사용할 생각에 신났다(평소에 if문과 while문을 많이 쓰다 보니 안 쓰던 제어문을 쓰면 뭔가 신난다.) 메인 메뉴 번호를 mainNum이라는 변수에 입력받고 mainNum에 따라 케이스를 나누려 했다. 여기까진 성공이었으나 trainerNum과 memberNum 또한 switch문으로 하려고 보니 mainNum으로 돌아갈 방법이 없었다(96번째 줄과 165번째 줄을 참고하면 된다.) exit()을 하면 프로그램 전체가 종료되어버리고, break를 두 번 쓸 수도 없는 노릇이었다. 어쩔 수 없이 goto문을 사용했다. switch문을 사용해서 if문보다 프로그램의 흐름을 직관적으로 알 수 있다는 것은 좋으나 goto문으로 인해 프로그램의 흐름을 읽기가 더 어려워졌다. 결국 그냥 if ~ else문을 사용하기로 했다.

 

#include "Trainer.h"
#include "Member.h"
#include <iostream>
using namespace std;

int main()
{
    int mainNum{};
    int trainerNum{};
    int memberNum{};

    Trainer trainer[2];
    Member member[5];
    string name{};
    int id{};
    int trainerID{};
    int trainerCnt{};
    int memberCnt{};
    string rsvCondition{ "Not yet" };

    // Main Menu(1. trainer    2. member    3. exit)
    while (true)
    {
        cout << "---------GymManager is running---------" << endl;
        cout << "1. trainer\t2. member\t3. exit" << endl;
        cout << "---------------------------------------" << endl;
        cin >> mainNum;

        // Trainer Menu(1. 정보입력    2. 전체 트레이너 정보확인    3. 예약확인    4. exit)
        if (mainNum == 1)
        {
            while (true)
            {
                cout << "-----------------------------Trainer Menu-----------------------------" << endl;
                cout << "1. 정보입력\t2. 전체 트레이너 정보확인\t3. 예약확인\t4. exit" << endl;
                cin >> trainerNum;

                // 1. 정보입력
                if (trainerNum == 1)
                {
                    if (trainerCnt < 2)
                    {
                        cout << "이름: ";
                        cin >> name;
                        trainer[trainerCnt].setName(name);
                        trainer[trainerCnt].setID(100 + trainerCnt);
                        trainerCnt += 1;
                    }
                    else
                    {
                        cout << "Trainer는 2명까지 입력할 수 있습니다." << endl;
                    }
                }
                
                // 2. 전체 트레이너 정보확인
                else if (trainerNum == 2)
                {
                    for (int i = 0; i < trainerCnt; i++)
                    {
                        cout << trainer[i].getID() << '\t' << trainer[i].getName() << endl;
                    }
                }

                // 3. 예약확인
                else if (trainerNum == 3)
                {
                    cout << "트레이너 ID: ";
                    cin >> trainerID;
                    for (int i = 0; i < memberCnt; i++)
                    {
                        if (member[i].getTrainerID() == trainerID)
                        {
                            cout << member[i].getID() << '\t' << member[i].getName() << '\t' << member[i].getTrainerID() << '\t' << member[i].getRsvCondition() << endl;
                            cout << "y(yes) / n(no)? ";
                            cin >> rsvCondition;
                            if (rsvCondition == "y")
                            {
                                member[i].setRsvCondition(rsvCondition);
                                cout << member[i].getID() << '\t' << member[i].getName() << '\t' << member[i].getTrainerID() << '\t' << member[i].getRsvCondition() << endl;

                            }
                            else if (rsvCondition == "n")
                            {
                                cout << "예약을 승인하지 않았습니다." << endl;
                            }
                            else
                            {
                                cout << "y 또는 n만 입력해주세요." << endl;
                            }
                        }
                    }
                }

                // 4. exit
                else if (trainerNum == 4)
                {
                    break;
                }

                // Exception
                else
                {
                    cout << "1, 2, 3, 4 중에 입력해주세요." << endl;
                }
            }
        }

        // Member Menu(1. 정보입력    2. 전체 회원 정보확인    3. 예약    4. exit)
        else if (mainNum == 2)
        {
            while (true)
            {
                cout << "----------------------Member Menu----------------------" << endl;
                cout << "1. 정보입력\t2.전체 회원 정보확인\t3.예약\t4.exit" << endl;
                cin >> memberNum;

                // 1. 정보입력
                if (memberNum == 1)
                {
                    if (memberCnt < 5)
                    {
                        cout << "이름: ";
                        cin >> name;
                        member[memberCnt].setName(name);
                        member[memberCnt].setID(200 + memberCnt);
                        memberCnt += 1;
                    }
                    else
                    {
                        cout << "Member는 5명까지 입력할 수 있습니다." << endl;
                    }
                }

                // 2. 전체 회원 정보확인
                else if (memberNum == 2)
                {
                    for (int i = 0; i < memberCnt; i++)
                    {
                        if (member[i].getTrainerID()) {
                            cout << member[i].getID() << '\t' << member[i].getName() << '\t' << member[i].getTrainerID() << '\t' << member[i].getRsvCondition() << endl;
                        }
                        else
                        {
                            cout << member[i].getID() << '\t' << member[i].getName() << '\t' << member[i].getTrainerID() << '\t' << "Not yet" << endl;
                        }
                    }
                }

                // 3. 예약
                else if (memberNum == 3)
                {
                    cout << "회원 ID: ";
                    cin >> id;
                    cout << "[예약 가능한 트레이너]" << endl;
                    for (int i = 0; i < trainerCnt; i++)
                    {
                        cout << trainer[i].getID() << '\t' << trainer[i].getName() << endl;
                    }
                    cout << "예약하고 싶은 트레이너 ID: ";
                    cin >> trainerID;
                    member[id - 200].setTrainerID(trainerID);
                }

                // 4. exit
                else if (memberNum == 4)
                {
                    break;
                }

                // Exception
                else
                {
                    cout << "1, 2, 3, 4 중에 입력해주세요." << endl;
                }
            }
        }
        // Exit
        else if (mainNum == 3)
        {
            break;
        }
        // Exception
        else
        {
            cout << "1, 2, 3 중에 입력해주세요." << endl;
        }
    }

    return 0;
}

 

if ~ else문을 사용하여 코드를 바꾼 결과다. 흠.. 사실 뭐가 더 효율적인지는 모르겠으나 if문이 많아 좀 더러워 보이긴 해도 goto문이 있는 것보다는 덜 복잡한거같다!

객체? 객체 배열!

Trainer trainer;

 

처음에는 객체로 생성했다. 그런데 트레이너 정보 입력을 받다 보니 트레이너가 두 명 이상이 되면 같은 객체에 덮어씌워지는 문제가 발생했다. 그렇다고 매번 객체를 새로 생성할 순 없었다. 반복문을 쓰는 이상 서로 다른 객체를 만들어내기가 어려웠기 때문이다. 그래서 객체 배열로 바꿨다!

 

Trainer trainer[2];

 

               // 1. 정보입력
                if (trainerNum == 1)
                {
                    if (trainerCnt < 2)
                    {
                        cout << "이름: ";
                        cin >> name;
                        trainer[trainerCnt].setName(name);
                        trainer[trainerCnt].setID(100 + trainerCnt);
                        trainerCnt += 1;
                    }
                    else
                    {
                        cout << "Trainer는 2명까지 입력할 수 있습니다." << endl;
                    }

 

트레이너는 두 명까지 입력받는 것이 조건이므로 길이가 2인 객체 배열을 선언했다. 그리고 trainerCnt로 트레이너 수를 세면서 인덱스로도 활용했다.

변수 초기화!

변수를 초기화하지 않을 경우 뜨는 빌드 오류

프로그램을 작성하던 중 중간 결과를 확인해보려고 디버그하지 않고 시작을 했는데 빌드 오류가 떴다. 변수 초기화를 해야 한다는 오류였다. 사실 나는 원래 변수 선언만 하고 초기화는 하지 않았다. 초기화하지 않으면 쓰레기 값이 들어간다는 것은 알고 있었지만 어차피 선언 후에 입력받을 변수들이라 굳이 초기화하지 않은 것이다. 예전(Visual Studio 2015)에는 초기화를 안 해도 빌드 오류가 난 적이 없었는데 요즘 컴파일러(Visual Studio 2019)는 초기화하지 않은 변수도 오류가 나는구나.. 

 

 

C++ 01.02 - 변수, 초기화 및 할당 (variables, initialization, and assignment)

01.02 - 변수, 초기화 및 할당 (variables, initialization, and assignment) 객체(Object) C++ 프로그램은 객체를 생성, 접근, 조작 및 파괴한다. 객체는 값을 저장하고 사용할 수 있는 메모리 조각이다. 객체는..

boycoding.tistory.com

읽어보면 변수 초기화의 중요성을 알 수 있다. 내가 입력받을 값이라 하더라도 일일이 입력받는지 확인하는 것보다는 아예 처음부터 초기화하여 나중에 혹시 모를 출력에 대비하는 편이 낫겠다는 생각이 들었다. 그래서 앞으로 변수 초기화는 꼭 해줄 예정! 

 

 

C++ 01.13 - 여러 가지 변수 초기화 방법 (variable initialization)

01.13 - 여러 가지 변수 초기화 방법 (variable initialization) 변수가 정의될 때, 즉시 해당 변수에 값을 지정할 수 있다. 이를 변수 초기화(variable initialization) 또는 짧게 초기화(initialization)라고 한..

boycoding.tistory.com

그리고 또 처음 안 사실.. 나는 지금까지 복사 초기화만 해왔는데 이게 가장 안 좋은 방법이었다...! 변수 초기화 방법이 여러 가지인 것도 이번에 처음 알게 되었다. 위의 링크를 읽어보면 일부 데이터 타입에서는 직접 초기화가 복사 초기화보다 더 뛰어날 수 있고,  복사 초기화와 직접 초기화는 일부 타입의 변수에서만 작동하지만 유니폼 초기화는 모든 데이터 타입에서 작동한다고 한다. 따라서 복사 초기화 < 직접 초기화 < 유니폼 초기화 순으로 성능이 더 좋거나 범용적이라는 것을 알 수 있다. 그래서 앞으로 유니폼 초기화를 사용하기로 했다!

2. Trainer.h와 Trainer.cpp


(왼) 구현 전 vs. 구현 후 (오)

초기화 리스트!

#include <iostream>
using namespace std;

class Trainer
{
private:
    int id;
    string name;
public:
    Trainer() { id = 0; name = ""; }
    void setID(int _id);
    int getID() { return id; }
    void setName(string _name);
    string getName() { return name; }
};

 

이것도 서칭하다 처음 알게 된 사실인데, 원래는 이렇게 생성자 본문에서 변수에 값을 할당하곤 했다. 그런데 이렇게 생성자 본문에서 값을 할당하는 것보다 초기화 리스트를 사용하는 것이 성능이 더 우수하다고 한다.

 

#include <iostream>
using namespace std;

class Trainer
{
private:
    int id;
    string name;
public:
    Trainer():id{}, name{}{}
    void setID(int _id);
    int getID() { return id; }
    void setName(string _name);
    string getName() { return name; }
};

 

그래서 초기화 리스트로 바꾸어보았다. const 또는 reference 변수와 같이 초기값이 필요한 멤버를 초기화할 수 있는 유일한 방법이라고도 하니 앞으로 자주 사용해야겠다!

 

 

C++ 09.06 - 생성자 멤버 초기화 리스트 (Constructor member initializer list)

생성자 멤버 초기화 리스트 (Constructor member initializer list) 이전 포스트 생성자에서 대입 연산자(=)를 사용하여 클래스 멤버 변수를 초기화했다: class Something { private: int m_value1; double m_valu..

boycoding.tistory.com

rsvCondition은 Trainer의 멤버 변수? Member의 멤버 변수!

rsvCondition은 예약 상태를 뜻하는 변수이다. 회원이 예약하기 전과 예약 후에는 "Not yet"이지만 트레이너가 이 회원의 예약을 승낙한 순간 "OK!"로 바뀐다. 처음에는 이 변수가 트레이너에 의해 좌지우지되는 것이므로 Trainer 멤버 변수로 적합하다 생각했다. 그런데 구현해놓고 보니 한 트레이너에게 여러 회원이 예약한 경우 rsvCondition이 통일되는 문제가 발생하였다. nana라는 이름을 가진 회원과 yoyo라는 이름을 가진 회원이 id가 101번인 kim 트레이너에게 모두 예약했다고 해보자. kim 트레이너는 nana 회원은 받지 않고 yoyo는 예약을 승낙했다. 이 경우 nana 회원의 rsvCondition은 "Not yet"이고 yoyo 회원의 rsvCondition은 "OK!"여야 하는데 둘 다 "OK!"가 돼버리는 것이다. 트레이너마다 rsvCondition을 줄 것이 아니라 회원마다 rsvCondition이 필요했다. 따라서 rsvCondition을 Member의 멤버 변수로 옮기게 되었다.

3. Member.h와 Member.cpp


(왼) 구현 전 vs. 구현 후 (오)

사실 트레이너 클래스를 구현하는 순간 회원 클래스 구현도 절반은 끝낸 셈이다. 앞서 만든 트레이너 클래스를 복붙하고 멤버로 바꾼 뒤 멤버만의 특성에 해당하는 것만 수정해주면 된다. 

 

프로그램 작성 과정을 말하자면 Main.cpp에서 메뉴의 큰 틀을 잡아놓고, Trainer 클래스와 Member 클래스 없이 구현할 수 있는 것은 다 구현해 놓았다. 그리고 Trainer 클래스를 만들면서 Main.cpp에서 Trainer 클래스를 사용하며 트레이너 메뉴를 구현했다. 마찬가지로 Member 클래스를 만들면서 main.cpp에서 Member 클래스를 사용하며 회원 메뉴를 구현했다. 

 

오늘의 느낀 점 요약

- goto문은 스파게티 코드가 될 수 있으니 사용하지 않는 것이 좋다.

- 변수 초기화는 필수! 복사 초기화 < 직접 초기화 < 유니폼 초기화 순으로 좋다.

- 생성자 본문에서 값을 할당하는 것보다 초기화 리스트를 사용하는 것이 성능이 더 우수하다.

 

GitHub - leeejihyun/Gym-Manager: 운동 관리 프로그램

운동 관리 프로그램. Contribute to leeejihyun/Gym-Manager development by creating an account on GitHub.

github.com