C++ 다형(polymorhphic)과 가상 멤버 함수(virtual member function)

프로그래밍/C++ 2019.10.18 댓글 Plorence

다형

파생 클래스에 대해 하는 행동이 기초 클래스에 대해 하는 행동과 다른 멤버 함수가 요구되는 상황이 있을 것입니다.(호출하는 객체에 따라 행동이 달라짐.)

즉 호출하는 객체에 따라 멤버 함수의 행동이 달라질 수 있습니다.

처한 상황에 따라 멤버 함수가 여러 가지 다른 행동을 할 수 있기 때문에, 여러 가지 형식을 가지고 있다는 의미에서 그러한 복잡한 행동을 다형이라고 부릅니다.

 

가상 멤버 함수(virtual method)

먼저 기초 클래스의 멤버 함수를 파생 클래스에서 재정의 하는 방법은 virtual 키워드를 사용하는 것입니다.

재정의할 때는 함수 시그니처가 동일해야 합니다.

간단한 예시를 들어보면 Person이라는 클래스를 정의하고 각각 Man, Girl이라는 클래스도 정의 후에 이 클래스는 Person 클래스의 상속을 받습니다.

Person 객체에는 정보를 출력하는 멤버 함수가 있고 Man, Girl클래스에는 기초 클래스 정보 출력 멤버 함수에 추가로 출력을 하려고 합니다. 

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
class Person {
private:
        int age = 0;
        char * name = 0;
public:
        Person(int p_age, const char * p_name) {
               age = p_age;
               name = new char[strlen(p_name) + 1];
               strcpy(name, p_name);
        }
        virtual void Show() { //가상 멤버 함수
               std::cout << "나이:" << age << std::endl;
               std::cout << "이름:" << name << std::endl;
        }
        ~Person() {
               delete[]name;
        }
};
class Man : public Person {
private:
public:
        Man(int p_age, const char * p_name) : Person(p_age, p_name) {
        }
        void Show() { //가상 멤버 함수,기초 클래스의 Show 멤버 함수를 재정의함
               std::cout << "남자입니다." << std::endl;
               Person::Show();
        }
};
int main(void) {
        Man man = Man(13, "Minseok");
        Person person = Person(15, "sumin");
        man.Show();
        person.Show();
}

먼저 Man클래스의 Show멤버 함수는 기초클래스의 Show멤버 함수에 덧붙여 "남자입니다."라는 문자열을 출력하고 있습니다.

재정의하려는 멤버 함수(기초 클래스의 멤버 함수)에 virtual 키워드를 붙여줘야 합니다.

이때 파생 클래스는 안 붙여도 되는데, 붙여주는 게 코드 분석에 더 도움됩니다.

