추상클래스 & 인터페이스 클래스

2023. 5. 19. 16:57프로그래밍 자료 조사

1. 추상 클래스

 

순수 가상 함수(pure virtual function)

C++에서 가상 함수(virtual function)는 파생 클래스에서 재정의할 것으로 기대하는 멤버 함수를 의미합니다.

따라서 가상 함수는 반드시 재정의해야만 하는 함수가 아닌, 재정의가 가능한 함수를 가리킵니다.

 

이와는 달리 순수 가상 함수(pure virtual function)란 파생 클래스에서 반드시 재정의해야 하는 멤버 함수를 의미합니다.

이러한 순수 가상 함수는 일반적으로 함수의 동작을 정의하는 본체를 가지고 있지 않습니다.

따라서 파생 클래스에서 재정의하지 않으면 사용할 수 없습니다.

 

C++에서 순수 가상 함수는 다음과 같은 문법으로 선언합니다.

 

문법

virtual 멤버함수의원형=0;

 

위와 같이 함수만 있고 본체가 없다는 의미로 함수 선언부 끝에 "=0"을 추가합니다.

 

추상 클래스(abstract class)

C++에서는 하나 이상의 순수 가상 함수를 포함하는 클래스를 추상 클래스(abstract class)라고 합니다.

이러한 추상 클래스는 객체 지향 프로그래밍에서 중요한 특징인 다형성을 가진 함수의 집합을 정의할 수 있게 해줍니다.

즉, 반드시 사용되어야 하는 멤버 함수를 추상 클래스에 순수 가상 함수로 선언해 놓으면, 이 클래스로부터 파생된 모든 클래스에서는 이 가상 함수를 반드시 재정의해야 합니다.

 

추상 클래스는 동작이 정의되지 않은 순수 가상 함수를 포함하고 있으므로, 인스턴스를 생성할 수 없습니다.

따라서 추상 클래스는 먼저 상속을 통해 파생 클래스를 만들고, 만든 파생 클래스에서 순수 가상 함수를 모두 오버라이딩하고 나서야 비로소 파생 클래스의 인스턴스를 생성할 수 있게 됩니다.

하지만 추상 클래스 타입의 포인터와 참조는 바로 사용할 수 있습니다.

 

예제

class Animal

{

public:

    virtual ~Animal() {}  // 가상 소멸자의 선언

    virtual void Cry()=0; // 순수 가상 함수의 선언

};

 

class Dog : public Animal

{

public:

    virtual void Cry() { cout << "멍멍!!" << endl; }

};

 

class Cat : public Animal

{

public:

    virtual void Cry() { cout << "야옹야옹!!" << endl; }

};

 

int main(void)

{

    Dog my_dog;

    my_dog.Cry();

    Cat my_cat;

    my_cat.Cry();

    return 0;

}

실행 결과

멍멍!!

야옹야옹!!

<내가 따라해보기>

위의 예제에서 추상 클래스인 Animal 클래스는 순수 가상 함수인 Cry() 멤버 함수를 가지고 있습니다.

Animal 클래스를 상속받는 파생 클래스인 Dog 클래스와 Cat 클래스는 Cry() 함수를 오버라이딩해야만 인스턴스를 생성할 수 있습니다.

 

추상 클래스의 용도 제한

C++에서 추상 클래스는 다음과 같은 용도로는 사용할 수 없습니다.

 

1. 순수 가상 함수로 한번 선언이 되었다면 반드시 자식 클래스에서 오버라이딩 해야 한다.

2. 추상 클래스의 일반 멤버 함수들은 접근 지정자에 따라 본인 클래스 내부, 혹은 자식 클래스 내부(protected, public)에서만 사용할 수 있다.

 

2. 인터페이스

 
#include <iostream>

class Camera {
public:
    void take() {
        std::cout << "take picture" << std::endl;
    }
};

class People {
public:
    void useCamera(Camera* p) { p->take(); }
};

int main() {
    People p;
    Camera c1;
    p.useCamera(&c1);
}

 

사진을 찍는 기능을 하는 take()함수를 가지고 있는 클래스 Camera가 있고
그 카메라를 사용하는 People클래스가 있다고 해보자.
main에서 People과 Camera객체를 생성하고 생성한 카메라를 사용하니 아무 문제가 없다.
그런데 기능이 더 좋아진 새로운 카메라가 나왔다고 생각을 해보자.
#include <iostream>

class Camera {
public:
    void take() {
        std::cout << "take picture" << std::endl;
    }
};

class HDCamera {
public:
    void take() {
        std::cout << "take picture HD" << std::endl;
    }
};

class People {
public:
    void useCamera(Camera* p) { p->take(); }
};

int main() {
    People p;
    Camera c1;
    p.useCamera(&c1);
    
    HDCamera hd;
    p.useCamera(&hd);
}

 

