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