icon안동민 개발노트

다형성과 가상 함수


다형성의 개념

 다형성(Polymorphism)은 객체 지향 프로그래밍의 핵심 개념 중 하나로, "많은 형태를 가진다"는 의미를 가집니다. 프로그래밍에서 다형성은 동일한 인터페이스를 통해 다양한 타입의 객체를 처리할 수 있는 능력을 말합니다.

 다형성의 종류

  1. 컴파일 타임 다형성 (정적 다형성)
  • 함수 오버로딩
  • 연산자 오버로딩
  • 템플릿
  1. 런타임 다형성 (동적 다형성)
  • 가상 함수를 통한 다형성

가상 함수 (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(가상 함수 테이블)을 통해 구현됩니다.

  1. 가상 함수를 가진 각 클래스는 자신만의 vtable을 가집니다.
  2. vtable에는 해당 클래스의 가상 함수 주소가 저장됩니다.
  3. 객체가 생성될 때, 객체 내부에 vtable에 대한 포인터(vptr)가 추가됩니다.
  4. 가상 함수 호출 시, 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부터 overridefinal 키워드가 도입되었습니다.

  • 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;
}

연습 문제

  1. Vehicle 클래스를 기본 클래스로 하고, Car, Motorcycle, Truck 클래스를 파생 클래스로 구현하세요. 각 클래스는 start(), stop(), accelerate(int speed) 메서드를 가져야 합니다.
  2. Employee 클래스를 기본 클래스로 하고, Manager, Engineer, Salesperson 클래스를 파생 클래스로 구현하세요. 각 클래스는 calculateSalary() 메서드를 가져야 하며, 직급에 따라 다르게 계산되어야 합니다.
  3. Animal 클래스를 기본 클래스로 하고, Mammal, Bird, Fish 클래스를 파생 클래스로 구현하세요. 각 클래스는 move(), makeSound() 메서드를 가져야 합니다. 추가로 Zoo 클래스를 만들어 여러 동물을 관리하고 상호작용할 수 있도록 구현하세요.


참고 자료