read

스무번째 열혈강의


english

이제 지난번 포스팅에 이어서, 순수 가상함수와 추상클래스에 대해 진행해보자.

class Employee{
private:
    char name[100];
public:
    Employee(char* name){
        strcpy(this->name, name);
    }
    void ShowYourName() const {
        cout << "name : " << name << endl;
    }
    virtual int GetPay() const {
        return 0;
    }
    virtual void ShowSalaryInfo() const{

    }
};

위 클래스는 조금 고쳐볼 부분이 있는데, 그 이유를 알아보자. 먼저 이 클래스는 기초 클래스로서만 의미를 가지고, 객체의 생성을 목적으로 정의된 것은 아니다. 즉, 클래스 중에서는 객체생성을 목적으로 정의되지 않은 클래스도 존재한다. 그럼 다음과 같은 문장이 되버리면 그것은 개발자의 실수가 된다.

Employee* emp = new Employee("Lee Dong Sook");

그러나 문법적인 문제는 있지 않다. 그럼 컴파일러에 의해 발견이 되지 않는데, 여기서 순수 가상함수 로 선언하여 객체의 생성을 문법적으로 막는 것이 좋다.

class Employee{
private:
    char name[100];
public:
    Employee(char* name){
        strcpy(this->name, name);
    }
    void ShowYourName() const {
        cout << "name : " << name << endl;
    }
    virtual int GetPay() const  = 0; //순수 가상함수
    virtual void ShowSalaryInfo() const = 0; //순수 가상함수
};

순수 가상함수는 함수의 몸체가 정의되지 않은 함수를 의미한다. 그리고 이를 표현하기 위해 0을 대입한다. 그런데 이것은 실제로 0을 대입하는 의미가 아니고, 명시적으로 몸체를 정의하지 않았음을 컴파일러에게 알리는 것이다. 따라서 컴파일러는 이 부분에서 함수의 몸체가 정의되지 않았다고 오류를 내리지 않는다. 그런데 Employee 클래스는 순수 가상함수를 지닌, 완전하지 않은 클래스가 되기 때문에 다음과 같이 객체를 생성하려하면 오류가 뜬다.

Employee* emp = new Employee("Lee Dong Sook");

그럼 2가지 새로운 이점이 생겼는데, 하나는 잘못된 객체 생성을 막았다는 것과, 또다른 하나는 Employee 클래스의 GetPay 함수와 ShowSalaryInfo 함수는 유도 클래스에 정의된 함수가 호출되게끔 돕는데 의미가 있을 뿐, 실제로 실행이 되는 함수는 아니었다. 이를 좀더 명확하게 표기할 수 있어졌다.

그럼 이런 클래스, 즉 하나 이상의 멤버함수를 순수 가상함수로 선언한 클래스를 바로 추상 클래스 라고 한다. 즉, 완전하지 않고 객체생성이 불가능한 클래스 라는 의미가 된다.


다형성


지금까지 가상함수의 호출관계에서 보인 특성을 다형성 이라고 한다. 그리고 이건 객체지향을 설명하는데 있어서 매우 중요하며, 모습은 같은데 형태는 다르다. 라고 말할 수 있다.

즉, C++로 옮겨보면, 문장은 같은데 결과는 다르다.가 될 수 있다. 그럼 다음 예제를 보고 충분히 이해해 보자.

#include <iostream>

using namespace std;

class First{
public:
    virtual void SimpleFunc(){ cout << "First" << endl; }
};

class Second: public First{
public:
    virtual void SimpleFunc(){ cout << "Second" << endl; }
};

int main(){
    First* ptr = new First();
    ptr->SimpleFunc();
    delete ptr;

    ptr=new Second();
    ptr->SimpleFunc();
    delete ptr;
    return 0;
}

ptr->SimpleFunc(); 이 문장은 두 번 등장하는데, ptr은 동일한 포인터 변수임에도 불구하고 실행결과는 다르다. 이는 포인터 변수 ptr이 참조하는 객체의 자료형이 다르기 때문이다. 이게 바로 C++의 다형성의 예다.


가상 소멸자와 참조자


가상함수 말고도 virtual 키워드를 붙여줘야 할 대상이 하나 더 있다. 그게 바로 소멸자이다.

virtual로 선언되 소멸자를 가상 소멸자 라고 하는데, 예제를 이용해 차근히 배워보자.

#include <iostream>

using namespace std;

class First{
private:
    char* strOne;
public:
    First(char* str){
        strOne = new char[strlen(str) + 1];
    }
    ~First(){
        cout << "~First()" << endl;
        delete []strOne;
    }
};

