매번 코드를 짜긴 짰는데.. 구조는 대강 설계할 수 있었으나 코드를 짤 때에는 gpt의 도움을 살짝씩 받았었다.

그래서 인터넷 검색없이 코드를 짜라고 하면 헷갈리고 아직 이해가 부족한 부분이 많다고 생각했다.

언리얼 엔진 문법을 익히기 전에 C++ 문법부터 제대로 익히고 들어가는게 이해하기 수월하고 맞을 것 같다는 생각이 들어 찐하게 학습을 진행하게 되었다.

 

이해하면서 정리를 했기 때문에 내가 어떻게 이해했는지와 비전공자들도 이해할 수 있을 정도로 아주아주 자세하게 과정을 적어놓았다.

부디 이 글을 읽는 대부분의 사람들이 다형성을 이해하고 돌아가면 좋겠다.

 

 

다형성
  • 객체지향 프로그래밍의 핵심 개념 중 하나
  • 하나의 인터페이스로 다양한 객체를 다룰 수 있다는 특징
  • 코드의 재사용성과 확장에 굉장히 용이

 

가상 함수(virtual)
  • 부모 클래스에서 선언된 함수이지만 파생 클래스에서 재정의(override)할 수 있음
    • 파생 클래스에서 정의해도 되고, 안 해도 됨
  • 파생 클래스에서 재정의를 하게 된다면 파생 클래스에서 재정의한 함수가 호출
  • 재정의 하지 않았다면 부모 클래스의 기본 구현된 함수가 호출

 

가상 함수 선언 문법
virtual void makeSound() const;  // 가상 함수 선언

// 파생 클래스에서 재정의하는 것은 선택

 

순수 가상 함수(virtual)
  • 파생 클래스에서 반드시 재정의해야 하는 멤버 함수
  • 재정의하지 않으면 사용할 수 없음(컴파일 에러 발생)
  • 다형성을 활용하여 파생 클래스마다 반드시 다른 동작을 구현하도록 강제

 

순수 가상 함수 선언 문법
virtual void makeSound() const = 0;  // 순수 가상 함수 선언

// 반드시 파생 클래스에서 makeSound를 재정의해야만 사용할 수 있음

(자세한 예시는 추상 클래스 예제 코드와 함께)

 

추상클래스(abstract class)
  • 특별히 기본 클래스로 사용되도록 설계된 클래스
  • 하나 이상의 순수 가상 함수가 포함되어 있음
    • 추상클래스로 정의하고 싶다면 멤버 변수 없이 순수 가상 함수로만 이뤄진 클래스 형태가 가장 바람직
  • 객체를 자체적으로 생성할 수 없고 반드시 상속을 거쳐야만 사용 가능(아래에서 자세히 설명)
    • 상속받은 파생 클래스 또한 반드시 부모의 순수 가상 함수를 override 해줘야 됨

 

객체 생성이 안되는 예제 코드

#include <iostream>
#include <string>

using namespace std;

class Animal
{
public:
    Animal() {};
    ~Animal() {};
    virtual void makeSound() const = 0;  // 순수 가상 함수
    //virtual void makeSound() const {};  // 가상 함수
};

class Cat : public Animal
{
};

class Dog
{
public:
    void makeSound()
    {
        cout << "Animal을 상속받지 않은 Dog 입니다." << endl;
    }
};

int main()
{
    Animal animal;  // 에러
    Cat cat;  // 에러
    Dog dog;

    /*animal.makeSound();
    cat.makeSound();
    dog.makeSound();*/

}

이유 : Animal 클래스에는 makeSound라는 순수 가상 함수가 선언되어있음.

즉, Animal 클래스는 추상 클래스가 됨. 따라서 Animal 클래스 자체적으로는 객체 생성이 불가능함

Cat 클래스는 Animal을 상속받는 자식 클래스가 되는데, 순수 가상 함수는 상속받은 객체에서 무조건 재정의해야하지만 Cat 클래스에서는 순수 가상 함수인 makeSound를 재정의하지 않아 객체 선언 불가능함.

 

그럼 이 코드가 정상적으로 동작할 수 있게 하는 방법은?

