본문으로 건너뛰기

안동민 개발노트

본문 시작

상속의 개념과 구현

기반 클래스의 상태와 동작을 파생 클래스가 확장하는 상속 문법과 접근 수준·생성 순서·재정의를 익힙니다.

객체 지향 프로그래밍의 핵심 개념 중 하나인 상속(Inheritance)을 다룹니다.

상속은 기존 클래스의 속성과 기능을 물려받아 새로운 클래스를 만드는 메커니즘으로, 코드 재사용성을 극대화하고 클래스 간의 계층 구조를 형성하는 데 매우 중요한 역할을 합니다.

상속은 객체 안에 부모 부분을 먼저 놓고 자식 부분을 덧붙이는 구조다

Dog가 Animal의 한 종류라면, Dog 객체는 Animal 멤버를 포함한 뒤 Dog만의 멤버를 추가합니다. 이 구조가 자연스러울 때 public 상속을 선택합니다.

  1. Base: Animal

    클래스 정의 Base: Animal 공통 상태: name, age 공통 동작: eat(), sleep() Derived: Dog : public Animal 추가 상태: breed 추가 동작: bark(), displayInfo()

  2. Animal 부분

    Dog 객체 안의 배치 Animal 부분 부모가 정의한 공통 멤버가 먼저 들어간다. Dog 확장 부분 자식이 추가한 멤버가 같은 객체 안에 붙는다.


상속이란?

상속은 기존 클래스(부모 클래스, 기반 클래스, 슈퍼 클래스)의 멤버(변수와 함수)를 새로운 클래스(자식 클래스, 파생 클래스, 서브 클래스)가 물려받는 것을 의미합니다.

자식 클래스는 부모 클래스의 모든 멤버를 가지며, 필요에 따라 새로운 멤버를 추가하거나 부모 클래스의 멤버를 재정의(override)할 수 있습니다.

상속의 장점
  • 코드 재사용성 증가: 기존 클래스의 코드를 다시 작성할 필요 없이, 상속을 통해 그대로 사용할 수 있습니다.
  • 유지보수성 향상: 부모 클래스의 변경 사항이 모든 자식 클래스에 자동으로 반영되므로, 코드 수정이 용이합니다.
  • 확장성: 기존 클래스를 수정하지 않고, 상속을 통해 새로운 기능을 추가할 수 있습니다.
  • 클래스 계층 구조 형성: 클래스 간의 관계를 명확하게 표현하여 코드의 구조를 이해하기 쉽게 만듭니다.
상속 관계 표현
  • A is a B 관계: 예를 들어, 개(Dog)는 동물(Animal)이다와 같이, 자식 클래스는 부모 클래스의 한 종류임을 나타냅니다.

상속의 기본 형식

C++에서 상속은 다음과 같은 형식으로 구현합니다.

C++ 상속 형식
class 자식클래스이름 : 접근지정자 부모클래스이름 {
    // 자식 클래스의 멤버 (새로운 멤버 추가 또는 부모 클래스 멤버 재정의)
};
  • : (콜론) 기호를 사용하여 상속 관계를 나타냅니다.
  • 접근지정자는 상속 방식(public, private, protected)을 지정합니다. (뒤에서 자세히 다룸)
예시 1: Animal 클래스를 상속받는 Dog 클래스
Animal.h
#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; }
};
Dog.h
#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; // 추가 정보 출력
    }
};
main.cpp
#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 소멸자 순서로 호출)
}
Dog 객체는 Animal 부분을 먼저 만들고 Dog 기능을 얹는다

자식 생성자는 부모 생성자를 먼저 호출해 기반 부분을 완성한 뒤, 자식 멤버를 초기화합니다. 소멸은 그 반대 순서입니다.

  1. 1
    Animal이 가진 공통 정보

    객체 안의 Base 부분 이름과 나이 조회, 기본 정보 출력처럼 모든 Animal에 공통인 책임을 담습니다. name = "바둑이" species = "개" age = 3 displayInfo(), getName()

  2. 2
    Dog가 추가한 전용 정보

    객체 안의 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”, “파생 클래스 내부”, “구현 재사용” 관점으로 나누면 세 접근 방식의 의도가 더 분명해집니다.

상속 접근 지정자는 부모 API를 자식 밖에 얼마나 공개할지 정한다

부모의 private 멤버는 직접 접근 대상이 아니다. 차이는 public/protected 멤버가 어디까지 보이느냐다.

  1. public

    부모 public API를 그대로 공개

  2. protected

    외부에는 숨기고 파생 내부만 사용

  3. private

    구현 재사용으로만 흡수

  4. private 멤버

    상속되어도 직접 접근 불가

