공부/C || C++

C++ 오버로딩(overloading) vs 오버라이딩(overriding), 가상함수(virtual function) 그리고 다형성(polymorphism)

sudo 2021. 7. 5. 18:45

C++을 공부한 사람이라면 누구나 한번쯤은 헷갈려 봤을 법한 오버로딩과 오버라이딩에 대해 정리해보자.

 

우선 오버로딩은 매우 간단하다. 함수 파라미터 타입이나 갯수만 다르게해서 다른 함수로 인식하도록 하는 것이다. 그 말은 함수의 리턴 타입만 다르게 해서는 오버로딩이 아니라는 의미이다. 예를 들어서

이렇게 리턴 타입만 다르고 함수 파라미터의 갯수와 타입이 같으면 오버로딩이 안된다는 것이다.

 

오버라이딩은 상속 관계에서 부모의 기능을 자식이 상속받을 때, 자식이 부모의 기능을 재정의하는 것을 의미한다. 예를 들어서 

class person {
public:

	void foo() { cout << "In the person class!" << endl; }
};

class student : public person {
public:

	void foo() { cout << "In the student class!"<< endl; }
};


int main()
{
	person p;
	student s;

	s.foo();
}

결과는 당연히

반환형, 파라미터 모두 같은 foo함수가 student 클래스에서 재정의된 걸 볼 수 있다.

 

부모 클래스에서 정의된 걸 자식 클래스에서 재정의 할 것이라고 명시적으로 나타내는 키워드가 있는데 그게 바로 virtual이다. 사실 virtual을 이야기하기 전에 다형성(polymorphism)을 먼저 이야기해보자

 

다형성이란 서로 다른 타입의 객체가 같은 이름의 동작을 서로 다르게 수행할 수 있다는 것이다. 이렇게 이야기하면 조금 어려운데 단순히 생각하면 Minion라는 클래스가 있는데 Minion 클래스의 자식 클래스에는 대포 미니언, 원거리 미니언, 근거리 미니언 등이 있을 것이다. 그러면 이 미니언들의 Attack이라는 동작은 미니언마다 다를 것이다. 세 미니언 모두 공격 모션, 공격 방법, 데미지 모두 다르다. 따라서 Attack 함수는 자식 클래스들에서 각각 Attack이라는 이름은 같지만 동작은 모두 다르게 재정의되어야 할 것이다. 이것이 한 다형성의 예다.

 

이 다형성을 구현할 수 있게 해주는 것이 virtual 키워드다. 게임 모작을 하다보면 은근히 이런 비슷한 경우가 많이 나왔었다.

class Monster{
public:
	list<Monster*> m_MonsterList;

	void attack() { cout << "Monster attack!" << endl; }
};

class Goblin : public Monster {
public:
	void attack() { cout << "Goblin attack!"<< endl; }
};

class Dragon : public Monster {
public:
	void attack() { cout << "Dragon attack!" << endl; }
};

class Ghost : public Monster {
public:
	void attack() { cout << "Ghost attack!" << endl; }
};


int main()
{
	Goblin* goblin = new Goblin;
	Dragon* dragon = new Dragon;
	Ghost* ghost = new Ghost;

	Monster* monster = new Monster;

	monster->m_MonsterList.push_back(goblin);
	monster->m_MonsterList.push_back(dragon);
	monster->m_MonsterList.push_back(ghost);

	list<Monster*>::iterator iter = monster->m_MonsterList.begin();

	for (; iter != monster->m_MonsterList.end(); ++iter)
	{
		(*iter)->attack();
	}

	delete goblin;
	delete dragon;
	delete ghost;
}

Goblin, Ghost, Dragon는 모두 Monster 클래스의 자식 클래스들이며 이것들을 한 리스트에 모두 넣어놨다가 꺼내면서 모두 각자의 Attack 멤버 함수를 호출할 것을 기대하고 작성한 코드지만 결과는 이렇다.

부모클래스의 attack만 호출된 상황

그렇다면 각자의 Attack을 호출하게 하려면 어떻게 해야할까? 바로 부모의 Attack 함수에 virtual 키워드를 붙여주면 된다(부모에만 붙이면 자동적으로 자식들에게도 virtual이 붙어진다고 하지만 명시적으로 붙여주는 것이 더 좋아보인다)

class Monster{
public:
	list<Monster*> m_MonsterList;

	virtual void attack() { cout << "Monster attack!" << endl; }
};

class Goblin : public Monster {
public:
	virtual void attack() { cout << "Goblin attack!"<< endl; }
};

