virtual은 컴파일 타임과 실행 타임 동작이 다르다.
가상함수는 기본 값 설정을 하지 말자. (제발 ㅜㅜ)
가상함수에 기본 값을 세팅하는 데 기저 클래스와 파생 클래스에서 기본 값이 다르면 어떻게 될까? 🤔
아래 예시는 이를 확인해 볼 수 있는 코드인데 동작 결과를 예측해 보자.
example
#include<iostream>
class Base
{
public:
virtual void foo(int a = 10) { std::cout << "Base : " << a << std::endl; }
};
class Derived : public Base
{
public:
virtual void foo(int a = 20) { std::cout << "Derived : " << a << std::endl; }
};
int main()
{
Base* p = new Derived;
p->foo(); //1.
}
result
Derived : 10
예상했던 결과 값인가?
💬 그냥, '제발 가상함수는 기본 값을 설정하지 말자'로 끝내고 싶지만 발생할 수 있는 문제점은 이해하고 넘어가는 게 좋을 테니 정리를 해보자.
우선 가상함수 컴파일 과정은 다음과 같다.
- 컴파일러는 컴파일 시점에 vtable을 만들어서 가상함수들의 주소를 가진다.(저장)
- 그리고 객체가 생성될 때 vptr이 가상 함수 테이블을 가리키도록 초기화된다.
그리고 컴파일 타임 과 실행 타임에 동작되는 것을 구분해서 이해를 해야 한다.
컴파일 타임
주석 1. 'p->foo()'의 코드는 컴파일 시점에 vptr가 연결되어 있지 않기에 'p->foo(10)'처럼 기반 클래스의 가상함수에 기본 값인 10으로 설정된다.
실행 타임
객체가 생성되고 vptr이 초기화된 이후 p->foo() 호출은 Derived::foo()의 주소를 가리키고 있으니 Derived::foo 가 호출되어 실행결과가 Derived : 10이 된 것이다.
💡 정리하면
가상함수는 컴파일 타임과 실행 타임 동작이 다르기에 정 써야 한다면 같은 값을 기본 값으로 설정하자.
클래스의 소멸자를 virtual(가상함수)로 하는 이유를 아시나요?
이는 OOP(객체지향프로그래밍)에서 가장 중요한 요소인 다형성과 관련이 있다.
아래 예시와 같이 기반클래스를 상속받은 파생클래스가 있을 때 아래 1. 2. 의 결과를 한 번 생각해 보자.
sample
#include <iostream>
class Base
{
public:
Base() { std::cout << "Base()" << std::endl; }
virtual ~Base() { std::cout << "~Base()" << std::endl; }
};
class Derived : public Base
{
public:
Derived() { std::cout << "Derived()" << std::endl; }
~Derived() { std::cout << "~Derived()" << std::endl; }
};
int main()
{
Derived* p = new Derived;
delete p; // 1.
Base* p = new Derived;
delete p; // 2.
}
주석 1. 은 Derived 클래스를 동적할당하고 메모리 해제를 하는 코드이고 결과는 다음과 같다.
Result
Base()
Derived()
~Derived()
~Base()
생성자는 기저클래스부터 호출되고 소멸자는 역순으로 호출된다.
주석 2. 는 1. 과 달리 파생클래스의 메모리를 기저클래스로 받아서 메모리를 해제하는 코드로 1. 의 결과와 같다.
만약 클래스의 소멸자를 가상함수가 아니게 변경하면 결과가 어떻게 될까?
//virtual ~Base() { std::cout << "~Base()" << std::endl; }
~Base() { std::cout << "~Base()" << std::endl; }
Result
Base()
Derived()
~Derived()
~Base()
Base()
Derived()
~Base()
이전과 달라진 결과는 파생클래스의 소멸자 ~Derived가 호출이 안된다.
왜 소멸자가 가상함수 일 때만 호출이 되는 걸까?🤔
예를 들어 가상함수가 없으면 아래의 경우 컴파일러는 할당되는 클래스의 타입(Base 클래스 타입)만 알고 있기에 메모리 해제 시 그 클래스의 소멸자만 호출된다.
ex) Base* p = new Derived;
하지만 소멸자가 가상함수(virtual)이면 컴파일러는 vtable에 파생 클래스의 소멸자주소를 저장하기에 기저 클래스 포인터를 통해 삭제가 될 때 파생 클래스의 소멸자가 호출될 수 있다. 이것은 다형성을 구현하는 핵심 메커니즘이기도 하다.
💡 그래서 기반 클래스로 사용되는 모든 클래스는 반드시 "소멸자를 가상함수"로 해야 한다.
기반 클래스의 소멸자가 virtual 이 아닌 경우에는 어떻게?
📜 C++ core guidelines에서는 이렇게 가이드해주고 있다.
C.35: 상위 클래스의 소멸자는 공개된 가상 소멸자 혹은 상속되는 비-가상 함수여야 한다.
말이 좀 어려운데 영어로 보면 좀 더 해석하기 쉽다.
C.35: A base class destructor should be either public and virtual, or protected and non-virtual
즉 '기반 클래스의 소멸자는 virtual 이거나 protected 이어야 한다.'는 의미다.
기반 클래스 소멸자가 virtual인 이유는 정리했기에 기반 클래스의 소멸자가 virtual이 아닌 경우에는 왜 protected여야 하는지 알아보자.
💬 경험적으로 기저 클래스의 소멸자가 가상함수가 아닌 사례를 본 적이 없지만, 누군가의 고민일지도 모르기에 왜 protected를 권장하는지 예시를 통해 한 번 알아보자.
sample
#include <iostream>
class Base
{
public:
Base() { std::cout << "Base()" << std::endl; }
protected: //1.
~Base() { std::cout << "~Base()" << std::endl; }
};
class Derived : public Base
{
public:
Derived() { std::cout << "Derived()" << std::endl; }
~Derived() { std::cout << "~Derived()" << std::endl; }
};
int main()
{
Base* p = new Derived;
delete p; // 2.
Derived* p2 = new Derived;
delete p2; // 3.
}
result
error: 'Base::~Base()' is protected within this context
27 | delete p;
| ^
14:9: note: declared protected here
14 | ~Base() { std::cout << "Base()" << std::endl; }
💡 Base 클래스의 소멸자가 가상함수가 아닐 때 protected로 해두면 이 처럼 컴파일 에러가 발생된다. 이는 메모리 정리 시 Derived 소멸자가 호출되지 않아 발생할 수 있는 메모리 누수를 원천적으로 막기 위한 방법이다.
주석 1. Base* 인 상태로는 delete 하지 못하게 하려는 것
주석 2. Base가 protected 이므로 접근 권한 문제로 컴파일 에러가 발생
주석 3. ~Derived()는 public 이므로 문제없이 호출됨
💬 특이 케이스의 예제들이지만 이로 인해 좀 더 virtual 함수에 대해 이해하는 계기가 되었으면 한다.
참고
C++ 핵심 가이드라인 한글화 프로젝트 링크
C++ Core Guidelines 원문 링크
'L C++' 카테고리의 다른 글
🚀성능 최적화 도구 - constexpr과 inline 함수 이해하기 (0) | 2025.03.21 |
---|---|
🤔 스마트 포인터(unique_ptr, shared_ptr) 생성 시 make_shared(or make_unique)를 써야 하는 이유? (0) | 2025.03.14 |
직접선언! 낄낄빠빠하자. (0) | 2024.09.09 |
trivial 뜻 : 하찮은, 그렇지만 하찮지 않은 trivial‼ (0) | 2024.08.25 |
컴파일러와 페어 프로그래밍 - 특별 멤버 함수 편 🚀 (0) | 2024.08.15 |