Publish:

태그: , ,

카테고리:


🤯 언리얼을 하기 위해 C++ 기억 되살리기 프로젝트

개체지향 프로그래밍 (OOP) 3

1. 상속

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Animal.h
class Animal
{
    public:
        Animal(int age);
    private:
        int mAge;
};

// Cat.h
class Cat : public Animal
{
    public:
        Cat(int age, const char* name);
    private:
        char* mName;
};

// Cat.cpp
Cat::Cat(int age, const char* name)
    : Animal(age)                       // 부모로부터 상속을 받았으므로, 부모의 생성자 호출
{                                       // 초기화 리스트 중인데 가장 먼저 할 일은 부모의 생성자를 호출하는 것
                                        // mAge는 부모에게 있으므로 부모한테 생성자 호출하라고 던지는 거임
    size_t size = strlen(name) + 1;
    mName = new char[size];
    strcpy(mName, name);
}

1-1. 상속이란?

  • 다른 클래스의 특성을 내려 받음
    • 베이스 클래스(부모 클래스), 파생 클래스(자식 클래스)
  • 파생 클래스의 개체는 다음의 것들을 가짐
    • 베이스 클래스의 멤버 변수
    • 베이스 클래스의 멤버 메서드
    • 자신의 생성자와 소멸자
  • 파생 클래스는 멤버 변수 및 메서드 추가 가능

1-2. 메모리를 보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Animal.cpp
Animal::Animal(int age)
    : mAge(age)
{
}

// Cat.cpp
Cat::Cat(int age, const string& name)
    : Animal(age)
{
    // 부모 생성자를 암시적으로 먼저 호출 후 자식 생성자 호출하여 초기화 됨
    size_t size = strlen(name) + 1;
    mName = new char[size];
    strcpy(mName, name);
}

// main.cpp
Cat* myCat = new Cat(2, "Mew");
// myCat 변수는 스택에 4바이트를 먹음


자식 클래스로 만든 오브젝트에 대한 포인터를 부모 포인터로 할 수 있음.

Animal* a = new Cat(...);

1-3. 생성자 호출 순서

  • 베이스 클래스의 생성자가 먼저 호출 됨
    • 명시적 또는 암시적으로
  • 그 다음으로 파생 클래스의 생성자가 호출 됨
  • 부모 클래스의 특정 생성자를 호출 할 땐 초기화 리스트를 사용해야 함
    • 안그러면 호출할 데가 없음. 자식 생성자 끝나고 나면 그다음부턴 대입임.
    1
    2
    3
    4
    5
    6
    
    Cat::Cat(int legs, int age, const string& callingName)
      : Animal(legs, age)             // 명시적 호출
      , mCallingName(callingName)
    {
    
    }
    

매개변수 없는 생성자가 ‘없는’ 부모 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Animal.cpp
Animal::Animal(int age)
    : mAge(age)
{
    // 매개변수 있는 생성자만 있음
}

// Cat.cpp
Cat::Cat(int age, const string& name)
{
    // <!--------- 컴파일 에러--------->
    // 초기화 리스트에서 부모 생성자를 호출하지 않으므로
    // 암시적으로 부모의 생성자 Animal()을 호출하게 됨
    // 그러나 부모엔 Animal()이 없고 Animal(int age)만 있음
    // 그래서 호출할 수 있는 생성자가 없어 컴파일 에러

    size_t size = strlen(name) + 1;
    mName = new char[size];
    strcpy(mName, name);
}

// main.cpp
Cat* myCat = new Cat(2, "Mew");

1-4. 자식 개체 지우기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Animal.cpp
Animal::~Animal()
{

}

// Cat.cpp
Cat::~Cat()
{
    // 자식부터 지워짐
    delete mName;
    // 이후 ~Animal()을 호출
}

// main.cpp
delete myNeighboursCat;
  • 생성자 호출 순서와 정반대 (자식->부모)
  • 자식 클래스 소멸자의 마지막에서 부모 클래스의 소멸자가 자동으로 호출됨

2. 다형성 (feat. 상속)

2-1. 다형성에 들어가기 전에, 멤버 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Animal.h
class Animal
{
    public:
        int GetAge();
    private:
        int mAge;
};

// Cat.h
class Cat : public Animal
{
    public:
        const char* GetName();
    private:
        char* mName;
};

// main.cpp
// 각 값은 스택 또는 힙에 위치해 있음 (myCat은 스택에, 값은 힙에...)
Cat* myCat = new Cat(5, "Coco");
Cat* yourCat = new Cat(2, "Mocha");

myCat->GetName();           // 그렇다면 멤버 함수는 어디에 있을까?
yourCat->GetName();
  • 멤버 함수도 메모리 어딘가에 위치해 있음
  • 멤버 함수는 컴파일 시에 한 번만 메모리에 할당됨
    • 그리고 각 개체를 오브젝트로 패스해줌

