다형성 Polymorphism

Gogi·2023년 6월 2일

C++언어 기초 목록

목록 보기
13/15

한 인터페이스로 여러 객체 관리

다형성을 통해 서로 다른 클래스의 객체를 동일한 인터페이스를 통해 동일한 자료형으로 다룰 수 있다. 다형성은 주로 가상 함수와 추상 클래스, 템플릿을 통해 구현된다. 코드의 재사용성과 확장성, 유연성을 높여주는 장점을 가지고 있으며 객체지향 프로그래밍의 주요 개념이다.


개념 설명 및 용어 설명

- 정적 다형성 (Static polymorphism)

정적 다형성은 컴파일 시점에 결정되며, 주로 함수 오버로딩과 템플릿을 통해 구현된다. 한 클래스가 여러 자료형을 다룰 수 있게 하여 코드의 재사용성과 가독성 향상의 이점을 얻을 수 있다. 컴파일 시점에 호출할 함수를 결정하므로 런타임 시 연산량을 줄여 성능 향상과 잘못된 자료형을 미리 찾아내 프로그램의 안정성을 높일 수 있다.


- 동적 다형성 (Dynamic polymorphism)

동적 다형성은 런타임에 결정되며 주로 가상 함수를 통해 구현된다. 런타임때 부모 클래스의 포인터나 레퍼런스를 통해 자식 클래스의 멤버 함수를 호출할 수 있기에 객체들을 관리, 수정, 확장하기가 쉬워진다.


- 인터페이스 Interface

인터페이스는 객체가 어떤 동작을 할 수 있는지를 정의하는 '계약서 혹은 규칙(Contract)'이라고 생각할 수 있다. 가령, 인터페이스 역할을 하는 Monster 추상 클래스를 선언한 뒤 Race AttackType ArmorType의 순수 가상 멤버 함수를 선언한다. 이후 해당 추상 클래스를 상속받는 자식 클래스 Goblin Ogre를 만들어 상속 받은 가상 함수들 Race AttackType ArmorType을 각각 정의한다. 고블린과 오우거의 공격유형은 서로 상이할 수 있지만 각각의 몬스터들은 모두 종족, 공격유형, 방어유형이란 통일된 인터페이스를 갖게 된다. 이로써 프로그래머들은 일관된 방식으로 객체를 다룰 수 있고 코드의 유연성과 재사용성을 높일 수 있다. 인터페이스는 일종의 카테고리 개념으로 볼 수 있다.


- 함수 시그니처 Function Signature

함수의 이름과 매개변수 목록을 포함하는 것을 말한다. 함수를 고유하게 식별하는 데 사용되며, 함수의 반환 자료형은 시그니처에 포함되지 않는다. void PrintNumber(int num); 에서 void 제외.


- 바인딩 Binding

컴파일러 또는 런타임에 변수, 함수나 멤버 함수를 호출할 때 어떤 함수를 호출할지를 결정하는 과정을 뜻한다. 정적, 동적 바인딩으로 구분된다.


- 정적 바인딩 Static Binding

컴파일 타임에 함수의 시그니처, 자료형을 기반으로 어떤 함수를 호출할지 결정한다. 런타임때 어떤 결정을 하는지에 따라 달라지는 것이 없으므로 미리 정해진 동작이나 결과를 변경할 필요가 없을 때 사용된다.


- 동적 바인딩 Dynamic Binding

런타임 동안에 객체의 실제 타입을 확인하여 어떤 함수를 호출할지 결정된다.


- 가상 함수 Virtual Function

부모 클래스에서 선언 및 구현, 정의되고 자식 클래스에서 재정의할 수 있는 멤버 함수이다.


- 순수 가상 함수 Pure Virtual Function

부모 클래스에서 선언 후 구현, 정의가 되지 않은 함수이다. 자식 클래스에서 재정의되지 않을 시 미완성인 클래스로 남게 된다. 따라서 순수 가상 함수가 포함된 클래스는 객체를 생성할 수 없다. 순수 가상 함수는 함수 선언 접두사에 virtual, 접미사에 = 0;을 붙여서 선언한다.


- 추상 클래스 Abstract Class

순수 가상 함수를 가진 클래스로, 객체를 직접 생성할 수 없는 클래스이다. 추상 클래스는 단지 인터페이스 정의를 목적으로 하거나, 자식 클래스에서 공통된 기능을 제공하기 위해 사용된다.


- 재정의 Override

상속 관계인 부모 클래스의 가상 멤버 함수를 자식 클래스에서 재정의하는 것을 말한다. 이를 함수 오버라이딩이라 표현한다. 일반 함수도 재정의할 수 있으나 이는 함수 오버로딩이라 표현하며 다시 정의한다기 보단 추가 정의의 개념이다. 동일한 이름, 기능을 가진 함수지만 각각 다른 매개변수를 받아 추가로 정의하는 것을 말한다. 함수 오버라이딩은 부모 클래스 가상 함수의 정의를 그대로 받아올 수도 있지만 비슷하거나 아예 다른 기능으로 새롭게 재정의할 수 있다.


- 가상 소멸자 Virtual Destructor

부모 클래스를 상속한 자식 클래스들이 동적으로 할당한 메모리를 해제하려면 부모 클래스의 소멸자를 가상 소멸자로 선언하여 동적 바인딩을 사용해야 한다. 일반 소멸자는 정적 바인딩을 통해 호출 되므로 부모 클래스의 포인터로 자식 클래스의 객체를 가리키고 있을 때 부모 클래스의 소멸자만 호출되고 자식 클래스의 소멸자는 호출되지 않을 수 있다.


사용 예시

class Animal 
{
public:
    virtual void makeSound() 
    {
        cout << "동물 울음소리" << endl;
    }
};

class Dog : public Animal 
{
public:
    void makeSound() override 
    {
        cout << "멍멍" << endl;
    }
};

class Cat : public Animal 
{
public:
    void makeSound() override 
    {
        cout << "야옹" << endl;
    }
};

int main() 
{
    Animal* animal1 = new Dog();
    Animal* animal2 = new Cat();

    animal1->makeSound();  
    // 출력: "멍멍"
    animal2->makeSound();  
    // 출력: "야옹"
    
    delete animal1;
    delete animal2;

    return 0;
}

Animal은 부모 클래스이며, DogCat은 자식 클래스다. Animal 클래스의 makeSound 멤버 함수는 가상 함수로 선언되어 있기에 자식 클래스에서 이를 재정의(Override)할 수 있다. 부모 클래스의 포인터를 사용해 makeSound 멤버 함수를 호출하면 각 객체의 자료형에 따른 멤버 함수가 실행된다. 따라서 기존의 가상 함수 내에 있던 "동물 울음소리" 대신 재정의된 "멍멍", "야옹"이 출력된다.


주의사항

  • 다형성을 적용한 클래스를 구현했다면 반드시 가상 소멸자를 선언해야 함.
profile
C, C++, C#, Unity

0개의 댓글