질문 배경:
다형성은 C++의 초기 발전 단계에서 객체 지향 프로그래밍의 핵심 특징 중 하나가 되었습니다. 목표는 구체적인 타입을 걱정하지 않고 기본 인터페이스를 통해 객체에 접근할 수 있는 것입니다. 이는 코드의 표현력과 유연성을 크게 확장합니다.
문제:
다형성이 없으면 코드는 유연성이 떨어집니다: 객체의 타입을 명시적으로 알아야 하며 switch/case를 사용하고 수동으로 타입을 변환해야 합니다. 이는 애플리케이션의 유지 보수와 확장성을 복잡하게 만들어 새로운 타입을 추가하는 것이 기존 코드를 변경하지 않고는 비용이 많이 들거나 불가능하게 됩니다.
해결책:
C++에서 다형성은 가상 함수를 사용하여 달성됩니다. 클래스는 가상 메소드를 선언하고 자식 클래스는 이를 구현합니다. 기본 클래스는 공통 인터페이스를 제공하며 실제 작업은 포인터나 참조가 지칭하는 실제 객체의 타입에 따라 달라집니다.
코드 예:
#include <iostream> class Animal { public: virtual void speak() const { std::cout << "Some animal sound\n"; } virtual ~Animal() {} }; class Dog : public Animal { public: void speak() const override { std::cout << "Woof!\n"; } }; void makeSound(const Animal& a) { a.speak(); } int main() { Dog dog; makeSound(dog); // 출력: Woof!\n} }
주요 특징:
virtual 키워드와 함께 선언되어야 합니다.override를 사용하는 것이 코드의 안전성을 높입니다.기본 클래스의 소멸자를 가상으로 선언하지 않으면 어떻게 되나요?
기본 클래스에 대한 포인터를 통해 객체를 삭제하면 기본 클래스의 소멸자만 호출되고 자식 클래스의 소멸자는 호출되지 않아 자원 누수가 발생합니다.
코드 예:
class Base { public: ~Base() { /*...*/ } }; class Derived : public Base { public: ~Derived() { /*...*/ } }; Base* obj = new Derived(); delete obj; // UB: Derived::~Derived가 호출되지 않음
가상 메소드를 부분적으로만 선언하고 소멸자는 비가상으로 남겨둘 수 있나요?
아니요, 클래스가 다형적이라면(하나 이상의 가상 함수가 있을 경우) 소멸자는 가상적이어야 메모리나 자원 유출을 방지할 수 있습니다.
정적(static)으로 선언된 멤버에 대해 가상 함수가 작동하나요?
아니요, 클래스의 정적 멤버는 특정 객체에 속하지 않기 때문에 가상일 수 없으며, 이에 대한 동적 바인딩 매커니즘은 존재하지 않습니다.
override 메소드를 잊어버려서 잘못된 메소드 오버라이드를 하게 됨.매우 큰 장치 클래스 계층이 있으며, 각 자식 클래스가 자신의 자원을 관리하지만 기본 클래스의 소멸자는 가상이지 않습니다. 기본 클래스를 통해 삭제 시 자원이 해제되지 않습니다.
장점: 프로젝트가 빠르게 구축되며 최소한의 가상 호출.
단점: 메모리 누수 및 잘못된 파괴. 유지 관리 및 확장이 극히 어렵습니다.
잘 설계된 다형적 계층에서 기본 클래스는 가상 함수와 가상 소멸자를 갖습니다. override 키워드와 RAII 원칙을 사용합니다.
장점: 안전한 자원 작업, 간단한 확장성, 테스트 가능성.
단점: vtable 조회로 인해 약간의 성능 저하가 있으며, 필요 없이 상속이 적용되는 경우 "over-engineering"에 대한 예방책이 됩니다.