#include <iostream>
#include <string>

using namespace std;

class Animal
{
public:
    Animal() {};
    ~Animal() {};
    virtual void makeSound() const = 0;  // 순수 가상 함수
    //virtual void makeSound() const {};  // 가상 함수
};

class Cat : public Animal
{
public:
    void makeSound() const override
    {
        cout << "Animal을 상속받았고, Cat에서 makeSound 순수가상함수 overriding 했음." << endl;
    }
};

class Dog
{
public:
    void makeSound()
    {
        cout << "Animal을 상속받지 않은 Dog 입니다." << endl;
    }
};

int main()
{
    //Animal animal;
    Cat cat;
    Dog dog;

    //animal.makeSound();
    cat.makeSound();
    dog.makeSound();

}

Cat에서 상속받은 추상 클래스 Animal의 순수가상함수 makeSound를 override으로 재정의 해줌으로써 객체 선언이 가능해짐.

결과

 

부모 클래스 포인터
  • 다형성을 구현하기 위해서 부모 클래스 타입의 포인터를 사용함
  • 이를 통해 해당 부모 클래스를 상속받는 다른 파생 클래스에도 쉽고 유연하게 접근 가능

 

부모 클래스 포인터 타입으로 객체를 관리하는 예제 코드 1 (다형성 활용)

Animal* animals[3];
animals[0] = new Cat();
animals[1] = new Dog();
animals[2] = new Bird();

for (int i = 0; i < 3; i++) {
    animals[i]->makeSound();  // 각 클래스의 makeSound()가 호출됨.
    delete animals[i];        // 동적 할당된 메모리 해제.
}

 

부모 클래스 포인터 타입으로 객체를 관리하는 예제 코드 2 (다형성 활용)

Animal* cat = new Cat();
cat->makeSound();  // Cat 클래스의 makeSound() 호출.

Animal* dog = new Dog();
dog->makeSound();  // Dog 클래스의 makeSound() 호출.

 

이렇게 부모 클래스 포인터 타입으로 하면 좋은 이유
Animal* animal = new Cat();  // Animal 포인터로 Cat 객체 다룸.
animal->makeSound();         // Cat의 makeSound()가 호출됨.

animal = new Dog();          // Animal 포인터로 Dog 객체 다룸.
animal->makeSound();         // Dog의 makeSound()가 호출됨.

  1. 부모 클래스인 Animal 타입의 animal 포인터 하나로 Animal을 상속받고 있는 파생 클래스에 접근하기 유용
  2. 메모리 낭비 최소화할 수 있음

 

부모 클래스 포인터 타입으로 객체를 관리하지 않는 예제 코드(다형성 활용 x)

Cat* cat = new Cat();
Dog* dog = new Dog();

cat->makeSound();  // Cat의 메서드 호출.
dog->makeSound();  // Dog의 메서드 호출.

delete cat;
delete dog;

 

인터페이스(Interface)
  • 순수 가상 함수로만 이뤄진 추상 클래스를 인터페이스라고 칭함
  • 인터페이스는 구현이 아닌 설계(구조)만 제공
  • 멤버 변수를 가지지 않으며 오로지 메서드만 정의
  • 다중 상속 가능(순수 가상 함수만 포함되어있으므로 충돌 가능성이 낮기 때문)

 

 

마지막 말

이렇게 큰 틀은 다형성이지만 그 안에 우리가 개발하면서 필요한 여러 지식들이 들어가 있는 모습을 볼 수 있다.

다형성을 사용하면 특히 메스드의 이름 같은 부분을 절약할 수 있어서 좋고, 다양하게 재정의하는 부분이 재밌게 느껴지는 것 같다.

'C++' 카테고리의 다른 글

[ C++ ] 프로그래머스 비밀지도  (0) 2025.04.25
[UE, C++] 캐릭터 움직여보기  (0) 2025.02.03
[ C++ ] Text_RPG 팀플 완료  (1) 2025.01.17
[Visual Studio] 한글 깨짐 현상  (0) 2025.01.15
[C++] 진짜 랜덤값? 가짜 랜덤값?  (0) 2025.01.14