Notice
Recent Posts
Recent Comments
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Archives
Today
Total
관리 메뉴

SYDev

C++ Chapter 08-2 : 가상함수(Virtual Function) 본문

Programming Lang/C++

C++ Chapter 08-2 : 가상함수(Virtual Function)

시데브 2023. 7. 19. 17:49

기초 클래스의 포인터로 객체를 참조하는 경우

 우선 글을 시작하기 전에 핵심부터 말하고 시작하겠다.

 

"C++ 컴파일러는 포인터 연산의 가능성 여부를 판단할 때, 포인터의 의 자료형을 기준으로 판단하지, 실제 가리키는 객체의 자료형을 기준으로 판단하지 않는다."

 

 예를 들어서 BaseFunc 이라는 함수를 멤버로 가진 Base라는 기초 클래스와 DerivedFunc 이라는 함수를 가진 Derived라는 유도 클래스가 있다고 가정해보자. 그렇다면 다음 문장은 컴파일 에러를 일으킨다.

Base * bptr = new Derived(); 
bptr -> DerivedFunc();	//compile error

 포인터 객체를 정의하고 연산의 가능성 여부를 판단할 때, 포인터의 자료형을 기준으로 판단한다.결론적으로 포인터 형에 해당하는 클래스에 정의된 멤버에만 접근이 가능하다는 것이다. 

 

 다시 아래 문장을 살펴보자.

Base * bptr = new Derived();
Derived * dptr = bptr;	//compile error

 bptr은 자료형 Base인 포인터이기 때문에 컴파일러에서는 bptr이 가리키는 대상이 클래스 Base의 객체일수도 있다고 판단하여 두 번째 문장에서는 컴파일 에러가 발생한다. 

 

함수의 오버라이딩과 포인터 형

 이제 우리는 가상함수라는 개념에 대해서 학습할 것인데, 가상함수를 학습하기 전에 먼저 다음 예제를 살펴보자.

#include <iostream>
using namespace std;

class First
{
public:
    void MyFunc() { cout<<"FirstFunc"<<endl;}
};

class Second : public First
{
public:
    void MyFunc() { cout<<"SecondFunc"<<endl;}
};

class Third : public Second
{
public:
    void MyFunc() { cout<<"ThirdFunc"<<endl;}
};

int main(void)
{
    Third * tptr = new Third();
    Second * sptr = tptr;
    First * fptr = sptr;

    fptr ->MyFunc();
    sptr ->MyFunc();
    tptr ->MyFunc();
    delete tptr;

    return 0;
}
FirstFunc
SecondFunc
ThirdFunc

 해당 예제의 다음 문장의 구조를 이해해보자.

fptr -> MyFunc();

 Fptr은 클래스 First형 포인트이기 때문에 First에 포함된 멤버함수에만 접근할 수 있다.

sptr -> MyFunc();

 Sptr은 클래스 First의 유도 클래스 Second형 포인트이기 때문에 클래스 First와 Second의 멤버 함수 모두 호출이 가능하지만, 함수 오버라이딩으로 인해서 Second의 MyFunc이 호출되었다.

tptr -> MyFunc();

 마지막으로 tptr은 Fitrst와 Second의 유도 클래스 Third형 포인터로, 함수 오버라이딩으로 인해 Third의 멤버 함수인 MyFunc이 호출된다.

 

가상함수(Virtual Function)

 그렇다면 이제 가상함수에 대해서 알아보자. 가상함수는 함수의 선언 앞에 virtual 키워드를 붙이면 해당 함수는 가상함수가 된다. 뿐만 아니라, 해당 함수를 오버라이딩하는 함수도 가상함수가 된다.

 

 해당 내용을 다음 예제를 통해 확인해보자

#include <iostream>
using namespace std;

class First
{
public:
    virtual void MyFunc() { cout<<"FirstFunc"<<endl;}   //가상함수 선언
};

class Second : public First
{
public:
    void MyFunc() { cout<<"SecondFunc"<<endl;}
};

