기록

[GPT가 알려주는] 다형성과 C++ virtual 키워드 본문

잡동사니/GPT가 알려주는

[GPT가 알려주는] 다형성과 C++ virtual 키워드

heylo 2025. 9. 16. 16:04
#include <iostream>
using namespace std;

// [기존]
// class Base {
//     public:
//         void what() { cout << "나는 Base!"; }
// };

// class Derived : public Base {
//     public:
//         void what() { cout << "나는 Derived!!"; }
// };

// [해결책]
class Base {
public:
    virtual void what() { cout << "나는 Base!" << endl; }
};

class Derived : public Base {
public:
    void what() override { cout << "나는 Derived!" << endl; }
};

int main() {
    Base* pointer = new Derived(); // 파생 클래스 Derived 객체를, Base* 포인터로 가리킴
    pointer->what(); // 어떤 함수가 실행될까?
}
/*
[기존] 결과: "나는 Base!"가 출력됨
왜냐하면 what() 은 정적 바인딩(compile time) 으로 결정돼서,
포인터 타입이 Base* 이니까 Base 클래스의 what 이 불린 거예요.

문제점
사실 객체는 Derived 인데,
Base* 포인터로 접근했더니 Base 버전의 함수가 호출됨.
즉, 다형성(Polymorphism)이 깨져버린 상태.
*/

/*
[해결]
"나는 Derived!" 가 출력됨

이유: virtual 로 선언된 함수는 동적 바인딩(runtime) 으로 동작하기 때문.
즉, 실행할 때 진짜 객체 타입(Derived) 를 보고 어떤 함수가 불릴지 결정함.

📌 정리
virtual 없는 경우:
포인터/참조 타입(Base*) 기준 → Base 함수 실행 (정적 바인딩)

virtual 있는 경우:
실제 객체 타입(Derived) 기준 → Derived 함수 실행 (동적 바인딩)

📌 비유
virtual 없는 경우
= “내 주민등록증에 아빠 이름이 Base라고 적혀 있으니, 나는 Base로만 불릴 거야.”

virtual 있는 경우
= “겉으로는 Base* 로 불리더라도, 실제 내가 Derived 라면 Derived 로 대답할 거야.”


👉 그래서 다형성(Polymorphism)을 지원하려면,
기본 클래스 함수는 virtual 로 선언하는 게 원칙이에요.
*/

좋습니다 🙂 지금 질문하신 부분을 단계별로 아주 쉽게 풀어서 설명드릴게요.


1. 다형성이란? (Polymorphism)

다형성 = "같은 모습(함수 호출)인데, 실제로는 다른 동작(함수 실행)"
즉, 부모 클래스의 포인터/참조로 자식 객체를 가리킬 때, 실제 객체 타입에 맞는 함수가 실행되도록 하는 성질이에요.

예시:

Base* p = new Derived();
p->what();
  • 겉으로 보면 Base* 타입이니까 원래라면 Base::what()이 실행될 것 같음.
  • 하지만 다형성이 적용되면 실제 객체는 Derived이므로 Derived::what()이 실행됨.

👉 같은 코드(p->what())인데 상황(실제 객체 타입)에 따라 다른 결과가 나오는 것 → 이게 다형성!


2. 왜 문제가 생겼을까? (virtual 없는 경우)

C++은 기본적으로 정적 바인딩(compile-time binding)을 사용합니다.
즉, 컴파일할 때 포인터/참조 타입만 보고 함수가 결정됨.

class Base {
public:
    void what() { cout << "나는 Base!"; }
};
class Derived : public Base {
public:
    void what() { cout << "나는 Derived!!"; }
};

Base* p = new Derived();
p->what();  // → "나는 Base!"

여기서 컴파일러는 p가 Base*니까 그냥 Base::what()을 불러버린 거예요.
→ 실제 객체가 Derived라는 사실을 무시.


3. virtual 키워드란?

virtual을 붙이면 함수 호출을 동적 바인딩(runtime binding)으로 바꾸라는 의미예요.

  • 실행 시간에 실제 객체 타입을 확인해서 어떤 함수를 부를지 결정합니다.
  • 그래서 Base*로 보더라도 실제로 Derived 객체를 가리키고 있으면 Derived::what()을 실행해요.
class Base {
public:
    virtual void what() { cout << "나는 Base!" << endl; }
};
class Derived : public Base {
public:
    void what() override { cout << "나는 Derived!" << endl; }
};

Base* p = new Derived();
p->what();  // → "나는 Derived!"

4. 정리 비유

  • virtual 없는 경우
    주민등록증에 "Base"라고 적혀 있으면 그냥 "Base"로만 불러요. (정적 바인딩)
  • virtual 있는 경우
    실제로는 "Derived" 사람이면, 겉으로 Base라고 불러도 스스로 "난 Derived야!" 하고 대답해요. (동적 바인딩)

✅ 결론:

  • 다형성 = "부모 포인터/참조로 자식 객체를 가리켜도, 자식의 동작이 실행되도록 하는 성질"
  • virtual = 동적 바인딩을 켜주는 키워드
  • 그래서 상속 관계에서 함수 오버라이딩을 하려면 부모 쪽 함수에 virtual을 꼭 붙여야 한다는 게 원칙이에요.

혹시 제가 여기서 더 나아가서, 가상 소멸자(virtual destructor) 개념까지도 이어서 설명해드릴까요?
(이 부분이 다형성 코드에서 굉장히 중요한 포인트거든요 👀)

 

 

좋습니다 🙂 지금 질문하신 부분을 단계별로 아주 쉽게 풀어서 설명드릴게요.


1. 다형성이란? (Polymorphism)

