[C++] 항목 34: 인터페이스 상속과 구현 상속의 차이를 제대로 파악하고 구별하자
카테고리: Cpp
태그: Cpp
이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 GPT에게 요약을 요청하여 작성되었습니다.
이펙티브 C++ 제3판, 스콧 마이어스 저자, 곽용재 번역
📦 6. 상속, 그리고 객체 지향 설계
👉🏻 항목 34: 인터페이스 상속과 구현 상속의 차이를 제대로 파악하고 구별하자
✅ public 상속의 두 가지 측면
public 상속은 인터페이스 상속과 구현 상속으로 나뉜다.
클래스 설계자가 선택할 수 있는 사항:
- 인터페이스만 상속
- 인터페이스 및 구현 모두 상속받고, 구현 오버라이드 가능
- 인터페이스 및 구현 모두 상속받고, 어떤 것도 오버라이드 불가능
📌 세 가지 함수 타입의 차이
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 { ... };
핵심:
- Shape 클래스는 순수 가상 함수인 draw()의 존재로 인해, 추상 클래스가 된다
✅ 1. 순수 가상 함수: 인터페이스만 상속
virtual void draw() const = 0;
특징:
- 순수 가상 함수를 물려받은 파생 클래스는 해당 순수 가상 함수를 다시 선언해야 한다
- 순수 가상 함수는 추상 클래스 안에서 정의를 갖지 않는다
목적:
순수 가상 함수를 선언하는 목적은 파생 클래스에게 함수의 인터페이스를 물려주는 것
Shape* ps = new Shape; // ❌ Shape는 추상 클래스
Shape* ps1 = new Rectangle; // ✅
ps1->draw(); // Rectangle::draw 호출
Shape* ps2 = new Ellipse; // ✅
ps2->draw(); // Ellipse::draw 호출
주의점:
- 순수 가상 함수 Shape::draw에도 정의를 붙일 수는 있다
- 단, 클래스 이름을 한정자로 붙여주어야 한다
ps1->Shape::draw(); // ✅ Shape::draw 호출
✅ 2. 단순 가상 함수: 인터페이스 + 기본 구현 상속
virtual void error(const string& msg);
목적:
단순 가상 함수를 선언하는 목적은 파생 클래스로 하여금 함수의 인터페이스뿐만 아니라 그 함수의 기본 구현도 물려주는 것
특징:
- 파생되는 클래스마다 기본으로 제공되는 error 함수를 써도 된다
단순 가상 함수의 예시
class Airport { ... };
class Airplane {
public:
virtual void fly(const Airport& destination);
...
};
// 목적지로 비행기를 날려보내는 단순 가상 함수
void Airplane::fly(const Airport& destination) { ... };
class ModelA : public Airplane { ... };
class ModelB : public Airplane { ... };
장점:
- ModelA와 ModelB는 Airplane::fly 함수를 물려받는다
- 클래스 사이의 공통적 특징이 명확해진다
- 코드가 중복되지 않는다
- 확장에 열려있다
- 유지 보수가 쉽다
⚠️ 단순 가상 함수의 문제점
비행 방식이 다른 ModelC 비행기에서 fly 함수 재정의하는 것을 빼먹음
class ModelC : public Airplane {
...
};
int main() {
Airport PDX(...);
Airplane* pa = new ModelC;
...
// Airplane::fly 함수 호출
// 재정의를 깜빡함!
pa->fly(PDX);
}
문제:
- 재정의를 강제할 방법이 없다
- 기본 구현이 적절하지 않은 경우에도 사용될 수 있다
✅ 해결 방법 1: 인터페이스와 구현 분리
class Airplane {
public:
virtual void fly(const Airport& destination) = 0;
...
protected:
void defaultFly(const Airport& destination);
};
// 목적지로 비행기를 날려보내는 기본 구현
void Airplane::defaultFly(const Airport& destination) { ... }
class ModelA : public Airplane {
public:
virtual void fly(const Airport& destination)
{ defaultFly(destination); }
...
};
class ModelB : public Airplane {
public:
virtual void fly(const Airport& destination)
{ defaultFly(destination); }
...
};
class ModelC : public Airplane {
public:
virtual void fly(const Airport& destination);
...
};
// 목적지로 'ModelC 비행기'를 날려보내는 구현
void ModelC::fly(const Airport& destination) { ... }
개선된 점:
- Airplane::fly 함수를 순수 가상 함수로 변경 → fly 함수의 인터페이스 제공 역할
- defaultFly 함수를 일반(비가상) 함수로 구현 → 파생 클래스에서 이 함수를 재정의할 수 없도록 함
단점:
- 클래스의 네임스페이스가 더러워진다 (fly 함수, defaultFly 함수 때문)
✅ 해결 방법 2: 순수 가상 함수에 구현 제공
class Airplane {
public:
virtual void fly(const Airport& destination) = 0;
...
};
// 순수 가상 함수 구현
void Airplane::fly(const Airport& destination) { ... }
class ModelA : public Airplane {
public:
virtual void fly(const Airport& destination)
{ Airplane::fly(destination); }
...
};
class ModelB : public Airplane {
public:
virtual void fly(const Airport& destination)
{ Airplane::fly(destination); }
...
};
class ModelC : public Airplane {
public:
virtual void fly(const Airport& destination);
...
};
// 목적지로 'ModelC 비행기'를 날려보내는 구현
void ModelC::fly(const Airport& destination) { ... }
개선된 점:
- 순수 가상 함수를 구현함으로써 문제 해결
- 이전보다 더 깔끔하다
- fly와 defaultFly가 합쳐짐
단점:
- 서로 다른 보호 수준(public, protected)으로 부여할 수 없어졌다
✅ 3. 비가상 함수: 인터페이스 + 필수 구현 상속
int objectID() const;
목적:
비가상 함수를 선언하는 목적은 파생 클래스가 함수 인터페이스와 더불어, 그 함수의 필수적인 구현을 물려받게 하는 것
핵심:
- 파생 클래스는 이 함수를 바꿀 수 없다
📌 함수 타입별 상속 특성 정리
| 함수 타입 | 인터페이스 상속 | 구현 상속 | 오버라이드 |
|---|---|---|---|
| 순수 가상 함수 | ✅ | ❌ (선택적) | 필수 |
| 단순 가상 함수 | ✅ | ✅ (기본) | 선택 |
| 비가상 함수 | ✅ | ✅ (필수) | 불가 |
⚠️ 피해야 할 설계 실수
1. 모든 멤버를 비가상 함수로 선언
문제점:
- 기본 클래스의 동작을 특별하게 만들 이유 사라짐
- 비가상 소멸자 문제(항목 7 참조)
- 가상 함수/비가상 함수의 성능 차이는 없다
2. 모든 멤버를 가상 함수로 선언
문제점:
- 파생 클래스에서 재정의가 안 되어야 할 함수는 비가상 함수로 만들어야 한다
🧐 정리
- 인터페이스 상속은 구현 상속과 다르다. public 상속에서 파생 클래스는 항상 기본 클래스의 인터페이스를 모두 물려받는다.
- 순수 가상 함수는 인터페이스 상속만을 허용한다. 파생 클래스가 반드시 구현해야 한다.
- 단순 가상 함수는 인터페이스 상속과 더불어 기본 구현의 상속도 가능하도록 지정한다. 파생 클래스가 선택적으로 재정의할 수 있다.
- 비가상 함수는 인터페이스 상속과 더불어 필수 구현의 상속도 가하도록 지정한다. 파생 클래스가 변경할 수 없다.
- 클래스 설계 시 각 함수의 특성에 맞는 타입을 선택하자. 모든 함수를 가상으로 만들거나 비가상으로 만드는 것은 좋지 않다.
댓글남기기