은닉성
지난 포스팅에서 객체 지향의 3대장중 상속성에 대해 알아 봤다.
이번 포스팅에서는 은닉성 다형성 등에 대해 알아보자.
은닉성이란 캡슐화라는 단어로 많이 쓴다. Data Hiding으로 몰라도 되는 것은 깔끔하게 숨기겠다. 라는 의미이다
왜 숨길까??
정말 위험하고 건드리면 안되는 경우와 다른 경로로 접근하길 원하는 경우에 사용한다.
자동차를 설계한다고 해보자.
- 핸들
- 페달
- 엔진
- 문
- 각종 전기선
- 등등
개발자 입장에선 위를 고려해야한다.
하지만 일반 구매자 입장에서 사용하는 것은
- 핸들
- 페달
- 문
이정도이다. 나머지 엔진이나 각종 전기선은 오히려 건드리면 큰일이 나곤한다.
#include <iostream>
using namespace std;
class Car
{
public:
void MoveHandle() {}
void PushPedal() {}
void OpenDoor() {}
void DisassembleCar() {} // 차를 분해한다.
void RunEngine() {} // 엔진을 구동시킨다.
void ConnectCircuit() {} // 전기선 연결
public:
// 핸들
// 페달
// 엔진
// 문
// 각종 전기선
};
int main()
{
Car car;
car.RunEngine();
return 0;
}
즉, Car 라는 클래스에서 사용자가 엔진을 구동시킬 수 있게 된다. 사용자는 이를 사용하면 안된다.
이를 제한을 둬야하는데 그게 바로 은닉성이다.
public , protected, private 3가지 키워드가 있다.
#include <iostream>
using namespace std;
// public : 누구한테나 공개. 누구든 사용가능
// protected : 가족(상속) 관계에만 공개
// private : 나만 사용 가능 << class 내부에서만(car) 가능
class Car
{
// 접근 지정자
public:
void MoveHandle() {}
void PushPedal() {}
void OpenDoor() {}
protected:
void DisassembleCar() {} // 차를 분해한다.
void RunEngine() {} // 엔진을 구동시킨다.
void ConnectCircuit() {} // 전기선 연결
private:
// 핸들
// 페달
// 엔진Q
// 문
// 각종 전기선
};
class SuperCar : public Car // 상속 접근 지정자
{
public:
};
int main()
{
Car car;
return 0;
}
다형성
다형성 Polymorphism = Poly + morph 겉은 똑같은데, 기능이 다르게 동작하는 것을 의미한다.
크게 두가지로 볼 수 있다.
오버로딩(Overloading) : 함수 중복 정의 = 함수 이름의 재사용
오버라이딩(Overriding) : 재정의 = 부모 클래스의 함수를 자식 클래스에서 재정의
#include <iostream>
using namespace std;
class Player
{
// 접근 지정자
public:
void MovePlayer() { cout << "Move Player!" << endl; }
void MovePlayer(int a) { cout << "Mover Player(int) ! " << endl; }
private:
int _hp;
};
int main()
{
Player p;
p.MovePlayer();
p.MoverPlayer(1);
return 0;
}
오버 로딩은 함수 이름은 같은데 매개변수가 달라서 함수 이름을 중복해서 사용가능 한 것을 말한다.
오버 라이딩을 하기 위해서는 정적 바인딩, 동적 바인딩에 대해 알아야 한다. 자세한 내용은 생략하고 동적 바인딩은 실행 시점에 호출 대상이 결정된다고 알고 있으면 된다.(그 반대인 정적 바인딩은 컴파일러가 자동으로 매칭을 해주는 것) 동적 바인딩을 원한다면 가상함수를 사용한다. virtual 키워드를 사용한다.
#include <iostream>
using namespace std;
class Player
{
// 접근 지정자
public:
virtual void Move() { cout << "Move Player!" << endl; }
private:
int _hp;
};
class Knight : public Player
{
public:
void Move() { cout << "Move Knight" << endl; }
public:
int _stamina;
};
class Mage : public Player
{
public:
public:
int _mp;
};
void Move(Player* player)
{
player->Move();
}
int main()
{
Knight k;
Move(&k);
return 0;
}
이렇게 함수를 재정의 해서 원하는 동작을 할 수 있게 만들어 줄 수 있다.
그렇다면 void Move(Plyaer* player) 함수에서 우리는 매개변수 인자로 Knight를 넣어줬는데 어떻게 컴파일러가 부모의 Move가 아닌 자식의 Move 호출할 수 있는 것일까??
그 이유는 바로 vftable, 가상 함수 테이블 덕분이다.
가상 함수 테이블은 class의 객체가 생성될 때, 생성자가 호출 되기전 만들게 된다. 이 테이블 안에는 포인터가 저장되어 있으며, 그 포인터는 어떤 함수를 실행해야하는지에 대한 주소가 담겨 있다. 예를 들어, 지금 같은 상황에서는 Knight의 Move를 실행하라 라는 주소의 위치가 저장되어 있는 것이다.
정확히 따지면 Player의 vftable이 먼저 채워지고 Knight의 vftable이 덮어 씌어진 것이다. 그래서 Knight만 테이블에 넣어진 것처럼 보인다.
만약 가상함수가 2개 이상이면 테이블 역시 늘어나게 된다.
실제로 디스어셈블해서 확인해보길 바란다.
멤버 변수 초기화
멤버 변수를 왜 초기화 해야할까? 간단한 예시로 기사 클래스를 보자.
#include <iostream>
using namespace std;
class Knight
{
public:
void Move() { cout << "Move Knight" << endl; }
public:
int _hp;
};
int main()
{
Knight k;
cout << k._hp << endl;
return 0;
}
이런 식으로 코드를 작성하게 되면 컴파일 설정에 따라 에러나 오류가 발생할 수 있다. 그 이유는 knight의 hp 값이 무엇인지 알 수 없기 때문에 사용할 수 없게 되는 것이다. 만약 기본 생성자를 넣었다고 생각하면 hp가 메모리에 할당 될 것인데 그 값이 정리 되지 않아 cccccccc 등의 값으로 들어가 있어 이상한 값이 출력 될 것이다.
결론적으로 버그 예방을 할 수 있고 포인터 등의 주소값이 연루되어 있는 경우 초기화를 해야한다.
초기화 방법은 다양하다.
1. 생성자 내에서 처리
2. 초기화 리스트
3. C++11 문법
특이한 점이 생성자 내에서 처리하는 것과 초기화 리스트로 초기화 하는 것에서 일반 변수는 별 차이 없지만 멤버 타입이 클래스인 경우 차이가 난다.
'C++ > 기초' 카테고리의 다른 글
[C++] 동적 할당 (1) | 2023.11.28 |
---|---|
[C++] 객체 지향 - 3 (0) | 2023.11.27 |
[C++] 객체 지향 - 1 (3) | 2023.11.27 |
[C++] 포인터 - 4 (다중 포인터, 다차원 배열) (0) | 2023.11.26 |
[C++] 포인터 - 3 (배열) (1) | 2023.11.25 |