[TIL] 24-01-08

Lev·2024년 1월 8일
0

📓TIL Archive

목록 보기
19/33

클래스 (이어서)

소멸자

#include <iostream>
#include <assert.h>

class Weapon
{

};

class FightUnit
{
public:
	FightUnit()
	{

	}

	FightUnit(int _Att, int _Hp)
		: Att(_Att), Hp(_Hp)
	{

	}

	virtual ~FightUnit()
	{

	}

private:
	int Att;
	int Hp;
};

class Player : public FightUnit
{
public:
	// [생성자]
	// 없어도 자동 생성되지만, 적어주는 것이 좋다
	// 초기화를 위해 생성
	Player()
		: FightUnit(10, 100), Sword(nullptr)
		// 멤버 이니셜라이저도 있다는 것을 잊지 말자
	{
		CreateSword();
	}

	/*
	[소멸자]
	- 생성자와 공통점
		- 특수한 함수라, 이름이 '~클래스명'으로 정해져 있다
		- 직접 만들지 않으면 디폴트 소멸자가 자동으로 생성된다
	- 생성자와 차이점
		- 멤버변수처럼 직접 호출할 수 있지만, 소멸할 때 자동으로 호출되기 때문에
		아무도 그렇게 사용하지 않는다
		- 자식의 소멸자가 먼저 호출되고 그 다음 부모의 소멸자가 호출된다 (생성자는 반대)
		- 인자를 넣을수 없다 (코드가 완전히 끝나고 나서 호출되는 형태이기 때문)
	- 부모의 포인터로 자식클래스를 관리할 경우 (다형성)
		- 소멸자가 호출될 때 부모의 소멸자만 호출된다
		- 이를 방지하기 위해 최상위 부모의 소멸자에 virtual을 붙여준다
		- 마찬가지로 자식의 소멸자에는 override를 붙여준다
	*/
	~Player() override
	{
		DeleteSword();	// 보통 소멸자에서 동적 할당을 정리한다
	}

	void CreateSword()
	{
		// 1. 이미 할당된 곳에 다시 동적 할당하지 말자
		// 마지막에 할당한 주소 이외엔 전부 잃어버리게 된다 (memory leak)
		if (Sword != nullptr)	
		{
			// assert(false)	// '한번 무기를 들면, 절대로 놓지 않는다'
			DeleteSword();	// '다른 무기를 들기 전에, 기존의 무기를 놓는다'
		}

		Sword = new Weapon();	// 동적 할당
	}

	void DeleteSword()
	{
		// 2. new를 했으면 delete가 반드시 따라와야 한다
		// 사실 이런 함수는 특정 상황이 아닌 이상 명시적으로 호출하지 않는다
		// 소멸자가 존재하기 때문
		delete Sword;
	}

private:
	Weapon* Sword = nullptr;
};

class Orc : public FightUnit	// 100마리
{};

class Dragon : public FightUnit	// 20마리
{};

class Kobolt : public FightUnit	// 20마리
{};

int main()
{
	_CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF | _CRTDBG_ALLOC_MEM_DF);

	{
		// 정적배열을 이용할 경우
		Orc ArrOrc[100];
		Dragon ArrDragon[20];
		Kobolt ArrKobolt[20];
		/*
		보통은 위와 같이 정적배열로 생성하지 않는다
		몬스터가 몇 종류일지, 몇 마리일지 알 수 없기 때문
		따라서 아래와 같이 다형성을 적극적으로 이용해 생성한다
		*/

		// 다형성을 이용할 경우
		FightUnit** AllMonster = new FightUnit * [140];

		for (int i = 0; i < 100; i++)
		{
			AllMonster[i] = new Orc();
		}

		for (int i = 100; i < 120; i++)
		{
			AllMonster[i] = new Dragon();
		}

		for (int i = 120; i < 140; i++)
		{
			AllMonster[i] = new Kobolt();
		}
	}
	{
		// 값형일 경우
		Player NewPlayer;
		/*
		- 스택영역에 생성된다
		- 생성자에 이어 자연스레 소멸자까지 호출된다
			=> FightUnit() -> Player() -> ~Player() -> ~FightUnit()
		*/

		// 다형성을 이용할 경우
		FightUnit* NewFightUnit = new Player();
		/*
		- 부모클래스의 포인터를 이용해 관리한다
		- 하지만 이 경우, 자식의 소멸자가 부모의 소멸자를 override하지 않으면
		  자식의 소멸자 대신 부모의 소멸자가 호출되어버리고, memory leak을 야기한다
		*/
	}
}

