본문 바로가기
c++

[C++]가상함수(Virtual Function), 다형성(Polymorphism)

by goblin- 2023. 6. 9.

 

class Base{
public:
	void BaseFunc() {cout<<"Base Function"<<endl; }
};

class Derived : public Base{
public:
	void DerivedFunc() { cout<<"Derived Function"<<endl;}
};

int main(void){
    Base *bptr = new Derived();		//컴파일 OK!
    bptr->DerivedFunc();			//컴파일 Error!
    ...
}

컴파일 에러 나는 부분 btpr->DerivedFunc(); 은 "DerivedFunc는 Base 클래스의 멤버가 아닙니다."라는 메시지를 전달하면서 컴파일 에러를 발생시킨다. 왜냐하면 bptr이 Base형 포인터이기 때문이다.

 

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

 

int main(void){
    Derived *dptr=new Derived();		//컴파일 OK!
    Base *bptr = dptr;					//컴파일 OK!
}

dptr은 Deirved 클래스의 포인터 변수니까, 이 포인터가 가리키는 객체는 Base 클래스를 직접 혹은 간접적으로 상속하는 객체이다. 따라서 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 은 First형 포인터  -> First 클래스에 정의된 Myfunc함수 호출

sptr 은 Second형 포인터 -> First,Second 마지막에 오버로딩한 클래스의 Myfunc함수 호출

tptr 은 Third형 포인터 -> First,Second,Third 마지막에 오버로딩한 클래스의 Myfunc함수 호출

 

가상함수(Virtual Function)

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

가상함수의 선언은 virtual 키워드의 선언을 통해서 이뤄진다. 그리고 이렇게 가상함수가 선언되고 나면, 이 함수를 오버라이딩 하는 함수도 가상함수가 된다. 따라서 위와 같이 FIrst 클래스의 MyFunc 함수가 virtual로 선언되면, 이를 오버라이딩 하는 Second 클래스의 MyFunc 함수도, 그리고 이를 오버라이딩 하는 Third 클래스의 MyFunc 함수도 가상함수가 된다.

 

#include <iostream>
using namespace std;

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

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

class Third: public Second{
public:
    virtual 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

함수 앞에 virtual을 입력하여 가상함수로 선언하면, 해당 함수호출 시, 포인터의 자료형을 기반으로 호출대상을 결정하지 않고, 포인터 변수가 실제로 가리키는 객체를 참조하여 호출의 대상을 결정한다.  포인터의 자료형은 모두 다르지만 가리키는 객체는 Third로 모두 같다 따라서 Myfunc() 함수는 모두 Third에서 가져오게 된다.

 

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

 

클래스 중에서는 객체생성을 목적으로 정의되지 않는 클래스도 존재한다. 예를 들어 Employee 클래스 같은 경우 하위 클래스를 만들어 상속시키는 역할만하고 직접적으로 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의 대입을 표시한다. 이것은 0의 대입을 의미하는 게 아니고, 명시적으로 몸체를 정의하지 않았음을 컴파일러에게 알리는 것이다. 따라서 컴파일러는 이 부분에서 함수의 몸체가 정의되지 않았다고 컴파일 오류를 일으키지 않는다.  그러나 Employee 클래스는 순수 가상함수를 지닌, 완전하지 않은 클래스가 되기 때문에 다음과 같이 객체를 생성하려 들면 컴파일 에러가 발생한다.

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

가상클래스를 사용함으로써 잘못된 객체의 생성을 막을 수 있었고 Employee 클래스의 GetPay 함수와 ShowSalaryInfo 함수는 유도 클래스에 정의된 함수가 호출되게끔 돕는데 의미가 있었을 뿐, 실제로 시행이 되는 함수는 아니었는데, 이를 보다 명확히 명시하는 효과도 얻게 되었다. 

하나 이상의 멤버함수를 순수 가상함수로 선언한 클래스를 가리켜 추상 클래스(abstract class)라 한다. (객체생성이 불가능한 클래스)

 

다형성(Polymorphism)

가상함수의 호출관계에서 보인 특성을 가리켜 다형성이라 한다. 다형성(polymorphism)이란 동질이상을 의미한다.

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

clas 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;
}

ptr -> SimpleFunc(); 은 두 번 실행되었지만 각자 객체의 자료형이 다르기 때문에 실행결과가 달라진다. 이것이 다형성이다.