가상 함수는 다형성을 구현할 수 있게 해줍니다 — 기본 클래스에 대한 포인터나 참조를 통해 파생 클래스의 객체를 처리하는 능력입니다. 가상 함수를 선언하기 위해 virtual 키워드를 사용합니다:
class Base { public: virtual void foo() { std::cout << "Base::foo"; } }; class Derived : public Base { public: void foo() override { std::cout << "Derived::foo"; } };
Base* 타입의 포인터로 foo()를 호출하면 해당 포인터가 Derived 객체를 가리키고 있을 경우 Derived의 구현이 호출됩니다.
추상 클래스는 적어도 하나의 순수 가상 함수를 포함합니다:
class Interface { public: virtual void process() = 0; // 순수 가상 함수 };
가상 상속은 다이아몬드 문제(diamond problem)를 해결합니다:
class A { }; class B : virtual public A { }; class C : virtual public A { }; class D : public B, public C { };
이를 통해 D 객체에 기본 클래스 A의 단일 인스턴스가 포함되도록 보장합니다.
가상 소멸자와 일반 소멸자의 차이점은 무엇입니까? 클래스가 상속을 위해 설계되었다면 소멸자를 가상으로 만들어야 합니까?
답변: 가상 소멸자는 기본 타입 포인터를 통해 삭제할 때 파생 클래스의 소멸자가 호출되도록 보장합니다. 이는 자원을 올바르게 해제하는 데 중요합니다.
class Base { public: virtual ~Base() {} };
소멸자가 가상이지 않다면 delete BasePtr; 시 파생 클래스 객체에는 Base의 소멸자만 호출되고, 상속된 필드의 자원은 해제되지 않습니다.
이야기
다양한 도구를 위한 공통 인터페이스 클래스를 갖춘 대형 금융 시스템에서 가상 소멸자가 선언되지 않았습니다. 하나의 상속 클래스에서 동적 자원이 사용되었습니다. 기본 타입 포인터를 통해 객체를 삭제할 때 메모리 누수가 발생했으며, 이는 산업 부하 단계에서만 발견되었습니다.
이야기
팀은 여러 상속을 사용하면서 가상 상속을 적용하지 않았습니다. 클래스 D는 중간 클래스를 통해 A를 두 번 상속했습니다. 이로 인해 상태가 중복되어 A의 멤버에 접근할 때 오류가 발생했습니다. 정적 분석 도구를 통해 감사 후에만 수정되었습니다.
이야기
로깅 플러그인 개발 프로젝트에서 추상 클래스를 사용했지만 소멸자를 가상으로 만들지 않았습니다. 인터페이스 포인터를 통해 플러그인을 삭제할 때 명확하지 않은 의존성과 후손 소멸자가 호출되지 않아 발생한 버그가 관찰되었습니다. 이 문제는 자원 풀에 영향을 미치고 자원 누수로 이어졌습니다.