[TIL] 24-01-03

Lev·2024년 1월 3일
0

📓TIL Archive

목록 보기
16/33

포인터 (이어서)

가상함수 포인터 vfptr

{
	int Arr[10];	// 40byte
	int* Ptr = Arr;	// 8byte => 정수 10개를 포함하고 있어도 포인터이기 때문

	int* PtrArr[10];	// 80byte
	int** PtrPtrArr = PtrArr;	// 8byte
}
{
	int Value0;	// 위치 : 100번지
	int Value1;	// 위치 : 230번지

	int* ArrPtr[2];	// 위치 : 500번지

	ArrPtr[0] = &Value0;	// 값 : 100번지
	ArrPtr[1] = &Value1;	// 값 : 230번지

	int** Ptr2D = ArrPtr;	// 위치 : 800번지 / 크기 : 8byte / 형태 : int** / 값 : 500번지 
}
{
	// 별이 하나씩 더 붙어도 같은 의미이다
	int *Value0;
	int *Value1;

	int** ArrPtr[2];

	ArrPtr[0] = &Value0;
	ArrPtr[1] = &Value1;

	int*** Ptr2D = ArrPtr;
}
{
	// 함수포인터의 형태로 바꿔도 변하는 것은 없다
	void(*Value0)();
	void(*Value1)();

	void(**ArrPtr[2])();

	ArrPtr[0] = &Value0;
	ArrPtr[1] = &Value1;

	void(***Ptr2D)() = ArrPtr;

	/*
	Value0
	위치 : 100번지

	Value1
	위치 : 230번지

	ArrPtr
	위치 : 500번지
	크기 : 8byte
	형태 : void(**[])()
	값 : ArrPtr[0] = 100번지, ArrPtr[1] = 230번지

	Ptr2D
	위치 : 800번지
	크기 : 8byte => 어쨌든 포인터이기 때문
	형태 : void(***)()
	값 : 500번지
	*/
}
/*
[가상함수 테이블] => vfptr
- Virtual을 가지고 있는 클래스를 만난 컴파일러는 가상함수 갯수만큼의 함수 포인터 배열을 만든다
- 생성자가 실행될 때 이 가상함수 테이블의 값들을 채워준다
- 함수포인터의 배열의 첫번째 주소를 가리키고 있다 (함수 2중 포인터)
- 이 포인터가 존재하기 때문에 Virtual을 가진 클래스의 크기는 최소 8byte이다
- 가상함수 테이블의 위치는 클래스 맨 앞이다
- C++ 관련 면접 단골 질문거리다(...)
*/

class FightUnit
{
public:
	FightUnit()
	{
		this;
		// __vfptr
		// [0] FightUnit::Damage(void)
		// [1] FightUnit::StatusRender(void)
	}

	virtual void Damage()
	{

	}

	virtual void StatusRender()
	{

	}
};

class Player : public FightUnit
{
public:
	// 자식생성자가 호출되기 전까지는
	// 가상함수 테이블이 여전히 부모의 함수를 가리키고 있다
	Player()
	{
		this;
		// __vfptr
		// [0] Player::Damage(void)
		// [1] FightUnit::StatusRender(void)
	}
	// 자식생성자가 호출된 후에는, 자식이 override한 함수가 있다면
	// 가상함수 테이블의 함수 포인터가 자식의 함수를 가리키도록 교체된다

	void Damage() override
	{

	}
};

