virtual이 존재하는 클래스는 vfptr이라는 가상함수 포인터라는 것을 가지고 있다는 것을 알 수 있다.
그래서 FightUnit의 크기가 8바이트이다.
그런데 virtual 함수가 여러 개여도 8바이트이다.
클래스에 virtual 함수가 여러 개면 생성될 때 생성자에서 자동으로 함수 포인터의 배열로 만들고, 그 배열의 포인터를 들고 있기 때문이다.
void(**vfptr)()
가 된다. 그래서 virtual 함수가 여러 개여도 클래스의 크기는 8바이트가 된다. 그리고 그 함수 포인터의 배열이 가상함수 테이블이다.
// 내부적으로 이런 느낌
void(*vfptrArr[2])()
=
{
FightUnitStatusRender,
FightUnitDamage
}
void(**vfptr)() = vfptrArr;
만약 FightUnitDamage()함수를 사용하면 해당 코드가 vfptr[1]()
로 대체된다.
만약 자식 클래스에서 virtual 함수를 오버라이드하면 자식 클래스의 생성자가 실행될 때 그 함수가 있던 vfptr[1]
을 자식의 함수로 바꾼다.
// 자식 클래스의 함수로 변경
void(*vfptrArr[2])()
=
{
FightUnitStatusRender,
PlayerDamage
}
이것들의 메모리맵을 보면 이해에 도움이 된다.
~~
정리)
1. 가상함수 테이블이란 함수 포인터의 배열의 첫번째 주소이다.
2. 함수 2중 포인터라고 할 수 있다.
3. 생성자에서 가상함수 테이블의 값들을 채워준다.
사용자 정의 자료형이다. 정수형 상수를 정의하는 자료형이다.
job
0 전사
1 마법사
2 궁수
DamageType
0 마법 데미지
1 물리 데미지
직업, 데미지 타입과 같은 데이터를 문자열로 직접 표현하는 것은 메모리가 많이 사용되고 실수할 가능성이 높다. 숫자로 대체하여 쓰더라도 직관적이지 않아 어려움이 있다. 그럴 때 enum을 사용하면 굿.
enum은 직관적으로 쓰면서도 손쉽게 int가 될 수 있다.
숫자를 명시해주지 않으면 자동으로 0부터 채운다. 숫자를 직접 지정할 수 있다.
몇개만 명시하면 명시하지 않은 부분은 자연스럽게 위쪽 EnumType + 1씩 된다. (하지만 안 씀)
enum Job {
Fighter, //Job::Fighter = 0
Mage,
};
int main()
{
// 이렇게 사용 가능
Job Fighter = Job::Fighter;
// 형변환으로 확인, 이렇게 사용하지는 말라고 하심
int FighterInt = Job::Fighter; // 0
int MageInt = Job::Mage; // 1
return 0;
}
enum class는 사용할 때 FullName으로 사용한다. DamageType::PDamage
얘는 문법적으로 형변환을 막았다. 그래서 선생님은 이걸 더 선호하신다. 선생님 표현으로 형변환은 '악'.
강제 형변환은 static_cast로 할 수 있지만 안 쓸 예정.
enum class DamageType {
PDamage,
MDamage,
};
typedef랑 거의 완전히 같은 문법인 using
.
typedef가 문법적으로 아름답지 못하다 생각해서(연산자도 없고 덜 명시적이라서) using도 만들었다고 한다.
// 자료형에 별명 붙여주는 게 typedef였음
typedef int int32;
// Job이라는 사용자 정의 자료형
enum class Job {
Fighter,
};
// 가능
typedef Job JobType;
// typedef가 문법적으로 아름답지 못하다 생각해서 using도 만듦, typedef랑 다른 게 없다.
using myint = int;
한 회사에 UI 담당하는 사람과 Monster 담당하는 사람이 있다고 하자. 몬스터가 죽으면 장비나 아이템을 떨군다. 떨어진 아이템을 먹으면 인벤토리로 들어가야 한다.
이름이 겹치는 것을 막기 위해 만든 것이 바로 namespace
이다. 접두사같은 기능. "내 클래스를 사용하려면 접두사를 붙여라" 라는 의미.
namespace UI {
class Item {
};
}
namespace Play {
class Item {
};
}
각각 UI::Item
, Play::Item
이 된 것이다.
using namespace UI;
: 이름에 UI를 사용하지 않아도 알아서 연결해주는 문법.
선생님이 극혐하시는 문법이다. 선생님은 namespace를 사용한다면 무조건 풀네임으로 사용하신다.
선생님 말씀) 이걸 사용해야 할 때는 보통 이름을 붙여주고 싶다기 보다는 작업을 나눌 때 사용하는 느낌이다.
cout의 앞에 붙는 std::
가 바로 namespace이다. 스탠다드의 줄임말로 C++ 언어차원에서 지원하는 기본 기능이다.
cout은 화면에 글자를 출력해준다.
cout의 정체는 전역 객체였던 것이다.
// 헤더 (.h)
namespace std {
class MyStream {
public:
void operator <<(const char* _Text) {
printf_s(_Text);
}
};
// 전역 객체
extern MyStream mycout;
}
// (.cpp)
std::MyStream mycout;
int main() {
std::mycout << "Hello World!\n";
}
우리 게임에 마을이 100개라고 할 때 아래처럼 배열로 만들면 한 번에 100개의 마을을 모두 만들게 된다. (불필요함) 내 플레이어의 눈에 보이는 주변만 처리하면 된다.
class Zone {
// 마을
};
int main()
{
Zone Arr[100]; // X
}
동적으로 무언가를 만들어야 할 때 사용한다.
new 자료형()
으로 사용한다.
new 연산자는 자료형의 포인터를 반환한다.
코드 | 메모리맵 |
---|---|
![]() | ![]() |
어떤 때는 만들고 어떤 때는 지울 수 있다는 점이 동적 할당의 핵심이다.
아래와 같이 활용할 수 있다.
Zone* CurZone = nullptr;
if (플레이어가 들어왔다면) {
CurZone = new Zone();
}
int MonsterCount = 10;
int PlayerLevel = 30;
if (PlayerLevel > 20)
{
MonsterCount = 30;
}
Monster* NewMonster = new Monster[MonsterCount];
delete[] NewMonster;
delete
로 동적 할당한 메모리를 삭제할 수 있다.
C++에서는 delete를 안 하면 무한히 생성할 수도 있다. (주의)
int* Ptr = new int(10);
delete Ptr;
delete[]
사용.Monster* NewMonster = new Monster[10];
delete[] NewMonster;
Monster* NewMonster = new Monster[MonsterCount]; // NewMonster의 위치 :500번지
Monster* NewMonsterArr = NewMonster; // NewMonsterArr의 값 : 500번지
delete NewMonsterArr; // 500번지 메모리 해제
Leak이란 new를 하고 delete를 하지 않은 메모리를 말한다.
C++ 프로그래밍에 심각한 문제를 발생시킬 수 있으므로 leak은 무조건 잡아야 한다.
_CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF | _CRTDBG_ALLOC_MEM_DF);
: 릭 체크 코드, 프로그램 시작부분에 놓는 게 좋다. 앞으로 무조건 사용.
class Monster {
int Att;
int Hp;
};
int main()
{
// leak 체크 코드
_CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF | _CRTDBG_ALLOC_MEM_DF);
Monster* NewMonster = new Monster();
//delete NewMonster; delete를 안 해봄.
}
(모든 과제에서 StringCount 사용했다.)
enum class StringReturn
{
Equal,
NotEqual
};
int StringCount(const char* const _Ptr)
{
int Count = 0;
while (_Ptr[Count])
{
++Count;
}
return Count;
}
StringReturn StringEqual(const char* const _Left, const char* const _Right)
{
int LeftCount = 0;
int RightCount = 0;
LeftCount = StringCount(_Left);
RightCount = StringCount(_Right);
if (LeftCount != RightCount) {
return StringReturn::NotEqual;
}
else {
// 글자 수가 같으면 진짜 같은지 체크
for (int i = 0; i < LeftCount; i++) {
if (_Left[i] != _Right[i]) {
//하나라도 다른 게 있으면 바로 NotEqual 리턴
return StringReturn::NotEqual;
}
}
// 여기까지 왔다면 모두 같은 것이므로 Equal 리턴
return StringReturn::Equal;
}
}
StringReturn NewReturn1 = StringEqual("AAA", "AAAA"); // NotEqual
StringReturn NewReturn2 = StringEqual("ABC123", "ABC123"); // Equal
마지막에 _Dest[LeftCount + RightCount] = 0;
로 문자열의 마지막을 표시해야 _Dest에 이미 문자열이 있었어도 의도대로 만들 수 있다.
void StringAdd(char* _Dest, const char* const _Left, const char* const _Right)
{
int LeftCount = 0;
int RightCount = 0;
LeftCount = StringCount(_Left);
RightCount = StringCount(_Right);
for (int i = 0; i < LeftCount; i++) {
_Dest[i] = _Left[i];
}
for (int i = LeftCount; i < LeftCount + RightCount; i++) {
_Dest[i] = _Right[i - LeftCount];
}
_Dest[LeftCount + RightCount] = 0;
return;
}
char Arr[100] = {};
StringAdd(Arr, "hello", "world"); //helloworld
int StringContains(const char* const _Dest, const char* const _Find)
{
int DestCount = StringCount(_Dest);
int FindCount = StringCount(_Find);
int Result = 0;
for (int i = 0; i < DestCount; i++) {
bool CheckFlag = true;
for (int j = 0; j < FindCount; j++) {
if (_Dest[i + j] != _Find[j]) {
CheckFlag = false;
}
}
if (CheckFlag == true) {
Result++;
}
}
return Result;
}
int Result1 = StringContains("ababcccccabab", "ab"); // 4
int Result2 = StringContains("ababcccccabab", "c"); // 5
StringReturn NewReturn1 = StringEqual("AAA", "AAAA"); // NotEqual
StringReturn NewReturn2 = StringEqual("ABC123", "ABC123"); // Equal
int NewReturn3 = strcmp("ABC123", "ABC123"); // 0
int NewReturn4 = strcmp("ABC123", "ABC"); // 1
char ArrTest[100];
sprintf_s(ArrTest, "%s%s", "AAAA", "BBBB");