이 Step에서 다루는 것

  • 구조체(예: StatInfo)를 함수에서 만들고 전달하는 3가지 방식
    • 값으로 반환(가장 안전/가장 추천)
    • 참조/포인터로 채우기(원본 수정)
    • 포인터 반환(수명 문제로 위험해질 수 있음)
  • 가장 치명적인 버그: Dangling Pointer(댕글링 포인터) 를 “스택 수명” 관점에서 이해하기

학습 목표

  • “지역 변수의 주소를 반환하면 왜 위험한지”를 스택 프레임 관점에서 설명할 수 있다.
  • 같은 기능을 값 반환 / 포인터 인자 / 참조 인자 3가지로 구현해보고 차이를 말할 수 있다.

CreatePlayer: 값 반환 (가장 안전, 기본 추천)

StatInfo CreatePlayer() {
    StatInfo info = {100, 10, 2};
    return info;  // 값으로 반환
}

StatInfo player = CreatePlayer();
  • info는 함수의 지역 변수라서 함수가 끝나면 스택 프레임과 함께 사라집니다.
  • 하지만 “값으로 반환”은 반환 과정에서 값이 바깥으로 전달되므로(복사/최적화 포함), 수명 문제가 없습니다.

보너스 감각: “복사 비용”이 걱정될 수 있지만, 컴파일러 최적화(RVO/NRVO 등)로 비용이 줄어드는 경우가 많습니다.
초반 학습 단계에서는 안전한 코드가 우선입니다.


CreateMonster: 포인터/참조로 채우기 (원본 수정)

포인터 버전: “없을 수도 있다(nullptr)”를 표현할 수 있음

void CreateMonster(StatInfo* info) {
    if (info == nullptr)
        return;

    info->hp = 40;
    info->attack = 8;
    info->defence = 1;
}

StatInfo monster;
CreateMonster(&monster);
  • 포인터 인자는 “원본을 직접 채운다”는 의미입니다.
  • 장점: nullptr로 “대상이 없다”를 표현 가능
  • 단점: 호출하는 쪽에서 널 체크 규칙을 지켜야 함

참조 버전: “반드시 있어야 한다”를 표현(더 안전)

void CreateMonsterRef(StatInfo& info) {
    info.hp = 40;
    info.attack = 8;
    info.defence = 1;
}

Battle: 포인터로 전투

void Battle(StatInfo* player, StatInfo* monster) {
    if (player == nullptr || monster == nullptr)
        return;

    while (true) {
        int damage = player->attack - monster->defence;
        if (damage < 0) damage = 0;
        monster->hp -= damage;
        if (monster->hp <= 0) return;

        damage = monster->attack - player->defence;
        if (damage < 0) damage = 0;
        player->hp -= damage;
        if (player->hp <= 0) return;
    }
}
  • 두 구조체 포인터로 “원본 스탯”을 직접 조작합니다.
  • 실전에서는 보통 HP가 0 아래로 내려가지 않게 0으로 고정(clamp)하는 처리도 자주 추가합니다.

Dangling Pointer (위험한 반환)

핵심: 지역 변수의 주소를 반환하면, 함수가 끝나는 순간 그 주소는 더 이상 “내 데이터”가 아닙니다.

StatInfo* CreatePlayer2() {
    StatInfo info = {100, 10, 2};
    return &info;  // ❌ 함수 종료 시 info는 스택에서 소멸
}

StatInfo* player = CreatePlayer2();
std::cout << player->hp << '\n';  // 쓰레기값 또는 크래시
  • info는 CreatePlayer2의 스택 프레임 내부에 있었기 때문에, 함수가 끝나면 그 프레임이 반납됩니다.
  • 그러면 &info로 얻은 주소는 “그냥 어떤 스택 자리”를 가리키는 값이 되어버리고,
    운이 나쁘면 다른 함수 호출로 덮어써져 쓰레기 값/크래시가 발생합니다.

개념 그림:

CreatePlayer2() 프레임 안의 info 주소를 반환
┌────────── CreatePlayer2 프레임 ──────────┐
│ info {hp=100, attack=10, defence=2}      │  ← &info
└─────────────────────────────────────────┘
함수 종료 → 프레임 반납 → 이 자리는 곧 덮어써질 수 있음

안전한 대안 3가지

  • (추천) 값 반환: StatInfo CreatePlayer() 처럼 반환
  • (원본 채우기) 참조/포인터 인자: void CreatePlayer(StatInfo& out) 형태
  • (고급) 동적 할당: new로 만들고 수동 해제(delete)가 필요
    → 초반에는 실수 위험이 크므로, 나중에 스마트 포인터를 배운 뒤 권장

체크 질문 (스스로 답해보기)

  • return &info;가 위험한 이유를 “스택 프레임” 관점으로 설명해보자.
  • 포인터 인자참조 인자는 언제 선택하는 게 더 좋을까?
    • “없을 수도 있다” vs “반드시 있어야 한다” 관점

profile
李家네_공부방

0개의 댓글