icon안동민 개발노트

추상 클래스와 순수 가상 함수


추상 클래스의 개념

 추상 클래스는 객체 지향 프로그래밍에서 중요한 개념으로, 하나 이상의 순수 가상 함수를 포함하는 클래스를 말합니다.

 추상 클래스의 주요 목적은 공통된 인터페이스를 정의하고 파생 클래스에서 이를 구현하도록 강제하는 것입니다.

 추상 클래스의 특징

  1. 직접적인 인스턴스화 불가능
  2. 포인터나 참조를 통해 사용 가능
  3. 파생 클래스에서 모든 순수 가상 함수를 구현해야 함
  4. 일반 멤버 변수와 일반 멤버 함수도 포함할 수 있음
class AbstractClass {
public:
    virtual void pureVirtualFunction() = 0;  // 순수 가상 함수
    virtual void normalVirtualFunction() { /* ... */ }  // 일반 가상 함수
    void nonVirtualFunction() { /* ... */ }  // 비가상 함수
    virtual ~AbstractClass() = default;  // 가상 소멸자
};

순수 가상 함수

 순수 가상 함수는 기본 클래스에서 구현을 제공하지 않는 가상 함수입니다.

 함수 선언 뒤에 = 0을 붙여 나타냅니다.

class Shape {
public:
    virtual double getArea() const = 0;  // 순수 가상 함수
    virtual double getPerimeter() const = 0;  // 순수 가상 함수
    virtual void draw() const = 0;  // 순수 가상 함수
    virtual ~Shape() = default;  // 가상 소멸자
};

 순수 가상 함수의 특징

  1. 기본 클래스에서 구현을 제공하지 않음
  2. 파생 클래스에서 반드시 구현해야 함
  3. 인터페이스를 정의하는 데 사용됨

추상 클래스 vs 구체 클래스

 추상 클래스를 상속받아 모든 순수 가상 함수를 구현한 클래스를 구체 클래스라고 합니다.

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 with radius " << radius << std::endl;
    }
};

인터페이스 클래스

 C++에는 Java나 C#과 같은 interface 키워드가 없지만, 순수 가상 함수만을 포함하는 추상 클래스를 통해 인터페이스를 구현할 수 있습니다.

class Drawable {
public:
    virtual void draw() const = 0;
    virtual ~Drawable() = default;
};
 
class Movable {
public:
    virtual void move(double dx, double dy) = 0;
    virtual ~Movable() = default;
};
 
class Shape : public Drawable, public Movable {
public:
    virtual double getArea() const = 0;
    virtual double getPerimeter() const = 0;
    virtual ~Shape() = default;
};

추상 클래스의 활용

 다형성 구현

 추상 클래스를 사용하면 다형성을 효과적으로 구현할 수 있습니다.

void drawShapes(const std::vector<std::unique_ptr<Shape>>& shapes) {
    for (const auto& shape : shapes) {
        shape->draw();  // 다형적 호출
    }
}
 
int main() {
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>(5));
    shapes.push_back(std::make_unique<Rectangle>(4, 3));
    drawShapes(shapes);
    return 0;
}

 프레임워크 설계

 추상 클래스는 프레임워크나 라이브러리 설계에 자주 사용됩니다.

 기본 동작을 정의하고 사용자가 특정 동작을 재정의할 수 있게 합니다.

class Application {
public:
    virtual void initialize() = 0;
    virtual void run() = 0;
    virtual void cleanup() = 0;
 
    void execute() {
        initialize();
        run();
        cleanup();
    }
 
    virtual ~Application() = default;
};
 
class MyApp : public Application {
public:
    void initialize() override {
        std::cout << "Initializing MyApp" << std::endl;
    }
 
    void run() override {
        std::cout << "Running MyApp" << std::endl;
    }
 
    void cleanup() override {
        std::cout << "Cleaning up MyApp" << std::endl;
    }
};
 
int main() {
    MyApp app;
    app.execute();
    return 0;
}

