[C++] 2.2 상속으로 깔끔하게

Date:     Updated:

카테고리:

태그:

인프런에 있는 홍정모 교수님의 홍정모의 게임 만들기 연습 문제 패키지 강의를 듣고 정리한 필기입니다.😀
🌜 공부에 사용된 홍정모 교수님의 코드들 보러가기
🌜 [홍정모의 게임 만들기 연습 문제 패키지] 강의 들으러 가기!


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도 둘 다 색깔과 위치 값을 객체마다 다양하게 설정하고 싶을테니 좀 더 일반화 하여 다양한 삼각형과 원을 그릴 수 있도록 다양한 값으로 설정될 수 있도록 멤버 변수 로 만들었다.

image

📜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클래스.

image

📜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;
}

TriangleCircle는 함수 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개 가지는게 된다.
      1. 상속받은 draw()
      2. 각자의 고유한 부분을 담은 drawGeometry()
  • 그러나 아래코드로 그대로 GeometricObject 클래스로 옮기면 drawGeometry()를 찾을 수 없는 오류가 생긴다.
    void draw()
    {
        beginTransformation();
        {
            translate(pos);
            drawGeometry();
        }
        endTransformation();
    }
    
  • drawGeometry()Geometric에 없는 Circle과 Triangle 각자의 고유한 함수이기 때문이다.
    • 그래서 GeometricObject 클래스에 형식적으로 아무 일도 하지 않는 빈 함수 drawGeometry()를 만든다.
      void drawGeometry()
      {
        // 아무일도 x
      }
      
  • 위와 같이 GeometricObject에 빈 함수 drawGeometry()를 만들어 주면 오류는 없어진다.
    • 그러나 각자 고유의 drawGeometry() 가 아닌 Geometric의 빈 함수 drawGeometry() 를 실행하게 되어 아무것도 그려지지 않는 결과가 나타난다.
    • drawFilledCircle 를 실행하는 Circle의 drawGeometry()가 실행되는 것이 아닌 부모인 빈 함수 drawGeometry()가 실행되는 것.
      • 이것이 Java와의 차이점이다.
        • Java에서는 자식에게 동일한 함수가 있다면 무조건 자식 함수를 우선적으로 오버라이딩하여 실행시키지만
        • C++에서는 자식에게 동일한 함수가 있어도 우선적으로 부모의 함수를 실행시킨다. ✨✨✨
          • virtual을 붙여 가상함수로 만들어 자식 함수가 호출되도록 해준다. 👉🏻 가상함수
          • C++에서는 virtual이 붙어야지만 오버라이딩을 한다.

가상 함수

  • 자식 클래스에서 오버라이딩 할것으로 기대하는 멤버 함수
  • 자식들마다 고유하게, 다르게 실행되야 하는 부분이 있다면 그 부분을 부모 클래스의 ✨가상 함수✨로 만들어서 자식들이 오버라이딩 하게 한다.

Java와 C++의 차이점

  • C++
    • 부모를 우선적으로 호출
      • 가상 함수가 아니라면 자식에게 동일한 함수가 있어도 오버라이딩 되지 않고 부모 함수를 호출한다.
      • 부모형 포인터로 접근시 부모의 멤버, 함수를 디폴트로 호출한다.
        • 자식형 포인터로 접근하면 오버라이딩 되고 상속받은 자식의 멤버, 함수가 호출되긴 하지만
        • my_tri.draw()
          • draw()는 부모인 GeometricObject의 함수이기 때문에 draw() 내부에 있는 drawGeometry()는 부모의 drawGeometry()가 호출된다.
            • drawGeometry()를 호출한 장본인이 부모형 타입이기 때문. draw()는 오로지 부모인 GeometricObject의 것이기 때문에.
        • 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 키워드

  1. virtual을 실수로 안 붙인다면?
    • 부모 클래스의 함수가 실행되는데 컴파일은 정상적으로 되니 문제를 찾기 힘들 수 있다.
  2. 오버라이딩 해야하는데 실수로 오타나 빠뜨린게 있다면?
    • 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
  • 해결 해야 할 문제
    • widthhegith을 부모인 GeometricObject에 넣자니 Tirangle, Circle같은 다른 자식 클래스들은 widthhegith을 쓰지도 않는데 상속받는게 되니까 메모리 낭비 문제가 생김
  • 해결 방법
    • 그냥 간단하게 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 만이 가지고 있는 sizeCircle, 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, heightCircle, 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;


🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기

댓글남기기