C++/C++ : Study

상속(1) - 예시

더블유제이플로어 2025. 3. 19. 04:11

Example

◆  상속이 필요한 아유

  ● Player 클래스

class Player
{
private:
	int x, y;
	int speed;
public:
	Player(int x, int y, int speed)
		: x{ x }, y{ y }, speed{ speed }
	{}
	void Move(int dx, int dy)
	{
		x += dx * speed;
		y += dy * speed;
	}
	void ShowPosition()
	{
		cout << x << "," << y << endl;
	}
};

상속에 대해서 알아보자.
상속이 왜 필요한지를 코드 기반으로 이해하는 시간을 가져보자.
지난 시간까지는 클래스에 대하여 공부를 하였다.
(상기) 코드를 보게 되면 어떤 클래스를 만들었구나
Player라는 자료형이 생겼고, 그걸 기반으로 객체를 만들었다.
int x, y, speed를 가지고 있고, private라서 외부에서 접근할 수 없다.
그리고 접근 가능한 public 영역을 보니깐
생성자가 하나가 있고, Move() , ShowPosition()이라는 멤버 함수를 가지고 있다.

생성자에 인자를 세 개를 받도록 되어 있기 때문에
이 객체를 만들려면 int 인자 3개를 넘어줘야 하고
넘어온 인자들은 각각 멤버 변수인 x, y, speed로 저장이 된다.
인자들은 이름이 멤버 변수와 반드시 같을 필요가 없다.
그리고 멤버 리스트 초기화를 했구나를 알 수 있다.

그러면은 플레이어 개체들을 얼마든지 만들 수 있었다.
p1, p2, p3 만들 수 있었다.
p1, p2, p3 에서는 각각 서로 다른 x, y, speed 데이터를 가질 수 있다.


◆  상속이 필요한 아유

   Player 관리를 위한 클래스
        ☞ 컨트롤 / 매니저 클래스라 부름

class PlayerHandler
{
private:
	Player* playerList[50];
	int playerNum;
public:
	PlayerHandler() : playerNum{ 0 } {}
	void AddPlayer(Player* p)
	{
		playerList[playerNum++] = p;
	}
	void ShowAllPlayerPosition() const
	{
		for (int i = 0; i < playerNum; i++)
		{
			playerList[i]->ShowPosition();
		}
	}
	~PlayerHandler()
	{
		for (int i = 0; i < playerNum; i++)
		{
			delete playerList[i];
		}
	}
};

게임을 만든다고 가정해보자.
Player가 사용자일 수 있고, 몬스터일 수 있다.
게임 내에 활동하는 플레이어를 관리하는 클래스를 만든다고 가정하자.
또 다른 클래스인 PlayerHandler 를 만든다고 가정하자.
이 클래스는 어떤 데이터를 가지고 있냐면 포인터의 배열을 가지고 있다.
Player* playerList[50]; 
50개의 칸을 가진 배열이다.
각각의 칸에는 Player의 포인터값이 들어간다.
그렇기에 칸에는 주소값들이 저장되는 배열이다.
실제 주소로 들어가게 되면 뭐가 있어야 하냐면 
x, y, speed 라는 것을 갖는 Player 객체가 있어야 된다는 것이다.

Player* playerList[50]; 의미는
메인 함수에서 만들어 놓은 모든 플레이어 객체의 주소값을 저장해 놓을 배열이다.
일반적인 배열이기 때문에 실제로 몇 개의 주소값이 들어가 있는지 알 수가 없기에
추적하기 위해서 playerNum이라는 데이터도 추가를 해두었다.

이 클래스는 또다른 클래스이기 때문에 이 클래스를 만들기 위한
생성자가 있어야 한다.
PlayerHandler 클래스 객체를 하나 만들면 playerNum을 일단 0으로 초기화하는
생성자가 있다.

AddPlayer(Player*) 멤버 함수가 있다.
여기다가 인자로 포인터 주소값을 넘겨주면 PlayerList 배열에 주소값을 추가하고
인덱스를 증가시킨다.
AddPlayer(Player*)를 추가할 때마다 PlayerList에 주소값이 추가로 생성되며
playerNum 이 하나씩 추가가 된다.