추상 클래스와 순수 가상 함수의 고급 기능

 순수 가상 함수의 구현 제공

 순수 가상 함수에도 기본 구현을 제공할 수 있습니다.

 이 경우 파생 클래스에서 해당 함수를 반드시 재정의할 필요는 없지만, 클래스는 여전히 추상 클래스로 남습니다.

class AbstractBase {
public:
    virtual void pureVirtual() = 0;
};
 
void AbstractBase::pureVirtual() {
    std::cout << "Default implementation of pure virtual function" << std::endl;
}
 
class Derived : public AbstractBase {
public:
    void pureVirtual() override {
        AbstractBase::pureVirtual();  // 기본 구현 호출
        std::cout << "Additional behavior in Derived" << std::endl;
    }
};

 추상 클래스의 생성자와 소멸자

 추상 클래스도 생성자와 소멸자를 가질 수 있습니다.

 이들은 파생 클래스의 객체가 생성되거나 소멸될 때 호출됩니다.

class AbstractBase {
public:
    AbstractBase() {
        std::cout << "AbstractBase constructor" << std::endl;
    }
 
    virtual ~AbstractBase() {
        std::cout << "AbstractBase destructor" << std::endl;
    }
 
    virtual void pureVirtual() = 0;
};
 
class Derived : public AbstractBase {
public:
    Derived() {
        std::cout << "Derived constructor" << std::endl;
    }
 
    ~Derived() override {
        std::cout << "Derived destructor" << std::endl;
    }
 
    void pureVirtual() override {
        std::cout << "Derived::pureVirtual" << std::endl;
    }
};
 
int main() {
    Derived d;
    return 0;
}
실행 결과
AbstractBase constructor
Derived constructor
Derived destructor
AbstractBase destructor

추상 클래스와 다중 상속

 C++에서는 다중 상속을 지원하므로, 여러 추상 클래스를 동시에 상속받을 수 있습니다.

 이를 통해 여러 인터페이스를 구현하는 클래스를 만들 수 있습니다.

class Printable {
public:
    virtual void print() const = 0;
    virtual ~Printable() = default;
};
 
class Serializable {
public:
    virtual std::string serialize() const = 0;
    virtual void deserialize(const std::string& data) = 0;
    virtual ~Serializable() = default;
};
 
class MyClass : public Printable, public Serializable {
public:
    void print() const override {
        std::cout << "MyClass instance" << std::endl;
    }
 
    std::string serialize() const override {
        return "MyClass data";
    }
 
    void deserialize(const std::string& data) override {
        std::cout << "Deserializing: " << data << std::endl;
    }
};

추상 클래스와 템플릿

 추상 클래스와 템플릿을 조합하여 유연하고 재사용 가능한 코드를 작성할 수 있습니다.

template<typename T>
class Container {
public:
    virtual void add(const T& item) = 0;
    virtual T get(int index) const = 0;
    virtual int size() const = 0;
    virtual ~Container() = default;
};
 
template<typename T>
class Vector : public Container<T> {
private:
    std::vector<T> data;
 
public:
    void add(const T& item) override {
        data.push_back(item);
    }
 
    T get(int index) const override {
        return data[index];
    }
 
    int size() const override {
        return data.size();
    }
};
 
int main() {
    Vector<int> v;
    v.add(1);
    v.add(2);
    v.add(3);
    std::cout << "Size: " << v.size() << std::endl;
    std::cout << "Element at index 1: " << v.get(1) << std::endl;
    return 0;
}

추상 클래스와 순수 가상 함수의 장단점

 장점

  1. 인터페이스 정의 : 공통된 인터페이스를 정의하여 코드의 일관성을 유지할 수 있습니다.
  2. 다형성 : 기본 클래스 포인터나 참조를 통해 파생 클래스 객체를 다룰 수 있습니다.
  3. 코드 재사용 : 공통 기능을 기본 클래스에 구현하여 코드 중복을 줄일 수 있습니다.
  4. 확장성 : 새로운 파생 클래스를 쉽게 추가할 수 있습니다.

 단점

  1. 복잡성 : 클래스 계층 구조가 복잡해질 수 있습니다.
  2. 성능 오버헤드 : 가상 함수 테이블을 통한 호출로 인해 약간의 성능 저하가 있을 수 있습니다.
  3. 디자인 시간 : 적절한 추상화 수준을 결정하는 데 시간이 걸릴 수 있습니다.