복사 생성자, 깊은 복사와 얕은 복사

💡 자료구조란?

  • 메모리에 새로운 자료를 어떻게 추가하여 배치할 것인가?
  • 어느 자료를 어떻게 삭제할 것인가?
  • 내가 원하는 자료를 어떻게 찾을 것인가?
#include <iostream>
#include <Windows.h>
#include <assert.h>

class IntArray
{
/*
클래스 안에서 아무것도 만들지 않아도, 아래 다섯가지는 디폴트로 생긴다

private:	// 디폴트 접근제한 지정자 (1)
	IntArray()	// 디폴트 생성자 (2)
	{}

	IntArray(const IntArray& _Other)	// 디폴트 복사 생성자 (3)
	{}

	~IntArray()	// 디폴트 소멸자 (4)
	{}

	void operator =(const IntArray& _Other)	// 디폴트 대입 연산자 (5)
	{}


따라서 아무것도 구현하지 않아도, 클래스 디폴트 기능을 사용할 수 있다
IntArray NewArray0 = IntArray();
IntArray NewArray1 = IntArray();
IntArray NewArray2 = IntArray(NewArray1);
NewArray0 = NewArray1;
*/

public:
	IntArray(int _Size)	// 생성자
	{
		Resize(_Size);
	}

	IntArray(const IntArray& _Other)	// 복사 생성자
	{
		NumValue = _Other.NumValue;
		ArrPtr = _Other.ArrPtr;	// 얕은 복사

		// Copy(_Other);
		// 얕은 복사 대신 깊은 복사를 사용하면 소멸자 호출시 발생하는 문제가 사라진다

	}

	~IntArray()	// 소멸자
	{
		Release();
	}

	// 멤버함수와 전역함수의 차이점 => 컴파일러가 첫번째 인자로 this를 넣어준다는 것 뿐!

	void operator =(const IntArray& _Other)	// 대입 연산자
	{
		
		NumValue = _Other.NumValue;
		ArrPtr = _Other.ArrPtr;	// 얕은 복사

		// Copy(_Other);
		// 얕은 복사 대신 깊은 복사를 사용하면 소멸자 호출시 발생하는 문제가 사라진다
	}

	int& operator[](int _Count)
	{
		return ArrPtr[_Count];
	}
    /*
    Q) 왜 int&형일까?
    직접적으로 ArrPtr이 가리키고 있는 값에 접근하기 위함이다.
    int형으로 return값을 받을 경우 복사본이 생성되는 것으로,
    이 경우 return값을 이용해 연산한 값은 ArrPtr이 가리키는 값에 영향을 주지 못한다.
    */

	int Num()
	{
		return NumValue;
	}

	void Copy(const IntArray& _Other)
	{
		NumValue = _Other.NumValue;

		Resize(NumValue);
		for (int i = 0; i < NumValue; i++)
		{
			ArrPtr[i] = _Other.ArrPtr[i];	// 깊은 복사
		}
	}

	void Resize(int _Size)
	{
		if (_Size <= 0)
		{
			MessageBoxA(nullptr, "배열의 크기가 0일수 없습니다", "치명적 에러", MB_OK);
			assert(false);
		}

		if (ArrPtr != nullptr)
		{
			Release();
		}

		NumValue = _Size;
		ArrPtr = new int[_Size];
	}

