클래스를 확장하여 재사용하기
여러분은 생성자로 클래스를 만들고 소멸자로 정리해 왔습니다. 상속은 그다음 단계입니다. 한 클래스의 멤버를 다른 클래스로 복사하는 대신, 새 클래스가 기존 클래스의 특수화된 버전이라고 선언하고 모든 것을 자동으로 물려받게 합니다.
기존 클래스가 기반 클래스(부모)이고, 새 클래스가 파생 클래스(자식)입니다. 파생 클래스는 기반 클래스의 모든 데이터와 동작에서 출발한 뒤, 자신을 다르게 만드는 부분을 추가하거나 변경합니다. 이것은 "is-a" 관계를 위한 C++의 주된 도구입니다. Dog는 Animal이고, SavingsAccount는 BankAccount입니다.
기본 문법: class Derived : public Base
파생 클래스 이름 뒤에 콜론, 접근 지정자, 기반 클래스 이름을 차례로 써서 상속합니다. 가장 흔한 형태는 public 상속입니다.
Dog는 name이나 eat()을 전혀 선언하지 않았지만, 둘 다 Animal에서 상속되었기 때문에 Dog 객체에서 동작합니다. 파생 클래스는 기반 클래스가 전혀 알지 못하는 bark() 같은 멤버를 자유롭게 추가할 수 있습니다.
protected: 자식만을 위한 멤버
기반 클래스의 private 멤버는 파생 클래스 내부에서도 접근할 수 없습니다. 상속은 캡슐화를 깨뜨리지 않습니다. 기반 클래스의 내부를 외부 세계에는 숨기되 하위 클래스에는 사용할 수 있게 하려면 접근 지정자 protected를 사용하세요.
세 가지 수준을 동심원으로 생각하세요. private는 "이 클래스만", protected는 "이 클래스와 그 후손들", public은 "모두"를 뜻합니다. 전체적인 그림은 접근 지정자를 참고하세요.
생성자와 소멸자 순서
파생 객체는 기반 클래스의 하위 객체를 포함하며, 파생 부분이 만들어지기 전에 그 기반 부분이 살아 있어야 합니다. 그래서 생성은 기반 클래스부터 먼저, 소멸은 파생 클래스부터 먼저(정확히 반대 순서로) 진행됩니다. 기반 클래스가 생성자 인자를 필요로 하면, 멤버 초기화 리스트를 통해 전달합니다.
출력이 그 순서를 구체적으로 보여 줍니다.
Animal ctor: Rex // 기반 클래스가 먼저 만들어진다
Dog ctor // 그다음 파생 부분
Dog dtor // 반대 순서로 소멸된다...
Animal dtor: Rex // ...기반 클래스가 마지막
: Animal(n)을 빠뜨렸고 기반 클래스에 기본 생성자가 없다면 코드는 컴파일되지 않습니다. C++은 기반 부분을 어떻게 만들어야 할지 전혀 모르기 때문입니다. 상속할 의도가 있는 기반 클래스는 거의 항상 소멸자를 선언해야 하며(다음 페이지에서 보듯이, 흔히 가상 소멸자를) 선언해야 합니다.
오버라이드: 기반 클래스 메서드 재정의하기
파생 클래스는 같은 시그니처를 가진 메서드를 선언하여 상속받은 메서드를 대체할 수 있습니다. 원본에는 여전히 Base::method()로 접근할 수 있습니다.
이것은 다형성이 아니라 단순한 *이름 가리기(name hiding)*입니다. 어떤 describe()가 실행되는지는 변수의 정적 타입에 따라 컴파일 타임에 결정됩니다. 이는 중대한 제약입니다. 실제로는 Circle을 가리키는 Shape&나 Shape*를 통해 호출하더라도 여전히 Shape::describe()가 실행됩니다. 이를 해결하려면 virtual이 필요하며, 그것이 다음 페이지의 주제입니다.
객체 슬라이싱(slicing)을 조심하세요
기반 클래스 참조나 포인터는 파생 객체를 가리킬 수 있기 때문에, 파생 객체를 기반 타입 변수에 복사하고 싶어집니다. 그러지 마세요. 파생 부분이 잘려 나갑니다.
a는 기반 클래스 이름표를 단 Dog가 아니라 진짜 Animal이므로, breed는 그 안에 아예 존재하지 않습니다. 파생 객체를 다형적으로 다루려면 기반 클래스의 참조(Animal&)나 포인터(Animal*)를 사용해야 하며, 기반 클래스 값을 사용해서는 절대 안 됩니다. 슬라이싱은 조용히 일어납니다. 깔끔하게 컴파일되고 그저 데이터를 슬그머니 버릴 뿐이라, 운영 환경까지 흘러가기 쉬운 상속 버그 중 하나입니다.
피해야 할 흔한 실수
- 기반 클래스의
private멤버에 자식에서 접근할 수 있다고 기대하기. 접근할 수 없습니다. 파생 클래스가 정당하게 필요로 하는 데이터에는protected를 쓰고, 진짜 내부 상태는private로 유지하세요. - 기반 클래스 생성자 인자 전달을 잊기. 기반 클래스에 기본 생성자가 없다면, 파생 클래스 생성자의 초기화 리스트에서 명시적으로 호출해야 합니다(
: Base(args)). - 파생 객체를 기반 클래스 값으로 슬라이싱하기.
Dog를Animal로 복사하면Dog고유의 모든 것이 사라집니다. 대신 기반 클래스 참조나 포인터를 전달하고 저장하세요. - 코드 재사용을 위해 상속을 남용하기. 상속은 "is-a"를 모델링합니다. 관계가 사실은 "has-a(~을 가진다)"라면(
Car는Engine을 가진다), 상속보다 합성(composition), 즉 멤버 객체를 택하세요.
다음: 가상 함수
방금 본 오버라이드는 컴파일 타임에 결정되었기 때문에, 기반 포인터를 통한 호출은 파생 버전을 무시했습니다. 다음 페이지인 가상 함수에서는 키워드 virtual과 override를 소개합니다. 이것은 런타임 타입이 어떤 메서드를 실행할지 결정하게 하는 메커니즘으로, 진정한 다형성을 가능하게 하고 왜 기반 클래스에 가상 소멸자가 필요한지를 설명합니다.
자주 묻는 질문
C++에서 상속이란 무엇인가요?
상속을 사용하면 기존 클래스(기반 클래스)를 바탕으로 새로운 클래스(파생 클래스)를 정의할 수 있습니다. 파생 클래스는 기반 클래스의 데이터 멤버와 멤버 함수를 자동으로 물려받으며, 새로운 것을 추가하거나 기존 동작을 대체할 수 있습니다. 이는 "is-a(~은 ~이다)" 관계를 모델링하며(Dog는 Animal이다), C++이 클래스 계층 전반에 걸쳐 코드를 재사용하고 확장하는 주된 방법입니다.
C++에서 public 상속과 private 상속의 차이는 무엇인가요?
public 상속(class Dog : public Animal)에서는 기반 클래스의 public 인터페이스가 파생 클래스에서도 public으로 유지되므로, Dog는 Animal이며 Animal이 기대되는 모든 곳에서 사용할 수 있습니다. private 상속에서는 물려받은 멤버가 private이 되어, 파생 클래스가 기반 클래스의 구현을 재사용하기는 하지만 그것을 대신할 수는 없습니다. public 상속이 압도적으로 일반적인 경우이며, private 상속은 "~을 이용해 구현됨" 식의 재사용에만 사용하세요.
C++에서 상속이 있을 때 생성자와 소멸자는 어떤 순서로 실행되나요?
생성자는 기반 클래스부터 먼저 실행됩니다. 파생 클래스 생성자의 본문이 실행되기 전에 기반 클래스가 완전히 생성됩니다. 소멸자는 정확히 그 반대 순서로 실행됩니다. 먼저 파생 클래스, 그다음 기반 클래스 순입니다. 이는 파생 객체가 만들어지거나 해체되는 동안, 그 객체가 의존하는 모든 부분이 이미 존재하거나(또는 아직 존재하도록) 보장합니다.