(어떤 개발자 분은 

당연하지만 Person객체가 Show 멤버 함수를 호출할 시에 Person객체의 멤버 함수가 호출되고

Man객체가 Show 멤버 함수를 호출할시에 재정의한 멤버 함수인 Show가 호출됩니다.

 

객체가 아닌 포인터,참조에 의한 가상 멤버 함수 호출

포인터, 참조에 의한 호출 멤버 함수의 선택은 객체형과는 좀 다릅니다.

가상 멤버 함수의 경우에 참조 대상의 자료형에 따라 호출할 멤버 함수가 결정됩니다.

(매우 중요!!, virtual 키워드의 핵심)

  • Person이 Person을 참조하는 경우 -> Person클래스의 멤버 함수를 호출

  • Person이 Man을 참조하는 경우 -> Man클래스의 멤버 함수를 호출

멤버 함수가 가상멤버 함수인 경우

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
class Person {
private:
        int age = 0;
        char * name = 0;
public:
        Person(int p_age, const char * p_name) {
               age = p_age;
               name = new char[strlen(p_name) + 1];
               strcpy(name, p_name);
        }
        virtual void Show() { //가상 멤버 함수
               std::cout << "나이:" << age << std::endl;
               std::cout << "이름:" << name << std::endl;
        }
        ~Person() {
               delete[]name;
        }
};
class Man : public Person {
private:
public:
        Man(int p_age, const char * p_name) : Person(p_age, p_name) {
        }
        void Show() {
               std::cout << "남자입니다." << std::endl;
               Person::Show();
        }
};
int main(void) {
        Man man = Man(13, "Minseok");
        Person person = Person(15, "sumin");
        Person & ref_man = man;
        Person & ref_person = person;
        ref_man.Show(); //Man클래스의 재정의한 Show멤버 함수
        ref_person.Show(); //Person클래스의 Show멤버 함수
}
남자입니다.
나이:13
이름:Minseok
나이:15
이름:sumin

 

가상 멤버 함수가 아닌 경우

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
class Person {
private:
        int age = 0;
        char * name = 0;
public:
        Person(int p_age, const char * p_name) {
               age = p_age;
               name = new char[strlen(p_name) + 1];
               strcpy(name, p_name);
        }
         void Show() {
               std::cout << "나이:" << age << std::endl;
               std::cout << "이름:" << name << std::endl;
        }
        ~Person() {
               delete[]name;
        }
};
class Man : public Person {
private:
public:
        Man(int p_age, const char * p_name) : Person(p_age, p_name) {
        }
         void Show() { 
               std::cout << "남자입니다." << std::endl;
               Person::Show();
        }
};
int main(void) {
        Man man = Man(13, "Minseok");
        Person person = Person(15, "sumin");
        Person & ref_man = man;
        Person & ref_person = person;
        ref_man.Show(); //Person클래스의 Show멤버 함수
        ref_person.Show(); //Person클래스의 Show멤버 함수
}
나이:13
이름:Minseok
나이:15
이름:sumin

가상 멤버 함수는 "객체가 아닌 포인터,참조에 의한 가상 멤버 함수 호출" 일 경우에 빛을 발휘됩니다.

참조, 포인터에서의 가상 멤버 함수는 참조 대상의 자료형에 맞는 멤버 함수를 호출하기 때문입니다.

기본적으로 업캐스팅되면 기초 클래스의 멤버 함수를 호출하기 때문입니다.(참조, 포인터가 아닌 객체일 때)

 

필요한 상황

만약 여러 클래스의 정보(이때 Person이라는 클래스가 조상에 해당함)를 호출하는 함수를 정의해봅시다.

void Show(Person & per) {
        per.Show();
}

참조 대상의 자료형에 따라 재정의한 멤버 함수를 호출하고 싶습니다.

특히나 Man,Girl클래스 말고도 더 많은 클래스를 정의하게 된다면 파생 클래스를 위해 Show함수를 하나하나 오버 로딩할 수는 없습니다.

(Show멤버 함수를 가상멤버 함수로 정의함으로써, 참조 대상의 자료형에 맞춰서 멤버 함수를 호출할 수 있기에 오버 로딩을 안 해도 됩니다.)

그래서 virtual키워드를 사용해 파생 클래스에서 재정의 하도록 돕습니다.

일반적으로 파생 클래스에서 재정의 되는 멤버 함수는 기초 클래스에서 가상으로 선언하는 것이 관행입니다.

사용할 때도 사용범위 결정 연산자를 사용해야 합니다.(안 그러면 재귀 호출)

 

마지막으로 알아야 할 사항

 

생성자

생성자는 가상으로 선언할 수 없습니다.

파생 클래스는 기초 생성자를 상속하지 않기 때문입니다.

(파생 클래스 객체의 생성은, 기초 클래스 생성자가 아니라 파생 클래스 생성자를 호출한다.)

 

파괴자

클래스가 기초 클래스로 사용된다면, 파괴자는 가상으로 선언해야 합니다.

왜냐하면 메모리 누수(memory leak)때문인데 이건 다음 페이지에서 다룹니다.

가상 파괴자의 필요성

 

프렌드

프렌드는 가상 함수가 될 수 없습니다.

멤버 함수만 가상 함수가 될 수 있는데, 프렌드는 클래스 멤버가 아니기 때문입니다.

이것 때문에 설계에 문제가 있다면, 그 프렌드 함수가 내부적으로 가상 멤버 함수를 사용하게 하여 문제를 해결할 수 있습니다.

 

가상 함수를 재정의 하지 않는다면?

기초 클래스 버전을 사용합니다.

만약 여러 클래스를 상속받았다면(기초 클래스 위에 또 기초 클래스가 있는 구조) 가장 최근에 재정의된 버전을 사용합니다.

단 기초 클래스 버전이 은닉되어 있는 경우에는 예외입니다.

가상 함수를 다시 정의하면 멤버 함수가 은닉됩니다.

class Person {
public:
       virtual void Show(int a);
};
class Man : public Person {
public:
       virtual void Show();
};
int main(void) {
       Man man = Man();
       man.Show(1);
       man.Show();
}

 

Man 클래스에서 재정의된 멤버 함수는 오버 로딩된 두 개의 함수 버전을 생성하지 않습니다.

이 재정의는 int형 매개변수를 사용하는 기초 클래스 버전을 가립니다.

어떤 함수를 파생 클래스에서 다시 정의하면 동일한 함수 시그내처를 가지고 있는 기초 클래스 선언만 가리는 것이 아니라, 매개변수 시그니처와는 상관없이 같은 이름을 가진 모든 기초 클래스 멤버 함수들은 다 가립니다.

이와 같은 사실 때문에 두 가지 규칙이 성립됩니다.

  • 상속된 멤버 함수를 재정의할 경우에는 오리지널 원형과 정확히 일치시킬 필요가 있습니다.

  • 기초 클래스 선언이 오버 로딩되어 있다면, 파생 클래스에서 모든 기초 클래스 버전들을 재정의해야 합니다.

 

첫 번째 규칙

오리지널 원형과 정확히 일치시킬 필요가 있다는 것은, 오버 라이딩 때문에 그렇습니다.

첫 번째 규칙에 예외가 있다면, 리턴형이 기초 클래스에 대한 참조나 포인터인 경우에 파생 클래스에 대한 참조나 포인터로 대체될 수 있다는 것입니다.

리턴형이 클래스형과 병행하여 바뀌는 것을 허용하기 때문에, 이 기능을 리턴형의 공변(covariance)이라고 합니다.

#include <iostream>
using namespace std;
class Person {
public:
       virtual Person* Show();
};
class Man : public Person {
public:
       virtual Man* Show();
};
int main(void) {
       Person * per = new Person();
       per->Show();
       per->Show();
}

 

두 번째 규칙

#include <iostream>
using namespace std;
class Person {
public:
       virtual Person* Show(int a);
       virtual Person * Show(int a,int b);
};
class Man : public Person {
public:
       virtual Man* Show(int a);
       virtual Man* Show(int a,int b);
};
int main(void) {
       Person * per = new Person();
       per->Show(1);
       per->Show(1);
}

 

마지막으로

개인적으로 가상 멤버 함수에 대해 이해하기가 어려웠습니다.

처음부터 잘하는 건 없으니, 천천히 곱씹으면서 보셨으면 좋겠습니다.

제 블로그 말고도 여러 블로그나 이해한 게 제대로 맞는지, 물어보면서 공부해보세요.

파이팅입니다.

댓글