	void Release()
	{
		if (ArrPtr != nullptr)
		{
			delete[] ArrPtr;
			ArrPtr = nullptr;
		}
	}

private:
	int NumValue = 0;
	int* ArrPtr = nullptr;
};

int main()
{
	_CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF | _CRTDBG_ALLOC_MEM_DF);

	// 기본문법 배열
	{
		/*
		불편한 점
		1. 정적할당이라 크기를 동적으로 바꿀 수 없다
		2. 직접적인 대입이 안된다
		3. 크기를 알아내는 방법도 불편하다

		객체지향에서는 내가 만든 객체로 표현하지 못할 것이 없으므로,
		원하는 기능이 있다면 직접 나만의 배열을 만들어 얼마든지 기능을 추가할 수 있다.
		
		...사실 이미 개선된 배열도 제공돼서 그냥 사용하면 되지만,
		면접 단골 질문이기 때문에 한번 만들어보면 좋다.
		*/
 
		int ArrValue0[10];
		int ArrValue1[10];

		// ArrValue0[11] = 5;	// 1
		// ArrValue0 = ArrValue1;	// 2
		int Value = static_cast<int>(sizeof(ArrValue0) / sizeof(int));	// 3
	}

	// 직접 만든 배열
	{
		IntArray NewArray = IntArray(5);

		NewArray.Resize(3);
		NewArray[0] = 20;	// NewArray.operator[](0) = 20;

		for (int i = 0; i < NewArray.Num(); i++)
		{
			NewArray[i] = i;
		}

		for (int i = 0; i < NewArray.Num(); i++)
		{
			std::cout << NewArray[i] << std::endl;
		}
	}

	/*
	얕은 복사의 문제점
	=> 값을 직접 복사하는 것이 아니라, 참조만 가져오기 때문에 발생하는 문제

	NewArray1의 ArrPtr이 NewArray0의 ArrPtr와 같은 곳을 가리키게 된다
	-> ~NewArray0()이 호출되어 NewArray0의 ArrPtr이 가리키는 메모리가 delete된다
	-> ~NewArray1()이 호출되었지만 NewArray1의 ArrPtr이 가리키는 곳은 이미 존재하지 않는다
	*/
	{
		IntArray NewArray0 = IntArray(5);
		for (int i = 0; i < NewArray0.Num(); i++)
		{
			NewArray0[i] = i;
		}

		IntArray NewArray1 = IntArray(5);
		NewArray1 = NewArray0;

		for (int i = 0; i < NewArray1.Num(); i++)
		{
			std::cout << NewArray0[i] << std::endl;
		}

		// ~NewArray0()
		// ~NewArray1()	// 에러 발생
	}
	{
		IntArray NewArray0 = IntArray(5);
		for (int i = 0; i < NewArray0.Num(); i++)
		{
			NewArray0[i] = i;
		}
		
		IntArray NewArray1 = IntArray(NewArray0);

		for (int i = 0; i < NewArray1.Num(); i++)
		{
			std::cout << NewArray0[i] << std::endl;
		}

		// ~NewArray0()
		// ~NewArray1()	// 에러 발생
	}
}

/*
[깊은 복사 Deep Copy] : 값을 복사하는 것

int Value0 = 0;
int Value1 = 0;
Value0 = Value1;	// 깊은 복사


[얕은 복사 Shallow Copy] : 참조를 복사하는 것
반드시 깊은 복사보다 나쁜 것이 아니라, 상황에 따라 필요한 방식이 다르다 (유념!)

int* Ptr0;
int* Ptr1;
Ptr0 = Ptr1;	// 얕은 복사
*Ptr0 = *Ptr1;	// 깊은 복사
*/

profile
⋆꙳⊹⋰ 𓇼⋆ 𝑻𝑰𝑳 𝑨𝑹𝑪𝑯𝑰𝑽𝑬 ⸝·⸝⋆꙳⊹⋰

0개의 댓글