객체지향프로그래밍(12) - 상속(2), 메모리 구조로 보는 상속

메모리 구조를 통해 상속 이해하기

Featured image

🔚 짧게 하는 복습

✅ 1. OOD(Object Oriented Design)을 통해서 OOP(Object Oriented Programming)를 구현할 수 있다.

✅ 2. 객체지향 프로그래밍의 장점을 이해한다.

혹시 기억이 안 난다면, 다시 돌아가자


저번 시간에는 Class Diagram을 그리는 방법과 상속하는 법을 이해했다.

이번 강의는 Class를 만들면 메모리 구조가 어떻게 되는지, 또 상속하면 메모리 구조가 어떻게 되는지 알아보도록 하겠다.


두 번째, 상속의 특징

우리가 저번 시간에 배운 상속은 코드의 중복을 없애는 것에 의미를 두었다.

하지만 처음 상속에 대해 배울 때를 기억하는가? 이렇게 설명했다.

이는 상위 클래스의 모든 변수와 메소드를 하위 클래스에서 사용할 수 있는 특징이 있다.

여기서 소개하지 않은 특징이 있다. 상위 클래스의 모든 변수와 메소드를 가지면서, 하위 클래스만의 변수와 메소드를 가질 수 있다.

아래의 코드 예를 보자.

이는 Class Diagram으로 그리면 아래와 같이 나타난다.

Class Diagram에서 볼 수 있듯이, walk나 age는 상위 클래스와 하위 클래스들 모두 사용이 가능하다.

그런데 하위 클래스에서 정의한 human의 study나 cat의 bark나 introduce는 상위 클래스에서 사용할 수 없다.

두 번째 상속의 특징이다. 위에서 아래로 속성을 넘겨줄 순 있으나, 아래에서 위로 속성을 줄 수는 없다.

위에서 아래로는 무조건 모든 속성을 넘겨주지만, 아래는 추가로 속성을 가질 수도 있고 아닐 수도 있다.

기억이 안 난다면, 저번 포켓몬 Class Diagram을 보자.

pokemon 상위 클래스를 상속하지만, 아래에서 하위 클래스들은 생성자/소멸자/복사 생성자를 제외하고는 자신만의 속성을 가지지 않는다.


다시 복기하는 구조체의 메모리 구조

앞으로 조금 더 유용한 상속의 기능을 이야기하기 전에, 메모리 구조에 대해서 알아보자.

우선 C++에서 클래스는 무엇과 본질이 같다고 했는가? 바로 구조체이다.

그렇다면 구조체의 메모리 구조는 어땠는가? 기억이 잘 안 날 것 같아서 다시 짧게 설명해보겠다.

가장 메모리 크기가 큰 원소를 찾아서, 그 원소의 크기로 배열을 만든다.

예를 들어 아래의 코드에서는, double형이 가장 크기 때문에 8바이트로 만든다.

typedef struct student{
    char* name; //포인터의 크기는 보통 8바이트(1 word)
    int stu_number; // 4바이트
    double score; //8바이트
}stu;

위의 이미지와 같은 모습으로 만들어진다. (편의를 위해 1칸을 4바이트 단위로 만듦)

그런데 여러분은 궁금한 점이 있을 것이다. int형은 4바이트인데 8바이트가 할당되면 어떻게 저장되는지를 말이다.

이는 간단한데, 앞의 4바이트만 사용하고, 나머지 4바이트는 사용하지 않는다. 즉 빈 곳인 채로 놔두는데, 이를 패딩이라 한다.

구조체에서 원소를 선언한 순서대로 채워지고, 이에 의해서 패딩은 사이즈가 달라질 수 있다.


클래스의 메모리 구조

클래스의 메모리 구조는 구조체와 거의 흡사하다.

조금 똑똑한 학생이라면 함수도 포인터(주솟값)을 가지니, 순서대로 변수를 넣고 함수 포인터도 넣으면 된다고 생각할 것이다.

예를 들면, 우리가 만든 animal의 메모리 구조는 아래 그림과 같이 말이다.

포인터는 8바이트, age는 4바이트.

즉 8바이트 2개를 만들어 16바이트를 만들어 놓고, 앞에는 age 뒤에는 walk() 함수의 포인터를 넣으면 된다고 생각할 수 있다.

굉장히 좋은 접근이다. 그런데 여기서 중요한 사실이 있다.

우리가 메모리를 가지는 이유는 무엇일까? 정보가 저장되어야 하기 때문이다.

즉, age나 breed나 name은 바뀔 수 있고 그 정보들을 저장하기 위해 변수가 필요하다.

그렇기에 변수에 메모리를 할당시켜주는 것이다.

반면에 함수는 어떤가? 함수의 내용이 프로그램 실행 중에 바뀔 일이 있을까? 일반적으로 없다.

그렇다면 모든 객체가 메소드의 메모리를 가져야 할까? 아니다.