추상 클래스 vs 인터페이스

 앞서 언급했듯 C++에는 Java나 C#과 같은 별도의 interface 키워드가 없지만, 순수 가상 함수만을 포함하는 추상 클래스를 통해 인터페이스와 유사한 개념을 구현할 수 있습니다.

 추상 클래스

  • 일부 구현을 포함할 수 있음
  • 생성자를 가질 수 있음
  • 멤버 변수를 가질 수 있음
  • 다중 상속 시 주의가 필요함

 인터페이스 (순수 가상 함수만을 포함하는 추상 클래스)

  • 구현을 포함하지 않음 (C++ 20 이전)
  • 주로 다중 상속에 사용됨
  • 멤버 변수를 가질 수 없음 (상수는 가능)
class Interface {
public:
    virtual void method1() = 0;
    virtual void method2() = 0;
    virtual ~Interface() = default;
};
 
class ConcreteClass : public Interface {
public:
    void method1() override {
        // 구현
    }
 
    void method2() override {
        // 구현
    }
};

C++ 20의 컨셉과 추상 클래스

 C++ 20에서 도입된 컨셉(Concepts)은 템플릿 매개변수에 대한 제약을 정의하는 새로운 방법을 제공합니다.

 이는 추상 클래스와 유사한 역할을 하지만, 컴파일 타임에 작동합니다.

#include <concepts>
 
template<typename T>
concept Drawable = requires(T t) {
    { t.draw() } -> std::same_as<void>;
};
 
template<Drawable T>
void render(const T& obj) {
    obj.draw();
}
 
class Circle {
public:
    void draw() const {
        std::cout << "Drawing a circle" << std::endl;
    }
};
 
int main() {
    Circle c;
    render(c);  // OK
    return 0;
}

 컨셉은 추상 클래스와 달리 런타임 다형성을 제공하지 않지만, 컴파일 타임에 타입 체크를 수행하여 더 빠른 코드를 생성할 수 있습니다.

실습 : 게임 캐릭터 시스템 설계

 다음 요구사항을 만족하는 게임 캐릭터 시스템을 설계하고 구현해보세요.

  • Character를 추상 기본 클래스로 정의
  • Warrior, Mage, Archer 등의 구체적인 캐릭터 클래스 구현
  • Attackable, Healable 인터페이스 정의 및 구현
  • GameEngine 클래스를 만들어 여러 캐릭터를 관리하고 상호작용할 수 있도록 구현
#include <iostream>
#include <vector>
#include <memory>
#include <string>
 
class Attackable {
public:
    virtual void attack(Attackable& target) = 0;
    virtual void receiveAttack(int damage) = 0;
    virtual ~Attackable() = default;
};
 
class Healable {
public:
    virtual void heal(int amount) = 0;
    virtual ~Healable() = default;
};
 
class Character : public Attackable, public Healable {
protected:
    std::string name;
    int health;
    int maxHealth;
    int attackPower;
 
public:
    Character(const std::string& n, int h, int ap)
        : name(n), health(h), maxHealth(h), attackPower(ap) {}
 
    void attack(Attackable& target) override {
        std::cout << name << " attacks for " << attackPower << " damage." << std::endl;
        target.receiveAttack(attackPower);
    }
 
    void receiveAttack(int damage) override {
        health -= damage;
        std::cout << name << " receives " << damage << " damage. ";
        std::cout << "Remaining health: " << health << std::endl;
    }
 
    void heal(int amount) override {
        int healedAmount = std::min(amount, maxHealth - health);
        health += healedAmount;
        std::cout << name << " heals for " << healedAmount << " health. ";
        std::cout << "Current health: " << health << std::endl;
    }
 
