티스토리 뷰

C++

[개념] C++의 다형성 - virtual 함수

다음김 2022. 6. 27. 22:57

 

해당 글은 '윤성우의 열혈 C++ 프로그래밍'을 참고하여 작성함.

 

 

C++에서 상속 파트를 공부하다 보면 기존에 자바에 익숙한 사람이라면 뭐지...? 싶은 부분이 있다.
자식 클래스 객체를 부모 클래스 객체에 할당하고 해당 객체의 메소드를 호출하는 경우, 자바라면 자식 클래스에 정의한 메소드가 호출된다. 반면 C++은 부모 클래스(기초 클래스)에 정의한 멤버 함수가 호출된다. 일단 해당 상황을 코드로 정리해보자.

 

class Parent {
    void print() {
        System.out.println("Parent");
    }
}

class Child extends Parent {
    @Override
    void print() {
        System.out.println("Child");
    }

    void print2() {
        System.out.println("Child2");
    }
}

class Main {  
    public static void main(String args[]) { 
        Parent obj = new Child();
        obj.print(); //"Child" 출력
        //obj.print2(); //컴파일 오류!
    } 
}

이 경우 obj 객체는 Parent 자료형이기 때문에 obj 객체로 print2 메소드 호출시 'print2 심볼을 찾을 수 없다'는 컴파일 오류가 발생한다. 반면 Parent 클래스에 정의되어 있는 print 메소드는 호출 가능하나 Parent 클래스에 정의되어있는 print 메소드가 아닌 실제 객체 자료형 Child 클래스에 정의되어있는 print 메소드가 호출되어 "Child"가 출력된다.



비슷한 코드를 C++로 구현하면 다음과 같다.

#include <iostream>
using namespace std;

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

class Derived : public Base {
public:
    void print() {
        cout << "Derived" << endl;
    }
    void print2() {
        cout << "Derived2" << endl;
    }
};

int main() {
    Base* obj = new Derived();
    obj->print(); //"Base" 출력
    //obj->print2();

    return 0;
}

유도 클래스 객체를 가리키는 기초 클래스 자료형의 포인터 obj 또한 print2 멤버함수 호출 시 'print2 멤버이름을 찾을 수 없다'는 메시지로 컴파일 오류가 발생한다. 반면 print 멤버함수 호출 시에는 자바와 달리 Derived 클래스에 정의된 함수가 아닌 Base 클래스에 정의된 함수가 호출된다. 즉 기초 클래스(부모 클래스)에 정의된 함수가 호출되는 것이다. 이는 바로 obj 포인터의 자료형이 Base 클래스이기 때문이다.

 

하지만 객체지향의 원리 중 하나인 다형성을 만족하기 위해서 실제 객체 자료형에 따라 호출되는 멤버함수가 다르길 원하는 상황이 있다. 이때 바로 virtual 키워드로 가상함수를 선언하면 된다. 가상함수를 통해 자바에서와 같이 실제 객체 생성을 위해 호출된 생성자의 클래스에 정의된 멤버함수가 호출될 수 있다.



위 예제에서 Base 클래스 내 print 멤버함수를 다음과 같이 정의하면 obj->print(); 호출 시 "Derived"가 출력될 것이다. (기초 클래스에 선언된 가상함수는 유도 클래스에 그대로 상속, 따라서 유도 클래스에서 가상함수 오버라이딩 시 virtual 키워드 생략 가능)

class Base {
public:
    virtual void print() {
        cout << "Base" << endl;
    }
};




번외

int main() {
    Derived obj2;
    Base obj = obj2;
    obj.print(); //항상 "Base" 출력
    return 0;
}

위와 같이 Derived 객체를 Base 객체에 대입하여 초기하하면 print 함수가 아무리 가상함수로 선언되어있다고 하더라도 항상 Base 클래스에 정의된 print 멤버함수가 호출된다. 그 이유는 조금만 생각하면 알 수 있는데 Base 객체 초기화 시 Base 클래스의 복사 생성자가 호출되어 obj의 실제 객체는 Derived 클래스형이 아닌 Base 클래스형이기 때문이다.

 