그냥 어떤 메소드가 호출되는지만 알면 된다. 함수는 객체마다 필요한 것이 아니라 하나의 공간에 있으면 충분하다. (보통 메소드의 포인터는 메모리 계층에서 Text Section에 존재한다.)

그래서 동일 변수의 구조체와 클래스를 만들고, 클래스에 아무리 메소드를 추가시켜도 클래스나 객체의 메모리 크기는 변하지 않는다.

아래의 코드를 돌려서 직접 확인해보자!


메모리 구조로 다시 보는 상속 생성자의 원리

이제는 메모리 구조를 통해서 상속의 생성자가 어떻게 이루어지는지 알아보자.

예시를 위해 위 animal 코드를 조금 수정해서 설명하자면,

#include <iostream>

using namespace std;

class animal {
private:
    int age;
public:
    animal() : age(1) {};

    void walk() { cout << "I'm Walking" << endl; }
};

class cat : public animal {
private:
    int cuteness;

public:
    cat() : animal(), cuteness(10000) {}
    cat(int _cuteness) : animal(), cuteness(_cuteness) {}

    void bark() { cout << "meow meow" << endl; }

    void introduce() { cout << "I'm cute x " <<  cuteness << endl; }
};

class human : public animal {
private:
    double tiredness;

public:
    human() : animal(), tiredness(10000) {}
    human(double _tiredness) : animal(), tiredness(_tiredness) {}

    void study() { cout << "I am studying x "<< tiredness << endl; }
};

int main() {
    animal badukee;
    human Minsu = human(200);
    cat navi = cat();

    badukee.walk();
    Minsu.walk();
    navi.walk();
    cout << endl;

    navi.bark();
    Minsu.study();
    cout << endl;

    navi.introduce();
}

main 함수의 처음부터 보면, badukee가 생성자를 호출하는 순간, class에서는 멤버 변수의 크기를 모두 계산해서 4바이트의 공간을 할당한다.

그리고 자신의 클래스 내에서 디폴트 생성자를 바로 찾는다.

그리고 그 생성자를 호출하면 아래와 같이 4바이트에 1이라는 숫자가 들어가고 객체 생성이 종료된다.

두 번째도 같다. Minsu가 생성자를 호출하는 순간, class에서는 멤버 변수의 크기를 모두 계산해서 16바이트의 공간을 할당한다. (패딩)

자신의 클래스 내에서 human 매개변수 한 개 오버로딩 생성자를 찾는다.

그런데 찾았더니 animal()이라는 생성자를 실행하라고 시킨다.

그렇기에 animal()을 실행하면, 똑같은 과정으로 Minsu도 badukee처럼 age에 1이라는 숫자가 들어가고 객체 생성이 종료된다.

그리고 난 후, 다음은 tiredness에 _tiredness가 들어가야 하니 200이 입력된다.

마지막으로 navi다. navi 역시 생성자를 호출하면 16바이트가 할당되고, 클래스 내에서 디폴트 생성자를 찾는다.

봤더니 animal()을 실행하라고 하고 age부터 순서대로 차게 된다.

마지막으로 cuteness에 10000이 들어가고 객체 생성이 종료된다.


상속에서의 메소드 호출의 원리

위에서 본 것처럼 생성자의 호출은 먼저 자신의 클래스를 확인하는 것을 알 수 있었다.

그렇다면 메소드 호출의 원리는 어떨까? 헷갈릴 필요 없이 여기서도 아래에서 위로 확인하면 된다.

예를 들어 badukee가 walk를 호출했다고 하자. 그럼 자신의 클래스 내에서 정의가 되어있기에, walk를 실행하면 된다.

그런데 Minsu와 navi는 어떨까? 자신의 클래스 내에서 walk가 정의되어 있지 않다.

그렇다면 위로 올라가서 상위 클래스의 정의를 보는 것이다.

walk가 animal이라는 상위 클래스에서 정의가 돼 있다! 그렇다면 고민할 것 없이 walk를 실행시키면 되는 것이다.

이 순서와 메모리 구조는 후에 배울 오버라이딩, 가상 함수라는 개념에서 매우 중요하니 꼭 알아두어야 한다.

자신의 클래스부터 아래에서 위로, 이것이 상속 메소드 호출의 핵심이다.


📖 오늘의 핵심(다 알기 전까지는 넘어가지 말자❗)

✅ 1. 클래스와 구조체의 메모리 구조의 공통점과 차이점을 안다.

✅ 2. 상속 시 생성자의 작동원리에 대해 안다.

✅ 3. 상속 시 메소드의 호출 순서에 대해 안다.

⚠️ 자신의 클래스부터 아래에서 위로, 꼭 숙지하도록 하자.

💣 과제, 이전에 다루었던 포켓몬 클래스의 크기를 예측하고, 직접 출력해본다(난이도 下)

🔜 더 공부해보기,

읽어볼 거리(1) - 배열의 메모리 구조

읽어볼 거리(2) - 구조체의 메모리 구조