1. 상속 오버라이딩(Inheritance Overriding)

상속 오버라이딩을 보자니, 전에 배웠던 함수 오버로딩이 생각나지 않나요? 오버로딩이 인자의 자료형이나 수가 다른 함수를 같은 이름으로 여러번 중복 정의하는 것이라면, 오버라이딩은 이미 있는 함수를 무시해버리고 새롭게 함수를 재정의하는 것이라고 말할 수 있습니다. 더 자세히 말하자면, 이 오버라이딩(Overriding, 재정의)는 부모 클래스와 자식 클래스의 상속 관계에서, 부모 클래스에 이미 정의된 함수를 같은 이름으로 자식 클래스에서 재정의 하는것을 의미합니다. (이 때, 부모의 멤버 함수와 원형이 완전히 같아야 합니다. 그리고 오버라이딩시 부모 클래스의 함수가 모두 가려집니다.)

#include <iostream>

using namespace std;

class A {
public:
	void over() { cout << "A 클래스의 over 함수 호출!" << endl; }
};

class B : public A {
public:
	void over() { cout << "B 클래스의 over 함수 호출!" << endl; }
};

int main()
{
	B b;
	b.over();
	return 0;
}

결과:

B 클래스의 over 함수 호출!

계속하려면 아무 키나 누르십시오 . . .


코드를 보시면, 5~8행에서 A란 클래스가 정의되었습니다. A 클래스 내부를 살펴보면 over라는 함수가 있습니다. 그리고, 10행을 보시면 B 클래스가 정의되었는데, 이 B 클래스는 A 클래스를 상속합니다. B 클래스 내부에도 over라는 함수가 존재합니다. 17행에서 객체 b를 만들고, 객체 내의 over 함수를 호출합니다. 결과를 보시면 아시겠지만, 부모 클래스의 over 함수가 무시되고 자식 클래스의 over 함수가 호출됩니다.


그러면, 자식 클래스의 over 함수때문에 가려진 부모 클래스의 over 함수는 어떻게 호출할까요? 우리가 전에 배웠던 네임스페이스(namespace)에서 범위 지정 연산자(::)를 봤었는데, 이 범위 지정 연산자로 부모 클래스의 함수를 호출할 수 있습니다. 혹은 부모 클래스의 포인터를 이용해서 호출할 수도 있습니다. 아래는 범위 지정 연산자를 통한 부모 클래스 내의 over 함수 호출 예제입니다.

..
class B : public A {
public:
	void over() { A::over(); cout << "B 클래스의 over 함수 호출!" << endl; }
};
..

결과:

A 클래스의 over 함수 호출!

B 클래스의 over 함수 호출!

계속하려면 아무 키나 누르십시오 . . .


저 위의 부분을 제외하고는 나머지가 모두 똑같은 코드이므로 생략했습니다. 여기서 보셔야 할 부분은, over 함수내에 추가된 부분인 "A::over();"입니다. 범위 지정 연산자를 통해 A 클래스 내의 over 함수를 호출할 수 있습니다. 이제 오버라이딩과 오버로딩. 확실히 구분 가시죠?


2. 가상 함수(Virtual Function)

가상 함수를 먼저 살펴보기전, 가상 함수가 왜 필요한지, 언제 쓰이는지 부터 한번 살펴보도록 합시다. 아래의 예제는 포인터 변수와 관련된 예제입니다. 결과를 한번 유추하고, 코드를 살펴보시면서 왜 이런 결과가 나오는가에 대해 곰곰히 생각해보세요.

#include <iostream>

using namespace std;

class Parent {
public:
	void func() { cout << "부모 클래스의 func 함수 호출!" << endl; }
};

class Child : public Parent {
public:
	void func() { cout << "자식 클래스의 func 함수 호출!" << endl; }
};

int main()
{
	Parent P, *pP;
	Child C;

	pP=&P;
	pP->func();
	pP=&C;
	pP->func();
	return 0;
}

결과:

부모 클래스의 func 함수 호출!

부모 클래스의 func 함수 호출!

계속하려면 아무 키나 누르십시오 . . .


코드를 보시면, 5행에서 Parent 클래스가 정의되었고, 이 클래스 내부를 살펴보면 func란 함수가 정의되어 있습니다. 10행에서 Child 클래스가 정의되고, 이 클래스는 Parent 클래스를 상속합니다. Child 클래스의 내부를 살펴봤더니 Parent 클래스에서 정의되었던 func를 Child 클래스에서 재정의(오버라이딩, Overriding) 하였습니다. 그리고 메인 함수 내부를 살펴보면, 17행에서 객체 P와, Parent 객체의 주소값을 담을수 있는 포인터 변수 pP가 선언되었습니다. 18행에서는 객체 C가 생성되었습니다.


이제부터 시작입니다. 20행에서 pP에 P의 주소값을 대입합니다. 그러고 나서 21행에서 pP가 가리키고 있는 객체의 func 함수를 호출합니다. 여기까지는 부모 클래스의 func 함수가 호출된다는 것을 짐작할 수 있습니다. 22행에서, C의 주소값을 Parent 포인터 변수인 pP에 담습니다. 그러고 나서, pP가 가리키고 있는 자식 객체의 func 함수를 호출하는듯 보였으나, 막상 결과를 보니 부모 클래스의 func 함수가 호출되었습니다. 이게 어찌된 일일까요?