상속 방식외부에서 보는 모습설계 의미
publicBase API가 유지된다Derived는 Base의 한 종류
protectedBase API가 외부에 숨겨진다파생 내부 구현 도구
privateBase가 인터페이스에서 사라진다상속보다 합성도 비교
상속 접근 지정자에 따른 멤버 접근 권한 변화
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.hDog.h의 예시 코드를 참고하세요.

  • Dog 클래스의 생성자에서 : Animal(n, "개", a)를 통해 Animal 클래스의 생성자를 명시적으로 호출하여 name, species, age를 초기화합니다.
  • DogAnimal 클래스 모두 소멸자를 가지고 있으며, Dog 객체가 소멸될 때 Dog의 소멸자가 먼저 호출되고, 그 다음 Animal의 소멸자가 호출됩니다.

멤버 함수 재정의 (Overriding)

자식 클래스는 부모 클래스의 멤버 함수를 재정의(Override)하여 자신에게 맞게 기능을 변경할 수 있습니다.

함수 재정의는 부모 클래스와 동일한 이름, 반환 타입, 매개변수 리스트를 가지는 멤버 함수를 자식 클래스에서 다시 정의하는 것을 의미합니다.

  • override 키워드 (C++11): 자식 클래스에서 재정의하는 함수에 override 키워드를 붙이면, 컴파일러가 재정의가 올바르게 이루어졌는지 검사해줍니다. (오타 등으로 재정의가 되지 않았을 경우 컴파일 오류 발생)
예시: 멤버 함수 재정의

Animal.hDog.h의 예시 코드를 참고하세요.

  • Dog 클래스는 Animal 클래스의 displayInfo() 함수를 재정의하여, 부모 클래스의 정보를 먼저 출력하고, 자신의 추가적인 정보(breed)를 출력합니다.
override는 부모 함수의 이름을 유지한 채 자식에게 맞게 동작을 바꾼다

Dog::displayInfo()는 Animal::displayInfo()와 같은 인터페이스를 유지하면서, 부모 출력 뒤에 견종 정보를 추가합니다.

  1. 호출
    호출 myDog.displayInfo() 사용자

    같은 이름의 함수를 호출한다.

  2. 자식 재정의
    자식 재정의 Dog

    displayInfo() Dog에 맞는 출력 흐름으로 들어간다.

  3. 부모 기능 재사용
    부모 기능 재사용 Animal

    displayInfo() 공통 정보 출력 뒤 breed를 덧붙인다.

확인할 것override가 맞는 조건틀리면 생기는 문제
함수 이름부모와 자식의 함수 이름이 같다.새 함수가 생길 뿐 재정의가 아니다.
매개변수반환 타입과 매개변수 목록이 호환된다.오버로딩처럼 보이거나 컴파일 오류가 난다.
override 키워드자식 함수 뒤에 override를 붙여 검사를 맡긴다.오타를 컴파일러가 잡아 주지 못한다.
부모 호출필요하면 Animal::displayInfo()를 먼저 호출한다.공통 출력이 빠지고 자식 정보만 남는다.

다중 상속 (Multiple Inheritance) (심화)

C++은 하나의 자식 클래스가 여러 부모 클래스를 상속받는 다중 상속(Multiple Inheritance)을 지원합니다.

다중 상속은 강력하지만, 복잡성을 증가시키고 여러 가지 문제점(예: 다이아몬드 상속 문제)을 야기할 수 있으므로 신중하게 사용해야 합니다.

특히 다이아몬드 상속 문제와 이를 해결하는 가상 상속(virtual inheritance)은 10장의 다음 단원인 ‘다중 상속과 가상 상속’에서 자세히 다룹니다.

상속을 적용할 때는 코드 재사용보다 대체 가능성, 접근 지정자, 소멸자 규칙을 먼저 점검해야 합니다.

상속은 코드 재사용보다 대체 가능성과 수명 규칙을 먼저 통과해야 한다

public 상속은 부모 자리에 자식을 넣어도 된다는 강한 설계 약속이다.

  1. is-a

    Base 자리에 Derived가 자연스러운가

  2. 공개 범위

    public 상속이 맞는 관계인가

  3. 수명

    Base 포인터 삭제 가능성

  4. 대안

    has-a 합성이 더 단순한가

점검 질문통과하면막히면
진짜 is-a인가?public 상속 후보합성으로 전환
Base API를 노출해도 되나?대체 가능성 확인private/protected 또는 합성
Base*로 삭제되나?virtual 소멸자 필요소유권 경로 단순화

상속은 public/protected/private 접근과 생성자·소멸자 호출 순서를 같이 봐야 구조가 안전합니다.

단순 구현 공유가 목적이라면 상속보다 합성이 더 단순한 선택일 수 있습니다.