나만의 작은 도서관

[C++][Class] 상속(Inheritance) 본문

C++/문법 및 메소드(STL)

[C++][Class] 상속(Inheritance)

pledge24 2025. 3. 26. 00:34

상속(Inheritance)이란?

상속 예제

  • 상속은 다른 클래스의 내용을 그대로 물려받아서 사용하는 것을 의미한다.
    • 좀 더 딱딱하게 말하면, 기존 클래스(부모 클래스 또는 상위 클래스)의 속성과 기능을 새로운 클래스에서 재사용하거나 확장할 수 있도록 하는 기능을 의미한다.
  • 상속을 이용하면 상하 관계에 있는 클래스들 내에 중복되는 코드가 생기는 것을 방지할 수 있다.
  • 상속 시, 상속을 하는(물려주는) 클래스를 기반 클래스 또는 부모 클래스라고 부르며, 상속을 받는(물려받는) 클래스를 파생 클래스 또는 자식 클래스라고 부른다. (앞으로의 설명에선 각각 기반 클래스, 파생 클래스라 부름)

 

상속의 기본 구조

class Base {} // 기반 클래스
class Derived : public Base {} // 파생 클래스
  • Base 클래스는 기반 클래스, Derived 클래스는 파생 클래스이다.
  • 상속 시, 파생 클래스는 기반 클래스에 대해 상속 접근 지정자를 지정한다. (위 예시에선 public)
  • “상속 접근 지정자”는 부모 클래스의 접근 지정자의 최대치를 지정한다고 보면 된다.
    • 예를 들어, 상속 접근 지정자를 protected로 지정하면 상속받는 멤버의 접근 레벨 최대치가 protected가 되어, public이 전부 protected로 변경된다.
    • 실제로 protected, private 상속은 거의 쓸 일이 없다.

파생 클래스의 메모리 구조

파생 클래스 메모리 구조

  • 파생 클래스는 기반 클래스의 멤버 변수와 멤버 함수를 상속받는다. 상속받은 멤버 변수와 멤버 함수에 대한 메모리는 파생 클래스 타입의 객체 생성 시 객체 메모리 앞부분에 위치한다.
  • 파생 클래스의 멤버 정보는 기반 클래스 뒤에 위치하며, 따라서 전체적인 메모리 순서는 기반 → 파생 순이 된다.

 

파생 클래스 메모리 구조 예제

#include <iostream>

using namespace std;

class Base {
protected:
    int base_a;
    char base_c;
};

class Derived : public Base{
public:
    void showAddress(){
        cout << "Object Addr : " << this << '\n';
        cout << "================================\n";
        cout << "base_a : " << &base_a << '\n';
        cout << "base_c : " << static_cast<void*>(&base_c) << '\n';
        cout << "derived_a : " << &derived_a << '\n';
        cout << "derived_c : " << static_cast<void*>(&derived_c) << '\n';
    }
private:
    int derived_a;
    char derived_c;
};

struct s{
    int struct_s;
    char struct_c;
};

int main(void){
    Derived derived;
    derived.showAddress();
}

 

 

실행 결과

  • 아래의 결과처럼 기반 → 파생 순으로 메모리가 정렬되어 있음을 알 수 있다.
Object Addr : 0x61fe10
================================
base_a : 0x61fe10
base_c : 0x61fe14
derived_a : 0x61fe18
derived_c : 0x61fe1c

 


업캐스팅과 다운캐스팅

업캐스팅(왼쪽), 다운캐스팅(오른쪽)

  • 하위 클래스를 상위 클래스로 캐스팅하는 것을 "업캐스팅(Upcasting)", 상위 클래스를 하위 클래스로 캐스팅하는 것을 "다운캐스팅(Downcasting)"이라고 부른다.
  • 업캐스팅의 경우, 위에서 알아보았듯 기반 → 파생 순으로 메모리가 정렬되어 있기 때문에 파생된 객체의 앞부분만 읽으면 기반 클래스 타입의 객체처럼 읽을 수 있다. 따라서 업캐스팅은 문제가 되지 않는 캐스팅이다.

업캐스팅 예제

// =========정적 할당==========
Derived derived1; // 정적 할당
// Base& base1 = derived1;처럼 레퍼런스로도 업캐스팅 가능
Base base1 = (Base)derived1;
cout << base1.s << '\\n'; // 출력 결과: Base

// =========동적 할당==========
Derived* derived2 = new Derived(); 
Base* base2 = (Base*)derived2;
cout << base2->s << '\\n'; // 출력 결과: Base
  • 반면에 다운 캐스팅은 기존 메모리보다 더 큰 메모리를 가진 객체로 캐스팅한다. 이 때문에 할당하지 않은 메모리에 접근하는 문제가 발생하므로, 보장되지 않는 한 절대로 함부로 다운 캐스팅을 하면 안 된다. (애초에 “동물은 개다”와 같이 is-a관계를 부정하는 캐스팅 방식이다.)

