방프리

17.12.23. Effective C++ 6. 상속, 그리고 객체 지향 설계 (항목34) 본문

C++/Effective C++

17.12.23. Effective C++ 6. 상속, 그리고 객체 지향 설계 (항목34)

방프리 2020. 1. 17. 08:12

항목 34 : 인터페이스 상속과 구현 상속의 차이를 제대로 파악하고 구별하자

 

public 상속의 개념을 두 가지로 분류해서 나누어 볼 수 있습니다. 

(1). 함수 인터페이스의 상속

(2). 함수 구현의 상속

이 두가지를 간단하게 나누어보자면 

함수인터페이스 상속 = 순수가상함수 

함수 구현의 상속 = 단순가상함수

로 나눌 수 있습니다.

다음 예제 코드를 보면서 천천히 살펴보겠습니다.

 

ex1) 

class Shape {

public:

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

virtual void error(const std::string&); //단순가상함수

int ObjectID() const(); //비가상함수

...

};

 

class Rectangle : public Shape { ... };

class Ellipse : public Shape { ... };

 

1번째로 선언된 draw 함수를 먼저 보겠습니다. 일단  draw 함수는 순수가상함수로 선언되었습니다.

순수 가상함수는 가장 큰 두 가지 특징을 가지고 있습니다. 첫번째로 순수 가상함수를 물려받은 

자식 클래스는 해당 순수 가상함수를 다시 선언후 구현부를 작성해야 합니다.

두 번째 특징은 순수 가상함수는 전형적으로 추상 클래스 안에서 정의를 갖지 않습니다.

즉, 순수 가상 함수를 선언하는 목적은 파생 클래스에게 함수의 인터페이스만을 물려주는 것입니다.

하지만 순수 가상 함수에도 정의를 제공할 수 있습니다. 호출도 가능하다는 뜻이죠.

다음 코드처럼 실행해도 아무 문제가 없다는 뜻이죠

 

ex2) 

Shape* ps = new Shape; // 에러! Shape는 추상 클래스

 

Shape* ps1 = new Rectangle;

ps1->draw(); // Rectagne 클래스의 함수 호출

 

Shape* ps2 = new Ellipse;

ps2->draw(); // Ellipse 클래스의 함수 호출

 

ps1->Shape::draw(); // 순수가상함수의 함수 호출

 

이제 두 번째로 선언된 error함수를 통해 단순가상함수에 대하 알아보겠습니다.

단순가상함수는 간단하게 말해서 부모클래스에서 정의된 함수를 자식클래스에서도 사용하는 함수입니다.

즉, 부모클래스에서 구현이 된 함수를 자식클래스가 그대로 사용이 가능하고 필요하다면

override를 통해 다시 재정의 할 수 있다는 뜻이죠.

한마디로 자식 클래스로 하여금 함수의 인터페이스뿐만 아니라 그 함수의 기본 구현도 물려받게 하자는 것입니다.

하지만 단순가상함수는 큰 단점을 가지고 있습니다. 바로 안전성인데요

만약 자식클래스에서 부모클래스와 다르게 동작해야 하는 함수를 재정의하지 않으면

예기치 않는 예외를 발생시킵니다. 하나하나 프로그래머가 꼼꼼히 체크하지 않으면 안되는 것이죠

그래서 단순가상함수를 제작할 땐 안전성까지 고려해가며 구현을 해주어야 합니다.

 

ex3)

class Airport { ... };

 

class Airplane {

public:

virtual void fly(const Airport&) = 0;

...

protected:

void defaultFly(const Airport&);

};

 

void Airplane::fly(const Airport& des)

{

//do something

}

 

void defaultFly(const Airport& des);

{

//do something

}

 

class ModelA : public Airplane { 

public:

virtual void fly(const Airport& des)

{ defaultFly(des); }

};

class ModelB : public Airplane { 

public:

virtual void fly(const Airport& des)

{ defaultFly(des); }

};

 

구현부와 선언하는 부분을 따로 분리하는 것이죠. fly함수는 순수가상함수이므로 반드시 선언해야하고

defaultFly함수는 따로따로 정의해줘야 하기 때문에 실수가 일어날 일을 줄여줍니다.

하지만 불필요한 함수들이 너무 많이 선언되었습니다. 지금은 단순히 예제 하나로만 확인했지만

프로젝트의 규모가 커지면 소스코드도 당연히 늘어날 것이기 때문에 불필요한 함수들이 많은 건 안좋겠죠?

 

ex4)

class Airplane {

public:

virtual void fly(const Airport&) = 0;

...

};

 

void Airplane::fly(const Airport& des)

{

//do something

}

 

class ModelA : public Airplane {

public:

virtual void fly(const Airport& des)

{ Airplane::fly(des); }

};

 

class ModelB : public Airplane {

public:

virtual void fly(const Airport& des)

{ Airplane::fly(des); }

};

 

마지막으로 선언된 objectID함수를 알아보겠습니다. 부모클래스에서 비가상함수로 선언된 함수는 

더 이상 자식클래스에서 수정하지 않겠다는 표현입니다. 바뀔 구현부가 더 이상 바뀔 생각도 없고

바꾸어서도 안되는 함수를 만들 때 사용됩니다.

 

이것만은 잊지 말자!

* 인터페이스 상속은 구현 상속과 다릅니다. public 상속에서, 파생 클래스는 항상 기본 클래스의

인터페이스를 모두 물려받습니다.

* 순수 가상 함수는 인터페이스 상속만을 허용합니다.

*단순(비순수) 가상 함수는 인터페이스 상속과 더불어 기본 구현의 상속도 가능하도록 지정합니다.

* 비가상 함수는 인터페이스 상속과 덥루어 필수 구현의 상속도 가하도록 지정합니다.

Comments