[C++] 2.2 상속으로 깔끔하게
카테고리: C++ games
태그: Cpp Graphics OpenGL Programming
인프런에 있는 홍정모 교수님의 홍정모의 게임 만들기 연습 문제 패키지 강의를 듣고 정리한 필기입니다.😀 
🌜 공부에 사용된 홍정모 교수님의 코드들 보러가기 
🌜 [홍정모의 게임 만들기 연습 문제 패키지] 강의 들으러 가기!
Chapter 2. 객체 지향으로 가는 길 : 상속
🔔 상속
- 공통되는 요소를 한군데에 모아놓고 여러 객체들이 함께 사용할 때.
 - 코드를 매번 반복적으로 재구현할 필요 없이 재사용할 할 수 있다.
 
단순 코드 반복
아래 코드는 지저분하다. 😤 비슷하거나 공통된 코드들이 반복되고 있다.
📜 main
#include "Game2D.h"
#include "Examples/PrimitivesGallery.h"
#include "RandomNumberGenerator.h"
namespace jm
{
	class Example : public Game2D
	{
	public:
		Example()
			: Game2D()
		{}
		void update() override
		{
			/* 노란 삼각형 그리는 부분 */
			beginTransformation();
			{
				translate(vec2{ -0.5f, -0.05f });
				drawFilledTriangle(Colors::yellow, 0.3f);
			}
			endTransformation();
			 /* 빨간 원 그리는 부분 */
			beginTransformation();
			{
				drawFilledCircle(Colors::red, 0.15f);
			}
			endTransformation();
		}
	};
}
int main(void)
{
	jm::Example().run();
	//jm::Game2D().init("This is my digital canvas!", 1280, 960, false).run();
	//jm::PrimitivesGallery().run();
	return 0;
}
클래스로 깔끔하게 묶기
📜Triangle.h
#pragma once
#include "Game2D.h"
namespace jm
{
	class Triangle
	{
	public:
		void draw()
		{
			beginTransformation();
			{
				translate(vec2{ -0.5f, -0.05f }); //고정된 값
				drawFilledTriangle(Colors::yellow, 0.3f); //고정된 값
			}
			endTransformation();
		}
	};
}
📜Circle.h
#pragma once
#include "Game2D.h"
namespace jm
{
	class Circle
	{
	public:
		void draw()
		{
			beginTransformation();
			{
				drawFilledCircle(Colors::red, 0.15f); // 고정된값
			} // 딱 0.15 반지름의 원만 그릴 수 잇음
			endTransformation();
		}
	};
}
📜 main
이렇게 삼각형 기능과 동그라미 기능을 각각 클래스로 따로 묶고 객체를 생성하여 기능을 사용하니 아래와 같이 훨씬 깔끔해진 것을 볼 수 있다.
#include "Game2D.h"
#include "Examples/PrimitivesGallery.h"
#include "RandomNumberGenerator.h"
#include "Triangle.h" ⭐
#include "Circle.h" ⭐
namespace jm
{
	class Example : public Game2D
	{
	public:
		Triangle my_tri;
		Circle my_cir;
		Example()
			: Game2D()
		{}
		void update() override
		{
			my_tri.draw();
			my_cir.draw();
		}
	};
}
int main(void)
{
	jm::Example().run();
	return 0;
}
좀 더 일반화 해보기
void draw()의translate(vec2{-0.5f, -0.05f});- 딱 고정된 (- 0.5f, - 0.05f) 좌표로만 이동할 수 있다는 한계가 있다.
 
- Triangle도 Circle도 둘 다 색깔과 위치 값을 객체마다 다양하게 설정하고 싶을테니 좀 더 일반화 하여 다양한 삼각형과 원을 그릴 수 있도록 다양한 값으로 설정될 수 있도록 멤버 변수 로 만들었다.
 

