#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; // 깊은 복사
*/