일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |
- operator overloading
- virtual function table
- base from member
- std::endl
- vector capacity
- increment operator
- placement new
- std::cout
- new&delete
- C++
- virtual destructor
- 더 지니어스 양면포커
- discord bot
- member function pointer
- constructor
- vector size
- dynamic_cast
- virtual function
- virtual inheritance
- pointer to member data
- return by reference
- this call
- conversion constructor
- std::vector
- diamond inheritance
- c++ multi chatting room
- c++ basic practice
- std::ostream
- delete function
- suffix return type
- Today
- Total
I'm FanJae.
[C++] Abstract class, Interface 본문
※ 본 포스트는 코드누리 C++ Basic 강의 내용을 보고 정리한 포스트입니다.
1. 추상 클래스(Abstract class)
class Shape
{
public:
virtual ~Shape() {}
virtual void draw() = 0;
};
int main()
{
Shape s; // error
Shape* p; // ok
}
- draw() 처럼 구현이 없고, =0 으로 끝나는 가상 함수를 Pure Virtual Function(순수 가상 함수)라고 한다.
- 이러한 순수 가상함수가 한 개 이상 있는 클래스를 추상 클래스라고 한다.
1-1. 추상 클래스의 특징
- 객체를 생성할 수 없다.
- 포인터 변수는 만들 수 있다.
1-2. 추상 클래스로 부터 파생된 클래스
class Shape
{
public:
virtual ~Shape() {}
virtual void draw() = 0;
};
class Rect : public Shape
{
public:
};
int main()
{
Rect r; // ??
}
- 위와 같은 경우에는 Shape와 동일하게 객체 생성이 불가능하다.
- 기반 클래스(추상 클래스)가 가진 순수 가상함수의 구현부를 제공하지 않으면 역시 추상 클래스이다.
- 객체를 생성할 수 있게 하려면 반드시 순수 가상함수를 override 해서 구현부를 제공해야 한다.
class Rect : public Shape
{
public:
virtual void draw() {}
};
- 즉, 추상 클래스의 의도는 특정 멤버 함수를 반드시 만들라고 지시하기 위한 것이다.
1-3. 순수 가상함수와 가상 함수의 차이점
#include <iostream>
#include <vector>
class Shape
{
virtual ~Shape() {}
virtual void draw() { std::println("draw Shape");
}
class Rect : public Shape
{
public:
void draw() { std::cout << "draw rect" << std::endl;}
}
class Circle : public Shape
{
public:
void draw() { std::cout << "draw circle" << std::endl;}
}
int main()
{
Rect r;
r.draw();
}
- 도형이라는 것을 그릴 수 있을까?
- 사각형(Rect)와 원(Circle)은 실제로 존재하는 개념이므로, 그릴 수 있다.
- 하지만, 도형(Shape)는 추상적인 개념으로 그리는 것은 불가능하다. 논리적으로 이것이 맞지 않다.
① Draw가 가상함수일때 이런 문제가 발생한다.
- Rect가 draw를 Override 하지 않는다면, Shape의 기본 구현을 물려 받는다.
- 하지만 논리적으로 이것이 맞는 동작일까? -> 절대 그렇지 않다는 것이다.
※ 즉, 가상 함수와 순수 가상 함수의 차이는 파생 클래스가 재정의를 해야 하는가 여부에 따라 갈린다.
#include <iostream>
#include <vector>
class Shape
{
virtual ~Shape() {}
// virtual void draw() { std::println("draw Shape");
virtual void draw() = 0;
}
class Rect : public Shape
{
public:
void draw() { std::cout << "draw rect" << std::endl;}
}
class Circle : public Shape
{
public:
void draw() { std::cout << "draw circle" << std::endl;}
}
int main()
{
Rect r;
r.draw();
}
2. Interface
#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);
}
- 초기에 People 클래스에서 사람이 Camera를 사용하는 기능이 있었다고 가정하자.
- 근데 시간이 지나서 HDCaemra가 추가되면 다음과 같이 처리할 수 있다.
#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);
}
- 문제는 새롭게 추가되는 요소 때문에 기존 요소(People)의 코드를 수정하는 것은 좋지 않다.
※ 개방-폐쇄 원칙(OCP, Open-Closed Principle)
- 소프트웨어 개체(클래스, 모듈, 함수등)은 확장에 대해 열려있어야 하고 수정에 대해서는 닫혀 있어야 한다.
- 즉, 새로운 요소가 추가되어도 기존 요소가 변경되지 않도록 설계해야 한다는 원칙이다.
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);
}
- 이 코드는 OCP를 지킬 수 없게 설계되어있다.
- 새로운 카메라가 등장하면 항상 people의 수정을 진행해야 하기 때문이다.
① 해결방법
- 카메라 사용자와 제작자 사이에 지켜야 하는 규칙을 설계한다. -> 추상 클래스를 사용한다.
- 카메라 사용자(People) -> 특정 제품이 아닌 규칙의 이름(추상 클래스)만 사용한다.
- 다양한 카메라 제품 -> 반드시 규칙을 지켜야 한다. 규칙을 담은 추상 클래스로부터 파생되어야 한다.
#include <iostream>
class ICamera
{
public:
virtual ~ICamera() { }
virtual void take() = 0;
};
class People
{
public:
// 객체는 못만들어도 포인터는 사용 가능
// 특정한 제품이 아닌 규칙대로 쓸 것임을 명시 가능.
void useCamera(ICamera* p) { p->take(); }
};
int main()
{
}
- 이와 같이 구현하면, 추후에 새로운 카메라가 추가되더라도 사용하는데 무리가 없다.
#include <iostream>
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;
}
};
class UHDCamera : public ICamera
{
public:
void take()
{
std::cout << "take picture UHD" << std::endl;
}
};
class People
{
public:
void useCamera(ICamera* p) { p->take(); }
};
int main()
{
People p;
Camera c;
p.useCamera(&c);
HDCamera hd;
p.useCamera(&hd);
UHDCamera uhd;
p.use_camera(&uhd);
}
- 이런식으로 만들어주면, People 코드는 수정되지 않아도 된다.
② 추상 클래스 vs 인터페이스
인터페이스 : 지켜야 하는 규칙(순수 가상 함수)만 있는것
추상클래스 : 지켜야 하는 규칙 + 다른 멤버도 있는 경우
class ICamera
{
public:
virtual ~ICamera() {}
virtual void take() = 0;
};
- 보통은 이쪽을 인터페이스라고 한다.
class ICamera
{
int color;
public:
void foo() {}
virtual ~ICamera() {}
virtual void take() = 0;
};
- 이쪽이 추상클래스이다. 지켜야 하는 규칙도 있지만, 상속으로 그외 물려주는 멤버 데이터,함수도 있기 때문이다.
※ C++은 추상 클래스 문법만 존재한다. Java, C# 등에서는 abstract, interface 라는 별도의 키워드가 존재한다.
③ 인터페이스를 만들때 유의 사항
class ICamera
{
virtual ~ICamera() {}
virtual void take() = 0;
};
- 인터페이스를 만들 때 결국 기반 클래스로 사용되므로 반드시 가상 소멸자를 사용한다.
- class 대신 struct를 사용하는 경우도 많다.
- 초기 People이 Camera를 사용할 때는 Camera 라는 객체 정보가 반드시 필요했다.
- 이처럼, 객체가 다른 객체와 강하게 결합되어 있는 것을 강한 결합(Tightly coupling)이라고 한다.
- 교체가 불가능하고 확정성 없는 경직된 디자인이다.
- 이와 달리, ICamera로 바꾼 이후의 People은 ICamera를 사용하기 때문에 추후 Camera 이외 다른 객체를 사용하더라도, 이 정보를 알 필요가 없다.
- 이처럼 객체가 다른 객체와 약하게 결합되어 있는 것을 약한 결합(Loosely coupling)이라고 한다.
- 이는 보통 인터페이스를 사용하는 것을 의미한다.
- 교체가 가능하고 확장성 있는 유연한 디자인이 특징이다.
'C++ > Basic' 카테고리의 다른 글
[C++] RTTI, Dynamic Cast (0) | 2024.08.22 |
---|---|
[C++] Virtual Function Table (0) | 2024.08.22 |
[C++] Upcasting, Virtual Function (0) | 2024.08.21 |
[C++] Inheritance(상속) (0) | 2024.08.20 |
[C++] Copy Constructor (0) | 2024.08.19 |