📜Triangle.h
#pragma once
#include "Game2D.h"
namespace jm
{
	class Triangle
	{
	public:
		vec2 pos = vec2{ -0.5f, -0.05f };
		RGB color = Colors::yellow;
		float size = 0.3f;
		void draw()
		{
			beginTransformation();
			{
				translate(pos);
				drawFilledTriangle(color, size);
			}
			endTransformation();
		}
	};
}
📜Circle.h
#pragma once
#include "Game2D.h"
namespace jm
{
	class Circle
	{
	public:
		vec2 pos = vec2{ 0.0f, 0.0f };
		RGB color = Colors::red;
		float size = 0.15;
		void draw()
		{
			beginTransformation();
			{
				translate(pos);
				drawFilledCircle(color, size);
			}
			endTransformation();
		}
	};
}
📜 main
아래와 같이 멤버변수에 접근하여 원하는 다양한 값으로 설정할 수 있게 되었다. 생성자에서 객체의 멤버 변수를 설정해 주었음.
#include "Game2D.h"
#include "Examples/PrimitivesGallery.h"
#include "RandomNumberGenerator.h"
#include "Triangle.h" ⭐
#include "Circle.h" ⭐
namespace jm
{
	class Example : public Game2D
	{
	public:
		Triangle my_tri;
		Circle my_cir;
		Example()
			: Game2D() // 생성자
		{
			my_tri.color = Colors::gold;
			my_tri.pos = vec2{ -0.5f, 0.2f };
			my_tri.size = 0.25f;
			my_cir.color = Colors::olive;
			my_cir.pos = vec2{ 0.1f, 0.1f };
			my_cir.size = 0.2f;
		}
		void update() override
		{
			my_tri.draw();
			my_cir.draw();
		}
	};
}
int main(void)
{
	jm::Example().run();
	return 0;
}
상속 : Circle클래스와 Triangle클래스의 공통된 요소들을 묶어 부모 클래스로 만들기
Triangl클래스와Circle클래스의 공통된 요소- 👉🏻 멤버 변수들 : color, pos, size
 
- 공통된 부분은 몰아서 따로 묶는게 프로그래밍 효율을 높이는 길 !
 - Triangle 클래스와 Circle 클래스의 공통된 요소들을 묶어 만든 
GeometricObject클래스. 

📜GeometricObject.h
#pragma once
#include "Game2D.h"  // Game2D 상속
namespace jm
{
	class GeometricObject
	{
	public:
		vec2 pos;
		RGB color;
		float size;
	};
}
- Triangle 클래스와 Circle 클래스의 공통된 요소를 여기에!
    
- 멤버 변수 color, pos, size
 - 함수 draw
        
- 그리는 내용은 다르지만 어느 정도 공통된 부분이 있고 동일한 기능이다. 함수 묶는건 아래에 후술.
 
 
 - 그리고 Triangle과 Circle이 
GeometricObject을 상속하게 한다. - 공통적인 요소였던 color, pos, size 은 상속받아 사용 가능해졌다. 굳이 선언하지 않아도.
 
📜Triangle.h
#pragma once
// Game2D는 GeometricObject.h에서 이미 include 했으므로
// Game2D를 include할 필요 x 
#include "GeometricObject.h"  ⭐
namespace jm
{
	class Triangle : public GeometricObject // 상속
	{
	public:  // 멤버 변수 선언이 사라졌다. 이제 pos, color, size는 상속받기 때문.
		void draw()
		{
			beginTransformation();
			{
				translate(pos);
				drawFilledTriangle(color, size);
			}
			endTransformation();
		}
	};
}
📜Circle.h
#pragma once
#include "GeometricObject.h"
namespace jm
{
	class Circle : public GeometricObject // 상속
	{
	public: // 멤버 변수 선언이 사라졌다. 이제 pos, color, size는 상속받기 때문.
		void draw()
		{
			beginTransformation();
			{
				translate(pos);
				drawFilledCircle(color, size);
			}
			endTransformation();
		}
	};
}
- class Circle : public GeometricObject
    
- Circle 클래스는 GeometricObject 클래스를 상속 받는다는 의미
 
 - GeometricObject 클래스의 모든 변수와 함수를 그냥 사용할 수 있게 됐다.
 
