실습 방식
- 증상을 먼저 재현한다.
- 가설을 세운다. (“null일 것 같다”, “인덱스 범위일 것 같다”)
- 브레이크포인트/Watch/호출 스택으로 가설을 검증한다.
- 수정 후 같은 입력으로 다시 실행하고 회귀 여부를 확인한다.
초기화 누락 (쓰레기값)
증상
- 2번 기사
_attack이 음수/이상값으로 출력됨.
원인
- 오버로드 생성자에서
_attack 초기화 누락.
Knight::Knight(int hp) : _hp(hp)
{
}
수정
Knight::Knight(int hp) : _hp(hp), _attack(10) {}
디버깅 포인트
- 디버그 빌드의 미초기화 패턴값(
0xCD...)을 Watch/메모리 창에서 확인.
- 생성자가 여러 개면 “모든 생성 경로”를 점검.
배열 인덱스 오버플로우
증상
- 10개를 출력해야 하는 루프에서 마지막에 Access Violation.
원인
i <= KNIGHT_COUNT 오타로 knights[10] 접근.
for (int i = 0; i <= KNIGHT_COUNT; i++)
knights[i]->PrintInfo();
수정
for (int i = 0; i < KNIGHT_COUNT; i++)
knights[i]->PrintInfo();
디버깅 포인트
- 호출 스택에서 크래시 직전 호출부 인덱스 값 확인.
- 규칙 고정: “배열 크기 N -> 유효 인덱스 0..N-1”.
비교 로직 오류 (IsDead)
증상
원인
bool Knight::IsDead() { return (_hp == 0); }
수정
bool Knight::IsDead() { return (_hp <= 0); }
디버깅 포인트
- 경계값 테스트(0, -1, 1)를 항상 같이 돌린다.
- HP 갱신 시 클램프(0~maxHp)도 함께 설계.
정수 오버플로우
증상
- 힐을 반복했는데 오히려 HP가 음수로 변해 사망 처리됨.
원인
int 범위를 넘어선 누적 연산.
- 특히 signed overflow는 C++에서 UB로 취급(래핑처럼 보여도 보장 아님).
수정
#include <algorithm>
void Knight::AddHp(int value)
{
long long next = static_cast<long long>(_hp) + value;
next = std::clamp(next, 0LL, static_cast<long long>(_maxHp));
_hp = static_cast<int>(next);
}
디버깅 포인트
- 조건부 중단점:
_hp < 0로 걸어서 최초 깨지는 시점 확인.
0으로 나누기 + 정수 나눗셈
증상
GetAttackDamage()에서 예외 발생 또는 비율 계산 오동작.
원인
_hp == 0일 때 분모가 0.
_hp / _maxHp를 정수로 계산해 0이 되어 버림.
수정
int Knight::GetAttackDamage()
{
int damage = _attack;
if (_hp <= 0 || _maxHp <= 0)
return damage;
int percentage = (_hp * 100) / _maxHp;
if (percentage <= 50)
damage *= 2;
return damage;
}
디버깅 포인트
- 나눗셈 코드면 “분모 0 여부”를 먼저 Watch에 추가.
- 정수 퍼센트 계산은
(x * 100) / y 습관화.
무한 재귀 -> 스택 오버플로우
증상
- 반격 로직 실행 시
OnDamaged 호출이 끝없이 반복되어 stack overflow.
원인
OnDamaged() 내부에서 상대의 OnDamaged()를 다시 호출해 탈출 조건 없이 순환.
attacker->OnDamaged(this);
수정(예시)
void Knight::OnDamaged(Knight* attacker, bool canCounter)
{
if (attacker == nullptr || IsDead())
return;
const int damage = attacker->GetAttackDamage();
AddHp(-damage);
if (canCounter && !IsDead() && damage > 0)
attacker->OnDamaged(this, false);
}
디버깅 포인트
- 호출 스택에 같은 함수가 반복되면 무한 재귀를 먼저 의심.
virtual 소멸자 누락 -> 메모리 누수
증상
Player* p = new Archer(); delete p; 반복 시 메모리 사용량 지속 증가.
원인
- 부모 소멸자가 virtual이 아니어서 자식 소멸자가 호출되지 않음.
수정
class Player {
public:
virtual ~Player() = default;
};
디버깅 포인트
- “부모 포인터로 delete”가 보이면 virtual 소멸자부터 확인.
스택 vs 힙 혼동
증상
Pet pet;의 주소를 보관한 뒤 소멸자에서 delete _pet 시 크래시.
원인
- 스택 객체 주소를 힙 소유 객체처럼 delete함.
Pet pet;
p = new Archer(&pet);
delete _pet;
수정
p = new Archer(new Pet());
디버깅 포인트
- 규칙:
delete는 new로 만든 메모리에만.
Double Free
증상
- 궁수 사망 시점 + 소멸자에서 같은
Pet을 두 번 해제.
원인
- 첫
delete 후 포인터 무효화 누락.
- 해제 책임이 여러 지점에 분산됨.
수정
void Archer::AddHp(int value)
{
Player::AddHp(value);
if (IsDead() && _pet != nullptr) {
delete _pet;
_pet = nullptr;
}
}
Archer::~Archer()
{
delete _pet;
_pet = nullptr;
}
디버깅 포인트
delete 호출 지점을 grep/검색으로 모두 확인해 “단일 책임”으로 정리.
Use-After-Free (다중 참조)
증상
- 화살 여러 개가 같은
Knight*를 타겟으로 가진 상태에서
기사 삭제 후 다른 화살이 _target 접근하며 크래시.
원인
- 한 객체를 여러 포인터가 참조하는데, 소유권/삭제 순서 규칙이 없음.
수정 방향
- 참조자(화살) 작업이 끝난 후 삭제하거나,
- 삭제 전에 모든 화살의 타겟을 무효화.
for (int i = 0; i < 10; i++)
arrows[i]->AttackTarget();
for (int i = 0; i < 10; i++)
arrows[i]->SetTarget(nullptr);
delete knight;
knight = nullptr;
디버깅 포인트
- UAF는 “삭제 시점”과 “크래시 시점”이 다를 수 있어 호출 스택만으로 부족할 때가 많음.
- 객체 생명주기 로그(생성/삭제/소유자)를 함께 남겨야 추적이 빨라짐.