본문 바로가기
C,C++/C++

[C++] 추상 클래스와 순수 가상함수를 알아보자(feat. 인터페이스, 다중상속)

by woohyeon 2020. 2. 20.
반응형

추상 클래스

추상 클래스란 순수 가상함수를 1개 이상 가지고 있는 클래스를 말한다. 순수 가상함수란, 구현되지 않은 가상함수를 말한다.

구현되지 않은 가상함수란, 상위 클래스에서 정의는 하지 않고 선언만 해놓은 가상함수를 말한다. 순수 가상함수는 다음과 같이 표시한다. 가상 함수에 0을 대입하여 해당 클래스에선 구현하지 않겠다라는 의미를 부여한다.

virtual void PureVirtualFunction() const = 0; // 순수 가상함수

 
가상 함수를 모른다면 https://woo-dev.tistory.com/50



첫 번째 클래스는 추상 클래스이며 두 번째 클래스는 추상 클래스가 아니다.

1.

class FirstClass
{
public:
   ...
   void SampleFunction() const = 0; // 순수 가상함수
   ...
private:
   ...
}



2.

class SecondClass
{
public:
   ...
   void SampleFunction() const; // 가상함수
   ...
private:
   ...
}


추상 클래스는 이름 그대로 추상적인 클래스이다. 추상적이라는 뜻은 구체적이라는 뜻의 반대되는 뜻이다. 추상 클래스는 그 구현이 완벽하지 않아 그 자체로는 사용이 불가능하며, 하위 클래스에 약간의 설계 또는 가이드라인을 주는 클래스이다. 가이드라인은 순수 가상함수를 통해 제공한다.  


한번 상위 클래스가 하위 클래스들에게 어떤 함수를 꼭 구현하도록 강제하고 싶다고 생각해보자. 만약 다음과 같이 선언한다면 하위 클래스에서 구현해야 하는 강제성을 가지지 않는다. 여기서 강제성이란 구현을 하지 않으면 에러를 띄워서 컴파일을 하지 못하게 하는 것이라고 볼 수 있다.

virtual void VirtualFunction() const; // just 가상함수



또한 함수를 선언만하고 구현하지 않았다고 컴파일러는 경고를 발생시킨다. 이는 다른 사람이 보기에 실수로 구현을 하지 않은 것인지 하위 클래스에서 꼭 구현하길 바라는 것인지 의도가 명확하지 않으며 애매한 것은 항상 프로그래밍에서 좋지 않다.


c++ 에선 순수 가상함수를 통해 이 의도를 명확하게 줄 수 있다.


만약 클래스 내에 순수 가상함수가 1개라도 있다면, 하위 클래스에서 무조건 정의하도록 강제한다. 만약 구현하지 않는다면 컴파일러는 상위 클래스에서 선언된 순수 가상함수가 하위 클래스에서 정의되지 않았다며 컴파일 에러를 발생시킨다. 아까 추상 클래스는 구체적이지 않은 불완전한 클래스라고 했다. 그 이유는 구현되지 않은 함수인 순수 가상함수를 가지고 있기 때문이다.

그럼 추상 클래스의 객체를 직접 생성해서 사용할 수 있을까? 정답은 No이다.
왜 안되는지 생각을 해보려면, 반대로 Yes라 생각하고 추상 클래스로 아래와 같이 개체 생성이 가능하다고 가정해보면 된다.

AbstractClass ac;



AbstractClass가 추상 클래스라고 할 때, ac라는 개체를 생성하면 ac는 멤버 함수로 다음과 같이 순수 가상함수를 호출할 수 있게 된다.

ac.PureVirtualFunction();


그런데 순수 가상함수는 AbstractClass에 정의되어 있지 않기 때문에 당연히 런타임에 에러가 날 것이다.
이와 같이 정의되지 않은 함수를 호출할 가능성이 있기 때문에, 이를 사전에 차단하고자 컴파일러가
컴파일 에러를 발생시켜 추상클래스 메모리를 생성하지 못하도록 막아준다.

단, 아래와 같이 추상 클래스 타입의 포인터 변수는 생성이 가능하다. 포인터 변수를 생성한다고 해당 클래스에 대한 메모리 레이아웃이 잡히는 것은 아니기 때문이다. 또한 자식 클래스의 메모리도 할당 가능하다.

AbstractClass* acPtr; // OK
AbstractClass* acPtr = new DerivedClass(); // OK


하지만 아래와 같이 추상 클래스 타입의 메모리를 할당할 순 없다. 정확히 말하면 추상 클래스의 메모리를 단독으로 생성 자체가 불가능하다.

AbstractClass* acPtr = new AbstractClass(); // 컴파일 에러
new AbstractClass(); // 컴파일 에러

 


인터페이스

그럼 인터페이스는 무엇일까?

인터페이스라는 개념은 하위 클래스들에서 꼭 정의해야할 함수를 정해주는 의미상으로 특별한 클래스이다.
멤버 변수와 일반 멤버 함수가 있든 없든 순수 가상함수를 1개 이상 가지는 클래스가 추상 클래스였다면, 오로지 순수 가상함수로만 이루어진 클래스를 인터페이스라고 한다.

다중 상속이 가능한 c++에서 잘못하면 멤버 변수와 함수가 겹칠 수 있기 때문에 다중 상속을 피하는 것이 좋은데, 인터페이스는 이 걱정 없이 다중 상속을 가능하게 해준다.

 예를 들어 날 수 있는 동물들을 클래스로 만든다고 할 때, 이들의 공통점인 날 수 있다라는 특징을 인터페이스로 만들어주는 것이다. Flyable이라는 인터페이스에 Fly라는 순수 가상함수를 만들어 날 수 있는 모든 동물들은 이 인터페이스를 상속하여 Fly 함수를 구현하도록 하는 것이다. 걸을 수 있는 동물들은 걸을 수 있다라는 특징을 인터페이스를 만들어주어 Walkable 인터페이스를 상속하도록 한다.

걸을 수도 날 수도 있다면 두 인터페이스를 모두 상속하면 된다. 인터페이스는 멤버 변수 또는 멤버 함수가 겹칠 일이 없기 때문에 다중 상속의 단점 없이 사용할 수 있다.

단, C++은 인터페이스라는 개념을 따로 기능적으로는 지원하지 않는다. 자바에선 인터페이스라는 기능을 언어 자체에서 지원하기 때문에 interface라는 키워드도 있고 멤버 변수도 생성 불가하며 순수 가상함수만 선언이 가능하다.


하지만 C++에선 인터페이스를 따로 지원하지 않기 때문에 인터페이스를 흉내내어 인터페이스처럼 사용한다. 그래서 멤버 변수를 선언하지 못하게 강제하거나 할 수는 없다는 단점이 있다. 따라서 인터페이스를 작성할 때 해당 클래스가 인터페이스라는 것을 알리기 위해 이름 맨 앞에 IFlyable, IWalkable과 같이 'I'를 붙이는 습관을 들이는 것이 좋다.

class IFlyable
{
public:
  void Fly() const = 0;  
};

class IWalkable
{
public:
  void Walk() const = 0;  
};

class Bird : public IFlyable, public IWalkable
{
public:
  ...
  void Fly() const { std::cout << "Fly" << std::endl; }
  void Walk() const { std::cout << "Walk" << std::endl; }
  ...
  
private:
...
};

 




댓글