다형성과 가상 함수
상속은 "A is a B" 관계를 통해 코드 재사용성을 높이는 강력한 도구입니다.
이제 상속과 더불어 객체 지향 프로그래밍의 또 다른 핵심 원리인 다형성(Polymorphism) 에 대해 알아보겠습니다.
다형성은 '많은 형태'를 의미하며, 하나의 인터페이스로 다양한 타입의 객체를 다룰 수 있게 해주는 C++의 강력한 기능입니다.
이 다형성을 구현하는 데 필수적인 요소가 바로 가상 함수(Virtual Function) 입니다.
다형성 (Polymorphism)이란?
다형성은 하나의 타입 또는 인터페이스로 여러 다른 형태의 객체를 다룰 수 있는 능력을 의미합니다.
즉, 동일한 이름의 함수를 호출하더라도, 실제 객체의 타입에 따라 다른 동작을 수행하도록 하는 것입니다.
예시
- 현실 세계: 리모컨의 '전원' 버튼은 TV, 에어컨, 오디오 등 어떤 기기를 조작하느냐에 따라 다르게 동작합니다. (TV는 켜고 끄고, 에어컨은 켜고 끄고, 오디오도 켜고 끄고)
- 프로그램:
draw()
라는 함수가 있다고 가정해 봅시다.Circle
객체에서draw()
를 호출하면 원을 그리고,Rectangle
객체에서draw()
를 호출하면 사각형을 그립니다. 동일한draw()
라는 호출이지만, 객체의 종류에 따라 실제 수행되는 기능은 달라지는 것입니다.
다형성은 주로 포인터(Pointer) 또는 참조자(Reference) 를 사용하여 구현됩니다.
특히, 기반 클래스 포인터/참조자가 파생 클래스 객체를 가리킬 때 다형성이 빛을 발합니다.
정적 바인딩과 동적 바인딩
함수 호출이 어떤 함수를 실행할지 결정되는 시점을 바인딩(Binding) 이라고 합니다.
-
정적 바인딩 (Static Binding / Early Binding)
- 컴파일 시점에 호출될 함수가 결정됩니다.
- C++에서 일반 함수 호출(비가상 함수)은 기본적으로 정적 바인딩을 사용합니다.
- 단점: 기반 클래스 포인터로 파생 클래스 객체를 가리킬 때, 기반 클래스의 함수만 호출됩니다. 이는 다형성을 구현할 수 없습니다.
-
동적 바인딩 (Dynamic Binding / Late Binding)
- 런타임 시점에 호출될 함수가 결정됩니다.
- 가상 함수(Virtual Function) 를 사용하여 구현됩니다.
- 장점: 기반 클래스 포인터로 파생 클래스 객체를 가리키더라도, 실제 객체의 타입에 따라 적절한 파생 클래스의 함수가 호출되어 다형성을 구현할 수 있습니다.
가상 함수 (Virtual Functions)
가상 함수는 기반 클래스에서 virtual
키워드를 사용하여 선언된 멤버 함수이며, 파생 클래스에서 재정의(override)될 수 있습니다.
가상 함수는 동적 바인딩을 가능하게 하여 다형성을 구현하는 핵심 메커니즘입니다.
가상 함수의 특징
- 기반 클래스에서 함수 선언 앞에
virtual
키워드를 붙입니다.가상 함수 선언 형식 class Base { public: virtual void print() { /* ... */ } };
- 파생 클래스에서 가상 함수를 재정의할 때
override
키워드를 사용할 수 있습니다 (C++11부터).override
는 필수는 아니지만, 재정의 오류를 컴파일 시점에 잡아주므로 사용하는 것이 좋습니다.파생 클래스에서 가상 함수 재정의 class Derived : public Base { public: void print() override { /* ... */ } // virtual 키워드는 파생 클래스에서는 생략 가능 };
- 가상 함수 테이블(vtable)과 가상 함수 포인터(vptr)를 사용하여 런타임에 호출될 함수를 결정합니다.
- 가상 소멸자: 기반 클래스의 소멸자는 반드시
virtual
로 선언하는 것이 좋습니다. 그렇지 않으면 기반 클래스 포인터로 파생 클래스 객체를delete
할 때 파생 클래스의 소멸자가 호출되지 않아 메모리 누수가 발생할 수 있습니다.
#include <iostream>
#include <string>
#include <vector> // std::vector를 사용하기 위해
// 기반 클래스
class Shape {
public:
// 가상 함수 선언: 동적 바인딩을 가능하게 함
virtual void draw() const {
std::cout << "도형을 그립니다." << std::endl;
}
// 가상 소멸자: 파생 클래스 객체가 기반 클래스 포인터로 delete 될 때 올바른 소멸자 호출 보장
virtual ~Shape() {
std::cout << "Shape 소멸자 호출." << std::endl;
}
};
// 파생 클래스 1
class Circle : public Shape {
public:
void draw() const override { // virtual 함수 재정의 (override 키워드 사용)
std::cout << "원을 그립니다." << std::endl;
}
~Circle() override {
std::cout << "Circle 소멸자 호출." << std::endl;
}
};
// 파생 클래스 2
class Rectangle : public Shape {
public:
void draw() const override { // virtual 함수 재정의
std::cout << "사각형을 그립니다." << std::endl;
}
~Rectangle() override {
std::cout << "Rectangle 소멸자 호출." << std::endl;
}
};
// 파생 클래스 3
class Triangle : public Shape {
public:
void draw() const override { // virtual 함수 재정의
std::cout << "삼각형을 그립니다." << std::endl;
}
~Triangle() override {
std::cout << "Triangle 소멸자 호출." << std::endl;
}
};
int main() {
// 1. 개별 객체로 호출 (다형성 X, 정적 바인딩)
std::cout << "--- 개별 객체 호출 ---\n";
Shape s;
Circle c;
Rectangle r;
Triangle t;
s.draw(); // Shape::draw() 호출
c.draw(); // Circle::draw() 호출
r.draw(); // Rectangle::draw() 호출
t.draw(); // Triangle::draw() 호출
std::cout << "\n--- 기반 클래스 포인터/참조자 사용 (다형성 O, 동적 바인딩) ---\n";
// 2. 기반 클래스 포인터로 파생 클래스 객체 가리키기
Shape* shapePtr1 = new Circle();
Shape* shapePtr2 = new Rectangle();
Shape* shapePtr3 = new Triangle();
shapePtr1->draw(); // Circle::draw() 호출
shapePtr2->draw(); // Rectangle::draw() 호출
shapePtr3->draw(); // Triangle::draw() 호출
delete shapePtr1; // Circle 소멸자 -> Shape 소멸자 호출
delete shapePtr2; // Rectangle 소멸자 -> Shape 소멸자 호출
delete shapePtr3; // Triangle 소멸자 -> Shape 소멸자 호출
std::cout << "\n--- std::vector를 이용한 다형성 ---\n";
// 3. std::vector에 다양한 Shape 객체 저장 (포인터로)
std::vector<Shape*> shapes;
shapes.push_back(new Circle());
shapes.push_back(new Rectangle());
shapes.push_back(new Triangle());
shapes.push_back(new Shape()); // 기반 클래스 객체도 추가
for (Shape* shape : shapes) {
shape->draw(); // 실제 객체 타입에 따라 다른 draw() 함수 호출
}
// 메모리 해제
for (Shape* shape : shapes) {
delete shape;
}
shapes.clear(); // 벡터 비우기
return 0;
}
위 예시에서 Shape
클래스의 draw()
함수가 virtual
로 선언되어 있기 때문에, Shape
포인터(Shape* shapePtr1
)가 Circle
객체를 가리키더라도 shapePtr1->draw()
를 호출하면 Circle::draw()
가 호출됩니다.
이것이 바로 다형성입니다. 만약 draw()
함수가 virtual
이 아니었다면, 항상 Shape::draw()
가 호출되었을 것입니다.