[C++] Virtual
virtual은 3가지 정도로 사용됩니다. OOP에 대한 개념이 잡혀있어야 설명을 제대로 이해하실 수 있을텐데.. 최대한 쉽게 설명 해보도록 하죠.
일단 OOP 개념 중 클래스 상속에 대한 것은 잘 아실겁니다. 이마저도 모르신다면 virtual에 대해서는 전혀 궁금해하실 필요가 없으니 그냥 넘어가시면 되구요.. 진도를 앞서나가는 호기심은 해롭습니다! :-)
class Bird {
// 중략
};
class Chicken : public Bird {
// 중략
};
class Eagle : public Bird {
// 중략
};
와 같은 구조가 있다고 합시다.
C++에서 public 상속은 is-a 관계로 설명할 수 있는데 쉽게 말하자면 Chicken is a Bird. 입니다.
따라서 Bird *pBird = new Chicken; 과 같이 Chicken 인스턴스(클래스가 실체화된 것)를 Bird 객체에 대입할 수 있습니다. 이를 다형성(polymorphism)이라고 하고요.
이런 지식을 깔아놓고 시작하죠.
1. 순수 추상(pure abstract, 혹은 순수 가상pure virtual) 함수 선언으로서의 virtual
순수 가상 함수란 함수 바디를 가지지 않은 함수를 말하며 그러한 함수를 가지고 있는 클래스를 추상 클래스(abstract class)라고 합니다.
그럼 abstact 시리즈는 어디서 쓰는가? 에 대한 예를 하나 들어보죠.
모든 Bird를 상속받은 객체는 bool canFly(); 란 멤버함수를 가지고 있다고 합시다. 이름만 봐도 알겠지만, 날 수 있으면 true, 못날면 false를 리턴합니다.
그럼 canFly() 멤버함수는 Chicken에서도 쓸 수 있어야 하고 Eagle 객체에서도 쓸 수 있어야 합니다. 하지만 이걸 Bird 클래스에 정의할 수는 없습니다. 당연하잖아요? Chicken은 false고 Eagle은 true인데요. 아, 여기서 Overriding을 생각하셨다면 아주 깊은 이야기까지 꺼내야 해서 좀 난감해지는데요. 순수 가상 함수를 정의하는 것과 단순히 Overriding하는 것은 객체 지향 설계상의 미묘한 차이가 있고, 뒤에서 설명할 virtual의 쓰임새 2.와도 관련이 있는데, 그냥 간단하게 생각해서 지금 이 경우는 Bird 객체에서 canFly() 함수가 할 일은 없으므로 순수 가상 함수로 선언하는 것이 맞습니다.
또 한가지 더, 그렇다면 그냥 Bird 클래스에 canFly()를 선언할 필요없이 Chicken 클래스랑 Eagle 클래스에 따로 만들어주면 될 것 아닌가? 라는 생각을 하실 수도 있습니다. 이렇게 되면 다형성을 이용해 Bird 객체에 Chicken이나 Eagle 객체를 담았을 때 canFly() 함수를 호출할 수 없게 됩니다. 즉, 다음의 코드가 불가능합니다.
Bird* pBird = new Chicken();
pBird->canFly(); // Bird 클래스 선언에는 canFly() 함수가 없습니다!
따라서 반드시 Bird 클래스는 canFly()에 대한 정의를 가져야 합니다. (사실 여기에도 다른 문제가 존재하는데, 이건 virtual 사용 용도 2. 번에서 살펴봅니다.)
즉, Bird를 상속받는 모든 클래스들은 canFly() 함수를 구현할 의무를 지닌다.. 정도가 되겠습니다. 물론 구현 안할 수도 있습니다. 그렇다면 그 클래스도 결국 추상 클래스가 되죠. 어쨌든, 이런 함수가 필요는 한데, 실제 구현은 자신을 상속받는 클래스들에게로 미루는 것이 바로 순수 가상 함수입니다. 선언은 아래와 같이 됩니다.
class Bird {
public:
virtual bool canFly() = 0;
// 이렇게 하면 순수 가상 함수로 선언되고
// Bird는 인스턴스화가 불가능한 추상 클래스가 됩니다.
};
class Chicken : public Bird {
public:
// 여기서 실제로 구현해줍니다.
bool canFly()
{
return false;
}
};
class Eagle : public Bird {
public:
// 여기서 실제로 구현해줍니다.
bool canFly()
{
return true;
}
};
이렇게 선언해놓으면, 다형성을 이용하여 Bird 객체(라는 용어가 맞지는 않습니다. 실제로 생성되는건 Bird 객체가 아니라 Chicken이나 Eagle 객체이기 때문입니다. 하지만 그냥 이렇게 이야기하도록 하죠) 에서 canFly() 함수를 호출하게 되는겁니다. 물론, 실제 호출되는 함수는 Bird 객체가 실제로 어떤 객체를 담고 있었느냐에 따라 C++이 자동으로 호출하게 됩니다. Bird에서 canFly()는 순수 가상 함수이기 때문에 자동으로 2. 에서 설명하는 동적 바인딩을 사용하게 됩니다.
그리고 앞서도 말했지만 순수 가상 함수를 가진 추상 클래스인 Bird는 실제로 객체를 생성(인스턴스화)할 수 없고 오직 포인터나 레퍼런스등의 용도로만 사용할 수 있습니다. 이게 무슨 필요냐.. 하실 분도 계시겠지만, 객체 지향 개념에서 가장 요긴하고 중요한 개념 중 하나입니다. 내공이 깊어지면 이해되실겁니다.
2. 상속, Overriding과 관련하여 동적 바인딩을 지시하기 위한 virtual
C++은 기본적으로 함수 호출에 정적 바인딩을 사용합니다. JAVA는 기본이 동적 바인딩이죠. 그렇다면 정적 바인딩과 동적 바인딩은 어떤 것인가? 에 대해 알아봅시다.
예를 들어 우리가 아래와 같은 함수 호출을 한다고 합시다.
void doSomething(int a, int b)
{
printf("%d, %d\n", a, b);
}
이 경우 컴파일러가 컴파일(일반적으로 말하듯이, 링킹 과정도 포함하는 것으로 합시다. 정확한 용어는 빌드죠)할 때 printf가 무엇인지는 명확합니다. 즉, 프로그램이 실행되면서 저 코드에서 호출하는 printf() 함수가 다른 것으로 바뀔 리는 없다는거죠.
하지만 아래와 같은 클래스 구조와 함수를 생각해봅시다.
class Bird {
public:
string getName { return string("Bird"); }
};
class Chicken : public Bird {
public:
string getName { return string("Chicken"); }
};
class Eagle : public Bird {
public:
string getName { return string("Eagle"); }
};
void doSomething (Bird* pBird)
{
cout << pBird->getName() << endl;
}
우리가 doSomething() 함수를 호출할 때는 다음 3가지 정도의 경우를 생각해볼 수 있습니다.
1) Bird 객체로 호출하는 경우
Bird bird;
doSomething(&bird);
2) Chicken 객체로 호출하는 경우(다형성)
Chicken chicken;
doSomething(&chicken);
3) Eagle 객체로 호출하는 경우(다형성)
Eagle eagle;
doSomething(&eagle);
그렇다면 우리가 원하는 doSomething() 호출 결과는 어떤 것일까요? 당연히 1)의 경우 "Bird"이고 2)는 "Chicken" 3)은 "Eagle" 이겠죠. 하지만 앞서 말했듯이 C++은 기본적으로 정적 바인딩을 사용합니다. 즉, 컴파일할 때 doSomething() 함수의 pBird는 Bird 객체이고, 따라서 무조건 Bird 클래스의 getName() 메서드를 호출하도록 만들어버립니다. 따라서, 실제로는 1)이던 2)던 3)이던 무조건 "Bird"가 출력됩니다. 이는 우리가 원하는 결과가 아닙니다. 따라서, "프로그램 실행 중 pBird가 실제로 가지고 있는 객체의 타입에 맞는 메서드 - 멤버 함수 - 를 호출" 해야할 필요가 있습니다. 이를 동적(실행 시간에 결정한다고 하여) 바인딩이라고 부르고, 이를 사용하기 위해서 virtual 키워드를 씁니다. 즉, 아래처럼 합니다.
class Bird {
public:
// 기반 클래스에만 virtual 키워드를 써주면 됩니다.
virtual string getName { return string("Bird"); }
};
class Chicken : public Bird {
public:
string getName { return string("Chicken"); }
};
class Eagle : public Bird {
public:
string getName { return string("Eagle"); }
};
void doSomething (Bird* pBird)
{
cout << pBird->getName() << endl;
}
이렇게 하면, 컴파일러는 실행 시간에 pBird가 어떤 객체의 포인터를 가지고 있는지를 판단하여 적절한 함수를 호출해주게 됩니다.
얼핏 보면 동적 바인딩이 더 좋아보이는 것 같지만, 실제로 동적 바인딩을 위해 컴파일러는 여러가지 정보를 추가적으로 유지해야 해서, 메모리 사용량과 수행 속도 면에서 약간의 손해를 보게 됩니다. JAVA가 대체적으로(대체적으로 입니다. 네이티브 컴파일링하면 이렇고 저렇고 태클 사양합니다) C++보다 느린 이유 중 하나가 기본적으로 동적 바인딩을 사용하기 때문입니다.
3. 다이아몬드형 상속에서 가상 기반 클래스 선언을 위한 virtual
다이아몬드 상속이란, 같은 기반 클래스를 가진 두 클래스를 동시에 상속받는 것을 이야기합니다. C++은 다중 상속이 가능하기 때문에 이런 문제가 발생하죠. 이러한 다중 상속은 득보다 실이 많다 하여 최근의 OOP 언어인 JAVA나 C#등에서는 지원하지 않습니다. 대신 interface 같은 것을 사용하죠.
어쨌든, 다이아몬드 상속의 예를 들어보겠습니다.
class A {
// ...
};
class B : public A {
// ...
};
class C : public A {
// ...
};
class D : public B, public C {
// ...
};
이런 형태의 상속은 기반 클래스 A가 B와 C에 각각 존재하므로 공간 낭비, 함수 호출 대상 모호 등의 문제점을 지니게 됩니다. 이를 해결하기 위해 가상 기반 클래스(virtual base class)를 사용하는데, virtual 키워드는 바로 이 가상 기반 클래스를 나타내기 위해 쓰입니다. 위 코드에서 가상 기반 클래스를 사용하면,
class A {
// ...
};
class B : virtual public A {
// ...
};
class C : virtual public A {
// ...
};
class D : public B, public C {
// ...
};
이렇게 됩니다.
이 설명이 부족하다면, MSDN http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vclang/html/_pluslang_Virtual_Base_Classes.asp 을 참고하세요.
(출처 : 'C++ virtual 키워드...' - 네이버 지식iN)
댓글
댓글 쓰기