class Second: public First{
private:
    char* strTwo;
public:
    Second(char* str1, char* str2) : First(str1){
        strTwo = new char[strlen(str2) + 1];
    }
    ~Second(){
        cout << "~Second()" << endl;
        delete []strTwo;
    }
};

int main(){
    First* ptr = new Second("simple", "complex");
    delete ptr;
    return 0;
}

위를 실행해볼 때, 소멸자가 어떻게 되야 하는지 생각해보자.

ptr을 삭제할때 First 포인터를 삭제하므로 ~First 소멸자만 출력이 나온다. 그러나 Second 클래스를 동시에 소멸시켜야 하는 게 맞다.

즉, 메모리 누수가 일어난다. 즉 포인터 변수의 자료형에 관계없이 모든 소멸자가 호출되어야 하는데, 이를 위해서는 소멸자에 virtual을 선언해야 한다.

virtual ~First(){
    cout << "~First()" << endl;
    delete []strOne;
}

가상함수와 마찬가지로 소멸자도 상속의 계층구조상 맨 위에 존재하는 기초 클래스의 소멸자만 virtual로 선언하면 유도 클래스도 모두 가상 소멸자가 된다. 또한 가상 소멸자가 호출되면 상속의 계층구조상 맨 아래에 존재하는 유도 클래스의 소멸자가 대신 호출되고 기초 클래스의 소멸자가 순차적으로 호출이 된다.


참조자의 참조 가능성


과연 참조자는 어떻게 될지 알아보자.

이전 포스팅에서 말했던 부분이 있다.

C++에서, AAA형 포인터 변수는 AAA 객체 또는 AAA를 직접 혹은 간접적으로 상속하는 모든 객체를 가리킬 수 있다.(객체의 주소 값을 저장할 수 있다.)

이건 참조자 에서도 그대로 적용된다.

C++에서, AAA형 참조자는 AAA 객체 또는 AAA를 직접 혹은 간접적으로 상속하는 모든 객체를 참조할 수 있다.

또한, 각 형태에 대한 포인터 변수에 따라 MyFunc이 호출됨 다고 했던 적이 있는데, 이 역시 참조자에 그대로 적용된다.

그럼 역시나 예제로 알아보자.

#include <iostream>

using namespace std;

class First{
public:
    void FirstFunc(){cout << "FirstFunc()" << endl;}
    virtual void SimpleFunc(){cout << "First`s SimpleFunc()" << endl;}
};

class Second: public First{
public:
    void SecondFunc(){cout << "SecondFunc()" << endl;}
    virtual void SimpleFunc(){cout << "Second`s SimpleFunc()" << endl;}
};

class Third: public Second{
public:
    void ThirdFunc(){cout << "ThirdFunc()" << endl;}
    virtual void SimpleFunc(){cout << "Third`s SimpleFunc()" << endl;}
};

int main(){
    Third obj;
    obj.FirstFunc();
    obj.SecondFunc();
    obj.ThirdFunc();
    obj.SimpleFunc();

    Second& sref = obj; //obj는 Second를 상속하는 Third 객체이므로 Second형 참조자로 참조가 가능하다.
    sref.FirstFunc(); //컴파일러는 참조자의 자료형을 가지고 함수의 호출가능성을 판단하므로, Third는 불가능
    sref.SecondFunc();
    sref.SimpleFunc(); // SimpleFunc는 가상함수다. 따라서 Third에 정의된 SimpleFUnc가 호출된다.(obj 는 Third 다.)

    First& fref = obj;
    fref.FirstFunc();
    fref.SimpleFunc();

    return 0;
}

참조자를 볼 때, 어떻게 참조가 가능한지 생각해보자.

출력을 보면,