2-2. 함수 오버라이딩, 바인딩, 가상함수

  • 자식 클래스에서 함수 내용을 재작성 하는 것
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Animal
{
    public:
        void Speak();   // " An animal";
}

class Cat : public Animal
{
    public:
        void Speak();   // "Meow";
}

Cat* myCat = new Cat();
myCat->Speak();         // Meow

Animal* yourCat = new Cat();
yourCat->Speak();       // An animal

정적 바인딩

c++에선 정적 바인딩을 함. 무늬 따라 간다는 것.

Cat* myCat = new Cat(5, "Mocha");

Cat* 으로 타입을 정의했으므로 무조건 Cat에 있는 함수를 호출해줄 거라는 것.

Animal* yourCat = new Cat(5, "Mocha");

마찬가지로 Animal…

동적 바인딩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Animal.h
class Animal
{
    public:
        virtual void Move();            // 가상 함수!
        virtual void Speak();           // 실행하는 것의 실체를 찾아서 호출해줘~
};

// Animal.cpp
void Animal::Move()
{
}
void Animal::Speak()
{
    std::cout << "An Animal" << std::endl;
}

// Cat.h
class Cat : public Animal
{
    public:
        void Speak();
};

// Cat.cpp
void Cat::Speak()
{
    std::cout << "Meow" << std::endl;
}

// main.cpp
Cat* myCat = new Cat(2, "Coco");
myCat->Speak();                     // Meow??

Animal* yourCat = new Cat(5, "Mocha");
yourCat->Speak();                   // Meow??

둘 다 실체는 Cat이지만 타입이 다름. 무늬가 Animal이다. 하지만 virtual이 붙었으므로 실체를 찾는다 -> 둘 다 Meow가 되는것 맞음.

가상 함수

  • 자식 클래스의 멤버 함수가 언제나 호출 됨
    • 부모의 포인터 또는 참조를 사용 중이더라도 (무늬가 뭐든 간에)
  • 동적 바인딩 / 늦은 바인딩 (dynamic / late)
    • 실행 중에 어떤 함수를 호출할지 결정한다
    • 당연히 정적 바인딩보다 느림
  • 이를 위해 가상 테이블이 생성됨
    • 모든 가상 멤버함수의 주소를 포함함
    • 이 테이블을 이용해 어떤 함수를 호출해야 하는지 찾음
      • 클래스 마다 하나 vs 개체마다 하나?
        • 한 클래스에 속한 모든 개체에 대해서 함수는 딱 하나만 있음. (위에 한 번만 할당된다고 했었음!)
        • 그렇기 때문에 가상 테이블도 각 개체마다 있는게 아니라, 클래스마다 하나씩 있는게 맞음.
      • 개체를 생성할 때 해당 클래스의 가상 테이블 주소가 함께 저장됨.
    • eg. Speak()이 어딨지? => 가상 테이블이 0x009ED8에 있고 Speak()는 두 번째 함수니까 0x009EDC에 저장된 주소에 있을 것임. (두번째니까 4바이트 x 2 해서 8바이트 더한 곳에 위치)

가상 소멸자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Animal.h
class Animal
{
    public:
        ~Animal();
    private:
        int mAge;
};

// Cat.h
class Cat : public Animal
{
    public:
        ~Cat();
    private:
        char* mName;
}

// main.cpp
Cat* myCat = new Cat(2, "Coco");
delete myCat;
// 이렇게 소멸자가 비 가상 소멸자인 경우
// ~Cat() 먼저 호출되고, 마지막에 부모 ~Animal() 호출하여 잘 지워짐.

Animal* yourCat = new Cat(5, "Mocha");
delete yourCat;
// 하지만 이 경우 무늬가 Animal이므로, 정적 바인딩으로 되어 ~Animal() 호출만 됨.
// ~Cat(); 호출 안 됨. 메모리 누수! char* mName;

💡 그래서 가상 소멸자를 만들어줘야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Animal.h
class Animal
{
    public:
        virtual ~Animal();
    private:
        int mAge;
};

// Cat.h
class Cat : public Animal
{
    public:
        virtual ~Cat();     // 부모가 가상이면 자식 소멸자도 자동으로 가상으로 됨
                            // 그치만 가독성을 위해 붙여주자
    private:
        char* mName;
}

// Cat.cpp
Cat::~Cat()                 // 요기엔 virtual 안붙임
{
    delete mName;
}

// main.cpp
Cat* myCat = new Cat(2, "Coco");
delete myCat;
// 이렇게 소멸자가 비 가상 소멸자인 경우
// ~Cat() 먼저 호출되고, 마지막에 부모 ~Animal() 호출하여 잘 지워짐.