    virtual void useSpecialAbility() = 0;
 
    bool isAlive() const { return health > 0; }
    std::string getName() const { return name; }
};
 
class Warrior : public Character {
public:
    Warrior(const std::string& name) : Character(name, 150, 20) {}
 
    void useSpecialAbility() override {
        std::cout << name << " uses Berserk Rage, increasing attack power!" << std::endl;
        attackPower += 10;
    }
};
 
class Mage : public Character {
private:
    int mana;
 
public:
    Mage(const std::string& name) : Character(name, 100, 15), mana(100) {}
 
    void useSpecialAbility() override {
        if (mana >= 20) {
            std::cout << name << " casts Fireball!" << std::endl;
            mana -= 20;
        } else {
            std::cout << name << " is out of mana!" << std::endl;
        }
    }
};
 
class Archer : public Character {
private:
    int arrows;
 
public:
    Archer(const std::string& name) : Character(name, 120, 18), arrows(20) {}
 
    void useSpecialAbility() override {
        if (arrows >= 3) {
            std::cout << name << " uses Multishot!" << std::endl;
            arrows -= 3;
        } else {
            std::cout << name << " is out of arrows!" << std::endl;
        }
    }
};
 
class GameEngine {
private:
    std::vector<std::unique_ptr<Character>> characters;
 
public:
    void addCharacter(std::unique_ptr<Character> character) {
        characters.push_back(std::move(character));
    }
 
    void simulateBattle() {
        std::cout << "Battle simulation starts!" << std::endl;
        
        for (size_t i = 0; i < characters.size(); ++i) {
            for (size_t j = 0; j < characters.size(); ++j) {
                if (i != j && characters[i]->isAlive() && characters[j]->isAlive()) {
                    characters[i]->attack(*characters[j]);
                    characters[i]->useSpecialAbility();
                }
            }
        }
 
        std::cout << "Battle simulation ends!" << std::endl;
    }
 
    void healAllCharacters(int amount) {
        for (auto& character : characters) {
            if (character->isAlive()) {
                character->heal(amount);
            }
        }
    }
 
    void showStatus() {
        for (const auto& character : characters) {
            std::cout << character->getName() << " is " 
                      << (character->isAlive() ? "alive" : "defeated") << std::endl;
        }
    }
};
 
int main() {
    GameEngine game;
 
    game.addCharacter(std::make_unique<Warrior>("Conan"));
    game.addCharacter(std::make_unique<Mage>("Gandalf"));
    game.addCharacter(std::make_unique<Archer>("Legolas"));
 
    game.simulateBattle();
    game.showStatus();
 
    game.healAllCharacters(50);
    game.showStatus();
 
    return 0;
}

 이 실습을 통해 추상 클래스와 순수 가상 함수를 사용하여 게임 캐릭터 시스템을 설계하고 구현해보았습니다. 이러한 접근 방식은 새로운 캐릭터 유형을 쉽게 추가할 수 있게 해주며, 다형성을 활용하여 캐릭터들을 일관된 방식으로 다룰 수 있게 합니다.

연습 문제

  1. Vehicle이라는 추상 기본 클래스를 만들고, Car, Motorcycle, Truck 등의 구체적인 차량 클래스를 구현해보세요. 각 클래스는 적절한 메서드(예 : start(), stop(), accelerate())를 가져야 합니다.
  2. 파일 시스템을 모델링하는 클래스 계층 구조를 설계해보세요. FileSystemItem을 추상 기본 클래스로 하고, FileDirectory 클래스를 구현하세요. 적절한 메서드(예 : getName(), getSize(), listContents())를 포함해야 합니다.
  3. 은행 계좌 시스템을 구현해보세요. Account를 추상 기본 클래스로 하고, SavingsAccount, CheckingAccount, InvestmentAccount 등의 구체적인 계좌 클래스를 만드세요. 각 클래스는 적절한 메서드(예 : deposit(), withdraw(), calculateInterest())를 가져야 합니다.

 참고자료