다형성 = "같은 모습(함수 호출)인데, 실제로는 다른 동작(함수 실행)"
즉, 부모 클래스의 포인터/참조로 자식 객체를 가리킬 때, 실제 객체 타입에 맞는 함수가 실행되도록 하는 성질이에요.

예시:

Base* p = new Derived();
p->what();
  • 겉으로 보면 Base* 타입이니까 원래라면 Base::what()이 실행될 것 같음.
  • 하지만 다형성이 적용되면 실제 객체는 Derived이므로 Derived::what()이 실행됨.

👉 같은 코드(p->what())인데 상황(실제 객체 타입)에 따라 다른 결과가 나오는 것 → 이게 다형성!


2. 왜 문제가 생겼을까? (virtual 없는 경우)

C++은 기본적으로 정적 바인딩(compile-time binding)을 사용합니다.
즉, 컴파일할 때 포인터/참조 타입만 보고 함수가 결정됨.

class Base {
public:
    void what() { cout << "나는 Base!"; }
};
class Derived : public Base {
public:
    void what() { cout << "나는 Derived!!"; }
};

Base* p = new Derived();
p->what();  // → "나는 Base!"

여기서 컴파일러는 p가 Base*니까 그냥 Base::what()을 불러버린 거예요.
→ 실제 객체가 Derived라는 사실을 무시.


3. virtual 키워드란?

virtual을 붙이면 함수 호출을 동적 바인딩(runtime binding)으로 바꾸라는 의미예요.

  • 실행 시간에 실제 객체 타입을 확인해서 어떤 함수를 부를지 결정합니다.
  • 그래서 Base*로 보더라도 실제로 Derived 객체를 가리키고 있으면 Derived::what()을 실행해요.
class Base {
public:
    virtual void what() { cout << "나는 Base!" << endl; }
};
class Derived : public Base {
public:
    void what() override { cout << "나는 Derived!" << endl; }
};

Base* p = new Derived();
p->what();  // → "나는 Derived!"

4. 정리 비유

  • virtual 없는 경우
    주민등록증에 "Base"라고 적혀 있으면 그냥 "Base"로만 불러요. (정적 바인딩)
  • virtual 있는 경우
    실제로는 "Derived" 사람이면, 겉으로 Base라고 불러도 스스로 "난 Derived야!" 하고 대답해요. (동적 바인딩)

✅ 결론:

  • 다형성 = "부모 포인터/참조로 자식 객체를 가리켜도, 자식의 동작이 실행되도록 하는 성질"
  • virtual = 동적 바인딩을 켜주는 키워드
  • 그래서 상속 관계에서 함수 오버라이딩을 하려면 부모 쪽 함수에 virtual을 꼭 붙여야 한다는 게 원칙이에요.

혹시 제가 여기서 더 나아가서, 가상 소멸자(virtual destructor) 개념까지도 이어서 설명해드릴까요?
(이 부분이 다형성 코드에서 굉장히 중요한 포인트거든요 👀)

 

 

 

좋습니다 👍 이제 가상 소멸자(virtual destructor) 이야기를 해드릴게요. 이건 다형성과 세트라고 보시면 돼요.


1. 문제 상황

다형성 코드에서 소멸자에 virtual을 안 붙이면 큰 문제가 생겨요.

예제:

#include <iostream>
using namespace std;

class Base {
public:
    ~Base() { cout << "Base 소멸자 실행\n"; }
};

class Derived : public Base {
public:
    ~Derived() { cout << "Derived 소멸자 실행\n"; }
};

int main() {
    Base* p = new Derived();
    delete p; // ??
}

출력 결과

Base 소멸자 실행

❌ Derived 소멸자가 실행되지 않음 → Derived 안에서 동적 할당한 메모리가 있으면 메모리 누수 발생!


2. 왜 이런 일이 생기나?

  • virtual 없는 소멸자: 정적 바인딩. 포인터 타입(Base*)을 보고 "Base 소멸자만 실행하면 되겠지"라고 착각.
  • virtual 소멸자: 동적 바인딩. 실제 객체 타입(Derived)을 확인하고, Derived 소멸자부터 실행한 뒤 Base 소멸자 실행.

3. 해결책: 가상 소멸자

class Base {
public:
    virtual ~Base() { cout << "Base 소멸자 실행\n"; }
};

class Derived : public Base {
public:
    ~Derived() override { cout << "Derived 소멸자 실행\n"; }
};

int main() {
    Base* p = new Derived();
    delete p;
}

출력 결과

Derived 소멸자 실행
Base 소멸자 실행

✅ 올바른 순서대로 소멸자가 호출됨 → 메모리 누수/자원 미해제 방지


4. 정리

  • 소멸자에 virtual 꼭 붙여야 하는 경우
    → 클래스가 다형성(virtual 함수 있음)으로 사용될 가능성이 있다면 반드시!
  • 안 붙여도 되는 경우
    → 상속받아 다형성으로 쓸 일이 전혀 없는 “단순한 클래스”라면 안 붙여도 괜찮음.

📌 비유

  • virtual 없는 소멸자: "겉보기(Base) 집만 치우고, 실제 안에 있던 방(Derived)은 안 치우는 꼴"
  • virtual 소멸자: "겉보기(Base) 집이더라도 실제 집 구조(Derived)까지 다 치워줌"

👉 결론:

  • **다형성(virtual 함수)**을 쓰는 클래스라면 소멸자에도 반드시 virtual을 붙여야 한다!
  • 그래야 delete Base* 같은 코드에서 Derived 자원도 안전하게 정리됨.

혹시 원한다면, 제가 소멸자에 virtual이 없는 경우 실제 메모리 누수 나는 시뮬레이션 코드까지 짜서 보여드려도 될까요? 👀