class Third : public Second
{
public:
    void MyFunc() { cout<<"ThirdFunc"<<endl;}
};

int main(void)
{
    Third * tptr = new Third();
    Second * sptr = tptr;
    First * fptr = sptr;

    fptr ->MyFunc();
    sptr ->MyFunc();
    tptr ->MyFunc();
    delete tptr;

    return 0;
}
ThirdFunc
ThirdFunc
ThirdFunc

 이전 예제와 바뀐 점은 클래스 First의 멤버함수 MyFunc 앞의 virtual 선언 뿐이다. 하지만 우리는 여기서 virtual 선언된 함수는 포인터 변수가 실제로 가리키는 객체를 참조하여 함수를 호출하는 것과, virtual 선언 한 번으로 해당 함수를 오버라이딩하는 모든 함수가 가상함수가 된 것을 알 수 있다. 

 

'오렌지미디어 급여관리 확장성 문제'의 완전한 해결

 그렇다면 이제 가상함수 개념을 이용하여 해결하지 못한 문제인 EmployeeHandler의 주석 처리된 부분을 고쳐보자.

#include <iostream>
#include <cstring>
using namespace std;

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 //가상함수 선언
    { }
};

class TemporaryWorker : public Employee
{
private:
    int workTime;     
    int payPerHour;     
public:
    TemporaryWorker(char * name, int pay)
        : Employee(name), workTime(0), payPerHour(pay)
    { }
    void AddWorkTime(int time) 
    {
        workTime += time;
    }
    int GetPay() const         
    {
        return workTime*payPerHour;
    }
    void ShowSalaryInfo() const
    {
        ShowYourName();
        cout<<"salary : "<<GetPay()<<endl<<endl;
    }
};

class PermanentWorker : public Employee
{
private:
    int salary; 
public:
    PermanentWorker(char * name, int money)
        : Employee(name),salary(money)
    { }
    int GetPay() const
    {
        return salary;
    }
    void ShowSalaryInfo() const
    {
        ShowYourName();
        cout<<"salary : "<<GetPay()<<endl<<endl;
    }
};

class SalesWorker : public PermanentWorker
{
private:
    int salesResult;     
    double bonusRatio;   
public:
    SalesWorker(char * name, int money, double ratio)
        : PermanentWorker(name, money), salesResult(0), bonusRatio(ratio)
    { }
    void AddSalesResult(int value)
    {
        salesResult += value;
    }
    int GetPay() const
    {
        return PermanentWorker::GetPay()    
                    +(int)(salesResult*bonusRatio);
    }
    void ShowSalaryInfo() const
    {
        ShowYourName();
        cout<<"salary : "<<GetPay()<<endl<<endl;    
    }
};

class EmployeeHandler
{
private:
    Employee* empList[50];
    int empNum;
public:
    EmployeeHandler() : empNum(0)
    { }
    void AddEmployee(Employee* emp)
    {
        empList[empNum++]=emp;
    }
    void ShowAllSalaryInfo() const
    {
        for(int i=0; i<empNum; i++)
            empList[i]->ShowSalaryInfo(); 
    }
    void ShowTotalSalaryInfo() const
    {
        int sum = 0;
        for(int i=0; i<empNum; i++)
            sum += empList[i]->GetPay();  
        cout<<"salary sum : "<<sum<<endl;      
    }
    ~EmployeeHandler()
    {
        for(int i=0; i<empNum; i++)
            delete empList[i];
    }
};

