가상 함수

  • 부모를 상속받은 자식 클래스에서 "재정의할 것"으로 기대하고 정해놓은 함수
  • 자식 클래스에서 가상함수를 재정의하면 이전에 정의되었던 내용들은 모두 새롭게 정의된 내용들로 교체된다.
  • 부모 클래스에서 virtual 키워드를 사용해 가상 함수를 선언하면 자식 클래스에서 재정의된 멤버함수도 자동으로 가상 함수가 된다.

 

사용법

  • 멤버함수 앞에 virtual을 붙이면 된다.

 

 

가상 함수 호출 방식

c++ 에서는 가상 함수가 아닌 일반적인 멤버 함수들의 호출은 컴파일 할 때 고정된 메모리 주소로 변환된다.  
➡️  정적 바인딩


가상 함수를 호출할 때는 컴파일러가 어떤 함수를 호출해야 하는지 미리 알 수 없다. 
왜냐하면, 가상 함수는 프로그램이 실행될 때(런타임 때) 객체를 결정하므로 컴파일 타임에 해당 객체를 특정할 수 없기 때문이다.
➡️ 동적 바인딩

 

 

가상 함수를 사용하는 이유

c++컴파일러는 virtual을 쓰지 않으면 모든 함수가 정적 바인딩으로 선언된다.

class Person;
class Man : public Person;
class Girl : public Person;

man,girl이 person클래스를 상속 받았다면 프로그램을 짜는 도중에 어쩔 수 없이
Man *gracefulMan = new Man;

Person *pMan = gracefulMan;


이런 식으로 써야 하는 경우가 많다.


예를 들어 사람 리스트를 만들었을 때 리스트는 동일 자료형을 가져야 하는데, 특정 자료형(남자 or 여자)으로 만들면
성별마다 따로 리스트를 만들어야 되지 않나?
그래서 남자, 여자 두 타입의 변수를 모두 받을 수 있는 부모 클래스 포인터를 쓴다.

 

Person *pMan = new Man;
Person *pGirl = new Girl;

List<Person*> list;

list.AddTail(pMan);
list.AddTail(pGirl);

이 때 문제가 c++에서 멤버함수를  정적 바인딩 한다는 것이다. 즉, 코드를 실행하기 전에 전에 어떤 함수가 불러질 지 결정된다.


예를 들어 Person,Man,Girl 모두 talk()라는 멤버함수를 가지고 있다고 하자.
그런데 pMan->talk()를 호출하면 Man의 talk()함수가 호출되길 바라지만, 실제로는 Person의 멤버함수가 호출되어 버린다.

 

Person *pMan = new Man;
delete pMan;

이 경우에도 우리는 Man의 소멸자 함수가 호출되기를 바라지만, delete는 Person의 소멸자를 호출해 버린다.

 

위와 같은 상황의 해결책으로 virtual을 쓰는 것이다.

 

만일 타입 변환을 쓰지 않는다면 굳이 virtual 키워드를 신경쓸 필요가 없다. 하지만 타입변환은 매우 편하다.
만약 스타크래프트에서 12부대의 유닛을 지정한다면 타입변환을 통해 marine이든 wraith든 다 하나의 배열에 담을 수 있다.

CUnit(*team)[12];

for(int i=0; i<12; i++){
	team[i]->MoveTo(마우스 클릭 위치);
}

위의 코드처럼 여러 타입의 객체를 같은 배열로 관리할 수 있다. 만일 저것을 타입 변환을 쓰지 않고 각각 처리하려고 했다면 
코드가 얼마나 복잡해 졌을까? ^^

 

 

 

사용 예시

#include<stdio.h>
#include<iostream>

class A
{
    public:
    virtual void Print(){
    	printf("A 클래스의 print() 함수\n");
    }
};

class B : public A
{
    virtual void Print(){
    	printf("B 클래스의 Print()함수\n");
    }
};

int main(){
	A obj_a;
    B obj_b;
    
    A* ptr = &obj_a;
    ptr->Print(); // A클래스의 프린트 함수 호출
    
    A* ptr2 = &obj_b;
    ptr2->Print(); // B클래스의 프린트 함수 호출
                       // 가상함수가 아닌 그냥 일반적인 멤버함수로 만들었다면 A의 프린트 함수를 호출했을 것이다.
                       // => 컴파일러는 포인터가 가리키는 객체가 아닌 "포인터 변수의 자료형을 기준"으로 결정하기 때문
                       // 하지만 가상함수를 사용하면 "포인터가 가리키는 객체를 기준"으로 호출함수를 결정하기 때문에
                       // B클래스의 프린트 함수가 호출되는 것이다.
    return 0;
}

 

 

+ Recent posts