다형성과 가상 함수
다형성의 개념
다형성(Polymorphism)은 객체 지향 프로그래밍의 핵심 개념 중 하나로, "많은 형태를 가진다"는 의미를 가집니다.
프로그래밍에서 다형성은 동일한 인터페이스를 통해 다양한 타입의 객체를 처리할 수 있는 능력을 말합니다.
다형성의 종류
1. 컴파일 타임 다형성 (정적 다형성)
- 함수 오버로딩
- 연산자 오버로딩
- 템플릿
2. 런타임 다형성 (동적 다형성)
- 가상 함수를 통한 다형성
가상 함수 (Virtual Functions)
가상 함수는 기본 클래스에서 선언되고 파생 클래스에서 재정의될 수 있는 멤버 함수입니다.
가상함수는 virtual
키워드를 사용하여 선언합니다.
class Animal {
public:
virtual void makeSound() {
std::cout << "The animal makes a sound" << std::endl;
}
};
class Dog : public Animal {
public:
void makeSound() override {
std::cout << "The dog barks" << std::endl;
}
};
class Cat : public Animal {
public:
void makeSound() override {
std::cout << "The cat meows" << std::endl;
}
};
가상 함수의 주요 특징
- 기본 클래스 포인터나 참조를 통해 파생 클래스의 객체를 다룰 때, 실제 객체의 타입에 따라 적절한 함수가 호출됩니다.
- 런타임에 올바른 함수가 결정됩니다. (동적 바인딩)
가상 함수의 동작 원리
가상 함수는 vtable(가상 함수 테이블)을 통해 구현됩니다.
- 가상 함수를 가진 각 클래스는 자신만의 vtable을 가집니다.
- vtable에는 해당 클래스의 가상 함수 주소가 저장됩니다.
- 객체가 생성될 때, 객체 내부에 vtable에 대한 포인터(vptr)가 추가됩니다.
- 가상 함수 호출 시, vptr을 통해 vtable에 접근하여 적절한 함수를 호출합니다.
순수 가상 함수와 추상 클래스
순수 가상 함수는 기본 클래스에서 구현을 제공하지 않는 가상 함수입니다.
선언 시 = 0
을 붙여 나타냅니다.
class Shape {
public:
virtual double getArea() = 0; // 순수 가상 함수
virtual void draw() = 0; // 순수 가상 함수
virtual ~Shape() = default; // 가상 소멸자
};
순수 가상 함수를 하나 이상 포함하는 클래스를 추상 클래스라고 합니다.
추상 클래스의 특징
- 객체를 직접 생성할 수 없습니다.
- 파생 클래스에서 모든 순수 가상 함수를 구현해야 합니다.
가상 소멸자
상속 관계에서 기본 클래스 포인터를 통해 객체를 삭제할 때, 소멸자가 가상이 아니면 파생 클래스의 소멸자가 호출되지 않을 수 있습니다.
이를 방지하기 위해 기본 클래스의 소멸자를 가상으로 선언해야 합니다.
class Base {
public:
virtual ~Base() {
std::cout << "Base destructor" << std::endl;
}
};
class Derived : public Base {
public:
~Derived() override {
std::cout << "Derived destructor" << std::endl;
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // 두 소멸자 모두 호출됨
return 0;
}
override와 final 키워드
C++ 11부터 override
와 final
키워드가 도입되었습니다.
override
: 가상 함수를 재정의함을 명시적으로 나타냅니다.final
: 더 이상 재정의될 수 없음을 나타냅니다.
class Base {
public:
virtual void foo() { std::cout << "Base::foo" << std::endl; }
};
class Derived : public Base {
public:
void foo() override final { std::cout << "Derived::foo" << std::endl; }
};
class Further : public Derived {
public:
// void foo() override { } // 컴파일 에러: foo는 final이므로 재정의할 수 없음
};
실습 : 도형 계층 구조 구현
다음 요구사항을 만족하는 도형 계층 구조를 구현해보세요.
Shape
를 추상 기본 클래스로 하고,Circle
,Rectangle
,Triangle
을 파생 클래스로 구현- 각 도형은
getArea()
,getPerimeter()
,draw()
메서드를 가져야 함 ShapeManager
클래스를 만들어 여러 도형을 관리하고 전체 면적과 둘레를 계산할 수 있도록 구현
#include <iostream>
#include <vector>
#include <memory>
#include <cmath>
class Shape {
public:
virtual double getArea() const = 0;
virtual double getPerimeter() const = 0;
virtual void draw() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double getArea() const override { return M_PI * radius * radius; }
double getPerimeter() const override { return 2 * M_PI * radius; }
void draw() const override { std::cout << "Drawing a circle" << std::endl; }
};
class Rectangle : public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double getArea() const override { return width * height; }
double getPerimeter() const override { return 2 * (width + height); }
void draw() const override { std::cout << "Drawing a rectangle" << std::endl; }
};
class Triangle : public Shape {
private:
double a, b, c; // sides
public:
Triangle(double side1, double side2, double side3) : a(side1), b(side2), c(side3) {}
double getArea() const override {
double s = (a + b + c) / 2;
return std::sqrt(s * (s - a) * (s - b) * (s - c));
}
double getPerimeter() const override { return a + b + c; }
void draw() const override { std::cout << "Drawing a triangle" << std::endl; }
};
class ShapeManager {
private:
std::vector<std::unique_ptr<Shape>> shapes;
public:
void addShape(std::unique_ptr<Shape> shape) {
shapes.push_back(std::move(shape));
}
double getTotalArea() const {
double total = 0;
for (const auto& shape : shapes) {
total += shape->getArea();
}
return total;
}
double getTotalPerimeter() const {
double total = 0;
for (const auto& shape : shapes) {
total += shape->getPerimeter();
}
return total;
}
void drawAll() const {
for (const auto& shape : shapes) {
shape->draw();
}
}
};
int main() {
ShapeManager manager;
manager.addShape(std::make_unique<Circle>(5));
manager.addShape(std::make_unique<Rectangle>(4, 6));
manager.addShape(std::make_unique<Triangle>(3, 4, 5));
std::cout << "Total Area: " << manager.getTotalArea() << std::endl;
std::cout << "Total Perimeter: " << manager.getTotalPerimeter() << std::endl;
manager.drawAll();
return 0;
}
연습 문제
Vehicle
클래스를 기본 클래스로 하고,Car
,Motorcycle
,Truck
클래스를 파생 클래스로 구현하세요. 각 클래스는start()
,stop()
,accelerate(int speed)
메서드를 가져야 합니다.Employee
클래스를 기본 클래스로 하고,Manager
,Engineer
,Salesperson
클래스를 파생 클래스로 구현하세요. 각 클래스는calculateSalary()
메서드를 가져야 하며, 직급에 따라 다르게 계산되어야 합니다.Animal
클래스를 기본 클래스로 하고,Mammal
,Bird
,Fish
클래스를 파생 클래스로 구현하세요. 각 클래스는move()
,makeSound()
메서드를 가져야 합니다. 추가로Zoo
클래스를 만들어 여러 동물을 관리하고 상호작용할 수 있도록 구현하세요.
참고자료
- Stroustrup, Bjarne. "The C++ Programming Language (4th Edition)"
- Meyers, Scott. "Effective C++ : 55 Specific Ways to Improve Your Programs and Designs"
- Lippman, Stanley B. "Inside the C++ Object Model"
- C++ Reference : Virtual functions
- ISO C++ Core Guidelines : C.121 : If a base class is used as an interface, make it a pure abstract class