객체지향프로그래밍(19) - 오버라이딩(2), 순수 가상 함수와 추상클래스

순수 가상 함수와 추상 클래스의 특징 이해

Featured image

🔚 짧게 하는 복습

✅ 1. 업 캐스팅과 다운 캐스팅의 정의와 원리를 안다.

✅ 2. 다운 캐스팅의 조심해야 할 점을 안다.

✅ 3. 정적 바인딩과 동적 바인딩의 차이와 장단점을 안다.

✅ 4. 가상 함수의 정의와 원리를 안다.

✅ 5. 다형성의 정의를 안다.

혹시 기억이 안 난다면, 다시 돌아가자


저번 시간에 이어 오버라이딩의 특징을 더 살펴보겠다.

오버라이딩을 통해서 구현할 수 있는 점들이 많다.

그중에서 오늘은 구조적인 관점에서 장점이 있는 추상 클래스를 배워보겠다.


override 키워드란?

저번 강의의 포켓몬 코드를 가져오겠다.

class pokemon {
public:
    pokemon() {}
    virtual void attack() { std::cout << "공격합니다~" << std::endl; }
};

class bukein : public pokemon {
    int type;

public:
    bukein() : pokemon(), type(Fire) {}
    void attack() {
        std::cout << "불 공격합니다~" << std::endl;
        std::cout << "확률에 따라 화상으로 추가 데미지를 줍니다." << std::endl;
    }
}; // 불 속성 브케인은 확률에 따라 추가 데미지를 주게하고 싶다.

class riako : public pokemon {
    int type;

public:
    riako() : pokemon(), type(Water) {}
    void attack() {
        std::cout << "물 공격합니다~" << std::endl;
        std::cout << "확률에 따라 자신의 hp를 회복합니다." << std::endl;
    }
}; // 물 속성 리아코는 확률에 따라 자신의 hp를 회복시키고 싶다.

class chicorita : public pokemon {
    int type;

public:
    chicorita() : pokemon(), type(Grass) {}
    void attack() {
        std::cout << "풀 공격합니다~" << std::endl;
        std::cout << "확률에 따라 보호막으로 다음 공격을 무시합니다." << std::endl;
    }
}; // 풀 속성 치코리타는 확률에 따라 보호막을 만들어 다음 공격을 무시하고 싶다.

이 코드에서 오버라이드 된 함수는 총 몇 개인가? 당연히 강의를 따라왔다면 3개라고 할 것이다.

그런데 보는 사람의 입장에서 어떤 함수가 override 되었는지 확인하기가 쉽지는 않다.

그렇기에, 오버라이드 된 메소드에는 override라는 키워드를 붙여서 한눈에 확인할 수 있다.

class pokemon {
public:
    pokemon() {}
    virtual void attack() { std::cout << "공격합니다~" << std::endl; }
};

class bukein : public pokemon {
    int type;

public:
    bukein() : pokemon(), type(Fire) {}
    void attack() override{ //override 됬음을 명시적으로 보여줌
        std::cout << "불 공격합니다~" << std::endl;
        std::cout << "확률에 따라 화상으로 추가 데미지를 줍니다." << std::endl;
    }
}; // 불 속성 브케인은 확률에 따라 추가 데미지를 주게하고 싶다.

class riako : public pokemon {
    int type;

public:
    riako() : pokemon(), type(Water) {}
    void attack() override{ //override 됬음을 명시적으로 보여줌
        std::cout << "물 공격합니다~" << std::endl;
        std::cout << "확률에 따라 자신의 hp를 회복합니다." << std::endl;
    }
}; // 물 속성 리아코는 확률에 따라 자신의 hp를 회복시키고 싶다.

class chicorita : public pokemon {
    int type;

public:
    chicorita() : pokemon(), type(Grass) {}
    void attack() override{ //override 됬음을 명시적으로 보여줌
        std::cout << "풀 공격합니다~" << std::endl;
        std::cout << "확률에 따라 보호막으로 다음 공격을 무시합니다." << std::endl;
    }
}; // 풀 속성 치코리타는 확률에 따라 보호막을 만들어 다음 공격을 무시하고 싶다.

사실 이쪽 용도보다는, 진짜 장점은 override 키워드를 붙이면 해야할 override를 안 했을 때 컴파일 에러를 줄 수 있다.

예시 코드를 한번 보자.

class Base {
public:
    virtual void foo();
};

class Derived : public Base {
public:
    void foo() const override; // Base 클래스의 foo 함수를 재정의
};

이렇게 코드를 짜면 컴파일 에러가 난다.

그 이유는 함수가 정확하게 같아야 오버라이딩이 가능한데 상위 클래스는 그냥 함수이고, 하위 클래스에서는 const함수라서 foo 함수의 오버로딩으로 생각하기 때문이다.

이렇게 시그니처가 달라서 다른 함수를 오버라이딩 하면, ‘void foo() const라는 메소드가 상위 클래스에 없어요!’하고 오류를 주는 것이다.