FirstFunc()
SecondFunc()
ThirdFunc()
Third`s SimpleFunc()
FirstFunc()
SecondFunc()
Third`s SimpleFunc()
FirstFunc()
Third`s SimpleFunc()

그럼 이해가 됬을 것이라 생각한다.

그럼 한가지만 더 보도록 하자.

void GoodFunction(const First& ref)

여기서 First 객체 또는 First를 직접혹은 간접적으로 상속하는 클래스의 객체가 인자의 대상이다.

인자로 전달되는 객체의 실제 자료형에 상관없이 함수 내에서는 First 클래스에 정의된 함수만 호출 할 수 있다.

는 사실을 잘 생각해보자.

이제 다음으론 꾸준히 진행해온, OOP 단계별 프로젝트 6단계를 진행할 것이다.


OOP 단계별 프로젝트 06단계


여기서는 이전 프로젝트에 두 클래스를 추가하려 한다.

  • NormalAccount
    • 보통예금계좌
  • HighCreditAccount
    • 신용신뢰계좌

앞서 정의한 Account 클래스는 이자와 관련된 내용이 없다. 그런데 일반 사용자가 이용하는 예금은 적게나마 이자가 지급되는 경우가 많음로 두 클래스를 만들 것이다.

보통예금계좌는 흔히 사용하는 것, 신용신뢰계좌는 신용도가 높은 고객에게 개설을 허용하는 높은 이율 계좌이다.

보통예금계좌를 의미하는 NormalAccount 클래스는 객체의 생성과정에서(생성자) 이율정보(이자비율의 정보)를 등록할 수 있게 할 것이다 그러나 신용신뢰계좌를 의미하는 HighCreditAccount 클래스는 다음 특성을 가진다.

  • NormalAccount 클래스와 마찬가지로 객체 생성시 기본이율을 등록할 수 있다.
  • 고객의 신용등급을 A, B, C로 나누고 계좌개설시 이 정보를 등록한다.
  • A, B, C 등급별로 각각 기본 이율에 7%, 4%, 2%의 이율을 추가로 제공한다.

사실 이자는 시간이 지나야 발생하지만, 편의상 입금시 이자가 더해지는 것으로 하자. 또한 다음 조건을 가진다.

  • 계좌개설 과정에서 초기 입금되는 금액은 이자가 계산되지 않는다.
  • 계좌개설 후 별도의 입금과정을 거칠 때에만 이자가 발생하는 것으로 간주한다.
  • 이자의 계산과정에서 발생하는 소수점 이하의 금액은 무시한다.

그리고 컨트롤 클래스인 AccountHandler 클래스에는 큰 변화가 없어야 한다. 단, 계좌의 종류가 늘어난 만큼 메뉴의 선택과 데이터의 입력과정에서의 불가피한 변경은 허용한다.

아무리 공부를 했다하더라도 바로 구현하기에 어려울 수 있다. 코드 분량도 많아질 것이다. 그러나 꼭 한번 해보고 답변을 보는 걸 추천한다.

코드가 길기 때문에 다음엔 아마 파일로 정리할 것이다.

#include <iostream>

using namespace std;
const int NAME_LEN = 20;

enum {MAKE=1, DEPOSIT, WITHDRAW, INQUIRE, EXIT};

enum {LEVEL_A = 7, LEVEL_B = 4, LEVEL_C = 2};

enum {NORMAL = 1, CREDIT = 2};

class Account {
private:
    int accID;
    int balance;
    char* cusName;

public:
    Account(int ID, int money, char* name);
    Account(const Account& ref);

    int getAccID() const;
    virtual void deposit(int money);
    int withDraw(int money);
    void showAccInfo() const;
    ~Account();
};

Account::Account(int ID, int money, char * name)
        : accID(ID), balance(money)
{
    cusName=new char[strlen(name)+1];
    strcpy(cusName, name);
}

Account::Account(const Account & ref)
        : accID(ref.accID), balance(ref.balance)
{
    cusName=new char[strlen(ref.cusName)+1];
    strcpy(cusName, ref.cusName);
}

int Account::getAccID() const { return accID; }

void Account::deposit(int money)
{
    balance+=money;
}

int Account::withDraw(int money)
{
    if(balance<money)
        return 0;

    balance-=money;
    return money;
}

void Account::showAccInfo() const {
    cout << "계좌ID : " << accID << endl;
    cout << "이름 : " << cusName << endl;
    cout << "잔액 : " << balance << endl;
}

Account::~Account() {
    delete []cusName;
}

class NormalAccount : public Account
{
private:
    int interRate;
public:
    NormalAccount(int ID, int money, char * name, int rate)
            : Account(ID, money, name), interRate(rate)
    {  }

    virtual void deposit(int money)
    {
        Account::deposit(money);
        Account::deposit(money*(interRate/100.0));
    }
};

class HighCreditAccount : public NormalAccount
{
private:
    int specialRate;
public:
    HighCreditAccount(int ID, int money, char * name, int rate, int special)
            : NormalAccount(ID, money, name, rate), specialRate(special)
    {  }

    virtual void deposit(int money)
    {
        NormalAccount::deposit(money);
        Account::deposit(money*(specialRate/100.0));
    }
};

class AccountHandler
{
private:
    Account * accArr[100];
    int accNum;

public:
    AccountHandler();
    void showMenu(void) const;
    void makeAccount(void);
    void depositMoney(void);
    void withDrawMoney(void);
    void showAllAccInfo(void) const;
    ~AccountHandler();

protected:
    void makeNormalAccount(void);
    void makeCreditAccount(void);
};

void AccountHandler::showMenu() const {
    cout << " ===== MENU ===== " << endl;
    cout << "1. 계좌개설 " << endl;
    cout << "2. 입금 " << endl;
    cout << "3. 출금 " << endl;
    cout << "4. 계좌정보 전체 출력 " << endl;
    cout << "5. 종료 " << endl;
}

void AccountHandler::makeAccount() {
    int sel;
    cout << "[계좌종류선택" << endl;
    cout << "1. 보통예금계좌" << endl;
    cout << "2. 신용신뢰계좌" << endl;
    cout << "선택 : ";
    cin >> sel;

    if(sel == NORMAL)
        makeNormalAccount();
    else
        makeCreditAccount();
}

void AccountHandler::makeNormalAccount() {
    int id;
    char name[NAME_LEN];
    int balance;
    int interRate;

    cout << "[보통예금계좌 개설]" << endl;
    cout << "계좌 ID : "; cin >> id;
    cout << "이름 : "; cin >> name;
    cout << "입금액 : "; cin >> balance;
    cout << "이자율 : "; cin >> interRate;
    cout << endl;

    accArr[accNum++] = new NormalAccount(id, balance, name, interRate);
}

void AccountHandler::makeCreditAccount() {
    int id;
    char name[NAME_LEN];
    int balance;
    int interRate;
    int creditLevel;

    cout << "[신용신뢰계좌 개설]" << endl;
    cout << "계좌 ID : "; cin >> id;
    cout << "이름 : "; cin >> name;
    cout << "입금액 : "; cin >> balance;
    cout << "이자율 : "; cin >> interRate;
    cout << "신용등급(1toA, 2toB, 3toC) : "; cin >> creditLevel;
    cout << endl;

    switch(creditLevel){
        case 1:
            accArr[accNum++] = new HighCreditAccount(id, balance, name, interRate, LEVEL_A);
            break;
        case 2:
            accArr[accNum++] = new HighCreditAccount(id, balance, name, interRate, LEVEL_B);
            break;
        case 3:
            accArr[accNum++] = new HighCreditAccount(id, balance, name, interRate, LEVEL_C);
    }
}

void AccountHandler::depositMoney() {
    int money;
    int id;
    cout << "[입 금]" << endl;
    cout << "계좌ID : "; cin >> id;
    cout << "입금액 : "; cin >> money;

    for(int i = 0; i < accNum; i++){
        if(accArr[i]->getAccID() == id){
            accArr[i]->deposit(money);
            cout << "입금완료" << endl;
            return;
        }
    }
    cout << "유효하지 않은 ID 입니다." << endl << endl;
}

void AccountHandler::withDrawMoney() {
    int money;
    int id;
    cout << "[출 금]" << endl;
    cout << "계좌ID : "; cin >> id;
    cout << "출금액 : "; cin >> money;
    for(int i = 0; i < accNum; i++){
        if(accArr[i]->getAccID() == id){
            if(accArr[i]->withDraw(money) == 0){
                cout << "잔액부족" << endl << endl;
                return;
            }

            accArr[i]->withDraw(money);
            cout << "출금완료" << endl;
            return;
        }
    }
    cout << "유효하지 않은 ID 입니다." << endl << endl;
}

AccountHandler::AccountHandler() : accNum(0) {}

void AccountHandler::showAllAccInfo() const {
    for(int i = 0; i < accNum; i++){
        accArr[i]->showAccInfo();
        cout << endl;
    }
}

AccountHandler::~AccountHandler() {
    for(int i = 0; i < accNum; i++){
        delete accArr[i];
    }
}

int main(){
    AccountHandler manager;
    int choice;

    while(1){
        manager.showMenu();
        cout << "선택 : ";
        cin >> choice;
        cout << endl;

        switch(choice){
            case MAKE:
                manager.makeAccount();
                break;
            case DEPOSIT:
                manager.depositMoney();
                break;
            case WITHDRAW:
                manager.withDrawMoney();
                break;
            case INQUIRE:
                manager.showAllAccInfo();
                break;
            case EXIT:
                return 0;
            default:
                cout << "Illigal selection.." << endl;
        }
    }
    return 0;
}
Blog Logo

구찌


Published

Image

구찌의 나도한번 해블로그

구찌의 개발 블로그 입니다.

Back to Overview