본문 바로가기
개발/C++ (98,03,11,14,17,20,23)

Modern C++ : 다형성(Polymorphism) (98, 11)

by snowoods 2025. 9. 19.

Modern C++

 

다형성 (Polymorphism)

 

개요

다형성(Polymorphism)은 객체 지향 프로그래밍의 핵심 원칙 중 하나로, '여러 형태를 가질 수 있는 능력'을 의미합니다. C++에서는 주로 부모 클래스의 포인터나 참조를 통해 자식 클래스의 객체를 다루면서, 동일한 함수 호출이 객체의 실제 타입에 따라 다른 동작을 하도록 만드는 가상 함수(Virtual Function) 메커니즘으로 구현됩니다.

이를 통해 코드의 유연성과 확장성을 높일 수 있으며, 다양한 객체들을 공통된 인터페이스로 관리할 수 있습니다.

 

C++ 버전별 주요 키워드 도입 시기

  • C++98 이전: virtual 키워드를 통해 다형성의 기본 개념이 지원되었습니다.
  • C++11: 다형성 및 클래스 설계를 더 명확하고 안전하게 만들어주는 여러 키워드가 도입되었습니다.
    • override: 자식 클래스의 함수가 부모 클래스의 가상 함수를 재정의한다는 것을 명시적으로 나타냅니다. 컴파일러는 재정의 규칙 위반 시 오류를 발생시켜 실수를 방지합니다.
    • final: 클래스가 더 이상 상속될 수 없거나, 가상 함수가 더 이상 재정의될 수 없음을 명시합니다.
    • default: 컴파일러가 생성하는 기본 생성자, 소멸자, 복사/이동 연산 등을 명시적으로 사용하겠다고 선언합니다.
    • delete: 특정 멤버 함수(생성자, 연산자 등)를 사용하지 못하도록 명시적으로 삭제합니다.

 

내용 설명

제공된 예제 코드는 Agent라는 기본 클래스와 이를 상속받는 Player, NPC 클래스를 통해 다형성을 보여줍니다.

  1. 가상 함수 (Virtual Function)
    Agent 클래스의 print_agent_data() 함수는 virtual 키워드로 선언되었습니다. 이는 해당 함수가 파생 클래스에서 재정의(Override)될 수 있음을 의미합니다.Agent* 타입의 포인터로 PlayerNPC 객체를 가리키고 print_agent_data()를 호출하면, 포인터의 타입(Agent*)이 아닌 실제 객체의 타입(Player 또는 NPC)에 맞는 재정의된 함수가 실행됩니다. 이를 동적 바인딩(Dynamic Binding)이라고 합니다.
  2. virtual void print_agent_data() const
  3. 가상 소멸자 (Virtual Destructor)
    부모 클래스의 포인터로 자식 객체를 다루다 delete를 통해 메모리를 해제할 때, 자식 클래스의 소멸자가 호출되지 않는 문제가 발생할 수 있습니다. 이를 방지하기 위해 부모 클래스의 소멸자는 반드시 virtual로 선언해야 합니다.이렇게 하면 delete 호출 시 객체의 실제 타입에 맞는 소멸자(예: ~Player())가 먼저 호출되고, 그 다음 부모 클래스의 소멸자가 순서대로 호출되어 메모리 누수 없이 안전하게 객체를 제거할 수 있습니다.
  4. virtual ~Agent() = default;
  5. final과 override
    PlayerNPC 클래스에서 print_agent_data 함수는 final 키워드와 함께 재정의되었습니다. final은 이 함수가 더 이상 다른 자식 클래스에서 재정의될 수 없음을 나타냅니다.만약 Player를 상속하는 SuperPlayer 클래스를 만들고 print_agent_data를 다시 재정의하려고 하면 컴파일 오류가 발생합니다. C++11부터는 override 키워드를 함께 사용하여 재정의 의도를 명확히 하는 것이 좋습니다.
  6. // 권장되는 방식 void print_agent_data() const override final
  7. void print_agent_data() const final
  8. default와 delete
    • ~Agent() = default;: 컴파일러가 생성하는 기본 소멸자를 사용하겠다는 의미입니다.
    • Agent() = delete;: Agent 클래스의 기본 생성자 사용을 금지합니다. 이로써 Agent 객체는 이름과 ID 없이는 생성될 수 없음을 강제합니다.

 