int main()
{
	int Size = sizeof(FightUnit);	// 8byte

	Player NewPlayer = Player();
	// 부모생성자가 먼저 호출되고, 자식생성자가 호출된다

	// NewPlayer.Damage() 를 호출할 경우
	// NewPlayer.vfptr[0]() 로 변경되어 호출된다
}
-	this	0x000000f8048ffae8 {...}	FightUnit *
-	__vfptr	0x00007ff7fb20ac30 {Test4.exe!void(* FightUnit::`vftable'[3])()} {0x00007ff7fb201145 {Test4.exe!FightUnit::Damage(void)}, ...}	void * *
	[0]	0x00007ff7fb201145 {Test4.exe!FightUnit::Damage(void)}	void *
	[1]	0x00007ff7fb20134d {Test4.exe!FightUnit::StatusRender(void)}	void *

-	this	0x000000f8048ffae8 {...}	Player *
-	FightUnit	{...}	FightUnit
-	__vfptr	0x00007ff7fb20ac50 {Test4.exe!void(* Player::`vftable'[3])()} {0x00007ff7fb2012a3 {Test4.exe!Player::Damage(void)}, ...}	void * *
	[0]	0x00007ff7fb2012a3 {Test4.exe!Player::Damage(void)}		void *
	[1]	0x00007ff7fb20134d {Test4.exe!FightUnit::StatusRender(void)}	void *
// 가상함수 테이블을 이해하기 위해 클래스가 아닌 일반적인 함수의 형태로도 알아보자

#include <iostream>

// 가상함수 테이블 (1)
void(*vfptrArr[4])() = {};

// 가상함수 포인터 (1)
void(**vfptr)() = vfptrArr;

// 클래스 (1)
//class FightUnit
//{
//public:
/*virtual */void FightUnitStatusRender()    // 가상함수 (1-1)
{
    printf_s("스테이터스 렌더 함수");
}

/*virtual */void FightUnitDamage()  // 가상함수 (1-2)
{
    printf_s("파이트 유니트 데미지 함수");
}

/*virtual */void FightUnitFightEnd()    // 가상함수 (1-3)
{
    printf_s("파이트 유니트 데미지 함수");
}

void FightUnit()    // 생성자 (1)
{
    // 생성자가 호출될 때, 아래와 같은 일이 벌어진다
    vfptr[0] = FightUnitStatusRender;
    vfptr[1] = FightUnitDamage;
    vfptr[2] = FightUnitFightEnd;

    /*
    vfptrArr = 
    {
        FightUnitStatusRender,
        FightUnitDamage,
        FightUnitFightEnd
    };
    */
}
//}


/*
// 가상함수 테이블 (2)
void(*vfptrArr[4])() = {};

// 가상함수 포인터 (2)
void(**vfptr)() = vfptrArr;

실제론 위와 같이 본인의 테이블을 따로 가지고 있지만
그냥 부모의 테이블을 덮어쓰는 형태로 이해해도 좋다
*/

// 클래스 (2)
//class Player : Fightunit
//{
//public:
void PlayerDamage() /*override*/    // override 함수 (2)
{
    printf_s("플레이어 데미지 함수");
}

void Player()   // 생성자 (2)
{
    vfptr[1] = PlayerDamage;

    /*
    vfptrArr =
    {
        FightUnitStatusRender,
        PlayerDamage,
        FightUnitFightEnd
    };
    */
}
//}

int main()
{
    // Player NewPlayer = Player();
    FightUnit();    // 부모 생성자
    Player();   // 자식 생성자

    vfptr[0](); // FightUnitStatusRender(); => 스테이터스 렌더 함수
    vfptr[1](); // PlayerDamage(); => 플레이어 데미지 함수

    int Value0 = sizeof(vfptr);  // 8byte
    int Value1 = sizeof(vfptrArr);  // 32byte
}

사용자 정의 자료형

enum

/*
n개의 갯수로 표현되는 값이 있을 때, 문자열로 표현하지 않는다
- 철자 실수가 발생할 가능성이 높음
- 메모리를 많이 소모함

e.g. Job
	0 전사
	1 마법사
	2 궁수
e.g. int DamageType
	0 마법데미지
	1 물리데미지
*/

// [enum]
// 사용자 정의 자료형 중, 정수형 상수를 정의하는 자료형
enum Job
{
	Fighter,	// Job::Fighter = 0
	Mage	// Job::Mage = 1
	// 명시하지 않으면 자동으로 맨 위부터 0으로 채운다
};
// 사실상 int가 아닌 것을 int로 변경하는 형변환이다
// 하지만 클래스간의 업캐스팅을 제외하곤 형변환은 자제하면 좋다...

// [enum class]
// 따라서 함부로 int로 변경되지 않는 enum class를 사용한다!
enum class DamageType
{
	PDamage,
	MDamage
};

enum class Test
{
	Test0 = 'a',	// 97
	Test1	// 98
	// 숫자를 명시해주면 거기서부터 1씩 증가하는 값이 된다
};

class Player
{
public:
	Job JobType;
	DamageType Type;
};

int main()
{
	{
		Job Fighter = Job::Fighter;	// Fighter(0)

		int FighterInt = Job::Fighter;	// 0
		int MageInt = Job::Mage;	// 1
	}
	{
		DamageType Type = DamageType::MDamage;

		// int Value = Type; => 불가능, int로 자동으로 변경해주지 않는다
		// 아래와 같이 형변환해주면 int로 바꿀 수 있긴 하다... 하지만 굳이?
		int Value0 = static_cast<int>(Type);
		int Value1 = (int)Type;
	}
}

typedef와 using

enum class Job
{
	Fighter,
	Mage
};

// [typedef]
// 특정 자료형에 별명을 붙여줄 수 있다
// 하지만 C++에선 사실 별로 사용할 필요가 없다...
typedef int int32;	// int를 int32 라고도 부르겠다는 뜻
typedef Job JobType;	// Job을 JobType 이라고도 부르겠다는 뜻

// [using]
// typedef와 같다
// 컴파일러가 그대로 치환해주는 것
using myint = int;


struct _PlayerData
{

};
typedef struct _PlayerData PlayerData;
// C에서는 struct를 간결하게 적기 위해 typedef를 사용했다

int main()
{
	struct _PlayerData NewPlayerData;	// C struct 사용법
	PlayerData NewPlayerData;	// typedef를 사용한 C struct 사용법

	myint A = 20;	// int A = 20;
}

cout

namespace

/*
e.g. 한 회사에 UI담장자와 Play팀에서 몬스터를 담당하는 사람이
'몬스터가 죽으면 -> 장비 등 아이템을 떨어트린다' 를 구현할 경우
(일반적으로는 class ItemIcon과 같이 이름을 다르게 만들어야 정상)
(여기서는 예제를 위해 같은 이름의 클래스를 만들었다고 가정)

이름이 겹치는 경우, namespace를 만들어 작업 영역을 구분해주자
이름 앞에 접두사를 붙이는 것과 같은 역할
*/

#include <iostream>

// UI 담당자의 Item
namespace UI
{
	class /*UI::*/Item
	{
	public:
		Item()
		{
			printf_s("인벤토리 아이템");
		}
	};
}

// 몬스터 담당자의 Item
namespace Play
{
	namespace Monster	// namespace의 중첩도 가능하다
	{
		class /*Play::*/Item
		{
		public:
			Item()
			{
				printf_s("몬스터 드랍 아이템");
			}
		};
	}
}

class Item
{
public:
	Item()
	{
		printf_s("전역 아이템");
	}
};

/*
using namespace UI;
- UI:: 를 적어주지 않아도 전부 적용해달라는 뜻
- main 안의 전역 Item 클래스와 UI Item 클래스가 모호해진다
- 가급적 이 방법 말고 namespace를 명시해주는 것이 좋다...
*/

int main()
{
	Item NewItem;	// 전역 아이템

	UI::Item NewUIItem;	// 인벤토리 아이템

	Play::Monster::Item NewPlayItem;	// 몬스터 드랍 아이템

	std::cout << "Hello World!\n";
	// std:: => C++ 언어 차원에서 제공하는 기본 기능 (스탠다드)
}

구현

#include <iostream>

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";	// std::mycout << "Hello World!\n";
	std::mycout.operator<<("Hello World!\n");	// std::mycout.operator<<("Hello World!\n");
	// 연산자 오버로딩
}

동적 할당

new

/*
[정적 할당]
- 고정된 크기의 메모리만 사용
- 원하는 만큼만 사용하는 것이 불가능
- 플레이 중간에 메모리의 크기를 수정하는 것이 불가능
- 메모리를 유동적으로 처리할 수 없다
- 따라서 보통 스택영역과 데이터영역의 메모리에 바인딩 된다
	전역변수 => 데이터영역
	지역변수 => 스택영역
	상수 => 코드영역

[동적 할당]
- 힙영역의 메모리를 사용한다
- 메모리의 크기는 운영체제가 관리해주지만, 거의 램 크기만큼 할당 가능
*/

class Monster
{
	
};

class Zone
{
	// Monster Arr[10];	// 정적 할당
};

int GValue = 10;	// 정적 할당

int main()
{
	{
		int LValue = 10;	// 정적 할당

		int* Ptr = new int(10);	// 동적 할당
		/*
		int* Ptr
		위치 : 1000번지 (스택영역)
		크기 : 8byte
		형태 : int*
		값 : ?

		-> new int() 함수 실행 (스택영역)

		int ???
		위치 : 500번지 (힙영역)
		크기 : 4byte
		형태 : int
		값 : 10
		*/
		delete Ptr;	// 힙영역 메모리 삭제
	}
	{
		// Zone Arr[100]	// 정적 할당
		
		// 아직 존재하지 않는 지역
		Zone* CurZone = nullptr;
		if (/*플레이어가 들어왔다면*/)
		{
			// 새로운 지역 생성 (삭제하는 것도 가능)
			CurZone = new Zone();	// 동적 할당
		}
	}
	
	/*
	_getch();

	while (true)
	{
		int* Ptr = new int();
	}
	=> 이런 식으로 무한히 생성하기만 하고 삭제하지 않으면 메모리가 가득 차버린다
	*/
}

메모리 누수 memory leak

#include <iostream>

class Monster
{
	int Hp;
	int Att;
	int Def;
};

/*
[leak] !!중요!!
new를 하고 delete 하지 않은 메모리
- C++에서 누수된 메모리는 프로그램에 큰 문제를 발생시킬 수 있다
- 따라서 반드시 체크 코드를 두고, 발견 즉시 해결해야 한다!
- 컴파일 에러나 마찬가지로 심각한 사안이다
*/

int main()
{
	// leak 체크 코드 => 외워두고 프로그래밍 시작부분에 적자
	_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

	// [동적 할당 하는 방법]
	{
		// 1. 객체 1개를 할당하는 방법
		Monster* NewMonster = new Monster();
		delete NewMonster;	// 안 할시 12byte 소모
	}
	{
		int ArrCount = 10;
		Monster NewArr[ArrCount];	// 정적 할당 불가능

		// 2. 배열로 할당하는 방법 => 동적 할당의 필요성이나 마찬가지!
		int MonsterCount = 10;
		int PlayLevel = 1;

		if (PlayLevel > 20)
		{
			MonsterCount = 30;
		}

		Monster* NewMonster = new Monster[MonsterCount];	// 동적 할당 가능

		delete[] NewMonster;	// 가리키고 있는 번지의 내용물을 삭제한다는 뜻

		/*
		Q) delete는 무엇을 삭제하는 걸까?

		Monster* NewMonster = new Monster[10];

		NewMonster
		위치 : 1000번지
		크기 : 8byte
		형태 : Monster*
		값 : 500번지

		???
		위치 : 500번지
		크기 : 120byte?
		형태 : Monster[]
		값 : ?

		delete[] NewMonster => 500번지의 내용을 삭제
		*/
	}

	/*
	C 스타일로 하는 동적 할당도 있다
	malloc remalloc void free(void *ptr);
	*/
}
(leak 체크 결과 예시)

Detected memory leaks!
Dumping objects ->
{75} normal block at 0x0000026C89A3F320, 12 bytes long.
 Data: <            > 00 00 00 00 00 00 00 00 00 00 00 00 
Object dump complete.

Console Game (enum ver.)

  • 상하좌우, 발포 키에 enum class 적용
// [GameEnum.h]
#pragma once

enum class KeyType
{
	LeftKeyS = 'a',
	LeftKeyB = 'A',
	RightKeyS = 'd',
	RightKeyB = 'D',
	UpKeyS = 'w',
	UpKeyB = 'W',
	DownKeyS = 's',
	DownKeyB = 'S',
	FireKeyS = 'q',
	FireKeyB = 'Q',
};
// [Player.cpp]
#include "Player.h"
#include <conio.h>
#include "ConsoleScreen.h"
#include "GameEnum.h"

Player::Player()
{

}

Player::Player(const int2& _StartPos, char _RenderChar)
	: ConsoleObject(_StartPos, _RenderChar)
{

}

void Player::Update()
{
	KeyType Value = static_cast<KeyType>(_getch());
	// KeyType은 enum class이기 때문에 자동으로 int로 바뀌지 않는다

	switch (Value)
	{
	case KeyType::LeftKeyS:
	case KeyType::LeftKeyB:
	{
		if ((Pos + Left).X != 0)
		{
			Pos += Left;
		}
		break;
	}
	case KeyType::RightKeyS:
	case KeyType::RightKeyB:
	{
		if ((Pos + Right).X != (ScreenX - 2))
		{
			Pos += Right;
		}
		break;
	}
	case KeyType::UpKeyS:
	case KeyType::UpKeyB:
	{
		if ((Pos + Up).Y != 0)
		{
			Pos += Up;
		}
		break;
	}
	case KeyType::DownKeyS:
	case KeyType::DownKeyB:
	{
		if ((Pos + Down).Y != (ScreenY - 1))
		{
			Pos += Down;
		}
		break;
	}
	case KeyType::FireKeyB:
	case KeyType::FireKeyS:
	{
		if (nullptr != IsFire)
		{
			*IsFire = true;
		}
	}
	default:
		break;
	}
}

void Player::SetBulletFire(bool* _IsFire)
{
	if (nullptr == _IsFire)
	{
		return;
	}

	IsFire = _IsFire;
}

과제

#include <iostream>
#include <string.h>	// str 함수들을 사용할 때 필요하다

int StringCount(const char* _Ptr)
{
	int Count = 0;

	while (_Ptr[Count])
	{
		++Count;
	}

	return Count;
}

enum class StringReturn
{
	Equal,
	NotEqual
};

// <과제1>
// 두 문자열이 동일한지 확인하는 StringEqual 함수 완성하기
StringReturn StringEqual(const char* const _Left, const char* const _Right)
{
	/*
	_Left
	위치 : 500번지 (스택영역 StringEqual 함수)
	크기 : 8byte
	형태 : const char* const
	값 : 50번지 (코드영역)

	_Right
	위치 : 508번지 (스택영역 StringEqual 함수)
	크기 : 8byte
	형태 : const char* const
	값 : 70번지 (코드영역)
	*/

	int LCnt = StringCount(_Left);	// int LCnt = static_cast<int>(strlen(_Left));
	int RCnt = StringCount(_Right);	// int RCnt = static_cast<int>(strlen(_Right));

	if (LCnt != RCnt)
	{
		return StringReturn::NotEqual;
	}

	for (int i = 0; i < LCnt; i++)
	{
		if (_Left[i] != _Right[i])
		{
			return StringReturn::NotEqual;
		}
	}

	return StringReturn::Equal;
}

// <과제2>
// 두 문자열을 순서대로 이어주는 StringAdd 함수 완성하기
void StringAdd(char* _Dest, const char* const _Left, const char* const _Right)
{
	/*
	_Dest
	위치 : 500번지 (스택영역 StringAdd 함수)
	크기 : 8byte
	형태 : char*
	값 : 1000번지 (스택영역 main 함수)

	Arr
	위치 : 1000번지 (스택영역 main 함수)
	크기 : 100byte
	형태 : char[]

	_Left
	위치 : 508번지 (스택영역 StringAdd 함수)
	크기 : 8byte
	형태 : const char* const
	값 : 50번지 (코드영역)

	_Right
	위치 : 516번지 (스택영역 StringAdd 함수)
	크기 : 8byte
	형태 : const char* const
	값 : 70번지 (코드영역)
	*/

	int LCnt = StringCount(_Left);
	int RCnt = StringCount(_Right);

	for (int i = 0; i < LCnt; i++)
	{
		_Dest[i] = _Left[i];
	}

	for (int i = 0; i < RCnt; i++)
	{
		_Dest[LCnt + i] = _Right[i];
	}

	_Dest[LCnt + RCnt] = 0;
	// _Dest가 받은 배열에 이미 문자열이 차있을 경우를 대비
	// 마지막 문자 이후 0을 넣으면 문자열은 거기까지만 출력된다

	return;
}

// <과제3>
// 어떤 문자열에 특정 문자열이 포함된 횟수를 반환하는 StringContains 함수 완성하기
int StringContains(const char* const _Dest, const char* const _Find)
{
	int DCnt = StringCount(_Dest);
	int FCnt = StringCount(_Find);

	if (DCnt < FCnt)
	{
		return 0;
	}

	int Cnt = 0;

	for (int i = 0; i < DCnt; i++)
	{
		if (i > DCnt - FCnt)
		{
			break;
		}

		for (int j = 0; j < FCnt; j++)
		{
			if (_Dest[i + j] != _Find[j])
			{
				break;
			}

			if (j == FCnt - 1)
			{
				Cnt += 1;
			}
		}
	}

	return Cnt;
}

int main()
{
	{
		// StringCount() => strlen()

		int Count0 = StringCount("AAA");
		int Count1 = static_cast<int>(strlen("AAA"));
	}
	{
		// StringEqual() => strcmp()

		StringReturn IsEqual0 = StringEqual("AAAA", "AAAA");	// Equal(0)
		StringReturn IsEqual1 = StringEqual("AAAAA", "AAAA");	// NotEqual(1)

		int Result0 = strcmp("AAAA", "AAAA");	// 1
		int Result1 = strcmp("AAAAA", "AAAA");	// 0
	}
	{
		// StringAdd() => sprintf_s() ...라는 것도 있다

		char Arr0[100] = {};	// {0, } 로 초기화해주는 것이 좋다
		StringAdd(Arr0, "abcd", "efgh");	// abcdefgh

		char Arr1[100] = {};	// {0, } 로 초기화해주는 것이 좋다
		StringAdd(Arr1, "gfadsgf", "fasdfsda");	// gfadsgffasdfsda

		char ArrTest[100];
		sprintf_s(ArrTest, "%s%s", "AAAAA", "BBBBB");	// AAAAABBBBB
		// 콘솔에 출력해주는 printf_s와 달리, sprintf_s는 가장 왼쪽 버퍼에 넣어준다
	}
	{
		int Cnt0 = StringContains("ababcccccabab", "ab");	// 4
		int Cnt1 = StringContains("ababcccccabcdab", "abcd");	// 1
		int Cnt2 = StringContains("eafknvkiojfknvfknvkfdahfknvk", "fknvk");	// 3
	}
}
profile
⋆꙳⊹⋰ 𓇼⋆ 𝑻𝑰𝑳 𝑨𝑹𝑪𝑯𝑰𝑽𝑬 ⸝·⸝⋆꙳⊹⋰

0개의 댓글