[Bug Report #1]

문제:

  • 2번 기사의 attack 값이 잘못된 쓰레기값(음수)로 출력됩니다.
  • 기대되는 동작은 2번 기사의 attack 값이 기본값 10으로 설정되는 것입니다.

코드 구조 분석

클래스 정의 (Knight)

  1. 멤버 변수:

    • _hp: 기사의 생명력 (기본값 100).
    • _attack: 기사의 공격력 (기본값 10).
  2. 생성자:

    • 기본 생성자: _hp100, _attack10으로 초기화.
    • 오버로드된 생성자: hp를 입력값으로 받아 설정. 하지만 _attack 초기화 누락.
  3. 멤버 함수:

    • PrintInfo: hpattack 값을 출력.

주요 문제점

  • 원인:
    • 오버로드된 생성자 Knight(int hp)에서 _attack 값을 초기화하지 않았습니다.
    • _attack은 초기화되지 않은 채로 메모리에 저장된 쓰레기값을 가집니다.
  • 증상:
    • 2번 기사_attack이 음수(쓰레기값)로 출력됩니다.

디버깅 및 해결 방법

1. 문제 코드 분석

Knight::Knight(int hp) : _hp(hp)
{
    // _attack 초기화가 누락됨
}
  • _attack 값을 초기화하지 않았으므로, 해당 멤버 변수는 메모리에 저장된 쓰레기값을 유지합니다.

2. 수정 방법

  1. 생성자 초기화 리스트에 _attack 추가

    Knight::Knight(int hp) : _hp(hp), _attack(10)
    {
        // _attack도 기본값으로 초기화됨
    }
  2. 멤버 변수 선언 시 초기화 (C++11 이상)

    class Knight {
    public:
        int _hp = 100;
        int _attack = 10;  // 기본값 설정
    };
  3. 생성자 내부에서 초기화

    Knight::Knight(int hp)
    {
        _hp = hp;
        _attack = 10;  // 초기화 추가
    }

수정된 코드

헤더 파일 (Knight.h)

#pragma once

class Knight
{
public:
    Knight();
    Knight(int hp);
    ~Knight();

    void PrintInfo();

public:
    int _hp = 100;        // 기본값 초기화
    int _attack = 10;     // 기본값 초기화
};

구현 파일 (Knight.cpp)

#include "Knight.h"
#include <iostream>
using namespace std;

// 기본 생성자
Knight::Knight() : _hp(100), _attack(10) {}

// 오버로드된 생성자
Knight::Knight(int hp) : _hp(hp), _attack(10) {}  // _attack 초기화 추가

// 소멸자
Knight::~Knight() {}

// 정보 출력 함수
void Knight::PrintInfo()
{
    cout << "HP: " << _hp << endl;
    cout << "ATT: " << _attack << endl;
}

메인 파일 (main.cpp)

#include <iostream>
using namespace std;
#include "Knight.h"

int main()
{
    // 기본값 생성
    Knight* k1 = new Knight();
    k1->PrintInfo();

    // HP 200으로 설정
    Knight* k2 = new Knight(200);
    k2->PrintInfo();

    delete k1;
    delete k2;

    return 0;
}

실행 결과

HP: 100
ATT: 10
HP: 200
ATT: 10
  • 결과 분석:
    • 1번 기사의 기본값(HP=100, ATT=10) 출력.
    • 2번 기사의 HP=200, ATT=10 출력.
    • _attack의 초기화 문제가 해결되었습니다.

추가 디버깅 방법

  1. 메모리 초기화 상태 확인:

    • Visual Studio의 메모리 창에서 _attack의 초기값 확인 가능 (보통 0xcdcdcdcd 등 쓰레기값).
  2. 정적 분석 도구 사용:

    • Clang-Tidy, SonarQube 등을 사용해 초기화되지 않은 멤버 변수를 탐지.
  3. 디버깅 포인트 추가:

    cout << "Debug: _attack = " << _attack << endl;
    • 초기화 여부를 출력하여 문제를 추적.

[Bug Report #2]

문제:

  • 프로그램에서 Knight 객체 10개를 생성한 후 출력하려 할 때 크래시가 발생합니다.
  • 문제 원인: 반복문에서 잘못된 범위(<=)로 인해, 배열의 유효하지 않은 인덱스에 접근하고 있습니다.

코드 분석

코드:

const int KNIGHT_COUNT = 10;

int main()
{
    Knight* knights[KNIGHT_COUNT];

    for (int i = 0; i < KNIGHT_COUNT; i++)  // 10개의 Knight 객체 생성
    {
        knights[i] = new Knight();
    }

    for (int i = 0; i <= KNIGHT_COUNT; i++)  // 잘못된 반복 조건 (<=)
    {
        knights[i]->PrintInfo();  // 유효하지 않은 인덱스 접근 시도
        delete knights[i];       // 메모리 해제
    }
}

문제 상세:

  1. 배열 크기와 유효 범위:

    • 배열 knights의 크기는 KNIGHT_COUNT = 10으로 선언되어 있으며, 유효한 인덱스는 0 ~ 9입니다.
    • 하지만, 두 번째 for문에서 조건 i <= KNIGHT_COUNTi = 10까지 반복을 시도하며 유효 범위를 초과합니다.
  2. 결과:

    • knights[10]은 유효하지 않은 메모리 공간에 접근합니다.
    • 따라서 프로그램이 크래시하거나 정의되지 않은 동작을 유발합니다.

디버깅 과정

1. 디버깅 도구 사용

  • 호출 스택(Call Stack):
    • 디버거에서 호출 스택을 확인하면, 문제가 발생한 정확한 위치(함수와 라인 번호)가 표시됩니다.
    • 여기서 knights[10]->PrintInfo()에서 문제가 발생했음을 확인할 수 있습니다.

2. 메모리 접근 검사

  • 디버거로 knights 배열의 메모리 상태를 확인합니다.
  • knights[10]는 배열 크기를 초과했으므로 쓰레기값(또는 nullptr)를 가리키게 됩니다.

문제 해결

1. 반복 조건 수정

for (int i = 0; i < KNIGHT_COUNT; i++)  // 올바른 반복 조건
{
    knights[i]->PrintInfo();
    delete knights[i];
}
  • 반복 조건을 i < KNIGHT_COUNT로 수정하여, 배열의 유효한 인덱스(0 ~ 9)만 접근하도록 합니다.

수정된 코드

헤더 파일 (Knight.h):

#pragma once

class Knight
{
public:
    Knight();
    Knight(int hp);
    ~Knight();

    void PrintInfo();

public:
    int _hp;
    int _attack;
};

구현 파일 (Knight.cpp):

#include "Knight.h"
#include <iostream>
using namespace std;

// 기본 생성자
Knight::Knight() : _hp(100), _attack(10) {}

// 오버로드된 생성자
Knight::Knight(int hp) : _hp(hp), _attack(10) {}

// 소멸자
Knight::~Knight() {}

// 정보 출력 함수
void Knight::PrintInfo()
{
    cout << "HP: " << _hp << endl;
    cout << "ATT: " << _attack << endl;
}

메인 파일 (main.cpp):

#include <iostream>
using namespace std;
#include "Knight.h"

const int KNIGHT_COUNT = 10;

int main()
{
    // Knight 포인터 배열
    Knight* knights[KNIGHT_COUNT];

    // Knight 객체 생성
    for (int i = 0; i < KNIGHT_COUNT; i++)
    {
        knights[i] = new Knight();
    }

    // Knight 객체 출력 및 메모리 해제
    for (int i = 0; i < KNIGHT_COUNT; i++)  // 수정된 반복 조건
    {
        knights[i]->PrintInfo();
        delete knights[i];
    }

    return 0;
}

실행 결과

HP: 100
ATT: 10
HP: 100
ATT: 10
...
HP: 100
ATT: 10
  • 모든 Knight 객체의 정보가 정상적으로 출력되며, 크래시 없이 프로그램이 종료됩니다.

추가 디버깅 방법

  1. 배열 범위 검사 도구 활용

    • Visual Studio와 같은 IDE에서 주소 범위 검사(AddressSanitizer)를 활성화하여, 배열 접근 오류를 탐지할 수 있습니다.
  2. 디버그 출력

    • 반복문 내부에서 디버깅 출력을 추가하여, 각 인덱스의 상태를 확인합니다.
    for (int i = 0; i <= KNIGHT_COUNT; i++)  // 원래 반복 조건
    {
        cout << "Debug: i = " << i << endl;
        knights[i]->PrintInfo();  // 크래시 발생 지점
    }

[Bug Report #3]

문제 설명

Knight 클래스에서 피격 데미지 실험 중, Knight2Knight1을 공격하여 한방에 처치하도록 설계했지만, "죽었다"는 로그가 출력되지 않는 문제가 발생했습니다. 코드를 한 줄씩 분석하여 원인을 찾고, 해결 방법을 제시하겠습니다.


코드 분석

main 함수

int main()
{
	Knight* k1 = new Knight();
	k1->_hp = 100;
	k1->_attack = 10;

	Knight* k2 = new Knight();
	k2->_hp = 2000;
	k2->_attack = 200;

	int damage = k2->_attack;
	k1->AddHp(-damage);

	if (k1->IsDead())
	{
		cout << "죽었습니다!" << endl;
	}
	else
	{
		cout << "엥? 살았습니다!" << endl;
	}

	delete k1;
	delete k2;
}
  1. Knight* k1Knight* k2 생성:

    • k1은 기본값으로 생성된 Knight 객체이며, hpattack 값을 수동으로 초기화합니다.
      k1->_hp = 100;
      k1->_attack = 10;
    • k2 역시 동일하게 생성되어 hpattack 값을 설정합니다.
      k2->_hp = 2000;
      k2->_attack = 200;
  2. 데미지 계산 및 적용:

    • k2의 공격력을 k1의 체력에서 차감합니다.
      int damage = k2->_attack;
      k1->AddHp(-damage);
    • AddHp 함수는 k1의 체력에서 200만큼 감소시킵니다.
  3. IsDead 함수 호출:

    • k1->IsDead() 함수로 k1이 사망 상태인지 확인합니다.
      if (k1->IsDead())
    • "죽었습니다!" 또는 "엥? 살았습니다!"라는 메시지를 출력합니다.
  4. 객체 해제:

    • delete k1delete k2를 호출하여 동적으로 할당된 메모리를 해제합니다.

Knight 클래스 정의

#pragma once

class Knight
{
public:
	Knight();
	Knight(int hp);
	~Knight();

	void PrintInfo();

	void AddHp(int value);
	bool IsDead();

public:
	int _hp;
	int _attack;
};
  1. 멤버 변수:

    • _hp: 기사의 체력을 나타냅니다.
    • _attack: 기사의 공격력을 나타냅니다.
  2. 생성자:

    • 기본 생성자는 hpattack을 각각 100과 10으로 초기화합니다.
    • 매개변수를 받는 생성자는 hp를 설정하고 attack은 기본값 10으로 초기화합니다.
  3. 멤버 함수:

    • AddHp: 체력 값을 증가 또는 감소시킵니다.
    • IsDead: 체력이 0인지 확인하여 생존 여부를 반환합니다.
    • PrintInfo: 체력과 공격력을 출력합니다.

Knight 클래스 멤버 함수 구현

void Knight::AddHp(int value)
{
	_hp += value;
}

bool Knight::IsDead()
{
	return (_hp == 0);  // 기존 코드
}
  1. AddHp 함수:

    • 체력을 증가 또는 감소시키는 함수로, value_hp에 더합니다.
  2. IsDead 함수:

    • 체력이 정확히 0일 때만 true를 반환합니다. 현재 코드는 <= 조건을 누락하여, 체력이 0 이하로 떨어져도 false를 반환할 수 있습니다.

문제 원인

IsDead 함수에서 체력이 0일 때만 사망 상태를 반환하도록 구현되었습니다. 그러나 피격 후 k1의 체력은 -100이므로 IsDeadfalse를 반환하고, "엥? 살았습니다!" 메시지가 출력됩니다.


해결 방법

IsDead 함수의 조건을 수정하여, 체력이 0 이하인 경우에도 true를 반환하도록 변경합니다.

수정된 코드

bool Knight::IsDead()
{
	return (_hp <= 0);  // 수정된 코드
}

수정 후 결과

  1. k1->AddHp(-200) 호출 후, k1->_hp-100이 됩니다.
  2. IsDead 함수가 true를 반환하므로 "죽었습니다!" 메시지가 출력됩니다.

최종 코드

bool Knight::IsDead()
{
	return (_hp <= 0);  // 수정된 조건
}
int main()
{
	Knight* k1 = new Knight();
	k1->_hp = 100;
	k1->_attack = 10;

	Knight* k2 = new Knight();
	k2->_hp = 2000;
	k2->_attack = 200;

	int damage = k2->_attack;
	k1->AddHp(-damage);

	if (k1->IsDead())
	{
		cout << "죽었습니다!" << endl;
	}
	else
	{
		cout << "엥? 살았습니다!" << endl;
	}

	delete k1;
	delete k2;
}

[Bug Report #4]

문제 설명

"생명력 구슬" 아이템을 사용해 체력을 랜덤하게 회복하도록 구현했으나, 테스트 과정에서 체력을 충분히 채운 후에도 캐릭터가 죽는 현상이 발생했습니다. 이 문제는 HP 값이 정수의 범위를 넘어서는 오버플로우로 인한 것으로 보입니다.


코드 분석 (한 줄씩)

main() 함수

Knight* k1 = new Knight();
k1->_hp = 100;
k1->_attack = 10;
  • 분석:
    • Knight 객체를 생성하고, 체력(_hp)과 공격력(_attack)을 초기화합니다.
    • 초기 체력은 100, 공격력은 10으로 설정됩니다.
const int TEST_COUNT = 100 * 10000; // 100만
const int TEST_VALUE = 100 * 10000; // 100만
  • 분석:
    • TEST_COUNT: 체력 증가 작업을 반복하는 횟수. 100만 번 수행합니다.
    • TEST_VALUE: 한 번에 증가시킬 체력 값. 100만으로 설정되어 있습니다.
for (int i = 0; i < TEST_COUNT; i++)
{
    k1->AddHp(TEST_VALUE);
}
  • 분석:
    • AddHp 함수를 호출하여 체력을 증가시킵니다.
    • 문제는 체력이 반복적으로 더해지면서 int의 최대값(약 21억)을 초과할 경우 오버플로우가 발생할 수 있다는 점입니다.
if (k1->IsDead())
{
    cout << "죽었습니다!" << endl;
}
else
{
    cout << "엥? 살았습니다!" << endl;
}
  • 분석:
    • IsDead() 함수는 체력이 0 이하인지 확인합니다.
    • 오버플로우가 발생해 _hp가 음수로 변환되면, IsDead() 함수가 잘못된 결과를 반환할 수 있습니다.
delete k1;
  • 분석:
    • 동적으로 생성한 Knight 객체를 해제합니다.

AddHp() 함수

void Knight::AddHp(int value)
{
    _hp += value;
}
  • 분석:
    • 체력을 더하는 간단한 함수입니다.
    • _hpint 타입으로 선언되어 있어, 오버플로우가 발생하면 음수로 변환됩니다.

IsDead() 함수

bool Knight::IsDead()
{
    return (_hp <= 0);
}
  • 분석:
    • 체력이 0 이하인지 확인합니다.
    • 오버플로우가 발생하면 _hp가 음수가 되어 잘못된 결과를 반환할 수 있습니다.

문제의 원인

  1. 오버플로우 발생:

    • _hpint 타입으로 선언되어 있어, 최대값(약 21억)을 초과하면 음수로 변환됩니다.
    • 이를 방지하려면 더 큰 범위를 제공하는 데이터 타입(long long 또는 double)으로 변경해야 합니다.
  2. 테스트 반복 횟수:

    • TEST_COUNTTEST_VALUE가 모두 매우 큰 값으로 설정되어 있어, 반복적으로 체력을 증가시키는 동안 오버플로우가 발생할 가능성이 큽니다.

해결 방법

1. 데이터 타입 변경

  • _hp의 타입을 double 또는 long long으로 변경하여 더 큰 범위를 제공.
double _hp;

2. 체력 최대값 제한

  • 체력 회복 시 최대값을 설정하여 오버플로우를 방지.
void Knight::AddHp(int value)
{
    _hp += value;
    if (_hp > 100)
    {
        _hp = 100;
    }
}

수정된 코드

Knight.h

#pragma once

class Knight
{
public:
    Knight();
    Knight(int hp);
    ~Knight();

    void PrintInfo();
    void AddHp(int value);
    bool IsDead();

public:
    double _hp; // 타입 변경
    int _attack;
};

Knight.cpp

#include "Knight.h"
#include <iostream>
using namespace std;

Knight::Knight() : _hp(100), _attack(10) {}

Knight::Knight(int hp) : _hp(hp), _attack(10) {}

Knight::~Knight() {}

void Knight::AddHp(int value)
{
    _hp += value;
    if (_hp > 100) // 최대 체력 제한
    {
        _hp = 100;
    }
}

bool Knight::IsDead()
{
    return (_hp <= 0);
}

void Knight::PrintInfo()
{
    cout << "HP: " << _hp << endl;
    cout << "ATT: " << _attack << endl;
}

main.cpp

#include <iostream>
using namespace std;
#include "Knight.h"

int main()
{
    Knight* k1 = new Knight();
    k1->_hp = 100;
    k1->_attack = 10;

    const int TEST_COUNT = 100 * 10000; // 100만
    const int TEST_VALUE = 100 * 10000; // 100만

    for (int i = 0; i < TEST_COUNT; i++)
    {
        k1->AddHp(TEST_VALUE);
    }

    if (k1->IsDead())
    {
        cout << "죽었습니다!" << endl;
    }
    else
    {
        cout << "엥? 살았습니다!" << endl;
    }

    delete k1;
}

실행 결과

  • 오버플로우 없이 체력이 정상적으로 증가하고, 최대값(100) 이상으로 설정되지 않습니다.
  • 프로그램이 제대로 동작하며, "엥? 살았습니다!"가 출력됩니다.

[Bug Report #5]

문제 개요

  • 새로운 게임 규칙:
    • 최대 체력(MaxHP) 개념이 추가됨.
    • 체력이 50% 이하로 떨어지면 공격 데미지를 2배로 적용.
  • 발생한 문제:
    • 두 기사가 서로 공격하는 테스트 중 프로그램이 크래시.
    • 크래시 원인: hp 값이 0일 때 정수를 0으로 나누는 예외 발생.

코드 분석

main 함수

int main()
{
    Knight* k1 = new Knight();
    k1->_hp = 100;
    k1->_maxHp = 100;
    k1->_attack = 100;

    Knight* k2 = new Knight();
    k2->_hp = 100;
    k2->_maxHp = 100;
    k2->_attack = 100;

    int damage = k1->GetAttackDamage();
    k2->AddHp(-damage);

    int damage2 = k2->GetAttackDamage();
    k1->AddHp(-damage2);
    
    delete k1;
    delete k2;
}
  • Knight 생성:
    • 두 기사를 생성하고 각각 초기값 설정.
    • _hp = 100, _maxHp = 100, _attack = 100.
  • 데미지 계산:
    • k1->GetAttackDamage()를 호출해 k2에게 데미지 부여.
    • k2->GetAttackDamage()를 호출해 k1에게 데미지 부여.

GetAttackDamage 함수

int Knight::GetAttackDamage()
{
    // hp 50% 이하 => maxHp / hp가 2 이상
    int damage = _attack;

    int ratio = _maxHp / _hp;
    if (ratio > 2)
        damage *= 2;

    return damage;
}
  • 데미지 계산 논리:
    1. 기본 데미지 _attack 초기화.
    2. ratio = _maxHp / _hp 계산:
      • hp가 0이 될 경우 정수를 0으로 나누는 예외 발생.
    3. ratio > 2이면 데미지 2배 적용.

AddHp 함수

void Knight::AddHp(int value)
{
    _hp += value;
    if (_hp < 0)
        _hp = 0;
    if (_hp > _maxHp)
        _hp = _maxHp;
}
  • 체력 변경 로직:
    • _hp 값이 0 이하가 되면 _hp = 0으로 고정.
    • _hp 값이 최대 체력 초과_hp = _maxHp로 고정.

문제 분석 및 해결

문제 원인

  1. GetAttackDamage 함수의 ratio 계산 문제:
    • hp0일 때, int ratio = _maxHp / _hp에서 0으로 나눔.
    • 이로 인해 런타임 에러 발생.

해결 방법

  1. hp == 0 예외 처리 추가:

    • hp가 0일 때 ratio를 계산하지 않고 바로 데미지를 리턴.

    • 수정 코드:

      int Knight::GetAttackDamage()
      {
          int damage = _attack;
      
          if (_hp == 0)  // hp가 0일 때 예외 처리
              return damage;
      
          int ratio = _maxHp / _hp;
          if (ratio > 2)
              damage *= 2;
      
          return damage;
      }
  2. 조건문으로 percentage를 활용한 논리 변경:

    • 비율 계산을 정수형 퍼센트(%)로 변환.

    • 수정 코드:

      int Knight::GetAttackDamage()
      {
          int damage = _attack;
      
          if (_maxHp == 0)  // maxHp가 0인 경우 기본 데미지 반환
              return damage;
      
          int percentage = (_hp * 100) / _maxHp;  // 체력을 퍼센트로 계산
          if (percentage <= 50)  // 체력이 50% 이하일 경우
              damage *= 2;
      
          return damage;
      }

디버깅 과정

  1. 에러 메시지 확인:

    • "0으로 나누기" 오류 발생.
    • 호출 스택 추적 결과 GetAttackDamage 함수의 ratio 계산 부분에서 발생.
  2. 조사식 및 조건부 중단점 설정:

    • _hp가 0인 경우를 확인하기 위해 조건부 중단점 설정.
    • 조건: k1->_hp == 0 or k2->_hp == 0.
  3. 수정 후 결과 확인:

    • 수정된 코드 실행 후 hp == 0 상태에서 예외 없이 정상 동작 확인.

최종 코드

수정된 GetAttackDamage 함수

int Knight::GetAttackDamage()
{
    int damage = _attack;

    if (_hp == 0)  // hp가 0일 경우 예외 처리
        return damage;

    if (_maxHp == 0)  // maxHp가 0일 경우 기본 데미지 반환
        return damage;

    int percentage = (_hp * 100) / _maxHp;  // 체력을 퍼센트로 계산
    if (percentage <= 50)  // 체력이 50% 이하일 경우
        damage *= 2;

    return damage;
}

결과

  • 체력이 50% 이하일 때 공격력이 2배로 적용됨.
  • hp == 0일 때도 프로그램이 크래시 없이 정상 작동.

[Bug Report #6]

문제 설명

  • 기사의 반격 시스템이 구현되었는데, 공격받으면 동일한 대상을 공격하도록 설계되었습니다.
  • 두 기사를 생성하고 반격 시스템을 테스트하는 중, 무한 재귀 호출로 인해 스택 오버플로우가 발생했습니다.

주요 코드 분석

void Knight::OnDamaged(Knight* attacker) {
    if (attacker == nullptr)
        return;

    // 체력 감소 처리
    int damage = attacker->GetAttackDamage();
    AddHp(-damage);

    // 무한 반격이 발생하는 코드
    attacker->OnDamaged(this);
}

문제점:
1. attacker->OnDamaged(this)가 재귀적으로 호출되며 무한히 반복됩니다.
2. 체력 처리가 완료되었음에도 죽거나 무효화 조건 없이 계속 반격이 발생합니다.


해결 방안: 수정 코드

  1. 체력이 0 이하인지 확인:

    • 체력이 0 이하인 경우 반격하지 않도록 조건 추가.
  2. 데미지가 0일 경우 처리:

    • 데미지가 없으면 반격하지 않도록 조건 추가.
void Knight::OnDamaged(Knight* attacker) {
    if (attacker == nullptr)
        return;

    // 공격 받음
    int damage = attacker->GetAttackDamage();
    AddHp(-damage);

    // 데미지가 0이거나 이미 죽었다면 종료
    if (damage == 0 || IsDead())
        return;

    // 반격 처리
    attacker->OnDamaged(this);
}

수정 후 추가된 조건:
1. damage == 0:

  • 공격력이나 데미지가 0이라면 불필요한 반격 방지.
  1. IsDead():
    • 현재 객체가 죽었으면 반격 종료.

디버깅과 예외 상황 처리

  1. 스택 오버플로우 확인:

    • Visual Studio의 스택 오버플로우 예외를 통해 OnDamaged 함수가 무한 호출되었음을 확인.
  2. 추적과 조사식 활용:

    • 재귀 호출이 발생하는 함수에 브레이크포인트 설정.
    • 체력과 데미지 값을 조사식으로 추적하여 조건 미비를 확인.

[Bug Report #7]

문제 설명

게임에서 다수의 클래스(Knight, Archer, Mage)를 관리하기 위해 공통 부모 클래스 Player를 사용하도록 설계했습니다. 추가적으로 Archer 클래스는 펫(Pet)을 가지며, 이를 관리하는 로직이 포함되어 있습니다. 문제는 플레이어를 생성하고 삭제하는 루프에서 메모리 누수가 발생하여 프로그램이 크래시하는 상황입니다.


코드 분석

주요 문제 원인:

  1. 소멸자에서 가상 함수(virtual) 키워드 누락

    • Player 클래스의 소멸자가 가상 함수로 선언되지 않았습니다. 따라서 delete p;를 호출했을 때, 파생 클래스의 소멸자가 호출되지 않습니다.
    • 결과적으로, Archer 클래스의 _pet 객체가 삭제되지 않고 메모리 누수가 발생합니다.
  2. Archer 클래스의 Pet 관리

    • Petnew를 통해 동적 할당되지만, 부모 클래스의 소멸자가 호출될 때 delete되지 않기 때문에 메모리 누수가 점점 증가합니다.

코드의 주요 동작 흐름

  1. 플레이어 객체 생성

    • 무한 루프에서 Knight, Archer, Mage 중 하나를 랜덤으로 생성.
    • 각 객체는 new를 통해 동적 메모리로 할당됩니다.
  2. 플레이어 객체 삭제

    • delete 키워드로 플레이어 객체 삭제.
    • 소멸자가 가상 함수로 선언되지 않아 파생 클래스의 소멸자가 호출되지 않는 문제 발생.
  3. 메모리 누수

    • 특히, Archer 클래스의 _pet 멤버는 동적으로 할당된 메모리가 해제되지 않아 메모리 누수가 발생.

수정 방안

  1. Player 소멸자에 virtual 키워드 추가

    virtual ~Player();
    • 이를 통해 delete 시 파생 클래스 소멸자가 제대로 호출됩니다.
  2. Archer 클래스의 소멸자에서 _pet 해제

    • 기존 코드에서 이미 _petdelete하고 있으므로, 수정 없이 유지.
  3. 추가 테스트

    • 프로그램 크래시 및 메모리 누수가 더 이상 발생하지 않도록 디버거와 메모리 도구를 사용해 확인.

수정 후 코드

Player.h 수정

#pragma once

class Player
{
public:
    Player();
    Player(int hp);
    virtual ~Player();  // 가상 소멸자 추가

    void PrintInfo();
    void AddHp(int value);
    bool IsDead();
    int GetAttackDamage();
    void OnDamaged(Player* attacker);

public:
    int _hp;
    int _maxHp;
    int _attack;
};

Player.cpp 유지

변경 없음.


Archer.h 유지

#pragma once
#include "Player.h"

class Archer : public Player
{
public:
    Archer();
    Archer(int hp);
    ~Archer();

public:
    class Pet* _pet;
};

Archer.cpp 유지

Archer의 소멸자가 제대로 호출되기 때문에 기존 로직을 유지합니다.


메인 함수 유지

int main()
{
    srand(static_cast<unsigned int>(time(nullptr)));

    while (true)
    {
        Player* p = nullptr;

        switch (rand() % 3)
        {
        case 0:
            p = new Knight();
            p->_hp = 100;
            p->_attack = 100;
            break;
        case 1:
            p = new Archer();
            p->_hp = 100;
            p->_attack = 100;
            break;
        case 2:
            p = new Mage();
            p->_hp = 100;
            p->_attack = 100;
            break;
        }		

        delete p;
    }
}

[Bug Report #8]


1. 문제 배경

  • 펫 관리 문제: 기존 코드에서 펫을 Archer 생성자에서 생성하다가, 사장님의 요청으로 펫 생성 로직을 메인 함수로 옮겼습니다.
  • 프로그램 크래시 발생: 펫 생성 방식을 변경한 뒤, 프로그램이 예상치 못한 크래시를 발생시킵니다.
  • 분석 목표: 크래시 원인을 정확히 파악하고 적절히 수정합니다.

2. 핵심 코드 분석

2.1. 메인 함수

switch (rand() % 3)
{
    case 0:
        p = new Knight();
        p->_hp = 100;
        p->_attack = 100;
        break;
    case 1:
        Pet pet; // 지역 변수로 펫 생성
        p = new Archer(&pet); // 펫의 주소를 넘김
        p->_hp = 100;
        p->_attack = 100;
        break;
    case 2:
        p = new Mage();
        p->_hp = 100;
        p->_attack = 100;
        break;
}
delete p; // Player 객체 삭제
  • case 1:
    • 지역 변수 Pet pet을 생성합니다. 이는 스택 메모리 영역에 저장됩니다.
    • new Archer(&pet)로 펫 객체의 주소를 넘겨줍니다.
    • 문제: 스택 메모리의 Pet 객체는 함수가 종료되면 소멸합니다. 따라서 포인터가 소멸된 메모리를 가리켜 크래시가 발생합니다.

2.2. Archer 클래스

Archer::Archer(Pet* pet) : _pet(pet) {}

Archer::~Archer()
{
    if (_pet != nullptr)
        delete _pet; // 포인터로 가리키는 객체 삭제
}
  • 소멸자 문제:
    • _petnew로 동적 생성된 객체를 가리켜야 합니다. 하지만 스택 변수의 주소를 받으면 소멸 시 delete로 스택 메모리를 해제하려고 시도합니다.
    • 결과적으로 크래시를 유발합니다.

3. 크래시 원인

  1. 스택 메모리 사용: 메인 함수에서 생성된 지역 변수 Pet pet의 주소를 넘겨줍니다.
  2. 소멸 시 잘못된 해제: 지역 변수의 주소를 동적 메모리로 착각해 delete를 호출합니다.

4. 해결 방법

4.1. 해결 코드

  • 메모리를 항상 동적으로 할당하고, 올바르게 관리합니다.
switch (rand() % 3)
{
    case 0:
        p = new Knight();
        p->_hp = 100;
        p->_attack = 100;
        break;
    case 1:
        p = new Archer(new Pet()); // 동적 메모리로 Pet 생성
        p->_hp = 100;
        p->_attack = 100;
        break;
    case 2:
        p = new Mage();
        p->_hp = 100;
        p->_attack = 100;
        break;
}
delete p; // Player 객체와 내부 펫 메모리 해제

4.2. 개선 사항

  1. 가상 소멸자 추가

    • 부모 클래스 Player의 소멸자에 virtual 키워드를 추가하여, 자식 클래스의 소멸자가 호출되도록 보장합니다.
    virtual ~Player();
  2. 스마트 포인터 도입

    • 메모리 누수를 방지하기 위해 std::unique_ptr 또는 std::shared_ptr를 사용합니다.
    std::unique_ptr<Pet> _pet;

[Bug Report #9]

  • 문제 상황:
    궁수가 죽으면 펫(Pet)이 같이 죽도록 구현한 코드에서 프로그램이 크래시가 발생했습니다.
    double free 에러, 즉 메모리를 두 번 해제해서 발생한 문제입니다.

  • 목표:
    크래시의 원인을 찾아 수정하여 프로그램이 정상적으로 작동하도록 만들기.


코드 분석

1. main 함수

Archer* archer = new Archer(new Pet());
archer->_hp = 100;
archer->_maxHp = 100;
archer->_attack = 20;

Knight* knight = new Knight();	
knight->_hp = 150;
knight->_maxHp = 150;
knight->_attack = 100;

int damage = knight->GetAttackDamage();
archer->AddHp(-damage);

delete archer;
delete knight;
  • 코드 흐름:

    1. 궁수(Archer)를 생성합니다.
      이때, 새로운 펫(Pet)도 함께 생성되어 Archer 생성자로 전달됩니다.
    2. 기사가 궁수를 공격하여 HP가 감소(AddHp(-damage)).
    3. 궁수가 죽으면 펫도 삭제됩니다(delete _pet).
    4. 마지막으로 궁수와 기사를 삭제합니다(delete archer, delete knight).
  • 문제의 원인:

    • 궁수가 죽을 때 AddHp에서 펫을 삭제합니다.
      이후 Archer 객체가 삭제될 때, 소멸자에서 delete _pet을 다시 호출하여 펫이 두 번 삭제됩니다.
      이로 인해 double free 에러가 발생합니다.

2. Archer 클래스

class Archer : public Player {
public:
    Archer(Pet* pet);
    ~Archer();

    virtual void AddHp(int value);

public:
    Pet* _pet;
};
  • 주요 코드:

    • 생성자:

      Archer::Archer(Pet* pet) : _pet(pet) {}
      • 펫 객체를 받아 멤버 변수 _pet에 저장.
    • 소멸자:

      Archer::~Archer() {
          if (_pet != nullptr) delete _pet;
      }
      • 소멸될 때 _petnullptr이 아니면 삭제.
    • AddHp 함수:

      void Archer::AddHp(int value) {
          Player::AddHp(value);
      
          if (IsDead()) {
              delete _pet;
          }
      }
      • 체력이 줄어드는 로직.
        궁수가 죽으면 펫도 삭제.

3. 문제 분석

  1. 문제의 흐름:

    • AddHp에서 궁수가 죽었을 때(IsDead()), delete _pet을 실행하여 펫을 삭제합니다.
    • 이후 Archer 객체가 삭제될 때 소멸자에서 또 한 번 delete _pet이 실행됩니다.
    • 이미 삭제된 메모리를 다시 삭제하려고 하면서 double free 에러가 발생합니다.
  2. 문제의 원인:

    • AddHp 함수에서 delete _pet을 실행한 후 _petnullptr로 설정하지 않아서 생긴 문제.
  3. 문제 상황 요약:

    • 두 번 삭제되는 펫 → 프로그램 크래시.

해결 방법

1. 내가 수정한 방식

  • 수정 코드:

    void Archer::AddHp(int value) {
        Player::AddHp(value);
    
        if (IsDead()) {
            delete _pet;
            _pet = nullptr; // 삭제 후 nullptr로 설정
        }
    }
  • 설명:

    • delete _pet 이후 _petnullptr로 설정하여 이후 추가 삭제를 방지합니다.
    • 소멸자에서도 _petnullptr인지 확인 후 삭제를 실행.

2. 선생님 방식

  • 소멸자에서도 안전장치 추가:

    Archer::~Archer() {
        if (_pet != nullptr) {
            delete _pet;
            _pet = nullptr; // 추가로 nullptr로 설정
        }
    }
  • AddHp 함수에서 삭제와 함께 초기화:

    void Archer::AddHp(int value) {
        Player::AddHp(value);
    
        if (IsDead()) {
            delete _pet;
            _pet = nullptr;
        }
    }
  • 설명:

    • 궁수가 죽으면 AddHp에서 펫을 삭제하고 nullptr로 설정.
    • 소멸자에서도 중복 삭제를 방지하기 위해 _pet을 확인.

최종 코드

void Archer::AddHp(int value) {
    Player::AddHp(value);

    if (IsDead()) {
        delete _pet;
        _pet = nullptr; // 삭제 후 nullptr로 설정
    }
}

Archer::~Archer() {
    if (_pet != nullptr) {
        delete _pet;
        _pet = nullptr; // 추가 안전장치
    }
}

[Bug Report #A]


문제 상황 요약:

이 코드는 궁수와 화살을 사용해 기사를 공격하는 기능을 구현하고 있습니다. 화살(Arrow) 클래스는 타겟(_target)을 관리하며, 타겟의 체력을 공격합니다. 테스트 중에 기사(Knight)가 죽었을 때, 죽은 기사를 타겟으로 화살이 계속 공격을 시도하면서 프로그램이 크래시(access violation)가 발생합니다.


문제 원인

  1. use-after-free 문제:

    • 기사가 죽으면 delete knight로 객체가 소멸됩니다. 하지만 화살(Arrow) 객체는 여전히 소멸된 기사를 가리키고 있어 _target->AddHp()_target->PrintInfo()를 호출하려고 하면 무효한 메모리 접근이 발생합니다.
  2. 반복문 순서 문제:

    • 화살을 순회하면서 타겟팅 작업을 하는 동안, 타겟이 유효한지 확인하지 않고 공격을 수행합니다.
    • 기사가 죽더라도 knight 포인터의 값이 초기화되지 않은 상태로 남아있어 예외가 발생합니다.

코드 분석

Arrow 클래스 분석:

  • Arrow::AttackTarget 메서드는 _targetnullptr인지 확인하고 체력을 깎는 작업을 수행합니다.
  • _target이 유효하지 않은 경우, nullptr로 초기화하지 않으면 죽은 객체를 참조하게 됩니다.
void Arrow::AttackTarget()
{
    cout << "<화살이 적을 피격합니다!>" << endl;

    // 공격 대상이 있다면
    if (_target != nullptr)
    {
        // 데미지를 입힌다
        _target->AddHp(-_damage);
        _target->PrintInfo();  // 여기서 _target이 죽어있으면 오류 발생
    }
}

main 함수 분석:

  • 기사가 죽으면 delete knight를 통해 소멸시킨 후에도 화살이 공격을 시도합니다.
  • 죽은 타겟(knight)을 가리키고 있는 화살은 여전히 타겟팅 작업을 수행하려고 하며, 이는 프로그램의 크래시로 이어집니다.
for (int i = 0; i < 10; i++)
{
    arrows[i]->AttackTarget();

    // 기사가 죽었으면 소멸시켜준다
    if (knight != nullptr)
    {
        if (knight->IsDead())
        {
            delete knight;
            knight = nullptr;  // knight 포인터 초기화
        }
    }

    delete arrows[i];
    arrows[i] = nullptr;
}

해결 방법

해결 방법 1: 타겟 유효성 검사를 강화

  • 화살(Arrow) 클래스의 AttackTarget 메서드에서 타겟이 유효한지 확인 후 작업을 수행합니다.
  • nullptr로 초기화되지 않은 타겟에 접근하지 않도록 주의합니다.
void Arrow::AttackTarget()
{
    if (_target == nullptr)
    {
        cout << "타겟이 없습니다. 공격을 중지합니다." << endl;
        return;
    }

    _target->AddHp(-_damage);

    if (_target->IsDead())
    {
        cout << "타겟이 사망했습니다." << endl;
    }
    else
    {
        _target->PrintInfo();
    }
}

해결 방법 2: 기사를 소멸하지 않고 관리

  • 기사가 죽더라도 화살이 타겟을 잃지 않도록 knight 객체를 삭제하지 않고, 죽은 상태로 유지합니다.
  • 모든 화살 작업이 끝난 후에 삭제 작업을 수행합니다.
for (int i = 0; i < 10; i++)
{
    arrows[i]->AttackTarget();
}

// 화살이 다 소멸된 후 실행
if (knight != nullptr)
{
    delete knight;
    knight = nullptr;
}

해결 방법 3: 스마트 포인터 사용

  • 화살과 타겟 객체를 스마트 포인터로 관리해 자동으로 메모리를 관리합니다.

최종 코드 (수정 후)

for (int i = 0; i < 10; i++)
{
    // 기사 생존 여부를 먼저 확인
    if (knight != nullptr && knight->IsDead())
    {
        delete knight;
        knight = nullptr;
    }

    // 타겟이 유효할 때만 공격
    if (knight != nullptr)
    {
        arrows[i]->AttackTarget();
    }

    delete arrows[i];
    arrows[i] = nullptr;
}

// 화살 작업이 끝난 후 삭제
delete archer;
delete knight;

profile
李家네_공부방

0개의 댓글