예제 코드

Agent.h

#pragma once

#include <iostream>
#include <string>

class Agent
{
public:
    Agent() = delete;

    Agent(const std::string &name,
          const std::uint32_t id,
          const std::uint32_t hp = 0U,
          const std::uint32_t energy = 0U)
        : m_name(name), m_id(id), m_hp(hp), m_energy(energy)
    {
        std::cout << "Agent Constructor!" << '\n';
    }

    virtual ~Agent() = default;

    virtual void print_agent_data() const
    {
        std::cout << "Agent hp: " << m_hp << ", energy: " << m_energy << '\n';
    }

protected:
    const std::string m_name;
    const std::uint32_t m_id;
    std::uint32_t m_hp = 0U;
    std::uint32_t m_energy = 0U;
};

class Player : public Agent
{
public:
    Player(const std::string &name,
           const std::uint32_t id,
           const std::uint32_t hp = 0U,
           const std::uint32_t energy = 0U)
        : Agent(name, id, hp, energy)
    {
        std::cout << "Player Constructor!" << '\n';
    }

    ~Player()
    {
        std::cout << "Player Destructor!" << '\n';
    }

    void print_agent_data() const final
    {
        std::cout << "Player hp: " << m_hp << ", energy: " << m_energy << '\n';
    }
};

class NPC : public Agent
{
public:
    NPC(const std::string &name,
        const std::uint32_t id,
        const std::uint32_t hp = 0U,
        const std::uint32_t energy = 0U)
        : Agent(name, id, hp, energy)
    {
        std::cout << "NPC Constructor!" << '\n';
    }

    ~NPC()
    {
        std::cout << "NPC Destructor!" << '\n';
    }

    void print_agent_data() const final
    {
        std::cout << "NPC hp: " << m_hp << ", energy: " << m_energy << '\n';
    }
};

 

main.cpp

#include <iostream>
#include <string>
#include <vector>

#include "Agent.h"

void printAllAgents(const std::vector<Agent *> &agents)
{
    for (const auto agent : agents)
    {
        agent->print_agent_data();
    }
}

int main()
{
    Agent agent1("A1", 0, 100, 25);
    Player player1("P1", 1, 250, 55);
    NPC npc1("N1", 2, 235, 41);

    const auto agents = std::vector<Agent *>{&agent1, &player1, &npc1};
    printAllAgents(agents);

    return 0;
}

 

실행 결과

Agent Constructor!
Agent Constructor!
Player Constructor!
Agent Constructor!
NPC Constructor!
Agent hp: 100, energy: 25
Player hp: 250, energy: 55
NPC hp: 235, energy: 41

printAllAgents 함수 내에서 agent->print_agent_data()를 호출했을 때, 각 포인터가 가리키는 실제 객체(Agent, Player, NPC)의 print_agent_data 함수가 각각 호출된 것을 확인할 수 있습니다.

 

활용팁

  • 추상 기본 클래스 (Abstract Base Class): 순수 가상 함수(virtual void func() = 0;)를 하나 이상 포함하는 클래스를 만들어 객체화는 불가능하지만, 파생 클래스들이 반드시 구현해야 할 인터페이스를 정의하는 용도로 사용할 수 있습니다. 이는 코드 설계를 강제하고 일관성을 유지하는 데 도움이 됩니다.
  • 스마트 포인터 활용: 예제에서는 C-스타일의 로우 포인터(Agent*)를 사용했지만, 실제 프로젝트에서는 std::vector<std::unique_ptr<Agent>>std::vector<std::shared_ptr<Agent>> 같은 스마트 포인터를 사용해 메모리를 안전하고 자동으로 관리하는 것이 좋습니다.