이런 상황이 일어난 이유는, C++ 컴파일러가 실제로 가리키는 객체의 자료형을 기준으로 하는게 아닌, 포인터 변수의 자료형을 기준으로 판단하기 때문입니다. 그럼, 실제로 가리키는 객체의 자료형에 따라 멤버 함수가 호출되도록 하려면 어떻게 해야할까요? 이 문제는 바로 우리가 배울 virtual 키워드를 함수의 선언문에 붙여주면 쉽게 해결이 가능합니다. 이렇게 virtual 키워드가 붙은 함수는 "가상 함수(Virtual Function)"라고 말합니다. 한번 위의 예제에서 virtual 키워드를 선언문 앞에다 붙여 결과를 다시 확인해보도록 할까요?

#include <iostream>

using namespace std;

class Parent {
public:
	virtual void func() { cout << "부모 클래스의 func 함수 호출!" << endl; }
};

class Child : public Parent {
public:
	virtual void func() { cout << "자식 클래스의 func 함수 호출!" << endl; }
};

int main()
{
	Parent P, *pP;
	Child C;

	pP=&P;
	pP->func();
	pP=&C;
	pP->func();
	return 0;
}

결과:

부모 클래스의 func 함수 호출!

자식 클래스의 func 함수 호출!

계속하려면 아무 키나 누르십시오 . . .


코드에서 선언문 앞에 virtual 키워드가 붙여진게 보이시죠? 이렇게 가상 함수로 선언되면, 실제로 가리키는 객체에 따라 실행되는 코드가 달라집니다. 참고로, 부모 클래스에서 멤버 함수 선언문 앞에 virtual 키워드가 존재한다면, 자식 클래스에서 오버라이딩(재정의, Overriding)한 함수도 저절로 가상 함수로 정의됩니다. 그러나, 소스 코드의 이해를 돕기 위해 자식 클래스에도 virtual를 명시해주어야 하는것이 관례입니다. 이번엔 순수 가상 함수(Pure Virtual Function)란 녀석을 살펴보도록 하겠습니다.


3. 순수 가상 함수(Pure Virtual Function)

가상 함수가 실제로 가리키는 객체에 따라 실행 코드가 달라지고, 재정의 할 수 있는 함수인 반면에, 순수 가상 함수는 함수의 선언만 있고 정의는 없는것으로, 자식 클래스에서 반드시 재정의하여야만 합니다. 그럼, 순수 가상 함수를 어떻게 만들 수 있을까요? 아래와 같이 가상함수에서 정의를 제외하고, 뒷부분에다 "= 0;"을 덧붙이시면 됩니다.

class Parent {
public:
	virtual void func() = 0;
};

class Child : public Parent {
public:
	virtual void func() { cout << "자식 클래스의 func 함수 호출!" << endl; }
};

위와 같이, 순수 가상 함수를 포함하는 클래스를 추상 클래스(abstract class)라고 말합니다. 한번 예제를 통해서, 추상 클래스가 어떤 녀석인지 한번 살펴보도록 합시다.

#include <iostream>

using namespace std;

class Parent {
public:
	virtual void func() = 0;
};

class Child : public Parent {
public:
	virtual void func() { cout << "자식 클래스의 func 함수 호출!" << endl; }
};

int main()
{
	// Parent P;
	Parent * P;

	P=new Child;
	P->func();
	return 0;
}

결과:

자식 클래스의 func 함수 호출!

계속하려면 아무 키나 누르십시오 . . .


바로 코드의 7행을 보시면, 순수 가상 함수가 선언되었습니다. 위에서 말한 그대로, 순수 가상 함수는 함수의 몸체부분이 정의되어 있지 않습니다. 10~13행에서 Child 클래스가 정의되고, 이 Child 클래스는 Parent 클래스를 상속합니다. Child 클래스의 안을 들여다보면, 12행에서 func 함수가 재정의된것을 보실 수가 있는데, 부모 클래스 내의 순수 가상 함수는, 자식 클래스내에서 반드시 재정의되어야 한다는 특징을 가지고 있습니다. 만약, 12행을 주석처리하게 되면 아래와 같은 오류가 발생할 것입니다.


에러:

1 IntelliSense: 추상 클래스 형식 "Child"의 개체를 사용할 수 없습니다.

            순수 가상 함수 "Parent::func"에 재정의자가 없습니다. c:\Users\h4ckfory0u\Documents\Visual Studio 2012\Projects\ConsoleApplication4\ConsoleApplication4\소스.cpp 19 8 ConsoleApplication4


다시 한번 말하지만, 순수 가상 함수는 반드시 자식 클래스에서 오버라이딩(overriding, 재정의)되어야 합니다. 이어서, 17행을 보도록 합시다. 이 주석을 풀어보면 다음과 같은 에러가 발생합니다.