두 번째로 AddPlayer(Player*)를 또 생성하게 되면 
또 다른 주소값이 추가되게 되고, playerNum 이 2가 된다.

여기에 들어가는 주소값은 인자로 넘겨주고 있다.

그다음 멤버함수를 보면 ShowAllPlayerPosition() 함수가 있다.
지금 멤버 변수로 가지고 있는 수만큼 루프를 돌면서
playerList에 있는 i 번째의 ShowPosition()을 호출해 주는 호출이다.
ShowPosition() 은 class Player에 있으며
ShowPosition() 은 Player가 가지고 있는 x, y 위치를 콘솔에 출력해 주는 함수이다.

playerList [i] -> ShowPosition();
playerList[i] 즉, 주소값에 들어가게 되면 Player 객체가 있을 것이고, 
Player 객체는 ShowPosition() 멤버 함수를 가지고 있다.
그럼 x, y의 위치가 화면에 출력될 것이다.

두 번째 루프에서는 다음 playerList로 가서 다른 Player 객체의
x, y의 위치가 화면에 출력될 것이다.

루프를 돌다 보면 playerNum에 실제 Player 객체만큼 수가 저장되어 있기 때문에
데이터 갯수 만큼 ShowPosition() 값이 콘솔창에 출력될 것이다

마지막으로 소멸자부분에서는
만일 PlayerHandler 의 사용이 끝났다고 한다면
자동으로 호출이 된다.
playerNum 갯수만큼 포인터를 해제해 준다.
delete를 했다는 의미는
Player 객체들이 Heap 메모리에 저장이 되어있다는 이야기이다.


    main 함수

int main()
{
		PlayerHandler playerHandler;
		playerHandler.AddPlayer(new Player(1,1,1));
		playerHandler.AddPlayer(new Player(5,5,1));
		playerHandler.AddPlayer(new Player(2,3,1));
		playerHandler.ShowAllPlayerPosition();
}

/*
1,1
5,5
2,3
*/

main 함수에서는 Player 들의 저장된 포인터를 담을 수 있는
데이터를 포함하는 PlayerHandler 클래스를 하나 만들어 주고
여기 AddPlayer()에다가 Heap 메모리 플레이어 객체를 만들어서
주소값을 하나씩 넣어주면 된다.

그래서 어떤 플레이어는 1 , 1 위치에 있고
다른 플레이어는 5, 5 위치에 넣도록 객체를 생성해서
배열에 저장을 해뒀으며
PlayerHandler 클래스의 멤버변수인 배열에다가 저장하였다.

ShowAllPlayerPosition()을 하게 되면 루프를 돌면서
각각의 ShowAllPlayerPosition() 멤버 함수를 호출하게 되니깐
콘솔에 출력이 잘 되는것을 확인할 수 있다.


◆  상속이 필요한 이유, 시나리오

   ● Player 이외에 Enemy와 NPC가 추가된다면?
   ● Enemy와 NPC의 이동 방식이 다르다면?
        ☞ Enemy : dx * speed * 1.5f;
        ☞ NPC : 이동 불가능

● Enemy 및 NPC 클래스를 추가 구현했을 때, 컨트롤 클래스를 얼마나 수정해야 하는가?

상속이 필요한 이유를 시나리오를 가정을 해서 생각을 해보자
Player 클래스 하나로만 사용하였으나, 실제로 게임에서 만드는 상황을
구체적으로 생각을 해보자면
플레이어 캐릭터가 칼을 들고 있다고 가정해 보자
플레이어 캐릭터들이 있고, 몬스터 플레이어 캐릭터들도 있다고 가정하자.
또는 아이템을 사고 파는 NPC 같은 캐릭터들도 있을 수가 있다.

이 사용자가 조종하는 플레이어하고 몬스터 플레이어와 NPC 플레이어는
가지고 있는 데이터나 동작 방식이 다를 수 있다.

그렇기에 몬스터를 관리하기 위한 Enemy 클래스도 만들고
NPC를 관리하기 위한 NPC 클래스도 만들었다.
사용자가 조종하는 플레이어는 Player 클래스를 만들었다.

