1. 포인터의 형 변환

(1) 객체와 포인터

#include <iostream>
using namespace std;

class Item
{
public:
    Item()
    {
        cout << "Item()" << endl;
    }

    Item(const Item& item)
    {
        cout << "const Item(const item& item)" << endl;
    }

    ~Item()
    {
        cout << "~Item()" << endl;
    }

public:
    int _itemType = 0;
    int _itemDbId = 0;

    char _dummy[4096] = {}; // 여러 정보로 인해 비대해진 멤버 변수

};


int main()
{
    {
        Item item; // Stack [ type(4) dbId(4) dummy(4096)]

        Item* item2 = new Item(); // Stack [ 주소 (4~8) ], Heap [ type(4) dbId(4) dummy(4096)]


    }

    return 0;
}

큰 더미 데이터를 가지는 item2 는 포인터이기 때문에 생성자와 소멸자를 호출하지 않는다.

즉 실제 데이터를 가지는 item은 4104 바이트 크기의 객체이고

포인터인 item2는 Item의 객체이지만 주소를 저장하고 있다(실제 데이터는 힙 영역에 있다).

 

추가)위에서 처럼 new 이후 delete를 사용하여 할당 해제를 해주지 않으면 메모리 누수가 발생한다.

 

(2) 배열의 포인터

포인터가 배열로 만들어지면 실제 데이터가 100개가 아닐 수도 있다.

배열의 각 요소는 단지 그의 주소를 저장하고 있을 뿐이다.

 

2. 연관성이 없는 클래스 간 포인터 변환

class Knight
{
public:
    int _hp = 0;    
};

int main()
{
    Knight* knight = new Knight();

    Item* item = (Item*)knight;

    return 0;
}

knight는 Knight의 데이터의 주소를 저장한다(데이터는 힙 영역에 있다).

Item 형식으로 knight에 명시적 형변환을 해준 item 에서는 문제가 발생하게 된다.

 

item의 Type과 DbId를 변경시키면, knight의 처음 4바이트를 Type의 값으로 수정하게되지만

원래 knight는 4바이트의 hp 영역만 가지고 있었으므로 엉뚱한 메모리 영역을 변경한 DbId의 값으로 변경하게 된다.

따라서 명시적 형변환은 주의하여 사용해야한다.

 

3. 연관성이 있는 클래스 간 포인터 변환

enum ItemType
{
    IT_WEAPON = 1,
    IT_ARMOR
};

class Item
{
public:
    Item()
    {
        cout << "Item()" << endl;
    }

    Item(int itemType) : _itemType(itemType)
    {

    }

    ~Item()
    {
        cout << "~Item()" << endl;
    } 

public:
    int _itemType = 0;
    int _itemDbId = 0;

    char _dummy[4096] = {}; // 여러 정보로 인해 비대해진 멤버 변수

};

class Weapon : public Item
{
    public:
    Weapon() : Item(IT_WEAPON) {cout << "Weapon()" << endl;};
    ~Weapon() {cout << "~Weapon()" << endl;};
};

class Armor : public Item
{
    public:
    Armor() : Item(IT_ARMOR) {cout << "Armor()" << endl;};
    ~Armor() {cout << "~Armor()" << endl;};
};

 

(1) 부모(아이템) -> 자식(무기)

부모는 자식인가?

(아이템은 무기인가?)

int main()
{
    Item* item = new Item();

    Weapon* weapon = (Weapon*) item;

    delete item;

    return 0;
}

위처럼 명시적 형변환을 하면, item은 기본적으로 Type과 DbId를 가지고 있고 weapon가 하나의 멤버변수(예로 데미지)를 들고 있다고 하면

delete item 이전에 weapon 포인터의 값을 수정하는 것은 임의의 메모리 값을 수정하는 것과 같은 위험한 동작이다.

(weapon은 4바이트의 영역만 가지고 있었는데 4바이트를 넘는 영역을 수정하게 됨)

 

(2) 자식(무기) -> 부모(아이템)

반대로 자식(무기) -> 부모(아이템) 형변환은 암시적 형변환으로도 잘 되는 것을 알 수 있다.

Weapon* weapon = new weapon();
Item* item = weapon;

 

3. 명시적 형변환의 필요성

위에서 명시적 형변환을 할 때는 항상 조심해야 한다는 것을 알 수 있다. 그렇다면 평생 명시적 형변환을 사용할 일은 없을까?

다음과 같이 20칸의 인벤토리에 1/2 확률로 무기 혹은 방어구가 들어가는 코드를 작성해본다.

int main()
{

    Item* inventory[20] = {};

    srand((unsigned int) time(nullptr));

    for(int i = 0; i < 20; i++)
    {
        int randValue = rand() % 2;
        switch(randValue)
        {
            case 0:
            inventory[i] = new Weapon();
            break;

            case 1:
            inventory[i] = new Armor();
            break;
        }
    }
    return 0;
}

위 처럼 Item 형 포인터인 inventory에는 Item의 자식으로 상속받고 있는 Weapon 또는 Armor가 들어가게 된다.

    for (int i=0; i<20; i++)
    {
        Item* item = inventory[i];
        
        if (item == nullptr)
            continue;
        
        if (item->_itemType == IT_WEAPON)
        {
            Weapon* weapon = (Weapon*) item;
        }
    }

 

item 포인터는 20개의 인벤토리 배열의 주소를 가지는데, 각 주소를 찾아가서 아이템 타입이 IT_WEAPON이면

item(각 인벤토리 칸)을 Weapon 타입으로 형변환 한다.

즉, 각 인벤토리 칸에 weapon이 들어가있다는 뜻이고, weapon->_atk = 20; 과 같이 

그 인벤토리 칸 안에 있는 weapon의 멤버 변수를 수정할 수 있게 된다.

 

4. 형 변환 이후 소멸자 문제

포인터를 사용 후에 weapon 또한 소멸되어야 한다.

그러나, item을 weapon 으로 형변환 해주었기 때문에 item만 소멸자가 호출되는 것을 볼 수 있다.

따라서 아래와 같이 직접 delete로 소멸시켜줘야 한다.

    for (int i=0; i<20; i++)
    {
        Item* item = inventory[i];

        if (item == nullptr)
            continue;
        
        if (item->_itemType == IT_WEAPON)
        {
            Weapon* weapon = (Weapon*) item;
            delete weapon;
        }
        else
        {
            Armor* armor = (Armor*) item;
            delete armor;
        }
    }

아니면 부모의 소멸자를 가상 함수로 만들어 주고 이후에 delete item을 해주면 된다.

class Item
{
public:
 // ~~
    virtual ~Item()
    {
        cout << "~Item()" << endl;
    } 
 // ~~
//~~
    for (int i=0; i<20; i++)
    {
        Item* item = inventory[i];

        if (item == nullptr)
            continue;
        
        delete item;
    }
//~~

즉 부모 클래스의 소멸자는 꼭 virtual을 붙여주어야 한다.

'기초 C++ 스터디 > 객체지향' 카테고리의 다른 글

6-5. 캐스팅  (0) 2023.06.02
6-4. 얕은 복사, 깊은 복사  (0) 2023.06.01
6-2. 형(type) 변환 (1)  (0) 2023.05.31
6-1. 동적 할당  (0) 2023.05.31
5-7. 객체지향 정리  (0) 2023.05.30