에러:
1 IntelliSense: 추상 클래스 형식 "Parent"의 개체를 사용할 수 없습니다.
            함수 "Parent::func"은(는) 순수 가상 함수입니다. c:\Users\h4ckfory0u\Documents\Visual Studio 2012\Projects\ConsoleApplication4\ConsoleApplication4\소스.cpp 17 9 ConsoleApplication4

즉, 추상 클래스는 객체를 만들 수 없다는 말이 됩니다. 왜냐, 몸체도 정의되어 있지 않은 추상 클래스의 객체를 만든다는 것은 생각해보면 아무런 의미가 없습니다. 하지만 18행과 같이, 추상 클래스의 포인터는 선언할 수 있습니다. 20행 처럼 자식 클래스의 객체를 만들어 그곳을 가리키게 하고, 부모 클래스의 포인터로 func 함수에 접근합니다. (참고로, 추상 클래스는 순수 가상 함수 선언을 하나라도 포함하면 그 클래스는 추상 클래스가 된다는 것입니다. 또한, 추상 클래스를 상속하고, 그 상속받은 클래스에서 순수 가상 함수를 정의하지 않으면 그 클래스도 추상 클래스가 됩니다.)


그럼, 이런 순수 가상 함수는 어떤데에 쓰일까요? 자, 한번 가정을 해보죠. 개, 고양이, 늑대 클래스가 있다고 칩시다. 그리고 이런 클래스들은 동물 클래스를 상속했다고 합시다. 동물 클래스 내에는 울다, 자다, 먹다 등의 행동을 가지고 있다고 하고 있고, 알고 계시듯, 각 동물마다 울음소리는 모두 다릅니다. 그렇기 때문에, 동물 클래스 내에 순수 가상 함수를 두어 동물 클래스를 상속한 클래스는 반드시 울다라는 함수를 재정의 하여야하는 방향성을 제시합니다.


만약, 동물 클래스를 추상 클래스로 두지 않았다면 어땠을까요? 개, 고양이, 늑대의 울음소리는 있어도, 이것들을 포함하는 "동물"의 울음소리는 존재할 수 없습니다. 동물이란 객체를 만들고 울다라는 함수를 호출하는것 자체가 오류인 셈이죠. 이해가셨나요?


4. 다중 상속(Multiple Inheritance)

다중 상속, 말 그대로 다중 상속(Multiple Inheritance)란 둘 이상의 클래스를 동시에 상속하는 것을 말합니다. 다중 상속에 대해서는 문법만 간단히 이해하고 넘어가도록 하겠습니다. 아래를 한번 보도록 합시다.

#include <iostream>

using namespace std;

class ParentOne {
public:
	void funcone() { cout << "ParentOne 클래스에서 funcone() 호출!" << endl; }
};
class ParentTwo {
public:
	void functwo() { cout << "ParentTwo 클래스에서 functwo() 호출!" << endl; }
};

class Child : public ParentOne, public ParentTwo {
public:
	void func() { funcone(); functwo(); }
};

int main()
{
	Child child;

	child.func();
	return 0;
}

결과:

ParentOne 클래스에서 funcone() 호출!

ParentTwo 클래스에서 functwo() 호출!

계속하려면 아무 키나 누르십시오 . . .


코드를 살펴보시면, 5~8행과 9~12행에서 각각 ParentOne, ParentTwo란 클래스가 정의되었습니다. 그리고 14행을 보시면 콤마(,)를 이용하여 Child 클래스가 ParentOne, ParentTwo 클래스를 모두 상속하고 있습니다. 16행을 보시면, 앞서 말한 두 클래스를 상속하였기 때문에 부모 클래스의 함수를 호출할 수 있는 것입니다.


되도록이면 다중 상속을 사용하길 권하지 않습니다. 다중 상속을 완전히 이해하셨고 다중 상속이 꼭 필요한 경우라면 쓰셔도 괜찮지만, 이 다중 상속은 문제점이 여럿 존재합니다. 상속받은 두 클래스에서 같은 이름의 함수가 있으면  어느 함수를 호출할지 모르는 모호성이 발생합니다. 이 경우는 범위 지정 연산자로 해결할 수 있습니다. 


<다이아몬드 상속(Diamond Inheritance)>


그리고, A, B, C, D란 클래스가 있다고 가정을 해봅시다. 이 상태에서 B 클래스가 A 클래스를 상속받고, C 클래스도 A 클래스를 상속 받은 뒤에, D 클래스가 B와 C 클래스를 상속 받는 경우에 A 클래스가 두번 상속되는 문제점이 있습니다. 즉, 다이아몬드 상속(Diamond Inheritance)인 이 경우에는 가상 상속을 통해 해결할 수 있습니다.


다시한번 말하지만 다중 상속에 대해서는 이해를 좀더 하시고 사용하시는게 좋습니다. 아니면 다중 상속을 쓰지 않는것도 하나의 방법이 될 수 있습니다.


이번 강좌는 여기서 그만 마치도록 하겠습니다. 수고하셨습니다. 다음 강좌에서는 연산자 오버로딩(Operator Overloding)에 대해 알아보도록 하겠습니다.