캡슐화
캡슐화는 데이터(멤버 변수) 와 그 데이터를 다루는 기능(멤버 함수) 을 하나의 클래스 안에 묶는 개념이다.
즉, 클래스의 멤버 변수를 변경하거나 출력하는 함수들을 모두 클래스 내부의 멤버 함수로 작성하는 방식이다.
예시
class Test
{
private:
int m_iX;
int m_iY;
public:
void Render();
void SetX_Y(int _iX, int _iY);
};
void Test::Render()
{
cout << m_iX << endl;
cout << m_iY << endl;
}
void Test::SetX_Y(int _iX, int _iY)
{
m_iX = _iX;
m_iY = _iY;
}
은닉화
은닉화는 멤버 변수를 외부에서 직접 접근하지 못하게 막고, 반드시 멤버 함수를 통해서만 접근하도록 제한하는 것이다.
즉, 데이터를 보호하고 잘못된 접근을 막기 위한 개념이다.
예시
class Test
{
private:
int m_iX;
int m_iY;
public:
void Render();
void SetX_Y(int _iX, int _iY);
};
위 코드에서 m_iX, m_iY는 private이므로 외부에서 직접 접근할 수 없다.
반드시 SetX_Y() 같은 멤버 함수를 통해 접근해야 한다.
접근 제어 지시자
클래스 멤버에 대한 접근 범위를 제어하는 키워드이다.
public
- 클래스 내부와 외부 모두 접근 가능
protected
- 클래스 내부와 자식 클래스에서 접근 가능
private
- 클래스 내부에서만 접근 가능
일반적인 사용 방식
-
클래스의 멤버 변수는
private으로 사용 -
클래스의 멤버 함수는
public으로 사용
상속성
상속은 기존 클래스의 기능과 데이터를 물려받아 새로운 클래스를 만드는 개념이다.
-
기존 클래스: 기반 클래스, 부모 클래스
-
새로 만든 클래스: 파생 클래스, 자식 클래스
예시
class CObj
{
public:
CObj() {}
~CObj() {}
protected:
};
class CPlayer : public CObj
{
public:
CPlayer() {}
~CPlayer() {}
private:
};
int main()
{
CPlayer Player;
return 0;
}
상속 시 주의점
파생 클래스를 선언할 때는 기반 클래스의 헤더 파일을 포함해야 한다.
자식 객체 생성 과정
자식 객체가 생성될 때의 순서는 다음과 같다.
-
메모리 할당
-
부모 생성자 호출
-
자식 생성자 호출
자식 객체 소멸 과정
자식 객체가 소멸될 때의 순서는 다음과 같다.
-
자식 소멸자 호출
-
부모 소멸자 호출
-
메모리 반환
부모와 자식에 같은 이름의 멤버가 있을 때
부모 클래스와 자식 클래스에 같은 이름의 변수가 있다면 이름이 겹치므로 구분해서 접근해야 한다.
예시
class CObj
{
public:
CObj() {}
~CObj() {}
protected:
int m_iA;
};
class CPlayer : public CObj
{
public:
CPlayer() {}
~CPlayer() {}
private:
int m_iA;
};
이 경우 자식 클래스의 m_iA와 부모 클래스의 m_iA가 동시에 존재한다.
부모 클래스의 멤버에 접근하고 싶다면 범위 지정 연산자 :: 를 사용해서 구분할 수 있다.
현재 객체 자신의 멤버에 접근할 때는 this 포인터를 통해 판단할 수도 있다.
is-a 관계
상속 관계는 보통 is-a 관계 로 표현한다.
즉, 자식 클래스는 부모 클래스의 한 종류이다.
예시
-
사람 - 남자
-
동물 - 강아지
즉, 남자는 사람이다, 강아지는 동물이다 와 같이 표현할 수 있다.
자식 클래스로 갈수록 특성이 더 구체화된다.
has-a 관계
has-a 관계는 소유 또는 포함 관계 를 의미한다.
즉, 어떤 객체가 다른 객체를 가지고 있는 관계이다.
예시
-
플레이어가 무기를 가진다
-
플레이어가 방어구를 가진다
이런 관계는 상속보다는 멤버 변수로 포함하는 방식 으로 표현하는 것이 적절하다.
객체 포인터 권한
부모 타입 포인터는 자식 객체를 가리킬 수 있다.
하지만 포인터의 타입 기준으로 접근 가능한 멤버가 결정된다.
예시
class CObj
{
public:
void Render();
};
class CPlayer : public CObj
{
public:
void Render_Player();
};
int main()
{
CObj* pPlayer = new CPlayer;
pPlayer->Render(); // 가능
pPlayer->Render_Player(); // 불가능
}
이유
생성된 객체는 CPlayer이지만, 포인터의 타입이 CObj* 이기 때문에 컴파일러는 CObj에 정의된 멤버만 사용할 수 있다고 판단한다.
즉, CPlayer에만 존재하는 Render_Player()는 호출할 수 없다.
정적 바인딩
정적 바인딩은 컴파일 타임에 어떤 함수를 호출할지 이미 결정되는 것을 말한다.
즉, 호출할 함수가 포인터 타입이나 변수 타입을 기준으로 미리 정해진다.
위 예제에서 CObj* pPlayer를 통해 접근하면 CObj 기준으로만 함수 호출이 결정되는 것도 정적 바인딩의 예이다.
상속의 이점
상속을 사용하면 공통된 기능을 부모 클래스에 모아둘 수 있다.
그래서 여러 객체들을 다음과 같은 방식으로 쉽게 관리할 수 있다.
-
중복 코드 감소
-
공통 기능 재사용
-
일관된 구조 유지
-
부모 타입으로 여러 자식 객체를 묶어서 관리 가능
다형성
다형성은 하나의 부모 타입으로 여러 자식 객체를 다룰 수 있는 성질이다.
같은 함수 호출이라도 실제 객체가 무엇인지에 따라 다른 동작을 하게 만들 수 있다.
다형성과 관련해서 자주 연결되는 개념은 다음과 같다.
정리
공통적인 기능이지만 세부적인 동작 방식이 다를 때는 오버라이딩 을 사용한다.
하나의 파생 클래스에서만 가지고 있는 고유한 기능이 필요할 때는 다운 캐스팅(dynamic cast) 을 사용한다.
즉, 다형성을 사용할 때는 다음 기준으로 생각하면 된다.
-
공통 기능의 동작 차이 → 오버라이딩
-
특정 자식만 가진 기능 사용 → 다운 캐스팅
한 줄 요약
-
캡슐화: 데이터와 기능을 하나로 묶는다.
-
은닉화: 데이터를 외부에서 직접 접근하지 못하게 막는다.
-
상속: 기존 클래스를 기반으로 새로운 클래스를 만든다.
-
다형성: 부모 타입으로 여러 자식 객체를 다룬다.