관리해야 하는 클래스가 3종류로 늘어났다.
그리고 몬스터 플레이어와 NPC의 이동 방식이 다르다면
Enemy는 x 만큼 움직이라고 했을때,  
Enemy : dx * speed * 1.5f;
NPC : 이동 불가능
구체적인 상황이 중요한게 아니라 방식이 다르다.
따로따로 클래스를 만들어준다.

원래 만들어두었던 PlayerHandler 클래스 방식은
플레이어만 관리하고 있었는데 이제는 GameHandler로 이름을 변경하여서
게임 안에 있는 플레이어, Enemy, NPC 들을 다 관리하도록 수정을 하고 싶다면
그러면 어떻게 코드로 수정해야 하나?

 


◆  상속이 필요한 이유, 시나리오 예상

Player* playerList [50];
Player 포인터를 저장하는 배열이다.
이 게임 안에 몬스터들이 있을 경우에는
Enemy* eList가 있어야 할 거고
NPC* nList도 따로 있어야 한다.
멤버 변수가 2개 추가해야 한다.

또한 int PlayerNum; 플레이어의 수를 저장하는 변수도
Enemy의 숫자, NPC의 숫자가 새로 만들어야 한다.
int 멤버 변수도 2개 추가해야 한다.

그다음에 AddPlayer(Player*)을 보면 인자로 Player 포인터로 받게 되어있다.
Enemy 포인터로 받는 멤버 함수,
NPC 포인터로 받는 멤버 함수가 또 추가가 되어야 한다.
멤버 함수도 2개 증가해야 한다.

플레이어, 몬스터, NPC의 위치를 한꺼번에 출력하려면
현재는 루프 속 playerList만 playerNum 개수만큼 돌고 있기 때문에
루프도 2개가 추가되어야 한다.

EnemyNum 만큼 루프가 돌면서 EnemyList의 ShowAllPlayerPosition() 호출하고
NPCNum 만큼 루프를 돌면서 NPCList의 ShowAllPlayerPosition() 호출한다.

그렇기에 for() 루프도 2개 추가 작성해야 한다.

마지막으로 소멸자 부분에서도 playerList 안에 있는 포인터들만 해제를 해줬으면 되었는데,
EnemyList, NPCList 모두 포인터들을 해제해줘야 하기에 추가 구현이 필요하다.

정리하자면
멤버 변수 추가가 필요하다.
        ☞ EnemyNum, NPCNum
클래스 별로 정보 추가 기능필요하다.
        ☞ AddPlayer(Enemy* e) , AddPlayer(NPC* N)
클래스 별 위치 정보에 대한 출력 반복문 필요하다.
        ☞ 반복문 2개 추가되어야 한다.
클래스 별 해제 필요까지 필요하다.
        ☞ delete 구문 2개 추가 되어야 한다.

어떤 기능을 추가할 때
관리하는 클래스에서 수정을 많이 해야 한다.
나중에 어떤 객체가 추가될지 알 수 없으며
여러 가지 상황들에서 코드를 많이 수정해야지만 제대로 동작하게 만들 수 있다.

코드를 작성하는 개발자, 프로그래머들은 굉장히 게으르다.
그래서 이렇게 (코드를 많이 수정해야 하는 상황을) 하기를 싫어한다.
어떤 기능을 추가할 때마다 코드를 많이 수정하고 싶지는 않다.
그러기 위해서 사용할 수 있는 기능이 바로 상속과 다형성이다.

상속과 다형성에는 많은 기능이 있는데
예제에서 보여주는 것은 상속과 다형성을 활용하게 되면 
이렇게 많은 코드 추가를 하지 않고도 Enemy와 NPC 등
기능을 추가해 줄 수 있다.

결국에는 상속과 다형성을 왜 사용을 하냐?
기능이 추가가 돼도 더 적은 코드만 수정해도 될 수 있게끔 하기 위해
소프트웨어를 설계하기 위해서 상속과 다형성을 활용한다.

상속과 다형성을 활용하게 되면은 조금의 수고만 들여도 프로그램을 수정하기 편리하다.


https://youtu.be/k_1qcpFjpms?si=W9UF2LS06EQxfy5u