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

Minsu._.Lighting·2023년 12월 8일
0

[ Effective C++ ] 정리 모음집
" C++ 프로그래머의 필독서, 스콧 마이어스의 Effective C++ 를 읽고 내용 요약 / 정리 "

[핵심]

" 순수 가상, 가상, 비가상 함수를 사용목적에 맞게 사용해 클래스를 구성하자! "

  • 인터페이스 상속은 구현 상속과 다르다!
  • public 상속에서 파생 클래스는 항상 기본 클래스의 인터페이스를 모두 물려 받는다!
  • 순수 가상 함수는 인터페이스 상속만을 허용한다!
  • 단순 가상 함수는 인터페이스 상속과 더불어 기본 구현의 상속도 가능하도록 지정한다!
  • 비가상 함수는 인터페이스 상속과 더불어 필수 구현의 상속도 가하도록 지정한다!

💡 인터페이스 상속? 구현 상속?

  • (public) 상속은 인터페이스 상속과 구현 상속으로 나뉜다.
    - 이 둘의 차이는 함수 선언과 함수 정의의 차이와 맥을 같이 한다


💡 멤버 함수 인터페이스는 항상 상속되게 되어있다

  • public 상속의 의미는 is-a 이므로 기본 클래스에 해당하는 것들로 모두 파생 클래스에도 해당 되어야 한다
    - 기본 클래스에서 동작하는 함수는 파생 클래스에서도 동작해야 한다


💡 순수 가상, 가상, 비가상 함수를 알아보자!

class Shape
{
public:
	virtual void draw() const = 0;
    virtual void error(const string& msg);
    int objectID() const;
    ...
};

class Rectangle : public Shape { ... };
class Ellipse : public Shape { ... };

📌 순수 가상 함수

virtual void draw() const = 0;

  • 순수 가상 함수의 목적은 파생 클래스에게 함수의 인터페이스만 물려 주려는 것
    - "Shape 클래스를 상속 받으면 draw()함수는 무조건 필요해. 다만, 구현은 마음대로 해도 좋아!"
    - 순수 가상 함수를 상속 받은 파생 클래스는 해당 순수 가상 함수를 다시 선언해야 함
    - 순수 가상 함수는 전형적으로 추상 클래스 안에서 정의를 갖지 않음

📢 순수 가상 함수도 정의를 제공할 수 있다!

Shape* ps = new Shape;

Shape* ps1 = new Rectangle;
ps1->draw();

Shape* ps2 = new Ellipse;
ps2->draw();

ps1->Shape::draw();
ps2->Shape::draw();

- 사용 시 ::(스코프 연산자)를 붙여줘야 한다
- 단순 가상 함수에 대한 기본 구현을 보다 안전하게 제공하는 메커니즘으로도 활용 된다

📌 단순 가상 함수

virtual void error(const string& msg);

  • 단순 가상 함수의 목적은 파생 클래스로 하여금 함수의 인터페이스 뿐만 아니라 함수의 기본 구현도 물려받게 하자는 것
    - "특별히 동작해야 한다면 직접 구현 해! 그게 아니라면 기본 구현 버전을 사용 해!

📌 단순 가상 함수 제공 시 함수 인터페이스와 기본 구현을 한꺼번에 지정하도록 두는 것은 위험하다!

class Airplane
{
public:
	virtual void fly();
    ...
};

void Airplane::fly()
{
	// 비행기의 기본 날기 동작
}

class ModelA : public Airplane { ... };
class ModelB : public Airplane { ... };

class ModelC : public Airplane			// 특별한 날기 동작을 하는 C 비행기
{
	...									// 하지만 fly() 함수가 선언되지 않음
}
  • 특별한 동작을 하는 파생 클래스에서 구현을 깜빡한다면 기본 버전으로 실행 된다.

📢 해결 방법 1 - 가상 함수의 인터페이스와 기본 구현의 연결관계 끊기

class Airplane
{
public:
	virtual void fly() = 0;
    ...
protected:
	void defaultFly();
};


void Airplane::defaultFly()
{
	// 비행기의 기본 날기 동작
}


class ModelA : public Airplane		// B도 마찬가지
{
public:
	virtual void fly()
    { defaultFly(); }
    ...
};


class ModelC : public Airplane
{
public:
	virtual void fly();
    ...
};

void ModelC::fly()
{
	C 비행기만의 날기 동작
}
  • fly 함수를 순수 가상 함수로 만들기
    - 파생 클래스 에서는 해당 함수의 구현을 할 수 밖에 없어진다
  • 기존의 기본 버전 함수는 defaultFly 라는 비가상 함수로 만듬
    - 기존 동작 방식을 원하는 A, B 비행기는 fly 호출 시 defaultFly를 호출하게
    - C 비행기는 자신만의 동작을 fly에 구현
    📢 defaultFly를 가상 함수로 두면 똑같은 문제 발생...
    - 파생 클래스에서 재정의를 안한다면...

📢 해결 방법 2 - 순수 가상 함수의 정의를 제공하기

class Airplane
{
public:
	virtual void fly() = 0;
    ...
};


class ModelA : public Airplane		// B도 마찬가지
{
public:
	virtual void fly()
    { Airplane::fly(); }
    ...
};


class ModelC : public Airplane
{
public:
	virtual void fly();
    ...
};

void ModelC::fly()
{
	C 비행기만의 날기 동작
}
  • fly 함수를 순수 가상 함수로 만들고 정의도 제공
    - A, B 비행기 에서는 스코프 연산자를 통해 Airplane::fly() 호출
    - C 비행기는 자신만의 동작을 fly에 구현

📌 비가상 함수

int objectID() const;

  • 비가상 함수의 목적은 파생 클래스가 함수 인터페이스와 그 함수의 필수적인 구현을 물려받게 하는 것
    - 모든 파생 클래스가 같은 동작을 필요로 한다면 선택



💡 클래스 설계 시 가장 흔한 실수

  • 클래스 설계 시 순수 가상 함수, 단순 가상 함수, 비가상 함수 셋의 차이점을 이용해 기능의 목적을 보다 정밀하게 지정할 수 있음.

📌 모든 멤버 함수를 비가상 함수로 선언하는 것

📢 가상 함수의 비용만 생각해서 쓰지 않으려고 하는 생각은 버리자!

📌 모든 멤버 함수를 가상 함수로 선언하는 것

  • 자신이 만든 구조에 자신이 없어 보일 수 있다
    - 분명 재정의가 필요 없는 함수도 있을텐데...
    - 비용 측면에서도 아쉽다...

📢 물론 맞을 수도 있다!
- 인터페이스 클래스의 경우...
- [ 항목 31 : 파일 사이의 컴파일 의존성을 최대로 줄이자 ] 참조

profile
오코완~😤😤

0개의 댓글

관련 채용 정보