여기서 복사 생성자의 인자는 Base 클래스로 선언되어있기 때문에 매개변수로 'Base 클래스 또는 Base 클래스를 직접 또는 간접적으로 상속하는 클래스'를 전달받을 수 있다. 즉 인자로 Derived 객체가 전달된 Base의 복사 생성자가 호출되어 Base 객체가 생성된 것이다.




순수 가상함수, 가상 소멸자

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

class Bag {
private:
    char* model;
    int price; 

public:
    Bag(char* model, int price) :price(price) {
        this->model = new char[strlen(model) + 1];
        strcpy(this->model, model);
    }
    ~Bag() {
        cout << "~Bag()" << endl;
        delete []model;
    }
    virtual void showSecret() const {}
};

class Carrier : public Bag {
private:
    char* pwd;
public: 
    Carrier(char* model, int price, char* pwd) : Bag(model, price) {
        this->pwd = new char[strlen(pwd) + 1];
        strcpy(this->pwd, pwd); 
    }
    ~Carrier() {
        cout << "~Carrier()" << endl;
        delete []pwd;
    }
    void showSecret() const { 
        cout << pwd << endl;
    }
};

class LuxuryBag : public Bag {
private:
    char* serialNum;
public:
    LuxuryBag(char* model, int price, char* serialNum) : Bag(model, price) {
        this->serialNum = new char[strlen(serialNum) + 1];  
        strcpy(this->serialNum, serialNum);
    }
    ~LuxuryBag() {
        cout << "~LuxuryBag()" << endl; 
        delete []serialNum;
    }
    void showSecret() const {
        cout << serialNum << endl; 
    }
};

int main() {
    Bag* bag1 = new Carrier("model1", 10000, "1234");
    Bag* bag2 = new LuxuryBag("model2", 1000000, "abcd1234");
    bag1->showSecret();
    bag2->showSecret();

    delete bag1;
    delete bag2;
    return 0;
}

실행결과

1234
abcd1234
~Bag()
~Bag()



위 코드에는 2가지 문제가 있다. 먼저 Bag 클래스는 유도 클래스 Carrier, LuxuryBag에게 공통 속성을 상속하기 위한 역할만을 할 뿐 실제 객체를 생성하지 않는다. 즉 오버라이딩된 함수 showSecretBag 객체를 통해 호출되지 않아 빈 몸체의 함수로만 정의되어 있다. 이 경우 Bag* bag = new Bag();는 잘못된 코드여야 하나 컴파일 오류 없이 잘 실행된다. 문제 해결을 위한 방법으로는 가상함수 showSecret을 순수 가상함수로 선언하여 Bag를 추상 클래스로 만드는 것이다. 아래 코드와 같이 수정하면 Bag 객체 생성 시 컴파일 오류가 발생한다. (순수 가상함수가 하나라도 포함된 클래스는 불완전하므로 추상 클래스이다.)

virtual void showSecret() = 0; //순수 가상함수 

//......

Bag* bag = new Bag(); //컴파일 오류!



두번째 문제로는 Bag의 소멸자만이 호출되어 Carrier, LuxuryBag의 생성자에서 할당한 메모리 공간을 해제하지 못해 메모리 누수 문제가 발생한다는 것이다. 해당 문제 해결을 위해서는 실제 객체 클래스의 소멸자를 호출할 수 있도록 소멸자에도 virtual 키워드를 붙여주면 된다. 이 경우 클래스 상속계층의 최하위 클래스의 소멸자 → 최상위 클래스의 소멸자가 순서대로 호출된다. (기초 클래스 소멸자의 virtual 키워드는 유도 클래스로 상속)

virtual ~Bag() {
    delete []name;
}

실행결과

1234
abcd1234
~Carrier()
~Bag()
~LuxuryBag()
~Bag()

가상 소멸자를 통해 Carrier, LuxuryBag에서 할당한 메모리 공간 pwd, serialNum 또한 안전하게 해제되는 것을 볼 수 있다.



자바에서의 모든 메소드는 기본적으로 C++의 가상함수라고 생각해도 무방할 것 같다. 다만 C++에서는 C의 특성을 남기고자 virtual 키워드로 가상함수를 선언할 수 있는 선택권을 남겨놓은 것이 아닌가 싶다. 아무튼 객체지향에서의 상속의 다형성을 코드에 구현하기 위해 virtual 키워드를 적절히 사용하면 좋을 것 같다.




끝.




공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
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
글 보관함