부모클래스에 초기화 하는 함수를 만들어 깔끔하게 초기화 해보자.
아래와 같이 일일이 멤버 변수를 초기화 하는 방법은 너무 복잡하다.
my_tri.color = Colors::gold;
my_tri.pos = vec2{ -0.5f, 0.2f };
my_tri.size = 0.25f;
my_cir.color = Colors::olive;
my_cir.pos = vec2{ 0.1f, 0.1f };
my_cir.size = 0.2f;
부모 클래스에 초기화 하는 함수를 따로 만들면 더 깔끔하게 초기화 할 수 있다.
📜GeometricObject.h
#pragma once
#include "Game2D.h"
namespace jm
{
	class GeometricObject
	{
	public:
		vec2 pos;
		RGB color;
		float size;
		void init(const RGB& _color, const vec2& _pos, const float& _size)
		{
			color = _color;
			pos = _pos;
			size = _size;
		}
	};
}
- void init(const RGB& _color, const vec2& _pos, const float& _size)
    
- 멤버 변수들을 초기화 하는 함수
 
 
📜 main
#include "Game2D.h"
#include "Examples/PrimitivesGallery.h"
#include "RandomNumberGenerator.h"
#include "Triangle.h"
#include "Circle.h"
namespace jm
{
	class Example : public Game2D
	{
	public:
		Triangle my_tri;
		Circle my_cir;
		Example()
			: Game2D()
		{ // 더 깔끔하게 초기화 할 수 있게 되었다. 
			my_tri.init(Colors::gold, vec2{ -0.5f, 0.2f }, 0.25f);
			my_cir.init(Colors::olive, vec2{ 0.1f, 0.1f }, 0.2f);
		}
		void update() override
		{
			my_tri.draw();
			my_cir.draw();
		}
	};
}
int main(void)
{
	jm::Example().run();
	return 0;
}
Triangle과 Circle는 함수 void init을 상속받아 사용할 수 있다. 이 함수를 이용해 더 깔끔하게 초기화할 수 있게 되었다.
my_tri.init(Colors::gold, vec2{ -0.5f, 0.2f }, 0.25f);
my_cir.init(Colors::olive, vec2{ 0.1f, 0.1f }, 0.2f);
🔔 가상함수와 오버라이딩
동일한 기능을 가지는 두 함수의 공통된 요소 묶기
📜Circle 클래스의 draw() 함수
void draw()
{
	beginTransformation();
	{
		translate(pos);
		drawFilledCircle(color, size);
	}
	endTransformation();
}
📜Triangle 클래스의 draw() 함수
void draw()
{
	beginTransformation();
	{
		translate(pos);
		drawFilledTriangle(color, size);
	}
	endTransformation();
}
Circle의 draw()와 Triangle의 draw()의 공통된 부분과 고유한 부분
- circle의 draw() 中 고유한 부분
    
- drawFilledCircle(color, size);
 
 - triangle의 draw() 中 고유한 부분
    
- drawFilledTriangle(color, size);
 
 - 나머지 부분은 다 같다.
 - 고유한 부분만 각자의 고유한 함수로 빼버리고 공통된 부분들은 묶어버려 부모 클래스의 함수로 만들어버리자
 
void draw()
{
	beginTransformation();
	{
		translate(pos);
		drawGeometry(); // 각자의 고유한 부분
	}
	endTransformation();
}
void drawGeometry()  
{
	// drawFilledCircle(color, size); 👉🏻 Circle 클래스의 경우
    // drawFilledTriangle(color, size); 👉🏻 Triangle 클래스의 경우
}
오버라이딩
- Circle의 
drawGeometry()와 Triangle의drawGeometry()의 내용은 다르다. 이건 각자 클래스의 고유한 부분이다. - 고유한 부분을 따로 drawGeometry() 이라는 함수로 묶으면 두 클래스의 draw()가 완전히 동일해진다.
 - 따라서 
draw()함수를 GeometricObject 클래스로 옮길 수 있게 된다.- Circle과 Triangle은 Geometric을 상속받아 각자 이렇게 함수를 2개 가지는게 된다.
        
- 상속받은 draw()
 - 각자의 고유한 부분을 담은 drawGeometry()
 
 
 - Circle과 Triangle은 Geometric을 상속받아 각자 이렇게 함수를 2개 가지는게 된다.
        
 - 그러나 아래코드로 그대로 GeometricObject 클래스로 옮기면 