class Dragon : public Monster {
public:
	virtual void attack() { cout << "Dragon attack!" << endl; }
};

class Ghost : public Monster {
public:
	virtual void attack() { cout << "Ghost attack!" << endl; }
};

결과는

각자 Attack함수를 잘 호출했다

왜 virtual keyword를 붙이면 각자 Attack 함수를 알맞게 호출하게 되는걸까?

 

일반적으로 함수의 호출은 컴파일 타임에 호출할 함수의 주소를 결정하는 정적 바인딩(static binding)을 사용한다. 위의 경우에도 Goblin*, Dragon*, Ghost*들이 Monster* 타입의 리스트에 들어가면서 업캐스팅이 이루어졌을 것이다. 하지만 여전히 goblin, dragon, ghost들의 타입은 Monster*이며 컴파일러는 정적 바인딩을 하면 Monster 클래스의 attack을 부를 수 밖에 없다.

 

그렇다면 정적 바인딩이 아니라 동적 바인딩이 되게 하면 해결할 수 있다. 즉, 객체의 포인터 타입을 보고 결정하는게 아니라 객체 포인터가 가리키는 것의 타입을 보고 어떤 함수를 부를지 결정하게 하면 되는데 이것이 런타임에 어떤 함수를 부를지 결정하게 되는 동적 바인딩(dynamic binding)이다. 함수가 virtual로 선언되어 있으면 그 함수는 동적으로 바인딩된다.

 

각 객체를 위한 메모리 공간이 할당되면 그 공간엔 vtable pointer가 있는데 virtual로 선언된 함수 호출시 vtable pointer가 가리키고 있는 테이블에 있는 함수의 주소를 보고 어떤 함수를 호출할 지 결정한다. 예를 들어 다음과 같은 예시가 있다고 하자(코드 출처: https://cosyp.tistory.com/228)

class Parent{
virtual void func1(){ cout<< AAA <<endl;  }
virtual void func2(){ cout<< BBB <<endl;  } 
virtual void func3(){ cout<< CCC <<endl;  } 
void func4(){ cout<< DDD<< endl; } 
}; 

class Child : public Parent{
virtual void func1(){ cout<< childA <<endl;  } 
virtual void func3(){ cout<< childC <<endl; } }

int main()
{
	Parent * p = new Parent;
	Parent * c = new Child; 
	Child * cc = new Child;
	p->func1(); // Parent의 func1 함수 호출 
	c->func1(); // Child의 Overriding 된 func1 호출
	c->func2(); // Parent의 func2 함수 호출
	c->func4(); // Parent의 func4 함수 호출, 가상테이블엔 없음
	cc->func3(); // Child의 func3 함수 호출 
}

이 경우에 객체와 vtable은 이런식으로 되어있을 것이다.

출처: https://cosyp.tistory.com/228

 

따라서 virtual선언이 동적 바인딩이 돼서 객체마다 우리가 기대하는 함수를 각각 호출할 수 있게 되는 것이다.

 

주의해야할 것이 있는데 만약에 자식 클래스의 멤버중에 동적 할당된 것이 있어서 소멸자에서 그 동적 할당된 메모리를 해제해주고 있다면 반드시 부모의 소멸자를 virtual로 선언해줘야 할것이다. 그래야 자식 클래스의 소멸자가 호출될 것이기 때문이다.

 

끝으로 순수 가상함수라는 것은 부모 클래스에서 정의하지 않지만 자식 클래스에서 반드시 재정의를 해줘야 하는 것이다(재정의 안하면 오류가 뜸). 형태는 다음과 같다. 그리고 이런 순수 가상 함수를 포함한 클래스를 추상 클래스(abstract class)라고 한다.

virtual 타입 함수명( 파라미터 ) = 0 { }

 

Reference:

https://velog.io/@underlier12/C-07-%EB%8B%A4%ED%98%95%EC%84%B1-dbk69cs1zz

 

C++ #07 다형성

07. 다형성 다형성의 기법 Polymorphism이란 여러 개의 서로 다른 객체가 동일한 기능을 서로 다른 방법으로 처리할 수 있는 기능을 의미한다. 예를 들어 칼, 대포, 총 등의 무기들은 공통적으로 '공

velog.io

https://cosyp.tistory.com/228

 

가상함수(Virtual function)와 가상함수테이블(vtable)의 이해

오버라이딩(Overriding) 가상함수를 이해하기 위해선 오버라이딩(Overriding) 에 대해서 알아야 한다. SourceCode(1) class Parent{ void show(){ printf("this is parent\n"); } } class Child : public Parent{..

cosyp.tistory.com