OOP
OOP(Object-Oriented Programming)에 대하여
OOP의 특징
‘객체 지향 프로그래밍‘으로
데이터와 그 데이터를 다루는 동작(함수)를 하나의
객체로 묶어 설계하는 방식
장점
- OOP는 ‘현실’에 존재하는 특정한 물체 표현에
적합하기에 직관적임
- 예를 들어 물뿌리개 객체를 만든다면
- 데이터 : 현재 물의 양, 뿌리는 양, 총 물의 양 등을 정의
- 동작 : 물 채우기, 물 뿌리기 등을 정의
- 데이터 : 현재 물의 양, 뿌리는 양, 총 물의 양 등을 정의
- 예를 들어 물뿌리개 객체를 만든다면
- 잘 설계된 OOP는 유지보수와 확장성을 모두 챙길 수 있음
- ‘플레이어’가 ‘도구’를 사용할 수 있을 때
- 그 ‘도구’ 클래스를 상속 받은 후,
새로이 ‘망치’ 클래스를 제작하면
‘플레이어’와 ‘도구’ 클래스는 건들지 않고
신규 기능 추가 가능
- 그 ‘도구’ 클래스를 상속 받은 후,
- ‘플레이어’가 ‘도구’를 사용할 수 있을 때
단점
- 설계가 복잡할 수 있음
- 물뿌리기는 통과 스프레이 부분을 나눠서 설계해야 하나??
- 뿌리는 대상을 어떻게 정의할까?
- 물도 하나의 객체로 정의해야 하나?
- 물뿌리기는 통과 스프레이 부분을 나눠서 설계해야 하나??
- 추상화의 성능 오버헤드
- 함수 호출 역시 기본적으론 추가 명령
- 동적 바인딩 & 간접 참조 등의 비우호적인 캐시 구조
- 함수 호출 역시 기본적으론 추가 명령
- 잘못된 설계의 경우는 오히려 디버깅 난이도의 증가
- 어떤 ‘클래스/함수’에서 호출하는지 추적이 어려운 경우 등
유지보수와 직관성이 떨어지기에 잘못 쓰면 안쓰는 것보다 못함
- 어떤 ‘클래스/함수’에서 호출하는지 추적이 어려운 경우 등
OOP의 핵심 요소
- 추상화
- 중요한 것만 외부에 노출, 세부 구현은 숨김
- 구현의 복잡성을 줄어들게 함
- 순수 가상 함수, Interface 스타일을 통해 추구
- 중요한 것만 외부에 노출, 세부 구현은 숨김
- 캡슐화
- 데이터와 행동을 묶으며, 외부 접근 제한
- 내부 구조 은닉 및 객체 상태 보호
- 접근 제어자, Const, Friend 키워드를 C++이 지원
- 데이터와 행동을 묶으며, 외부 접근 제한
- 상속
- 기존 클래스를 기반으로 신규 클래스를 정의해 기능 확장
- 중복 코드를 제거하여 코드 재사용성 증가
- 상속 기능 및 상속 지정자 를 C++이 지원
- 기존 클래스를 기반으로 신규 클래스를 정의해 기능 확장
- 다형성
- ‘같은’ 호출이지만 동작은 다르게 구현
- 자식 클래스들의 유연성을 증가
- Virtual, Override, Overloading 등의 기능 존재
- ‘같은’ 호출이지만 동작은 다르게 구현
C++에서 OOP를 지원하는 기능 및 개념
-
- 클래스
- 객체의 설계도
- 실제 객체를 생성하기 위한 ‘틀’ 같은 개념으로 인식
- C++에서 class 키워드를 기반으로 선언
(Class vs Struct?)
- 클래스
-
- 객체
- 클래스를 기반으로 생성된 것
(실제 메모리에 올라간 것)
- New 키워드 등을 통해 Heap 영역에 할당되거나
멤버, 지역 변수 등 다양한 곳에서 쓰인다
(New Vs Malloc?)
- 객체
- 접근 제어자
- Public : 누구나 접근 가능
- Protected : 자식 클래스는 접근 가능
- Private : 해당 클래스만 접근 가능
- 다만 예외로 Friend 선언한 클래스는
private 키워드가 선언된 곳도 접근이 가능하다
(그렇기에 Friend 키워드를 캡슐화를 망칠 수 있는 요소라 부르기도?)
- 다만 예외로 Friend 선언한 클래스는
- Public : 누구나 접근 가능
- 상속
- 상속 지정자를 통해
부모 클래스의 기능을 이어 받음 - 아래 코드의 예시로, Derived 클래스는 Base의 모든 기능을 포함한다
- 상속 지정자를 통해 여러 옵션이 존재
- public : 부모의 접근 제어자를 그대로 받음
- protected : 부모의 public을 protected로 변경하여 받음
- private : 부모의 모든 요소를 private로 변경하여 받음
- public : 부모의 접근 제어자를 그대로 받음
- 상속 지정자를 통해
class Base { };
class Derived : public Base {
};
- Virtual 키워드
- 가상 함수를 선언하는 키워드
- 해당 키워드 사용한 함수를 포함하는 객체는
‘가상 함수 테이블’을 포함하여 생성됨
(그래서 크기가 약간 늘어남) - Virtual 이 없으면 ‘정적 바인딩’
있으면 ‘동적 바인딩’이라 부를 수 있음
- 가상 함수를 선언하는 키워드
- override 키워드
- 가상 함수를 ‘새로 정의’하는 용도의 키워드
- 다만, 실제로 동작을 하기 보다는 가독성의 측면에 가까운 편
(없어도 똑같이 동작은 되지만, 있으면 ‘상속 받은 가상 함수’라는 가독성이 생김) - 또한 virtual 함수가 아니라면 컴파일 오류를 발생시켜주기에 안정성이 높아짐
- 가상 함수를 ‘새로 정의’하는 용도의 키워드
- 동적 & 정적 바인딩?
- 정적 바인딩 : 컴파일 타임에 호출할 함수 결정
(보통 캐스팅한 포인터 타입에 따라 함수를 호출함) - 동적 바인딩 : 런타임에 실제 객체 타입 확인 후, 호출 함수 결정
(동적 바인딩이 vtable을 사용)
- 정적 바인딩 : 컴파일 타임에 호출할 함수 결정
- 순수 가상 함수
- 구현이 없는 가상 함수
(ex : virtual a() = 0;)
(=0 부분을 매크로 등으로 선언하여 사용하기도 함) - 자식 클래스에서 오버라이딩을 강제
(구현하지 않을 경우, 그 클래스도추상 클래스가 된다) - 인터페이스 등에서 자주 사용하는 방식으로
특정 기능에 대한 구현을 요구하는 용도로 응용 가능
- 구현이 없는 가상 함수
- 추상 클래스
- 하나 이상의 ‘순수 가상 함수’를 포함하는 클래스
- 인스턴스 생성이 불가함
(생성 시도시, 컴파일 에러 발생)
(C2259 - 추상 클래스를 인스턴스화할 수 없음!)
- 하나 이상의 ‘순수 가상 함수’를 포함하는 클래스
// 인터페이스 형식
class IDamagable
{
public:
virtual void Damage() = 0;
}
class Actor : public IDamagable
{
// Damage 미 구현시 Actor 클래스는 인스턴스화 불가 -> 구현 강제를 통한 기능 부여
}
- 오버로딩
- 같은 이름의 함수/연산자를 매개변수를 다르게 하여 ‘여러 개’ 만드는 것
- 리턴 타입만 다른 경우는 생성 불가
- 같은 이름의 함수/연산자를 매개변수를 다르게 하여 ‘여러 개’ 만드는 것
void Print(int x);
void Print(double x);
void Print(const string& x);
void Print(int x, int y);
int Foo();
double Foo(); // ❌ 리턴 타입만 다르면 구분 안 됨
- friend 키워드
- 특정 함수/클래스가 해당 클래스의 private/protected 영역에 접근 가능하도록 함
- 특별한 예외 처리이며, 강력하지만 동시에 OOP 원칙을 해치기에 유의하여 사용해야함
- 연산자 오버로딩 or ‘밀접’한 관계를 만들때 사용 가능
- ‘정말 필요한 경우’에만 사용을 권장하는 편
- 연산자 오버로딩 or ‘밀접’한 관계를 만들때 사용 가능
- 특정 함수/클래스가 해당 클래스의 private/protected 영역에 접근 가능하도록 함
class A {
private:
int secret = 10;
friend void Debug(A& a); // 전역 함수
friend class B; // B 클래스 전체
friend void C::Print(A&); // C 클래스의 일부 함수
};
//
void Debug(A& a) {
cout << a.secret; // ✔ 접근 가능!
}
class B{
void Test(A a)
{
a.secret += 50;// ✔ 접근 가능!
}
}
TMI - 소멸자에 Virtual을 붙여야 하는 이유?
class Base {
public:
~Base() { cout << "Base dtor\n"; }
};
class Derived : public Base {
public:
~Derived() { cout << "Derived dtor\n"; }
};
Base* ptr = new Derived();
delete ptr;
-
다음과 같은 경우
상속받은 Derived의 소멸자는 호출되지 않음! - 업캐스팅한 경우, 쉽게 발생 가능
- 업캐스팅 : 부모 클래스의 포인터로 자식 객체를 가리킴
- Derived* 로 참조했다면, 양 쪽 모두 호출됨
- 업캐스팅 : 부모 클래스의 포인터로 자식 객체를 가리킴
- 생성자는 부모 - 자식 순 호출
소멸자는 자식 - 부모 순
댓글남기기