상속의 개념과 구현
기반 클래스의 상태와 동작을 파생 클래스가 확장하는 상속 문법과 접근 수준·생성 순서·재정의를 익힙니다.
객체 지향 프로그래밍의 핵심 개념 중 하나인 상속(Inheritance)을 다룹니다.
상속은 기존 클래스의 속성과 기능을 물려받아 새로운 클래스를 만드는 메커니즘으로, 코드 재사용성을 극대화하고 클래스 간의 계층 구조를 형성하는 데 매우 중요한 역할을 합니다.
Dog가 Animal의 한 종류라면, Dog 객체는 Animal 멤버를 포함한 뒤 Dog만의 멤버를 추가합니다. 이 구조가 자연스러울 때 public 상속을 선택합니다.
- Base: Animal
클래스 정의 Base: Animal 공통 상태: name, age 공통 동작: eat(), sleep() Derived: Dog : public Animal 추가 상태: breed 추가 동작: bark(), displayInfo()
- Animal 부분
Dog 객체 안의 배치 Animal 부분 부모가 정의한 공통 멤버가 먼저 들어간다. Dog 확장 부분 자식이 추가한 멤버가 같은 객체 안에 붙는다.
상속이란?
상속은 기존 클래스(부모 클래스, 기반 클래스, 슈퍼 클래스)의 멤버(변수와 함수)를 새로운 클래스(자식 클래스, 파생 클래스, 서브 클래스)가 물려받는 것을 의미합니다.
자식 클래스는 부모 클래스의 모든 멤버를 가지며, 필요에 따라 새로운 멤버를 추가하거나 부모 클래스의 멤버를 재정의(override)할 수 있습니다.
상속의 장점- 코드 재사용성 증가: 기존 클래스의 코드를 다시 작성할 필요 없이, 상속을 통해 그대로 사용할 수 있습니다.
- 유지보수성 향상: 부모 클래스의 변경 사항이 모든 자식 클래스에 자동으로 반영되므로, 코드 수정이 용이합니다.
- 확장성: 기존 클래스를 수정하지 않고, 상속을 통해 새로운 기능을 추가할 수 있습니다.
- 클래스 계층 구조 형성: 클래스 간의 관계를 명확하게 표현하여 코드의 구조를 이해하기 쉽게 만듭니다.
- A is a B 관계: 예를 들어, 개(Dog)는 동물(Animal)이다와 같이, 자식 클래스는 부모 클래스의 한 종류임을 나타냅니다.
상속의 기본 형식
C++에서 상속은 다음과 같은 형식으로 구현합니다.
class 자식클래스이름 : 접근지정자 부모클래스이름 {
// 자식 클래스의 멤버 (새로운 멤버 추가 또는 부모 클래스 멤버 재정의)
};:(콜론) 기호를 사용하여 상속 관계를 나타냅니다.접근지정자는 상속 방식(public, private, protected)을 지정합니다. (뒤에서 자세히 다룸)
Animal 클래스를 상속받는 Dog 클래스
#pragma once
#include <iostream>
#include <string>
// 부모 클래스 (기반 클래스)
class Animal {
private:
std::string name; // 이름
protected:
std::string species; // 종 (자식 클래스에서 접근 가능)
int age; // 나이 (자식 클래스에서 접근 가능)
public:
Animal(std::string n, std::string s, int a) : name(n), species(s), age(a) {
std::cout << "Animal 생성자 호출: " << name << " (" << species << ")" << std::endl;
}
virtual ~Animal() { // virtual 소멸자 (다형성 관련, 뒤에서 자세히 다룸)
std::cout << "Animal 소멸자 호출: " << name << std::endl;
}
void displayInfo() const {
std::cout << "이름: " << name << ", 종: " << species << ", 나이: " << age << std::endl;
}
// Getter 함수
std::string getName() const { return name; }
std::string getSpecies() const { return species; }
int getAge() const { return age; }
};#pragma once
#include "Animal.h" // Animal 클래스 포함
// 자식 클래스 (파생 클래스)
class Dog : public Animal { // public 상속
private:
std::string breed; // 견종
public:
// Dog 생성자: Animal 생성자 호출하여 초기화
Dog(std::string n, int a, std::string b) : Animal(n, "개", a), breed(b) {
std::cout << "Dog 생성자 호출: " << getName() << " (" << breed << ")" << std::endl;
}
~Dog() override { // override 키워드는 선택 사항 (C++11부터)
std::cout << "Dog 소멸자 호출: " << getName() << std::endl;
}
void bark() const {
std::cout << "멍멍!" << std::endl;
}
void displayInfo() const override { // 부모 클래스의 displayInfo() 재정의 (override)
Animal::displayInfo(); // 부모 클래스의 displayInfo() 먼저 호출
std::cout << "견종: " << breed << std::endl; // 추가 정보 출력
}
};#include "Dog.h" // Dog 클래스 포함
int main() {
Dog myDog("바둑이", 3, "진돗개"); // Dog 객체 생성
myDog.displayInfo(); // Animal::displayInfo() + Dog::displayInfo()
myDog.bark(); // Dog::bark()
// 부모 클래스의 멤버에도 접근 가능
std::cout << myDog.getName() << "는 " << myDog.getAge() << "살입니다.\n";
return 0; // myDog 객체 소멸 (Dog 소멸자 -> Animal 소멸자 순서로 호출)
}자식 생성자는 부모 생성자를 먼저 호출해 기반 부분을 완성한 뒤, 자식 멤버를 초기화합니다. 소멸은 그 반대 순서입니다.
- 1Animal이 가진 공통 정보
객체 안의 Base 부분 이름과 나이 조회, 기본 정보 출력처럼 모든 Animal에 공통인 책임을 담습니다. name = "바둑이" species = "개" age = 3 displayInfo(), getName()
- 2Dog가 추가한 전용 정보
객체 안의 Derived 부분 견종, 짖기, Animal 출력 뒤 견종을 덧붙이는 override가 Dog 쪽 책임입니다. breed = "진돗개" bark() displayInfo() override
| 시점 | 호출되는 함수 | 읽어야 할 의미 |
|---|---|---|
| 생성 시작 | Animal 생성자 → Dog 생성자 | 기반 클래스가 준비되어야 자식 멤버가 안전하게 붙는다. |
| 사용 중 | getName(), bark(), displayInfo() | 상속받은 인터페이스와 추가 인터페이스를 같은 객체에서 쓴다. |
| 소멸 시작 | Dog 소멸자 → Animal 소멸자 | 자식 자원을 먼저 정리하고 부모 부분을 마지막에 정리한다. |
상속 접근 지정자
자식 클래스가 부모 클래스의 멤버를 상속받을 때, 상속 접근 지정자를 사용하여 상속받는 멤버의 접근 권한을 제어할 수 있습니다.
public 상속- 부모 클래스의
public멤버는 자식 클래스에서도public이 됩니다. - 부모 클래스의
protected멤버는 자식 클래스에서protected가 됩니다. - 부모 클래스의
private멤버는 자식 클래스에서 접근할 수 없습니다.
protected 상속- 부모 클래스의
public멤버는 자식 클래스에서protected가 됩니다. - 부모 클래스의
protected멤버는 자식 클래스에서protected가 됩니다. - 부모 클래스의
private멤버는 자식 클래스에서 접근할 수 없습니다.
private 상속- 부모 클래스의
public멤버는 자식 클래스에서private이 됩니다. - 부모 클래스의
protected멤버는 자식 클래스에서private이 됩니다. - 부모 클래스의
private멤버는 자식 클래스에서 접근할 수 없습니다.
- 대부분의 경우
public상속을 사용합니다. 이는 A is a B 관계를 명확하게 표현하며, 부모 클래스의 인터페이스를 자식 클래스가 그대로 유지하도록 합니다. protected또는private상속은 특수한 경우에 사용됩니다.
상속 접근 지정자는 코드 안에서만 보이는 문법 차이가 아니라, 외부 사용자에게 부모 인터페이스를 얼마나 노출할지 결정하는 설계 장치입니다.
다음 표처럼 “외부 API”, “파생 클래스 내부”, “구현 재사용” 관점으로 나누면 세 접근 방식의 의도가 더 분명해집니다.
class Base {
public:
int publicVar;
protected:
int protectedVar;
private:
int privateVar;
};
// public 상속
class PublicDerived : public Base {
public:
void accessBaseMembers() {
publicVar = 10; // OK: public 멤버는 그대로 public
protectedVar = 20; // OK: protected 멤버는 그대로 protected
// privateVar = 30; // 오류! private 멤버는 접근 불가
}
};
// protected 상속
class ProtectedDerived : protected Base {
public:
void accessBaseMembers() {
publicVar = 10; // OK: public 멤버가 protected로 변경됨
protectedVar = 20; // OK: protected 멤버는 그대로 protected
// privateVar = 30; // 오류! private 멤버는 접근 불가
}
};
// private 상속
class PrivateDerived : private Base {
public:
void accessBaseMembers() {
publicVar = 10; // OK: public 멤버가 private로 변경됨
protectedVar = 20; // OK: protected 멤버는 private로 변경됨
// privateVar = 30; // 오류! private 멤버는 접근 불가
}
};
int main() {
PublicDerived pd1;
pd1.publicVar = 100; // OK: public 멤버는 외부에서 접근 가능
ProtectedDerived pd2;
// pd2.publicVar = 100; // 오류! protected 멤버는 외부에서 접근 불가
PrivateDerived pd3;
// pd3.publicVar = 100; // 오류! private 멤버는 외부에서 접근 불가
return 0;
}생성자와 소멸자의 상속
- 생성자: 자식 클래스의 생성자는 부모 클래스의 생성자를 명시적으로 호출하여 부모 클래스의 멤버를 초기화해야 합니다. 이는 멤버 초기화 리스트를 통해 수행합니다. 만약 부모 클래스의 생성자를 명시적으로 호출하지 않으면, 부모 클래스의 기본 생성자(매개변수 없는 생성자)가 자동으로 호출됩니다.
- 소멸자: 자식 클래스의 소멸자는 자동으로 부모 클래스의 소멸자를 호출합니다. 소멸자 호출 순서는 자식 클래스 소멸자 -> 부모 클래스 소멸자 순서입니다.
Animal.h와 Dog.h의 예시 코드를 참고하세요.
Dog클래스의 생성자에서: Animal(n, "개", a)를 통해Animal클래스의 생성자를 명시적으로 호출하여name,species,age를 초기화합니다.Dog와Animal클래스 모두 소멸자를 가지고 있으며,Dog객체가 소멸될 때Dog의 소멸자가 먼저 호출되고, 그 다음Animal의 소멸자가 호출됩니다.
멤버 함수 재정의 (Overriding)
자식 클래스는 부모 클래스의 멤버 함수를 재정의(Override)하여 자신에게 맞게 기능을 변경할 수 있습니다.
함수 재정의는 부모 클래스와 동일한 이름, 반환 타입, 매개변수 리스트를 가지는 멤버 함수를 자식 클래스에서 다시 정의하는 것을 의미합니다.
override키워드 (C++11): 자식 클래스에서 재정의하는 함수에override키워드를 붙이면, 컴파일러가 재정의가 올바르게 이루어졌는지 검사해줍니다. (오타 등으로 재정의가 되지 않았을 경우 컴파일 오류 발생)
Animal.h와 Dog.h의 예시 코드를 참고하세요.
Dog클래스는Animal클래스의displayInfo()함수를 재정의하여, 부모 클래스의 정보를 먼저 출력하고, 자신의 추가적인 정보(breed)를 출력합니다.
Dog::displayInfo()는 Animal::displayInfo()와 같은 인터페이스를 유지하면서, 부모 출력 뒤에 견종 정보를 추가합니다.
- 호출호출 myDog.displayInfo() 사용자
같은 이름의 함수를 호출한다.
- 자식 재정의자식 재정의 Dog
displayInfo() Dog에 맞는 출력 흐름으로 들어간다.
- 부모 기능 재사용부모 기능 재사용 Animal
displayInfo() 공통 정보 출력 뒤 breed를 덧붙인다.
| 확인할 것 | override가 맞는 조건 | 틀리면 생기는 문제 |
|---|---|---|
| 함수 이름 | 부모와 자식의 함수 이름이 같다. | 새 함수가 생길 뿐 재정의가 아니다. |
| 매개변수 | 반환 타입과 매개변수 목록이 호환된다. | 오버로딩처럼 보이거나 컴파일 오류가 난다. |
| override 키워드 | 자식 함수 뒤에 override를 붙여 검사를 맡긴다. | 오타를 컴파일러가 잡아 주지 못한다. |
| 부모 호출 | 필요하면 Animal::displayInfo()를 먼저 호출한다. | 공통 출력이 빠지고 자식 정보만 남는다. |
다중 상속 (Multiple Inheritance) (심화)
C++은 하나의 자식 클래스가 여러 부모 클래스를 상속받는 다중 상속(Multiple Inheritance)을 지원합니다.
다중 상속은 강력하지만, 복잡성을 증가시키고 여러 가지 문제점(예: 다이아몬드 상속 문제)을 야기할 수 있으므로 신중하게 사용해야 합니다.
특히 다이아몬드 상속 문제와 이를 해결하는 가상 상속(virtual inheritance)은 10장의 다음 단원인 ‘다중 상속과 가상 상속’에서 자세히 다룹니다.
상속을 적용할 때는 코드 재사용보다 대체 가능성, 접근 지정자, 소멸자 규칙을 먼저 점검해야 합니다.
public 상속은 부모 자리에 자식을 넣어도 된다는 강한 설계 약속이다.
- is-a
Base 자리에 Derived가 자연스러운가
- 공개 범위
public 상속이 맞는 관계인가
- 수명
Base 포인터 삭제 가능성
- 대안
has-a 합성이 더 단순한가
| 점검 질문 | 통과하면 | 막히면 |
|---|---|---|
| 진짜 is-a인가? | public 상속 후보 | 합성으로 전환 |
| Base API를 노출해도 되나? | 대체 가능성 확인 | private/protected 또는 합성 |
| Base*로 삭제되나? | virtual 소멸자 필요 | 소유권 경로 단순화 |
상속은 public/protected/private 접근과 생성자·소멸자 호출 순서를 같이 봐야 구조가 안전합니다.
단순 구현 공유가 목적이라면 상속보다 합성이 더 단순한 선택일 수 있습니다.