싱글턴 객체 / 전방 선언 / cin관련 함수 / 포인터 / const 에 관한 이야기
C++를 이용하여 텍스트 기반 RPG 게임을 만드는 과정에 있어서 생긴 이슈들을 중점으로 글을 정리해 볼 것이다.
1. 싱글턴 객체 생성
- 싱글턴 객체의 생성은 어디에서 해야 할까?
- 싱글턴 객체로 구현한 클래스를 정의한 cpp 파일에서 하면 된다.
//Character.h
private:
static Character* instance;
//Character.cpp
...
Character* Character::instance = nullptr;
Character* Character::GetInstance(string name)
{
if (instance == nullptr)
{
instance = new Character(name);
}
return instance;
}
- 그러나 싱글턴 객체 또한 new를 해서 delete를 해주어야 하므로 게임이 종료되는 순간 delete를 호출해야 한다.
- 예를 들면 아래와 같다.
- 캐릭터가 죽을때
- 게임이 클리어 되서 끝날때
- 강제로 게임 종료를 할 때
- 그러나 이렇게 하면, 여러군데에서 delete를 call해야 하기에 이 객체를 unique_ptr로 만드는 것을 생각해 보았다.
- 아래와 같이 구성해 보았다.
- unique_ptr은 소유권이 하나이므로, move 연산자를 사용해 주어야 한다.
- 또한, GetInstance의 리턴 타입을 레퍼런스로 해야한다.
- 그 이유는 unique_ptr은 복사될 수 없기 때문이다.
- unique_ptr의 사용에 대해서는 추가적인 글을 통해 자세히 설명할 예정이다.
//Character.h
...
private:
static unique_ptr<Character> instance;
//Character.cpp
...
unique_ptr<Character> Character::instance = nullptr;
unique_ptr<Character>& Character::GetInstance(string name)
{
if (!instance)
{
instance = unique_ptr<Character>(new Character(name));
}
return instance;
}
//main.cpp
...
unique_ptr<Character> playerCharacter = move(Character::GetInstance(name));
- unique_ptr로 만들지 않고 delete를 하지 않은 상태로 메모리 누수 체크를 해보면 아래와 같이 메모리 누수가 발견되는 것을 볼 수 있다.
- 18번째 줄이 바로 instance = new Character(name); 이 부분이다.
- 게임 종료 시 delete를 호출하게 되면 해당 메모리 누수는 발생하지 않으며, unique_ptr로 만든 버전을 사용해도 메모리 누수가 발견되지 않는 것을 확인할 수 있었다.
2. 전방 선언
클래스 구조
- Item클래스
- 인터페이스 클래스
- 함수중에 Character* 를 매개변수로 가지는 Use라는 함수가 존재
- 해당 클래스를 상속받은 여러 클래스 존재
- Item 자식 클래스
- Use 함수를 override해서 구현
- Character 클래스
- Item* 를 저장하는 vector 를 멤버변수로 가짐
발생하는 문제
- Item클래스가 있는 Item.h에서 #include "Character.h"를 하고
- Character 클래스가 있는 Character.h에서 #include "Item.h"를 하면 순환 참조 문제가 생긴다.
- 위 에러 메세지가나오게 되는데, 이는 서로가 서로의 헤더를 계속 포함시켜 컴파일 시 문제가 발생하는 것이다.
해결방법
- 전방 선언
- 헤더 파일에서는 include를 하지 않고, class Item; 이런식으로 전방선언을 통해 헤더를 구성하고
- include는 .cpp 파일에 해서 구체적인 구현을 .cpp에서 하면 된다.
- 그러나 이렇게 각자의 헤더가 다른 헤더를 참조하도록 구현된 것은 좋은 클래스 구조가 아닐 수도 있으니, 구현하면서 다시 한번 고민해 보는 것도 좋을 것이다.
- 아래처럼 구성하면 컴파일이 된다.
3. 숫자 입력 처리
- cin.fail()
- int 타입을 입력받을 때 우리가 string을 입력하는 경우 등 잘못된 입력을 하면, true를 반환
- 오류 발생 시 failbit 플래그가 설정된다.
- cin.clear()
- 입력 스트림의 오류 플래그(위에서 언급한 failbit 등)를 초기화
- 다시 입력 스트림을 정상 상태로 복구한다.
- cin.ignore()
- 입력 스트림에 남아있는 불필요한 입력 데이터를 무시
- ignore(count, delimiter)
- count는 무시할 최대 문자의 갯수 (기본값 1)
- delimiter는 종료할 문자 (기본값 EOF)
- 활용 예제
- 입력 값이 int이고, 0이상 3 이하의 숫자만 입력받고, 아니면 다시 입력을 받도록 한다.
4. 포인터 타입 및 delete
※ 포인터 타입 vector
vector를 통해 Item을 저장하는 상황에서 우리는 왜 Item* 를 vector의 타입으로 사용해야 할까?
- 그 이유는 다형성 때문이다.
- 우리가 단순히 Item만 저장할 것이 아니라, Item을 상속받아 만든 자식 클래스들의 객체 또한 포함되기 때문이다.
- 아래의 코드를 보면 이해할 수 있다.
- 다형성 특성을 잘 이용하기 위해 상속을 사용해야 하고,
- 이를 문제없이 수행하기 위해서는 item* 타입으로 vector에 저장해야 한다.
vector<Item*> items;
...
Item* item1 = new Sword();
Item* item2 = new Armor();
items.push_back(item1);
items.push_back(item2);
...
for(Item* item : items)
item.use();
- 우선 문제가 생기는 버전의 코드를 한번 살펴보자
- 만약 Item타입으로 선언 (vector<Item>items) 한다면, use() 함수를 쓸 때 우리가 원하는 동작을 하지 않을 것이다.
- C++는 기본적으로 정적 바인딩이기 때문에, 아래처럼 구현한 후, use함수를 쓰면 부모인 Item 클래스의 use 함수가 호출될 것이다.
- 정적 바인딩은 실제로 무엇을 가리키는지가 아니라 타입을 따라간다는 의미이다.
- vector안에 있는 것들은 모두 Item 타입이기 때문에 Item의 use만 call된다.
//Item.h
class Item
{
public:
Item(string name)
:mName(name)
{}
virtual void use()
{
cout << "use" << mName << "\n";
}
private:
string mName;
};
//GoodItem.h
class GoodItem : public Item
{
public:
GoodItem(string name)
:mName(name)
, Item(name)
{
}
void use() override
{
cout << "Good use" << mName << "\n";
}
private:
string mName;
};
//Character.cpp
...
void Character::Show()
{
for (Item item : mItems2)
{
item.use();
}
}
//main.cpp
...
Character* playerCharacter = Character::GetInstance("player");
Item item("item ");
GoodItem item2("good item ");
playerCharacter->Push(item);
playerCharacter->Push(item2);
playerCharacter->Show();
개선된 버전은 아래와 같다.
- vector<Item*> items로 바꾼 후
- 각 함수들만 포인터로 동작하도록 수정하면 된다.
//main.cpp
...
Item* item = new Item("item");
Item* item2 = new GoodItem("good item");
playerCharacter->PushItem(item);
playerCharacter->PushItem(item2);
playerCharacter->ShowItems();
※ new 하면 delete 하기
- 메모리 누수 발생하면 코드 break 해주는 함수
- _CrtSetBreakAlloc(long NewValue);
- 위의 함수의 매개변수는 우리가 메모리 릭 발생한 코드 블럭의 숫자를 넣어주면, 해당 위치에서 멈추게 되고
- 추가적으로 call stack을 보면서 디버깅을 해보자
- delete 해야하는 곳
- Character
- 싱글턴 객체로 생성된 character를 게임이 종료될 때 해제하기
- Item
- inventory 타입: vector<Item*> mItems;
- 캐릭터가 아이템을 사용할 때, inventory에서 item을 하나씩 빼주고, 해당 포인터를 해제하기
- 캐릭터가 가지고있는 inventory를 돌면서, 동적 할당 된 아이템들을 해제하기
- Monster
- 랜덤하게 생성된 monster 포인터를 monster 처치 후 delete 해주기
- Character
- 구현 방식
- Destroy 함수를 Character 클래스에 구현하여, 게임이 종료될 때 Destroy 함수를 통해 inventory와 Character 자신을 delete 하도록 구현
- 아이템을 사용하는 곳에서 pop_back과 delete를 통해 해제
- monster는 몬스터가 처치된 곳에서 delete를 통해 해제
- 관련 코드
- UseItem 함수는 아이템을 사용할 때 해제해주는 함수
- Destroy함수는 게임 종료시 호출되는 함수
void Character::UseItem()
{
if (!mItems.empty())
{
Item* item = mItems.back();
cout << item->GetName() << "사용\n";
mItems.pop_back();
delete item;
}
}
void Character::Destroy()
{
for (Item* item : mItems)
{
delete item;
}
mItems.clear();
delete instance;
}
그러나 아직 원인을 찾을 수 없는 메모리 누수가 계속 발생했고, 해당 문제를 해결하기 위해서 스마트 포인터를 사용하여, 개선할 예정이다.
- 해당 메모리 누수의 문제를 찾았다.
- 문제점은 바로 Item 클래스(인터페이스 클래스) 의 소멸자를 virtual로 만들어주지 않아서 생긴 문제이다.
- 아래와 같이 Item 클래스에서 소멸자를 가상함수로 선언해주니, 해당 메모리 누수를 막을 수 있었다.
virtual ~Item() = default;
- unique_ptr로 바꿔서 메모리 누수가 없는 버전을 완성시켜 깃허브에 업로드 할 예정이다. (모든 메모리 누수를 해결)
- https://github.com/GbLeem/Cpp_Issues/tree/main/TEXT_RPG
5. Const 쓰기
https://www.youtube.com/watch?v=4fJBrditnJU
- const를 써서 코드의 안정성을 높이자는 말을 많이 했는데, 코드를 짜다가 컴파일러가 알려주는 오류를 통해 다시 한번 느끼게 되었다.
- 일단 리마인드를 해보면,
- 함수 뒤에 const가 붙는다면, 해당 함수 내부에서 값을 바꾸지 않는다는 의미로 볼 수 있다.
- 즉 getter 함수에서 이런식으로 구현하는 것이 좋다. (read only)
- 아래 코드를 보면
- for range문에서 const를 써서 우리는 값을 바꾸지 않고, 출력만 할 것이다 라는 의미로 썼던 것이다.
- 그러나 Item.h 파일에서 GetName()를 상수 멤버함수로 만들지 않아서, item-> 이 부분에 "호환되지 않은 형식 지정자" 오류가 발생했다.
- 주석 처리한 것 처럼 함수 뒤에 const를 붙여서 read only 함수로 만들어주는 것이 코드의 안정성을 높여주게 된다.
//Character.cpp
void Character::ShowItems()
{
for (const Item* item : mItems)
{
cout << item->GetName() << " ";
}
}
//Item.h
string GetName() { return mName; } //error!
//string GetName() const { return mName; } //good
'C++' 카테고리의 다른 글
static Keyword (0) | 2025.03.27 |
---|---|
unique_ptr 써보기 (2) | 2025.01.17 |
C++ 면접 대비 정리 (0) | 2025.01.09 |
C++ 디자인 패턴 (0) | 2025.01.06 |
C++ TIL day 13 (1) | 2025.01.03 |