Animal* yourCat = new Cat(5, "Mocha");
delete yourCat;
// 하지만 이 경우 무늬가 Animal이므로, 정적 바인딩으로 되어 ~Animal() 호출만 됨.
// ~Cat(); 호출 안 됨. 메모리 누수! char* mName;
  • 멤버함수의 가상성은 상속됨
  • 부모 클래스에서 가상으로 선언된 멤버 함수가 있다면 전부 가상함수
    • virtual 키워드가 없어도!
    • 자식 클래스에서 동일한 시그니처를 가진 함수 역시 가상함수!

2-3. 다중 상속

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Faculty.h
class Faculty
{
};

// Student.h
class Student
{
};

// TA.h
class TA : public Student, public Faculty
{
    // Student(), Faculty() 순으로 호출
};

class TA : public Faculty, public Student
{
    // Faculty(), Student() 순으로 호출
};

class TA : public Student, public Faculty
    : Faculty()
    , Student()
{
    // Student(), Faculty() 순으로 호출
};
  • 자식 클래스에서 등장한 부모 클래스 순서대로
    • 초기화 리스트의 순서는 상관 없음!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Faculty.h
class Faculty
{
    public:
        void DisplayData();
};

// Student.h
class Student
{
    public:
        void DisplayData();
};

// TA.h
class TA : public Faculty, public Student
{
};

// main.cpp
TA* myTA = new TA();
myTA->DisplayData();                // <-- 컴파일 에러! 뭘 호출해줘야 할지 모르니까.

// 해결 방법
myTA->Student::DisplayData();       // 이렇게 직접 부모클래스를 넣어줘야 함.

가상 부모 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Animal.h
class Animal
{
};

// Tiger.h
class Tiger : virtual public Animal
{
};

// Lion.h
class Lion : virtual public Animal
{
};

// Liger.h
class Liger : public Tiger, public Lion
{
    // Tiger, Lion에 붙은 virtual public Animal 의 virtual 키워드는
    // Animal이 하나만 있게 보장을 해준다
    // 그치만 가급적 이렇게 쓰지 말고 인터페이스(interface)를 써라
};

2-4. 추상 클래스 (abstract)

구체적인 함수의 구현이 안되어 있는 클래스가 추상 클래스 (함수 하나라도)
-> 즉 순수 가상함수를 가지고 있는 부모 클래스를 말한다!

  • 추상 클래스에서 개체를 만들 수 없음
    • 구현된 함수가 없는데. 개체를 만들면. 함수 호출하면 에러나니까.
  • 추상 클래스를 포인터나 참조형으로는 사용 가능 (동적 바인딩! 자식에서 구현된게 호출되니까)
    • Animal myAnimal; : 컴파일 에러. 추상 클래스는 개체 생성 안됨.
    • Animal* myAnimal = new Animal(); : 컴파일 에러. 개체 생성 안된다구!
    • Animal* myCat = new Cat(); : 개체 생성 잘 됨.
    • Animal& myCatRef = *myCat; : myCatRef는 myCat의 주소를 가리킨다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Animal.h
class Animal
{
    public:
        virtual ~Animal();
        virtual void Speak() = 0;       // <- 이거! "구현하지 않을게" 순수 가상 함수
        // 이 함수가 있음으로써 Animal class는 추상 클래스가 된다.
    private:
        int mAge;
};

// Cat.h
class Cat : public Animal
{
    public:
        ~Cat();
        void Speak();
    private:
        char* mName;
}
  • 순수 가상 함수
    • 구현체가 없는 멤버 함수
    • 자식 클래스가 구현해야 함
    • 구현 안하면 컴파일 에러!

2-5. 인터페이스 (Interface)

추상 클래스랑 거의 비슷한데, 인터페이스는 함수만 있음.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// IFlyable.h
class IFlyable
{
    public:
        virtual void Fly() = 0;
};

// IWalkable.h
class IWalkable
{
    public:
        virtual void Walk() = 0;
};

class Bat : public IFlyable, public IWalkable
{
    public:
        void Fly();
        void Walk();
};

class Cat : public IWalkable
{
    public:
        void Walk();
};
  • C++은 자체적으로 인터페이스를 지원 안 함
  • 그래서 순수 추상 클래스를 사용하여 흉내내는 거임
    • 순수 가상 함수만 가짐
    • 멤버 변수는 없음
      1
      2
      3
      4
      5
      
      class IFlyable
      {
      public:
          virtual void Fly() = 0;
      };
      

이슈 및 공부한 것을 기록해두는 개인 블로그 입니다. 댓글, 피드백 환영합니다 🙂

Update:

댓글남기기