HDCamera클래스가 새로 생겼다. 기능은 Camera와 같다.
그리고 메인에서 HDCamera객체를 만들고 쓰면 되는데 문제가 있다.
People클래스의 useCamera가 인자로 받는건 Camera클래스이기 때문에 HDCamera는 되지 않는다.
문제를 해결하려면 인자를 다르게 함수를 똑같이 만들면 된다.
#include <iostream>

class Camera {
public:
    void take() {
        std::cout << "take picture" << std::endl;
    }
};

class HDCamera {
public:
    void take() {
        std::cout << "take picture HD" << std::endl;
    }
};

class People {
public:
    void useCamera(Camera* p) { p->take(); }
    void useCamera(HDCamera* p) { p->take(); }
};

int main() {
    People p;
    Camera c1;
    p.useCamera(&c1);
    
    HDCamera hd;
    p.useCamera(&hd);
}

 

이제는 문제 없이 실행된다.
하지만 이 일련의 과정은 원칙을 못 지키고 있다.
보면 HDCamera는 나중에 추가된 것인데, 기존에 있던 코드가 수정되었다. 이는 잘못되었다.
이러한 원칙을 OCP라고 한다.

  ① 개방 폐쇄의 법칙 (OCP : Open Close Principle)

    => 기능 확장(모듈, 클래스, 함수 추가)에 열려 있고, 수정(기존 코드 수정)에는 닫혀 있어야 한다는 원칙

    => 새로운 카메라 클래스가 추가되어도 기존 클래스의 코드를 수정하지 않도록 만들어야 한다.

 

또, People클래스에서 Camera를 정확히 지목하여 인자로 받는데
이를 강한 결합이라고 한다.

  ② 강한 결합 (tightly coupling)

    => 객체와 다른 객체와의 관계가 강하게 연결되어 있는 것.

    => 교체 불가능하고 확장성이 없다.

 

 

이제 위의 문제를 해결해보자.
#include <iostream>

class Camera {
public:
    void take() {
        std::cout << "take picture" << std::endl;
    }
};

class People {
public:
    void useCamera(Camera* p) { p->take(); }
};

int main() {
    People p;
    Camera c1;
    p.useCamera(&c1);
}

 

HDCamera가 추가 되기 전의 코드를 그대로 가지고 왔다.
이번엔 Camera와 People을 먼저 만들지 말고, 규칙을 먼저 만들어보자
이를 계약에 의한 설계라고 한다.

  ① 계약에 의한 설계

    => 사람과 카메라 제작자 사이에 지켜야 하는 규칙을 먼저 설계한다.

    => 규칙은 추상 클래스를 사용해서 설계한다.

 

  ② 규칙

    => 모든 카메라는 ICamera로부터 파생 되어야 한다.

 

#include <iostream>

class ICamera {
public:
    virtual void take() = 0;
};

class People {
public:
    void useCamera(ICamera* p) { p->take(); }
};

int main() {
}

  ③ 카메라 사용자 (People 클래스)

    => 규칙대로만 사용하면 된다.

    => 순수 가상 함수로 되어 실물 카메라가 없어도 People클래스를 먼저 만들 수 있다.

 

  ④ 모든 카메라 제작자 (Camera 클래스)

    => 반드시 규칙을 지켜야 한다.

 

#include <iostream>

// 인터페이스
class ICamera {
public:
    virtual void take() = 0;
};

class People {
public:
    void useCamera(ICamera* p) { p->take(); }
};

class Camera : public ICamera {
public:
    void take() {
        std::cout << "take picture" << std::endl;
    }
};

class HDCamera : public ICamera {
public:
    void take() {
        std::cout << "take picture HD" << std::endl;
    }
};

int main() {
    People p;
    Camera c1;
    p.useCamera(&c1);
    
    HDCamera hd;
    p.useCamera(&hd);
}

 

<내가 따라해보기>

 

이제는 규칙만 지킨다면 문제없이 새로운 카메라를 추가할 수 있다.
이러한 규칙을 인터페이스라고 한다.
따라서 ②의 내용을 조금 바꾸면
  ② 규칙
    => 모든 카메라는 ICamera로부터 파생 되어야 한다.

  ② 규칙
    => 모든 카메라는 ICamera 인터페이스를 구현 해야 한다.

  ⑤ 약한 결합 (loosely coupling)

    => 객체와 다른 개체와의 관계가 약하게 연결되어 있는 것(인터페이스를 사용해서 통신)

    => 교체 가능하고 확장성이 좋다.

 

'프로그래밍 자료 조사' 카테고리의 다른 글

벡터  (1) 2023.05.26
행렬  (0) 2023.05.24
삼각함수 공부  (0) 2023.05.15
윈도우 메시지, 메시지 큐, 메시지 루프, 윈도우 프로시저  (0) 2023.05.12
함수포인터  (0) 2023.05.10