동적 할당한 메모리는 사용 후에 delete를 해줘야 한다. 그런데 C++ 프로그래머들은 명시적으로 delete할 필요가 없다. 소멸자가 있기 때문이다.
delete같은 끝나고 해야할 일을 소멸자에 넣으면 된다. (소멸자는 인자가 없다.)
정적 할당된 값형의 객체는 main 함수의 스택이 파괴될 때 소멸자가 호출된다.
동적 할당된 객체는 메모리가 파괴될 때 소멸자가 호출된다.
class IntArray
{
public:
IntArray()
{
}
~IntArray() // 소멸자
{
// 소멸자 함수 내부
}
}
소멸자는 특수한 함수이다. 이름이 정해져 있다. : ~클래스명()
(사실 ~
가 이름이라고 볼 수 있다.)
생성자와 다르게 일반 멤버변수처럼 직접 호출할 수 있는데 아무도 직접 호출하지 않는다. 소멸 시에 자동으로 호출되기 때문이다. 보통 소멸자에서 동적 할당을 정리한다. leak을 안 남기려고 사용할 수 있다.
상속관계에서 자식의 소멸자가 먼저 호출된다.
부모 형태로 자식 클래스를 관리할 경우 소멸자가 호출될 때 부모형의 소멸자만 호출되는 경우가 있다. 부모의 소멸자에 virtual 붙여서 해결할 수 있다. 자식쪽 소멸자에서 override 붙여주는게 좋다.
// Player를 아예 Player로 사용하지 않고 부모인 FightUnit로 업캐스팅
FightUnit* NewFightUnit = new Player();
기본적으로 컴파일러가 만들어주는 클래스의 함수들과 기능들이 있는데
이런 것을 클래스 디폴트 기능이라고 한다.
디폴트 기능이기 때문에 아무 것도 없는 클래스에서도 사용 가능하다.
"메모리를 어떻게 배치할 것인가"가 자료구조이다.
그 메모리적 구조에서 어떻게 새로운 자료를 추가할 것인지, 삭제할 것인지를 표현하는 것이 자료구조이다. (+ 원하는 자료를 어떻게 찾을 것인지)
정적 배열 불편한 점들
1. 대입이 안된다
2. 크기를 동적으로 바꿀 수 없다.
3. 그 크기를 알 수 없다. (알 수 있는데 불편함)
sizeof(ArrValue0) / sizeof(int)
내가 만든 객체가 어떠한 기능이 없다면 내가 그 기능을 추가하거나 만들면 된다.
위 정적 배열의 불편한 점을 고치기위해 내 배열을 만들어서 기존 문법의 어려움을 해결하고 편리하게 사용할 수 있다.
그런데 그게 이미 만들어져 있는 경우가 많다. 그런 게 자료구조이다.
메모리맵 그려서 아래 코드가 왜 터지는지 알아보기
#include <iostream>
#include <ConsoleEngine/EngineDebug.h>
class IntArray
{
// private: 디폴트 접근제한 지정자
public:
IntArray(int _Size)
{
ReSize(_Size);
}
IntArray(const IntArray& _Other)
{
NumValue = _Other.NumValue;
ArrPtr = _Other.ArrPtr;
}
~IntArray()
{
Release();
}
void operator=(const IntArray& _Other)
{
NumValue = _Other.NumValue;
ArrPtr = _Other.ArrPtr;
}
int& operator[](int _Count)
{
return ArrPtr[_Count];
}
int& Test(int _Count)
{
return ArrPtr[_Count];
}
int Num()
{
return NumValue;
}
void ReSize(int _Size)
{
if (0 >= _Size)
{
MsgBoxAssert("배열의 크기가 0일수 없습니다");
}
NumValue = _Size;
if (nullptr != ArrPtr)
{
Release();
}
ArrPtr = new int[_Size];
}
void Release()
{
if (nullptr != ArrPtr)
{
delete[] ArrPtr;
ArrPtr = nullptr;
}
}
private:
int NumValue = 0;
int* ArrPtr = nullptr;
};
int main()
{
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;
}
}
이유 및 결론) delete된 메모리를 delete하려고 했기 때문에 터진다. 근본적인 원인은 얕은 복사를 했기 때문이었다. 얕은 복사란 참조만 복사하는 복사를 말한다. 같은 값을 갖는 배열을 만들려고 했던 의도대로 하려면 깊은 복사를 해야 한다.
void Copy(const IntArray& _Other)
{
NumValue = _Other.NumValue;
// 깊은 복사를 해줘야 합니다.
ReSize(NumValue);
for (int i = 0; i < NumValue; i++)
{
ArrPtr[i] = _Other.ArrPtr[i];
}
}
값을 복사하는 것이 깊은 복사이고
참조를 복사하는 것이 얕은 복사이다.
int Value0 = 0;
int Value1 = 10;
Value0 = Value1; // 깊은 복사
int* Ptr0;
int* Ptr1;
Ptr0 = Ptr1; // 얕은 복사
*Ptr0 = *Ptr1; // 깊은 복사
둘 중에 더 낫거나 안 좋은 것은 없고 의도에 따라 사용하면 된다.
전역 사용하면 절차지향
클래스와 객체를 사용하면 객체지향
내일 배울 템플릿을 사용하면 제네릭 프로그래밍, 템플릿 메타 프로그래밍이라고 한다.
그래서 C++을 멀티 패러다임 언어라고 한다.