int main(void)
{
    // 직원 관리를 목적으로 설계된 컨트롤 클래스의 객체 생성
    EmployeeHandler handler;

    // 정규직 등록
    handler.AddEmployee(new PermanentWorker("KIM", 1000));
    handler.AddEmployee(new PermanentWorker("LEE", 1500));

    // 임시직 등록
    TemporaryWorker * alba = new TemporaryWorker("Jung", 700);
    alba -> AddWorkTime(5); 
    handler.AddEmployee(alba);  
    // AddEmployee는 매개변수를 Employee형 포인터 변수로 선언하여 받지만, 가상함수이기 때문에 포인터가 가리키는 객체의 자료형에 영향을 받음.           

    // 영업직 등록
    SalesWorker * seller = new SalesWorker("Hong", 1000, 0.1);
    seller->AddSalesResult(7000); 
    handler.AddEmployee(seller);
    // AddEmployee는 매개변수를 Employee형 포인터 변수로 선언하여 받지만, 가상함수이기 때문에 포인터가 가리키는 객체의 자료형에 영향을 받음.

    // 이번 달에 지불해야 할 급여의 정보
    handler.ShowAllSalaryInfo();

    // 이번 달에 지불해야 할 급여의 총합
    handler.ShowTotalSalaryInfo();

    return 0;
}
name : KIM
salary : 1000

name : LEE
salary : 1500

name : Jung
salary : 3500

name : Hong
salary : 1700

salary sum : 7700

 전 예제와 바뀐 점은 클래스 Employee에 추가된 두 개의 가상함수 말고는 없지만, 이제야 주석 없이도 정상적으로 실행 결과가 나타난다. 위 예제에도 써있듯이, AddEmployee는 매개변수를 Employee형 포인터 변수로 선언하여 받지만, 가상함수이기 때문에 포인터가 가리키는 객체의 자료형에 영향을 받는다. 따라서, 각 객체에 맞는 함수 AddEmployee가 적절하게 호출되었다.

 

순수 가상함수(Pure Virtual Function)와 추상 클래스(Abstract Class)

 위 예제에서 Employee 클래스를 살펴보면 기초 클래스로서 의미만 가질 분, 객체의 생성을 목적으로 정의된 클래스가 아니다. 따라서 해당 클래스의 객체 생성을 문법적으로 막기 위해서, 가상함수를 다음과 같이 '순수 가상함수'로 선언하는 것이 좋다. 

class Employee
{
private:
    char name[100];
public:
    Employee(char * name) { . . . . }
    void ShowYourName() const { . . . . }
    virtual int GetPay() const = 0;		//순수 가상함수        
    virtual void ShowSalaryInfo() const = 0;	//순수 가상함수
};

 여기서 '순수 가상함수'란 함수의 몸체가 정의되지 않은 함수를 의미한다. 그리고 이를 표현하기 위해서 위 예제와 같이 함수에 '0의 대입'을 표시한다. 이렇게 순수 가상함수가 포함된 클래스는 완전하지 않은 클래스가 되어 객체 생성이 문법적으로 제한된다. 또한, 이렇듯 하나 이상의 멤버함수를 순수 가상함수로 선언한 클래스를 '추상 클래스(abstract class)'라 한다.

 

다형성(Polymorphism)

 지금까지 설명한 가상함수의 호출 관계에서 보인 특성을 가리켜 '다형성'이라 한다. 그리고 이는 객체지향을 설명하는데 있어서 매우 중요한 요소이다.

 

 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(void)
{
    First * ptr = new First();
    ptr -> SimpleFunc();	//아래에 동일한 문장 존재
    delete ptr;

    ptr = new Second(); 
    ptr -> SimpleFunc();	//위에 동일한 문장 존재
    delete ptr;

    return 0;
}
First
Second

 위 예제의 main 함수에는 같은 문장이 두 번 등장한다. 심지어 ptr은 동일한 포인터 변수이다. 그럼에도 불구하고 실행 결과는 다르다. 포인터 변수가 참조하는 객체의 자료형이 다르기 때문이다. 이것이 C++에서의 '다형성'의 예시이다.

 

 

 + ptr은 그저 주소를 저장하는 변수일 뿐이다. 따라서, delete ptr을 진행해도 변수 ptr이 사라지는 것은 아니다. ptr이 가리키는 주소에는 변화가 없고 할당된 메모리 공간만 해제될 뿐이다.

 


출처 : 윤성우, <윤성우의 열혈 C++ 프로그래밍>, 오렌지미디어, 2010.05.12