꼭 명심하자, 오버라이딩함수의 시그니처가 정확히 같을 때 가능하다.


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

다음 우리가 만든 코드에서 pokemon이라는 객체를 실제로 만들 일이 있는가?

업 캐스팅용으로 객체를 만드는 것을 제외하고는 없다. 그리고 아마 없을 것이다.

애초에 다형성의 구현을 위해 상속을 했기 때문에, pokemon 클래스의 객체는 만들어질 이유가 없다.

이렇게 몇몇 클래스는 구조상의 이득 (확장과 유지보수) 을 위해 만들어지고, 실제 객체가 만들어지지 않는다.

이런 클래스를 추상 클래스라고 하고, 다르긴 하지만 다른 언어에서는 인터페이스라는 용어로도 비슷한 목적으로 사용한다. (읽어볼 거리 참조)

C++에서는 언어 차원에서 객체를 만들 수 없도록 추상 클래스를 만드는 방법을 제공하고 있다.

바로 순수 가상 함수라는 것을 하나 이상 포함하면 된다.

순수 가상 함수함수 본문이 없는 가상 함수이다. 대신, 파생 클래스에서 반드시 오버라이딩되어야 한다.

class pokemon {
public:
    pokemon() {}
    virtual void attack() = 0 {}
};

이렇게만 해주면 된다. 간단하지 않은가?

순수 가상 함수추상 클래스의 존재 이유는 다형성추상화에 있다.

같은 추상 클래스를 상속함으로써 다양한 구현을 가능하게 함으로 다형성의 구현에 도움을 준다.

그리고 추상 클래스를 먼저 정의함으로써 OOD(object oriented design)을 편리하게 하도록 도와준다.

핵심적인 구조를 만드는 것에 집중할 수 있게 해주고, 세부적인 구현 사항은 추후에 생각하는 장점이 있는 것이다.

마지막으로, 상속의 특징을 그대로 들고 오기 때문에 확장성이나 유지보수에 도움이 된다.

다만, 상속의 단점도 그대로 들고 오기 때문에 복잡성이나 메모리, 시간적 손해도 여전히 남아있다.

항상 말하지만, 절대적인 방법은 없다. 장단을 잘 따져서 본인이 맞는 구조를 선택해야 한다.


📖 오늘의 핵심(다 알기 전까지는 넘어가지 말자❗)

✅ 1. 오버라이드 키워드의 용도와 사용 이유를 안다.

✅ 2. 순수 가상 함수의 특징과 용도를 안다.

✅ 3. 추상 클래스의 정의를 안다.

✅ 4. 순수 가상 함수와 추상 클래스의 장단점을 안다.

⚠️ 오버라이딩은 시그니처가 같아야 한다.

💣 과제, 없음

🔜 더 공부해보기,

읽어볼 거리(1) - 인터페이스란?

읽어볼 거리(2) - 인터페이스와 추상 클래스의 차이

읽어볼 거리(3) - 사실 순수 가상 함수도 본문을 가질 수 있다.

순수 가상 함수가 본문이 없어야한다는 것은 잘못된 설명이다, 하지만 추상 클래스 자체의 성질을 살리기 위해 본문을 비우는 것이 일반적이다.

#include <iostream>

enum { Fire, Water, Grass };

class pokemon {
public:
    pokemon() {}
    virtual void attack() { std::cout << "공격합니다~" << std::endl; }
    // 본문이 있는 추상 클래스
};

class bukein : public pokemon {
    int type;

public:
    bukein() : pokemon(), type(Fire) {}
    void attack() override { //override 됬음을 명시적으로 보여줌
        pokemon::attack();
        std::cout << "확률에 따라 화상으로 추가 데미지를 줍니다." << std::endl;
    }
}; // 불 속성 브케인은 확률에 따라 추가 데미지를 주게하고 싶다.

class riako : public pokemon {
    int type;

public:
    riako() : pokemon(), type(Water) {}
    void attack() override { //override 됬음을 명시적으로 보여줌
        pokemon::attack();
        std::cout << "확률에 따라 자신의 hp를 회복합니다." << std::endl;
    }
}; // 물 속성 리아코는 확률에 따라 자신의 hp를 회복시키고 싶다.

class chicorita : public pokemon {
    int type;

public:
    chicorita() : pokemon(), type(Grass) {}
    void attack() override { //override 됬음을 명시적으로 보여줌
        pokemon::attack();
        std::cout << "확률에 따라 보호막으로 다음 공격을 무시합니다." << std::endl;
    }
}; // 풀 속성 치코리타는 확률에 따라 보호막을 만들어 다음 공격을 무시하고 싶다.
int main() {
    pokemon* pomons[3]; // 포인터를 저장하는 배열이어야하니!
    chicorita pomon1;
    bukein pomon2;
    riako pomon3;

    pomons[0] = &pomon1;
    pomons[1] = &pomon2;
    pomons[2] = &pomon3;

    for (int i = 0; i < 3; i++) {
        pomons[i]->attack();
    }
}