drawGeometry()를 찾을 수 없는 오류가 생긴다.void draw() { beginTransformation(); { translate(pos); drawGeometry(); } endTransformation(); } drawGeometry()는 Geometric에 없는 Circle과 Triangle 각자의 고유한 함수이기 때문이다.- 그래서 GeometricObject 클래스에 형식적으로 아무 일도 하지 않는 빈 함수 
drawGeometry()를 만든다.void drawGeometry() { // 아무일도 x } 
- 그래서 GeometricObject 클래스에 형식적으로 아무 일도 하지 않는 빈 함수 
 - 위와 같이 GeometricObject에 빈 함수 
drawGeometry()를 만들어 주면 오류는 없어진다.- 그러나 
각자 고유의 drawGeometry()가 아닌Geometric의 빈 함수 drawGeometry()를 실행하게 되어 아무것도 그려지지 않는 결과가 나타난다. - drawFilledCircle 를 실행하는 
Circle의 drawGeometry()가 실행되는 것이 아닌 부모인 빈 함수drawGeometry()가 실행되는 것.- 이것이 Java와의 차이점이다.
            
Java에서는 자식에게 동일한 함수가 있다면 무조건 자식 함수를 우선적으로 오버라이딩하여 실행시키지만C++에서는 자식에게 동일한 함수가 있어도 우선적으로 부모의 함수를 실행시킨다. ✨✨✨virtual을 붙여 가상함수로 만들어 자식 함수가 호출되도록 해준다. 👉🏻 가상함수C++에서는virtual이 붙어야지만 오버라이딩을 한다.
 
 - 이것이 Java와의 차이점이다.
            
 
 - 그러나 
 
가상 함수
- 자식 클래스에서 
오버라이딩할것으로 기대하는 멤버 함수 - 자식들마다 고유하게, 다르게 실행되야 하는 부분이 있다면 그 부분을 부모 클래스의 ✨가상 함수✨로 만들어서 자식들이 오버라이딩 하게 한다.
 
Java와 C++의 차이점
- C++
    
- 부모를 우선적으로 호출
        
- 가상 함수가 아니라면 자식에게 동일한 함수가 있어도 오버라이딩 되지 않고 부모 함수를 호출한다.
 - 부모형 포인터로 접근시 부모의 멤버, 함수를 디폴트로 호출한다.
            
- 자식형 포인터로 접근하면 오버라이딩 되고 상속받은 자식의 멤버, 함수가 호출되긴 하지만
 my_tri.draw()- draw()는 부모인 GeometricObject의 함수이기 때문에 draw() 내부에 있는 drawGeometry()는 부모의 drawGeometry()가 호출된다.
                    
- drawGeometry()를 호출한 장본인이 부모형 타입이기 때문. 
draw()는 오로지 부모인 
GeometricObject의 것이기 때문에. 
 - drawGeometry()를 호출한 장본인이 부모형 타입이기 때문. 
draw()는 오로지 부모인 
 
- draw()는 부모인 GeometricObject의 함수이기 때문에 draw() 내부에 있는 drawGeometry()는 부모의 drawGeometry()가 호출된다.
                    
 my_tri.drawGeometry()- drawGeometry()를 호출한게 my tri인데 이는 자식형 타입인 Triangle 이므로 자식인 Triangle의 고유한 drawGeometry()가 호출된다.
 
 
 
 - 부모를 우선적으로 호출
        
 - Java
    
- 가상함수가 디폴트다.
        
- 따라서 자식이 오버라이딩 하면 자식 함수를 무조건 우선적으로 호출한다.
 
 - 부모형 포인터로 접근하더라도 자식의 오버라이딩 된 함수를 디폴트로 호출한다.
 
 - 가상함수가 디폴트다.
        
 
📜GeometricObject.h
- draw()
 - drawGeometry()
    
- 가상함수이므로 오버라이딩 한 자식이 있다면 자식만의 drawGeometry()를 호출
 - 일부러 오버라이딩 하라고 빈 내용
 
 
#pragma once
#include "Game2D.h"
namespace jm
{
	class GeometricObject
	{
	public:
		vec2 pos;
		RGB color;
		float size;
		void init(const RGB& _color, const vec2& _pos, const float& _size)
		{
			color = _color;
			pos = _pos;
			size = _size;
		}
		virtual void drawGeometry() // 가상 함수
		{
			// 빈 내용
		}
		void draw()
		{
			beginTransformation();
			{
				translate(pos);
				drawGeometry();
			}
			endTransformation();
		}
	};
}
📜Circle.h
#pragma once
#include "GeometricObject.h"
namespace jm
{
	class Circle : public GeometricObject
	{
	public:
		void drawGeometry()  // 오버라이딩
		{
			drawFilledCircle(color, size);
		}
	};
}
📜Triangle.h
#pragma once
#include "GeometricObject.h"
namespace jm
{
	class Triangle : public GeometricObject 
	{
	public:
		void drawGeometry() // 오버라이딩
		{
			drawFilledTriangle(color, size);
		}
	};
}
안전 장치 : override 키워드
virtual을 실수로 안 붙인다면?- 부모 클래스의 함수가 실행되는데 컴파일은 정상적으로 되니 문제를 찾기 힘들 수 있다.
 
- 오버라이딩 해야하는데 실수로 오타나 빠뜨린게 있다면?
    
- void drawGeometry const () 이런 함수의 경우 뒤에 붙은 const까지 똑같이 해야 동일한 함수라고 여겨지며 성공적으로 오버라이딩 되는데
 - 만약 const를 빼먹었다면 오버라이딩 되지 않을 것이다.
 - 컴파일은 정상적으로 되니 문제를 찾기 힘들 수 있다.
 
 
override키워드는 이런 실수를 방지하는 안전장치 역할을 한다.
- 꼭 오버라이딩 되야 할 함수 바로 뒤에 
override를 붙여준다.void func() override- 어디 실수해서 오버라이딩이 안되면 컴파일 오류를 내어 알려준다.
        
virtual void drawGeometry() const // in 부모클래스 void drawGeometry() override // in 자식클래스- 위 코드와 같은 경우 컴파일 오류가 발생한다.
            
- const를 뒤에 붙이는 것을 깜빡해 오버라이딩이 제대로 되지 않았기 때문이다.
 override키워드를 붙여주었기 때문에 컴파일 오류로 알려준다. 이 키워드가 없었다면 그냥 본인만의 고유한 함수구나 하고 컴파일에 문제가 없었을 것. ***
 
 - 위 코드와 같은 경우 컴파일 오류가 발생한다.
            
 
 - 어디 실수해서 오버라이딩이 안되면 컴파일 오류를 내어 알려준다.
        
 
🔔 공통된 부분들에서 본인만의 다른 변수가 더 필요할 때
- GeometricObject의 자식으로 Box 클래스를 추가해보자.
    
- Box 클래스는 Circle, Triangle과 달리 너비와 높이가 필요하다.
        
- Circle, Triangle 👉🏻 
color,size - Box 👉🏻 
color,width,height 
 - Circle, Triangle 👉🏻 
 
 - Box 클래스는 Circle, Triangle과 달리 너비와 높이가 필요하다.
        
 - 해결 해야 할 문제
    
width와hegith을 부모인 GeometricObject에 넣자니 Tirangle, Circle같은 다른 자식 클래스들은width와hegith을 쓰지도 않는데 상속받는게 되니까 메모리 낭비 문제가 생김
 - 해결 방법
    
- 그냥 간단하게 
width,height는 Box클래스만의 변수로 두면 됨. 
 - 그냥 간단하게 
 - Box, Circle, Triangle 모든 자식들의 공동 멤버 변수 👉
pos,color- 부모클래스에서 모든 자식들이 공통으로 가지는 멤버 변수 
pos,color를 초기화하는 함수를 만든다. - 모든 자식들이 공통으로 가지는 멤버 변수 
pos,color는 부모클래스에서 선언한다. 
 - 부모클래스에서 모든 자식들이 공통으로 가지는 멤버 변수 
 - Circle, Triangle 👉 
size - Box 👉 
width,height- 모든 자식들이 다 공통으로 가지는 것은 아닌 어떤 자식들만 가지는 멤버 변수들은 각자의 자식클래스에서 선언한다.
 - 자식클래스 각각의 초기화 함수를 만들고  👉 
size👉width,height - 모든 자식들이 가지는 공통된 멤버 변수의 초기화는 부모클래스의 초기화 함수를 이용한다. 👉
pos,color- GeometricObject::init(_color, _pos);
 
 
 
📜GeometricObject.h
- 모든 자식들이 공통으로 가지고 있는 
pos,color를 초기화 시켜준다.- void init(const RGB& _color, const vec2& _pos)
 
 
#pragma once
#include "Game2D.h"
namespace jm
{
	class GeometricObject
	{
	public:
		vec2 pos; 
		RGB color; 
		void init(const RGB& _color, const vec2& _pos)
		{
			color = _color;
			pos = _pos;
		}
		virtual void drawGeometry() const
		{
			//
		}
		void draw()
		{
			beginTransformation();
			{
				translate(pos);
				drawGeometry();
			}
			endTransformation();
		}
	};
}
📜Circle.h, 📜Triangle.h
- 모든 자식들이 공통으로 가지고 있는 
pos,color는 부모 init을 호출해 초기화 하고- GeometricObject::init(_color, _pos);
 
 - Circle, Triangle 만이 가지고 있는 
size는 Circle, Triangle 만의 init으로 초기화 해준다.- size = _size;
 
 
#pragma once
#include "GeometricObject.h"
namespace jm
{
	class Circle : public GeometricObject
	{
	public:
		float size; ⭐
		void init(const RGB& _color, const vec2& _pos, const float& _size)
		{
			GeometricObject::init(_color, _pos); // color, pos는 부모 초기화 함수로
			size = _size; 
		}
		void drawGeometry() const override
		{
			drawFilledCircle(GeometricObject::color, this->size);
		}
	};
}
📜Box.h
- 얘는 init 함수에 매개변수 4개가 필요.
 - 모든 자식들이 공통으로 가지고 있는 
pos,color는 부모 init을 호출해 초기화 하고- GeometricObject::init(_color, _pos);
 
 - Box 만이 가지고 있는 
width,height는 Circle, Triangle 만의 init으로 초기화 해준다.- width = _width;, height = _height
 
 
#pragma once
#include "GeometricObject.h"
namespace jm
{
	class Box : public GeometricObject
	{
	public:
		float width, height; ⭐
		void init(const RGB& _color, const vec2& _pos, 
			const float& _width, const float& _height) 
		{
			GeometricObject::***init***(_color, _pos);
			/*
			GeometricObject::color = _color; // 사실 초기화 함수 안 쓰고 이렇게 해줘도 된다.
			GeometricObject::pos = _pos;
			*/
			width = _width;
			height = _height;
		}
		void drawGeometry() const override
		{
			drawFilledBox(Colors::blue,
				this->width, this->height);
		}
	};
}
📜main
Example()
			: Game2D()
		{
			my_tri.init(Colors::gold, vec2{ -0.5f, 0.2f }, 0.25f);
			my_cir.init(Colors::olive, vec2{ 0.1f, 0.1f }, 0.2f);
			my_box.init(Colors::black, vec2{ 0.0f, 0.5f }, 0.5f, 0.3f); ⭐⭐
		}
🔔 순수 가상 함수
그냥 일반 가상 함수 는 강제하는 느낌이 없다. ‘오버라이딩 한게 있으면 그거 실행해라. 없으면 내꺼 실행하구~’ 이런 느낌이다. 무조건 *모든 자식클래스가* 오버라이딩을 해야하는 함수라고 강제하고 싶을때 순수 가상 함수를 사용한다.
순수 가상 함수: 모든 자식 클래스가 오버라이딩 해야 하는 함수. 필수적으로 구현해야 하는 함수를 순수 가상 함수로 설정해주자.
문법
- 앞에 
virtual을 붙여준다. - 함수의 바디가 없으며
    
- 어차피 모든 자식들이 새로 구현해야 하므로 내용이 있을 필요가 없음
 
 - 뒤에 
= 0;을 붙여 준다. 
virtual void drawGeometry() const = 0;
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
      
    
댓글 남기기