다운캐스팅 예제

// =========정적 할당==========
Base base1; 
Derived* derived1 = static_cast<Derived*>(&base1); // 강제 변환
cout << derived1->s << '\\n'; // 할당하지 않은 메모리 접근(런타임 오류)

// =========동적 할당==========
Base* base2 = new Base()
Derived* derived2 = (Derived*)base2; 
cout << derived2->s << '\\n'; // 할당하지 않은 메모리 접근

 

 

업캐스팅, 다운캐스팅이 자주 사용되는 상황

  • 업캐스팅은 주로 객체를 동적 할당할 때나 다형성을 활용할 때 자주 사용된다.
  • 다운캐스팅은 업캐스팅한 객체를 원래 타입으로 되돌려서 사용해야할때(ex. 파생 클래스에만 정의된 함수를 호출해야 하는 경우) 자주 사용한다.
  • => 업캐스팅과 다운캐스팅 둘 다 자주 사용하므로 타입 변환에 대한 실수를 조심해야한다.

알아둬야 할 점

 

생성자와 소멸자의 호출 순서

  • 파생 클래스는 기본적으로 기반 클래스로부터 상속받았음을 알고 있기 때문에, 파생 클래스는 생성자 / 소멸자 호출 시, 알아서 기반 클래스의 생성자 / 소멸자를 호출한다.
  • 하지만 생성자와 소멸자는 기반 클래스의 호출 순서가 다르다. 건물을 세울 때 기반부터 세우고, 건물을 부술 때 내용(파생)부터 부수는 것처럼 생성자는 기반 → 파생 순으로, 소멸자는 파생 → 기반 순으로 실행된다. 코드로 표현하면 아래와 같다.
// constructor: Base -> Derived
Derived() : Base() {
    /* ------ process Derived's contructor ------*/
    // Derived 클래스가 관리하는 메모리 초기화...
    /*-------------------------------------------*/
}

// destructor: Derived -> Base
~Derived() {
    /* ------ process Derived's destructor ------*/
    // Derived 클래스가 관리하는 메모리 정리...
    /*-------------------------------------------*/
    ~Base();
}

 

 

생성자와 소멸자의 호출 순서 예제

#include <iostream>

using namespace std;

class Base {
public:
    Base() { cout << "Base Constructor!" << '\\n'; }
    ~Base() { cout << "Base Destructor!" << '\\n'; }
};

class Derived : public Base {
public:
    Derived() { cout << "Derived Constructor!" << '\\n'; }
    ~Derived() { cout << "Derived Destructor!" << '\\n'; }
};

int main(void){
    Derived derived;
    return 0;
}

 

 

실행 결과

  • 결과적으로, 전체적인 호출 순서는 아래와 같이 기반 생성자 → 파생 생성자 → 파생 소멸자 → 기반 소멸자가 된다.
Base Constructor!
Derived Constructor!
Derived Destructor!
Base Destructor!

 

 

생성자 작성 시 주의할 점

  • 파생 클래스의 생성자를 작성할 때 초기화 리스트에서 기반 클래스의 생성자를 명시적으로 호출하지 않을 경우, 자동으로 생성된 기반 클래스의 기본 생성자들이 호출될 수 있다.
  • 따라서 실수를 줄이기 위해 기반 클래스의 생성자를 명시적으로 호출해 주는 습관을 들이는 것이 좋다. (같은 이유로 멤버 클래스도 명시적으로 호출하는 것이 좋다.)
  • 그러나 생성자와는 반대로 소멸자는 기반 클래스의 소멸자를 명시적으로 호출하면 안 된다.
// 파생 클래스 생성자.
Derived() : /*명시적 호출*/Base(args...) {}

 

 

기반 클래스의 멤버 함수 사용 시 주의할 점

  • 파생 클래스는 기반 클래스의 멤버 함수를 사용할 수 있지만 기반 클래스의 함수를 호출할 경우 파생 클래스에 정의된 멤버 변수에 접근할 수 없다. (함수는 정의된 위치의 변수에만 접근하기 때문)
  • 따라서, 아래 예시처럼 파생 클래스 타입의 객체에서 함수를 호출해도, 함수가 정의된 위치인 기반 클래스의 변수에 접근한다.
#include <iostream>

using namespace std;

class Base {
public:
    void ShowString() {cout << str << '\\n';}

private:
    string str = "Base"; // 같은 이름의 변수
};

class Derived : public Base {

private:
    string str = "Derived"; // 같은 이름의 변수
};

int main(void){
    Derived derived;
    derived.ShowString(); // 실행 결과: Base
    return 0;
}

 


참고 자료

https://www.linkedin.com/pulse/inheritance-object-oriented-programming-oop-unveiling-kalana-heshan-oz82c/

https://modoocode.com/209

https://learn.microsoft.com/en-us/cpp/cpp/constructors-cpp?view=msvc-170