Function Pointer

함수 선언이나 변수 선언을 할때, 항상 앞에는 타입이 존재했다. return 받을 타입이나, 아니면 이 타입으로 저장한다는 식으로. 그렇다면 함수를 변수로 사용할수 있을까? 의 질문의 시작이 함수 포인터의 시작이다.

일단 함수를 뭔가 변수로 설정하려고 한다면, 함수의 signature 이 중요하다. 일단은 사실상 함수의 이름은 신경 써주지 않는다고하고 아래의 코드를 보면 int(int a, int b) 이런식으로 된다. 근데 어떻게 이름을 줄까? 라는 생각을 해보는데. 이건 typedef 로 생각하면 된다.

그래서 signature 은 typedef int(FUNC_TYPE)(int a, int b) 이런식으로 주면 된다. 하지만 Modern C++ 에서는 더 편한걸로 using 을 사용해서, using FUNC_TYPE = int(int a, int b) 이런식으로 표현이 가능하기도 하다. 그런다음에 FUNC_TYPE* fn 이라고 생성하면 함수의 포인터라고 할수 있다.

int Add(int a, int b)
{
    return a + b;
}

int main()
{
    int Add(int a, int b);
    return 0;
}

아래의 코드를 봐보기전에, 뭔가 함수의 return 값을 봐보면, 함수의 시작 주소가 있다는걸 힌트를 알수 있다. 그 말은 즉슨 포인터를 이용해, 그 함수를 point 하게 둘수 있다는 말이다. 아래와 같이 fn 즉 FUNC_TYPE 의 signature 만 맞으면, 함수를 호출이 가능하다는 소리이다. 그리고 아래의 (*fn) 이 signature 같은 경우는 fn 을 타고 들어가서 (1, 2) 를 넣어준다라고 생각하면 될것 같다.

int Add(int a, int b)
{
    return a + b;
}

int main()
{
    typedef int(FUNC_TYPE)(int, int);
    FUNC_TYPE* fn;
    fn = Add;

    int result = fn(1, 2);
    int result = (*fn)(1, 2);
}

근데 구지 생각을 해보면, 왜 이렇게 까지 써야되냐? 라는 질문을 할 수 있는데, 그 이유는 함수의 signature 가 동일하게 되면 쉽게 스위칭이 가능하다 라는 말이다. 아래의 코드를 한번 봐보자. Add 를 다 쓴 이후 Sub 으로 바로 스위칭이 가능하다. 어떻게 보면 나머지를 고칠 필요 없이, 강력한 Tool 로 쓸수 있다.

int Add(int a, int b)
{
    return a + b;
}

int Sum(int a, int b)
{
    return a - b;
}

int main()
{
    typedef int(FUNC_TYPE)(int, int);
    FUNC_TYPE* fn;
    fn = Add;
    // add.. 를 다 쓴이후
    fn = Sub;
    // sub 처리
}

또 다른 예제를 알아보자. 아래의 Item 클래스를 만들어줬고 Item pointer 를 return 해주는 코드가 있다고 하자. 뭔가 같은 기능을 계속 인자만 바꿔서 주는게 굉장히 코드가 길어지고, 유지 보수가 좋지 않다. 그래서 인자값으로 함수의 안전체크나 조건문들을 넘겨주게 된다면 이 코드의 유지 보수성은 더 올라갈것이다.

class Item
{
public:
    Item() : _itemId(0), _rarity(0), _ownerId(0){}
public:
    int _itemId;
    int _rarity;
    int _ownedId;
};

Item* FindItemByItemId(Item items[], int itemCount, int itemId)
{
    for(int i = 0; i < itemCount; i++)
    {
        Item* item = &items[i];
        if (item->_itemId == item)
            return item;
    }

    return nullptr;
}

Item* FindItemByRarity(Item items[], int itemCount, int rarity)
{
    for(int i = 0; i < itemCount; i++)
    {
        Item* item = &items[i];
        if (item->_rarity == rarity)
            return item;
    }

    return nullptr;
}

그래서 더 낳은 코드를 한번 봐보겠다. 그리고 이번엔 rarity 를 체크 해보겠다.

class Item
{
public:
    Item() : _itemId(0), _rarity(0), _ownerId(0){}
public:
    int _itemId;
    int _rarity;
    int _ownedId;
};

void IsRareItem(Item* item)
{
    return item->_rarity >= 2;
}

Item* FindItem(Item items[], int itemCount, bool(*selector)(Item*item))
{
    for(int i = 0; i < itemCount; i++)
    {
        Item* item = &items[i];
        if (selector(item))
            return item;
    }

    return nullptr;
}

int main()
{
    Item item[10] = {};
    item[3]._rarity = 2;
    Item* rareItem = FindItem(items, 10, IsRareItem);
}

그런데, 물론 장점도 있지만 단점도 있다. 아까 계속 언급한 함수의 signature 가 달라진다면 물론 다르게 표현을 해야된다. 아래의 코드를 보면 실패하게 된다 왜냐하면 isOwnerItem 의 signautre 가 아까 IsRareItem 과 다르기 때문이다. 그래서 아래의 코드를 보면 같이 맞춰지게 int 값도 따로 추가해주면 된다. 즉 꼬리의 꼬리물기가 되서, 뭔가 함수의 인자수가 많아져서 좋지 않은 구조가 된다. 즉 결론은 함수도 주소가 있어서 함수포인터를 사용해서 함수의 call 바로 할수 있다.

class Item
{
public:
    Item() : _itemId(0), _rarity(0), _ownerId(0){}
public:
    int _itemId;
    int _rarity;
    int _ownedId;
};

void IsRareItem(Item* item, int)
{
    return item->_rarity >= 2;
}

bool isOwnerItem(Item* item, int ownderId)
{
    return item->_ownderId == ownderId;
}

Item* FindItem(Item items[], int itemCount, bool(*selector)(Item*item, int), int value)
{
    for(int i = 0; i < itemCount; i++)
    {
        Item* item = &items[i];
        if (selector(item, value))
            return item;
    }

    return nullptr;
}

int main()
{
    Item item[10] = {};
    item[3]._rarity = 2;
    Item* rareItem = FindItem(items, 10, isOwnerItem);
}

사실은 블로그를 커버하다가, 한번도 typedef 에 대해 설명을 하지 않았다. typedef 그냥 형태 만 봤을때 typedef [] [] 이런식으로 생겼다. 하지만 봤을때 오른쪽이 커스텀 타입을 정의를 했었다. 근데 이걸 더 정확하게 보면, 선언 문법에서 typedef 를 앞에다 붙이는쪽으로 왔었다. 아래와 같이 선언을 하면, 바로 앞에 typedef 를 붙여지는거다. 그래서 Code Segment 를 봤을때 아래를 보면 함수의 Signature 은 (int, int)의 인자를 받고 int 로 받고, 그다음에 함수의 포인터이기때문에 (*PFUNC) 라는걸 선언을 한거다. 그다음에 typedef 를 넣으면 된다.

int INTEGER;
int *POINTER;
int FUNC();

int (*PFUNC)(int, int);

근데 또 단점이 있다. 위와같이 사용할때는 전역함수 / 정적함수만 담을 수 있다. 즉 호출 규약이 정해져있다는 소리이다. 아래처럼 Member Function 에서는 에러가 나온다는걸 확인할수있다.

typedef int(*PFUNC)(int, int);

class Knight
{
public:
    // Static Function
    static void HelloWorld(){}
    
    // Member Function
    int GetHP(){ return _hp; }

    int _hp = 100;
}

int main()
{
    PFUNC fn;
    // fn = GetHP; // 에러
    // fn = &Knight::GetHp;
    return 0;
}

그래서 이걸 멤버함수에 속한다라는걸 보여주어야기 때문에 아래의 코드 처럼 하면된다. 아래에 보면, 주소값을 달라는 표시도 해주어야한다. 이건 C 언어의 호환성 때문에 한다.


class Knight
{
public:
    // Static Function
    static void HelloWorld(){}
    
    // Member Function
    int GetHP(){ return _hp; }

    int _hp = 100;
}

typedef int(Knight::*MEMBERPFUNC)(int, int);

int main()
{
    PMEMFUNC mfn;
    mfn = &Knight::GetHp;

    Knight k1;
    (k1.*mfn)(1, 1);

    Knight* k2 = new Knight();
    (k2->*mfn)(1,1);
    delete k2;

    return 0;
}

일반적으로 자신과 다른 클래스가 있고 멤버함수가 동일하다고 하더라도, 이미 지정해주었기 때문에, 객체를 바꾸더라도 실행이 안된다는거에 주의하자.

Functor

함수 객체는 함수처럼 동작하는 객체를 뜻하는데, 위와 같이 함수 포인터의 단점이 너무 잘보였었다. 일단 함수의 signature 가 동일한 친구들 만 사용됬었고 다르다고 하다면, 인자를 늘려가야하는 큰단점, 즉 generic 하게 사용하지 못한다는 점이 큰 단점이 였다. 또 다른 큰 단점은 자세하게 debug 하지 않으면 객체의 상태의 유지성을 모른다는거다. 예를 들어서 Knight 의 객체 안에 _hp 라는 field 가 있는데, 함수 포인터 같은 경우는 인자만 넘기지, 그 field 가 뭘하는지, 유지 됬는지 잘모른다는 뜻이다.

함수처럼 동작하는 객체라는게 뭘까라는 걸 알아보자. 일단 함수 처럼 작동하려면 힌트는 () 이런 연산자가 필요하다. () 연산자 오버로딩이 필요하다는거다.

class Functor
{
public:
    void operator() ()
    {
        cout << "Functor Test1" << endl;
    }
    bool operator() (int num)
    {
        cout << "Functor Test2" << endl;
        _value += num;
        cout << _value << endl;
    }
private:
    int _ value = 0;
}

int main()
{
    Functor functor;
    functor();

    bool ret = functor(3);
    return 0;
}

예시로 보여준건 MMO 에서 함수 객체를 사용하는 예시가 있다. 게임은 클라와 서버가 있는데, 서버같은 경우는 클라가 보내준 네트워크 패킷을 받아서 처리하는데 만약에, 클라가 (5,0) 으로 좌표로 이동 시켜줘! 라고 서버한테 요청을 했다고 하자. 실시간 MMO 라고 하면 클라가 정말 많을텐데, 이때 처리할때 사용할수 있다. 이때, Functor 만들어준 시점과 그리고 실제 실행할 시점을 분리 시키는걸 볼수 있다. 아래가 바로 Command Pattern 을 비슷하게 사용해서 Tracking 이 가능할거다.

class MoveTask
{
public:
    void operator()()
    {
        // TODO
        cout << "Move" << endl;
    }
public:
    int _playerId;
    int _posX;
    int _posY;
}

int main()
{
    MoveTask task;
    task._playerId = 100;
    task._posX = 5;
    task._posY = 3;

    // 나중에 여유될때 일감을 실행
    task();
    return 0;
}

Template Basics

Template 이란 함수나 클래스를 찍어내는 툴이라고 생각하면 된다. 템플릿의 종류는 Function Template 과 Class Template 이 존재한다. 일단 예시같은 경우는 이런거다 여러 다른 타입들을 받는 똑같은 함수가 존재한다고 하자. 다 똑같은 기능을 가지고 있지만 인자값으로 다르게 존재 하는걸 볼수 있다. 이거를 한번에 묶을수 있는 존재가 있을까? 라는 생각이든다. 그게 바로 형식을 틀로 잡을수 있는 template 이 있다. 즉 조커 카드이다.

#include <iostream>
using namespace std;
void Print(int a){ cout << a << endl;}

void Print(float a){ cout << a << endl; }

void Print(double a){ cout << a << endl; }

Template 에 대해서 한번 봐보자. 일단 아래의 코드처럼 typename T 라고 지정해준걸 볼수 있다. 그리고 그 인자의 type 을 Print() 함수에 넣어진걸 확인 할 수 있다. 즉 어떤 타입이 들어오든지 T 로 들어와서 컴파일러가 각각 type 을 정해주는걸로 볼수 있다. 아래 처럼 코드를 실행 해보았을때, 잘 빌드가 성공된걸 볼수 있다. 그리고 맨 아래에 보면 Print 뒤에 <double> 이란게 나와있다. 이거 같은 경우는 컴파일러에 맡기는것보다 내가 이런 형식을 원한다라는걸 강제적으로 줄수도 있다. 이말은 즉슨 컴파일러에게 힌트를 주는거다. 그리고 아래의 Add 함수를 봐보자. 여기에서는 인자가 같을때 즉 (int, int), (double, double), 이런식으로 타입이 같은 인자가 들어왔을때도 줄일수도 있고, return 값으로도 T 를 줄수도 있다. 이 말은 또 각기 다른 타입의 인자를 넣어줄수도 있다. PrintMultiple() 을 참고하자. 그리고 객체의 있는걸 Type 으로 넘겨준다고 하면 실행 되지 않는다. 그 이유는 Knight 라는 클래스는 프로그래머가 만든 커스텀 한 타입이기때문에 지원되지 않아서 ofstream 에 있는걸 가져다가 오버로딩을 해줘야한다.

template<typename T>
void Print(T a)
{
    cout << a << endl;
}

template<typename T>
T Add(T a, T b)
{
    return a + b;
}

template<typename T1, typename T2>
void PrintMultiple(T1 a, T2 b)
{
    cout << a << " " << b << endl;
}

class Knight
{
public:
    // ...
public:
    int _hp = 100;
};

// 연산자 오버로딩 (전역함수)
ofstream& operator<<(ofstream& os, const Knight& k)
{
    os << k._hp << endl;
    return os;
}

int main()
{
    Print(50);
    Print(50.0f);
    Print<double>(50.0);

    ret = Add(2, 3);

    PrintMultiple("Hello", 3);

    Knight k1;
    Print(k1); // 연산자 오버로딩이 없으면 안됨
    return 0;
}

위와 같이 template 은 조커카드였다. 하지만 어떤객체에 대해서 특수 조커 카드를 만들려고 했을때, 어떻게 해야될까? 라는 질문을 할수 있다. 즉 어떤 규칙을 따르는 template 을 template 특수화라고 한다.아래의 코드를 봐보자. 아래와 같이 어떤 특정한걸 주고 싶을때는 객체를 인자로 넘겨주고 temmplate 안에 있는거는 비어있게 해줘야 template 의 특수화가 된다.

tempalte<>
void Print(Knight a)
{
    cout << "Knight" << endl; 
    cout << a._hp << endl;
}

int main()
{
    Knight k1;
    Print(k1);
    return 0;
}

이제 class template 을 보기위해 아래 코드를 봐보자. 일단 RandomBox 라는 클래스가 있고, 만약에 GetRandomData() 가 만약에 Float 로 내뱉는 return 이 필요하다고 하면 float 데이터를 담을수 있는 바구니가 필요하면서, float 을 return 하는 GetRandomData() version 을 만들어야할것이다. 그래서 이 다음 코드를 봐보면 template 이 적용된걸 볼 수 있다.

class RandomBox
{
public:
    int GetRandomData()
    {
        int idx = rand() % 10;
        return _data[idx];
    }
public:
    int _data[10];
}

int main()
{
    srand(static_cast<unsigned int>(time(nullptr)));
    RandomBox rb1;
    for (int i = 0; i < 10; i++)
    {
        rb1._data[i] = i;
    }
    int value1 = rb1.GetRandomData();
    cout << value1 << endl;
    
    RandomBox rb2;
    for (int i =0; i< 10; i++)
    {
        rb2._data[i] = i;
    }
    int value2 = rb2.GetRandomData();
    cout << value2 << endl;
    return 0;
}
template<typename T>
class RandomBox
{
public:
    T GetRandomData()
    {
        int idx = rand() % 10;
        return _data[idx];
    }
public:
    T _data[10];
}

int main()
{
    srand(static_cast<unsigned int>(time(nullptr)));
    RandomBox<int>rb1;
    for (int i = 0; i < 10; i++)
    {
        rb1._data[i] = i;
    }
    int value1 = rb1.GetRandomData();
    cout << value1 << endl;
    
    RandomBox<float> rb2;
    for (int i =0; i< 10; i++)
    {
        rb2._data[i] = i + 0.5f;
    }
    float value2 = rb2.GetRandomData();
    cout << value2 << endl;
    return 0;
}

그런데 typename 을 무조건 붙여야되는건 아니다. 즉 다시 말해서 template 안에 들어가는건 골라줘야하는 목록 이라고 볼수 있다. 예를들어서 아래의 코드를 봐보자. SIZE 에다가 인자를 아무거나 주되 int type 인자를 받아야하는 어떤 설정을 따로 해줄수 있다. 하지만, rb1, rb2 가 같이 instantiate 했지만, 서로 다른 객체라는걸 알수 있어서 함수의 인자의 signature 이 같다고 하더라도, 객체는 다르게 이루어졌다고 볼수 있다. 위와 같이 함수에서 템플릿 특수화를 썻던것처럼, 클래스에서도 template 특수화를 사용할수 있다.

template<typename T, int SIZE>
class RandomBox
{
public:
    T GetRandomData()
    {
        int idx = rand() % SIZE;
        return _data[idx];
    }
public:
    T _data[SIZE];
}

int main()
{
    srand(static_cast<unsigned int>(time(nullptr)));
    RandomBox<int, 10>rb1;
    for (int i = 0; i < 10; i++)
    {
        rb1._data[i] = i;
    }
    int value1 = rb1.GetRandomData();
    cout << value1 << endl;
    
    RandomBox<int, 20> rb2;
    for (int i =0; i< 20; i++)
    {
        rb2._data[i] = i;
    }
    int value2 = rb2.GetRandomData();
    cout << value2 << endl;

    //rb1 = rb2; // 서로 다른 객체
    return 0;
}

템플릿 특수화를 한번 보자. 위의 코드에서 무조건 double 로 return 하는 값들로만 모아보자. 아래와 같이 보면 double 로 뭔가 규약을주고, 사이즈와 관련된것들은 int 로 아무인자나 받게 보여진다. 만약 double 로 호출을 한다고 하면 RandomBox<double, int SIZE> 가 호출이 되고, 그 외의 것들은 RandomBox 가 호출이 될거다.

template<int SIZE>
template<typename T, int SIZE>
class RandomBox
{
public:
    T GetRandomData()
    {
        int idx = rand() % SIZE;
        return _data[idx];
    }
public:
    T _data[SIZE];
}

class RandomBox<double, int SIZE>
{
public:
    double GetRandomData()
    {
        int idx = rand() % SIZE;
        return _data[idx];
    }
public:
    double _data[SIZE];
}

int main()
{
    srand(static_cast<unsigned int>(time(nullptr)));
    RandomBox<int, 10>rb1;
    for (int i = 0; i < 10; i++)
    {
        rb1._data[i] = i;
    }
    int value1 = rb1.GetRandomData();
    cout << value1 << endl;
    
    RandomBox<double, 20> rb2;
    for (int i =0; i< 20; i++)
    {
        rb2._data[i] = i + 0.5;
    }
    double value2 = rb2.GetRandomData();
    cout << value2 << endl;

    //rb1 = rb2; // 서로 다른 객체
    return 0;
}

Callback Function

Callback 함수 같은 경우는 결국 다시 호출하다. 약간 Call me back 과 같다. 또는 다시 역으로 호출하다라는 느낌이다. Functor 와 마찬가지로 어떤 상황이 일어나면 이 기능을 호출해줘 라는 느낌으로 사용하면 된다.

class Item
{
public:
    int _itemId = 0;
    int _rarity = 0;
    int _ownderId = 0;
}

class FindByOwnderId
{
public:
    bool operator()(const Item* item) // 수정이 필요없음
    {
        return (item->_ownerId == _ownderId);
    }
public:
    int _ownerId;
}

class FindByRarity
{
public:
    bool operator()(const Item* item) // 수정이 필요없음
    {
        return (item->_rarity == _rarity);
    }
public:
    int _rairty;
}

template<typename T>
Item* FindItem(item items[], int itemCount, T selector)
{
    for (int i = 0; i < itemCount; i++)
    {
        Item* item = &items[i];
        // TODO Condition
        if (selector(item))
            return item;
    }
    return nullptr;
}

int main()
{
    Item Items[10];
    items[3]._ownerId = 100;
    items[5]._rarity = 1;
    FindByOwnderId functor1;
    functor1._ownerId = 100;

    FindByRarity functor2;
    functor2._rarity = 1;

    Item* item1 = FindItem(items, 10, functor1);
    Item* item2 = FindItem(items, 10, functor2);
    
    return 0;
}

Resource

Source Code

TextRPG

일단 파일 구조는 아래와 같다.

├── Creature
|   ├── Creature.cpp
│   ├── Creature.h
│   ├── Monster.cpp
|   ├── Monster.h
|   ├── Player.cpp
│   └── Player.h
├── Game
|   ├── Field.cpp
│   ├── Field.h
│   ├── Game.cpp
│   └── Game.h
└── TextRPG.cpp

Resource

Source Code

Obejcted Oriented Programming

Objected Oriented Programming 이란 객체지향 프로그래밍을 의미하며, 기존에 있던 절차지향 프로그래밍 같은경우에는 코드가 분산되서, readability 도 떨어지면서, 사용자에게 배려하는 느낌이 전혀 없어진다. 근데 객체지향에서의 귀찮은 점도 존재한다. 예를들어서 “참, 두세줄이면 끝날 코드인데, 꼭 이렇게 까지 내가 해야할까?” 라는 생각도 들수도 있다. 하지만 객체 지향을 통해서 사용자의 편의성을 극대화 하며, 사용자의 실수를 최대한 제작자가 차단을 해야한다라는 가정하에 객체지향적 프로그래밍을 해야한다.

일단 바로 코드로 넘어가자. 일 단 아래와 같이 Knight 에 대한 간단한 class 를 생성했다. class 생성할떄 Modify 할수 있는 조건이있는데 그게 바로 publicprivate 이있다. 그 이외에건 지금은 생략하고, Inheritance 에서 더 자세하게 설명을 할건데. 지금은 public 은 공공으로 사용할수 있는 변수나 함수(method) 라고 하자. public 으로 기본으로 지정된 Member Variable 과 Methods 가 존재한다.

생각을해보자 클래스라는건 빵틀이라고 생각하면 된다. 예를들어서 Knight 이라는 틀은 기본적으로 Health bar, Attack Attributes, and its position 을 들고 있을거다. 그리고 움직일수도있고, 공격할수도 있고, 죽을수도 있다. 그렇게해서 여러개의 Knight 을 만들수 있을것이다. 이제 구현부에 대해서 이야기 하자. Member Function 또는 Method 같은 경우는 구현부를 Knight class 안에다가 구현할수 있으며, 또는 Knight::Attack 이렇게 구현할수 있는데, 이말은 Knight 클래스 안에 속해 있는 함수다라는 말이다. 똑같은 함수인 move 가 있다고 보여지는데, 하나는 Knight::Move 이고 다른 하나는 parameter 로 Knight 의 주소값을 가져온다고 보여진다. void Move(Knight*) 이 signature 같은 경우, 다른 Knight 를 instantiate 했을때 사용할수 있고, Knight::Move 그 객체가 들고 있는 built-in 함수라고 생각하면 된다.

class Knight
{
public:
// Member Variable
    int _hp;
    int _attack;
    int _posX;
    int _poxY;

// Member Function
    void Move(int y, int x);
    void Attack();
    void Die(int hp)
    {
        _hp = 0;
        cout << "Die " << endl;
    }
};

void Move(Knight* knight, int y, int x)
{
    knight->_posX = x;
    knight->_posY = y;
}

void Knight::Move(int y, int x)
{
    _posY = y;
    _posX = x;
    cout << "Move" << endl;
}

void Knight::Attack()
{
    cout << "Attack" << endl;
}

int main()
{
    // Instantiation
    Knight knight;

    // setting the `knight` member variable
    knight._hp = 100;
    knight._attack = 10;
    knight._posX = 0;
    Knight._posY = 0;
    return 0;
}

Constructor & Destructor

앞에서 말했듯이, 클래스에 ‘소속’된 함수들을 Member Functions 또는 Methods 라고 한다. 이중에 특별한 친구들이 있다. 바로 [시작][끝] 을 알리는 함수들이 있다. 즉 탄생과 소멸을 칭하는 생성자 가 있고 소멸자 가있다. 생성자 같은 경우 여러가지로 존재할수 있고, 소멸자는 단 1개만 가능하다. 생성자같은 경우 그냥 틀이기 때문에 return 값이 없다.

바로 코드를 봐보자. 기본적으로 생성자를 만들때에는 Knight() 라는 생성자를 만들면서, 주로 Member Variable 을 초기화 시켜주는 역활을 한다. 방금 전 생성자는 여러개의 생성자가 존재할수 있다고 했다. 초기에 parameter 를 받을수 있는 생성자가 있을수도 있고(기타 생성자), 그리고 다른 객체를 Copy 할수 있는 생성자가 될수도 있고, 그리고 타입 변환 생성자인 경우도 있다.

아래와 같이 봐보면, Knight() 기본 생성자 같은 경우 member variable 을 세팅 하는걸 볼수 있고 그 기타 생성자 같은 경우는, 객체를 생성할때 parameter; hp, attack, posX and posY 값을 받아오는 생성자가있다.

Copy Constructor(복사 생성자)는 생성자는 생성자인데, 자기 자신의 참조 타입을 인자로 받는다. 클래스를 생성할때, input 값을 받을때, 자기 자신을 받는다고 생각하면 된다. 즉 이 복사 생성자를 만들때는, ‘똑같은’ 데이터를 지닌 객체가 생성되길 원할때, 복사 생성자를 만든다고 생각하면된다.

아래의 코드에 K2 를 생성할때 보자. K1 을 인자로 Copy Constructor 로 인해서 k2 생성해준다. 결과적으로 클론을 하는거나 마찬가지이다. 그렇다면 복사생성자가 없다는 가정하에 k2(k1) 를 빌드 했었을때, 빌드가 된다. default 로 암시적으로 복사생성자가 만들어진다고 생각하면 된다. 그렇다면 왜 명시적으로 만들까? 만약에 인자로 참조값이나 포인터로 요구하는 인자값이 들어갔으면 어떻게할까? 라는 질문에 해답이 있다.

잠시 코드 아래에 k4 를 생성하는것과 k3 를 생성하는것을 보자. 엄연히 같게 보이지만, 서로 다른 역활을 하고 있다. k4 는 기본생성자로 만들졌고, k1 에다가 k4 를 복사 한다고 생성하고, k3 같은 경우 생성을 하는 동시에 복사를 한다. 즉 k3 는 복사생성자로 호출 되는거고, k4 는 기본생성자로 만들어준다음에, 복사를 하는거다.

생성자를 지금 까지 봐보았는데, 이게 생성자를 명시적으로 만들지 않으면, 암시적(implicit) 생성자가 된다. 즉 아무 인바도 받지 않는 [기본 생성자]가 컴파일러에 의해 자동으로 만들어진다. 그러나, 명시적(explicit) 으로 아무생성자 하나 만들면, 자동으로 만들어지던 [기본 생성자] 는 더이상 만들어지지 않는다. 아래의 코드를 실행했을때 main 에 있는 knight instantiate 했을때 기본생성자를 만들지 못하게 된다. k5 같은 경우 명시적으로 만든 생성자가 call 하게 된다.

마지막으로 잠깐 언급했던 기타 생성자를 봐보자. 기타 생성자 같은 경우 앞에 말했다싶이 여러가지 인자를 받으면서 객체를 생성하는것으로 볼수 있었다. 그중에 인자를 1개만 받는 기타 생성자는 타입 변환 생성자 라고 한다. Type Conversion Constructor 같은 경우에는, hp 를 넣어주는거에 더불어, k5 = 1 이라고 만들면 객체를 생성할수 있다. 하지만 여기서 암시적으로 생성된 생성자가 문제가 생긴다. 왜냐하면 k5 = 1 은 우리가 원치 않을수도 있기 때문이다. 그래서 타입변환 생성자에 explicit 즉 명시적인걸로 막아줘야한다. 즉 명시적으로 call 할때도 k5 = (knight)1 이런식으로 해야 더 코드가 명료해진다고 볼수 있다.

class Knight
{
public:
    // Constructor.
    Knight()
    {
        cout << "Knight() 기본생성자 called" << endl;
        _hp = 100;
        _attack = 10;
        _posX = 0;
        _posY = 0;
    }

    // Type Conversion Constructor
    // explicit!
    explicit Knight(int hp)
    {
        cout << "Knight(int ) called" << endl;
        _hp = hp;
        _attack = 10;
        _posX = 0;
        _posY = 0;
    }

    // Etc Constructor
    Knight(int hp, int attack, int posX, int posY)
    {
        _hp = hp;
        _attack = attack;
        _posX = posX;
        _posY = posY;
    }

    // Copy Constructor
    Knight(const Knight& knight)
    {
        _hp = knight._hp;
        _attack = knight._attack;
        _posX = knight._posX;
        _posY = knight._posY;
    }

    // Destructor
    ~Knight(){cout << "Knight() 소멸자 called" << endl;}

    void Move(int y, int x);
    void Attack();
    void Die(int hp)
    {
        this->_hp = 0;
        cout << "Die" << endl;
    }

public:
    int _hp;
    int _attack;
    int _posX;
    int _posY;
};

int main()
{
    Knight k1;
    k1._hp = 100;
    k1._attack = 20;

    // 1) Copy Constructor
    Knight k2(k1);

    // 2) Copy Constructor
    Knight k3 = k1;

    // 3) Copy Constructor
    Knight k4;
    k4 = k1;

    // 4) explicit
    Knight k5(10);

    // 5) Type Conversion Constructor
    // implicit version -> compliler will automatically switch
    int num = 1;
    float f = num; // explicit version float f = (float)num;

    Knight k6;
    k6 = 1; // ?

    return 0;
}

Inheritance

객체 지향 프로그래밍에서 중요한 속성들이 있는데 아래와 같다.

  1. 상속성
  2. 은닉성
  3. 다형성

만약 지금까지 배워온거라고 한다면, 클래스는 하나의 설계도 인데, 그러면 여러가지 instance 를 만들어야한다고 가정했을때, 똑같은 데이터 같은경우는 struct 로 관리하면 된다고 치지만, 똑같은 기능이 있는 클래스도 다시 만들어줘야하느냐? 라는 질문도 할수 있다. 그렇다고 한다면, 뭔가 설계도를 더 계층 구조로 짜면 어떨까? 라는 질문에서 비롯된게 상속성(Inheritance)을 볼수있다. 여기에서는 상속성(Inheritance)를 살펴보자.

상속이라는 단어는 결국 부모가 있고, 그 자식에게 유산을 물려주는것으로 볼수 있다. 즉 기능등을 물려줄수 있다고 한다. 아래의 코드를 봐보자. 클래스에서 상속을 받고 있다 라는 문법을 쓰자면 class Knight : public Player 이렇게 사용할수 있는것으로 볼수 있다. 즉 Player 의 member variable 과 functions 를 상속받을수 있게된다. 그렇다면 궁금할수 있는게, 메모리에 어떻게 잡혀질까? 라고 궁금해 할수 있다. 메모리 관점에서 보면 [ Kinght ] 안에 [ Player ] 가 있다고 생각하면 된다.

코드를 잠깐 봐보면, 일단 부모님에서 정의 한 method 를 Knight 에서 재정의 해서 사용할수 있다는것을 볼수 있고, 부모님의 본래의 함수를 call 을 하려면, k::Player::Move() 라고 사용할수 있을거다.

class Player
{
public:
    Player(){ _hp=0; _attack=0, _defence=0; cout << "player constructor" << endl; }
    Player(int hp){ _hp = hp; }
    ~Player(){ cout << "player destructor" << endl; }
    void Move() { cout << "Player " << endl; }
    void Attack() { cout << "Player Attack" << endl; }
    void Die() { cout << "Player Die " << endl; }

public:
    int _hp;
    int _attack;
    int _defence;
};

class Knight : public Player
{
public:
    Knight() {
        _stamina=0;
        cout << "Knight Constructor" << endl;
    }
    Knight(int stamina) : Player(100)
    {
        _stamina=stamina;
        cout << "Knight Constructor" << endl;
    }
    ~Knight() {cout << "Knight Desturctor" << endl; }

    // redefined : 부모님의 유산을 거부하고 새로운 이름으로 만듬
    void Move() { cout << "Knight Move" << endl; }
public:
    int _stamina;
};

class Mage : public Player
{
public:
    int _mp;
};

int main()
{
    Knight k;
    k._hp = 100;
    k._attack = 20;
    k._defence = 10;
    k.Attack();
    k.Player::Move(); // player move (parent)
    k.move(); // knight move (child)
    return 0;
}

그렇다고 한다면, constructor / destructor 측면에서 한번 다시 생각해보자. constructor 같은 경우 여러개의 constructor 을 생성할수 있고, destructor 같은 경우 하나만 존재한다는 언급을 했었다.

그렇다면 위의 코드를 한번 봐보자, 일단 child 나 parent 의 기본 생성자와 소멸자를 만들었다. 그렇다면 궁금즘은 이거다 생성자는 class 가 instantiate 했을때, 또는 탄생했을때 호출되는 함수라고 했었다. 그렇다고 한다면 Knight 를 생성했을때, Player 의 생성자가 호출이 될지 knight 의 생성자가가 호출 될지 궁금증이 생긴다. 결론적인 답은 둘다 호출하자 이렇게 생성이된다. 그리고 생성이되는 순서는 부모님 먼저 호출이 되고 그다음에 child 가 호출이 된다음에 소멸될때에는 자식이 먼저 호출이 되고, 그다음 부모님이 호출이 된다고 볼수 있다.

좀더 자세하게 child class 의 생성자가 언제 call 되는 영역이 어딘지 확인해보자. 아래와 같이 볼수 있다. 일단 Child 가 Instantiate 했을때, Knight 의 생성자 Knight() 이거나 Knight(int stamina) 가 call 이되면서, 선처리 영역에서 부모의 생성자가 호출이 된다. 그래서 부모인 Player() 가 호출이 되고 cout 으로 생성자가 호출 됬다는걸 확인 할 수 있다. 생성자와 달리 소멸자같은 경우는, ~Knight() 가 호출이 되고, 즉 child 가 호출이 된다음, 후처리 영역에서 Parent 인 ~Player() 소멸자가 호출 되는걸 볼수 있다. 그리고 추가해야할 문법은, 부모님의 생성자를 다른걸 선택? 하고 싶으면 Knight(int stamina) : Player(100) 이런식으로 해서 선처리 영역에서 Player(int hp) 를 호출 하게끔 하면 된다.

class Player
{
public:
    Player(){ _hp=0; _attack=0, _defence=0; cout << "player constructor" << endl; }
    Player(int hp){ _hp = hp; }
    ~Player(){ cout << "player destructor" << endl; }
    void Move() { cout << "Player " << endl; }
    void Attack() { cout << "Player Attack" << endl; }
    void Die() { cout << "Player Die " << endl; }

public:
    int _hp;
    int _attack;
    int _defence;
};

class Knight : public Player
{
public:
    Knight() {
        /*
         * 선(처리) 영역
         * - 여기서 Player() 생성자 호출
         */
        _stamina=0;
        cout << "Knight Constructor" << endl;
    }
    Knight(int stamina) : Player(100)
    {
        //

        /*
         * 선(처리) 영역
         * - 여기서 Player() 생성자 호출
         */
        _stamina=stamina;
        cout << "Knight Constructor" << endl;
    }
    ~Knight() {cout << "Knight Desturctor" << endl; }
    /*
     * 후처리영역
     * - 여기서 Player() 소멸자 호출
     */
    void Move() { cout << "Knight Move" << endl; }
public:
    int _stamina;
};

int main()
{
    Knight k;
    return 0;
}

결국에는 상속을 쓰면, 코드가 간결해지고 가독성이 높아진다는걸 알수 있다.

Hiding

Hiding 은 한마디로 은닉성(Data Hiding) 또는 Encapsulation 이라고 한다. 여기서 이야기하는건 데이터의 권한 문제라고 생각하면 된다. 그렇다면 왜 숨기고 이걸 보호 해야하냐 라는 질문을 할 수 있다. 대표적인 이유는 중 하나는 정말 위험하고 유저가 함부러 건드리면 안되는 경우가 있고, 나머지 하나는 다른 경로로 접근하길 원하는 경우가 있다. 예를 들어서, 자동차가 있다 유저가 실제로 보고 작동할수 있는건, Handle, Excel Pedal, and Break 가 있다. 물론 자동차를 관리하는 사람들을 제외 하고, 일반인들은 엔진이나 엔진에 묶여있는 와이어를 손을 덴다고 하면, 차가 쉽게 망가지기 마련이다. 어떤부분은 유저들에게 안보여지게 하고, 다른 부분들은 보여지는거다. 그러면 이런것을 어떻게 문법으로 적용을 할것인가? 라는 질문을 할수있다. 이걸 접근 지정자 라고 한다. 접근지정자 같은 경우 아래 세가지가 있고, 그에 관한 설명이 있다. 코드를 한번봐보자.

  • public : 누구한테나 실컷 사용하세요
  • protected : 나의 자손들한테만 허락
  • private : 나만 사용할께 (즉 자신 내부에서만)
class Car
{
public: // 접근 지정자
    void MoveHandle() {}
    void PushPedal() {}
    void OpenDoor() {}

    void TurnKey(){ RunEngine(); }

protected:
    void DisassembleCar() {}
    void RunEngine() {}
    void ConnectCircuit() {}
};

위의 코드 같이, MoveHandle,PushPedal,OpenDoor,TurnKey 같은 경우는 유저가 자동차의 겉표면? 쉽게 사용할수 있는 기능들이다. 하지만 DisassembleCar, RunEngine, ConnectCircuit 같은 경우는 private 으로 class 내부에서만 사용할수 있지만, 상속을 받을수 있기 때문에 protected 로 보호할수있다. 또 TurnKey() 내부 함수에서 protected 로 지정된 함수를 call 할수 있게 해놓았다.

class Car
{
public: // 접근 지정자
    void MoveHandle() {}
    void PushPedal() {}
    void OpenDoor() {}

    void TurnKey(){ RunEngine(); }

protected:
    void DisassembleCar() {}
    void RunEngine() {}
    void ConnectCircuit() {}
};

class SuperCar : public Car // 상속 접근 지정자
{
public:
    void PushRemoteController(){ RunEngine(); }
};

int main()
{
    Car car;
    return 0;
}

그렇다면 위에서 이야기 했던 상속 부분을 좀 더 생각해보자. 접근 지정자가 있다고 한더라면, 상속 접근 지정자를 빼먹을수 있다. 상속 접근 지정자 같은 경우, 다음 세대에 어떻게 부모님의 유산을 어떻게 물려줄지? 가 테마라고 생각하면된다. 즉 부모님한테 물려받은 유산을 꼭 나의 자손한테도 똑같이 물려줘야하지 않는다는 뜻이다. 멤버 접근 지정자처럼 상속 접근 지정자에 대한 설명을 해볼까 한다.

  • pubilc : 공개적으로 상속? 부모님의 유산 설계 그대로! (public -> public / protected -> protected)
  • protected : 보호받는 상속? 내 자손들한테만 물려줄꺼야! (public -> protected / protected -> protected )
  • private : 개인적인 상속? 나까지만 잘쓰고 -> 자손들에게 아예 안물려 줄꺼야! (public -> private / protected -> private)

아래의 코드를 한번 봐보자. 일단 SuperCar 라는 클래스는 Car 로 부터 private 으로 상속받았기 때문에 만약 SuperCar 라는 상속받은 아이는 Car 에 대한것을 접근할수 있다. 즉 SuperCar는 자기까지만 욕심많게 상속을 받고 물려주지 않은것으로 보여진다. 이 코드에서 만약 상속 접근 지정자를 안했을 경우 private 으로 인식하게 된다.

class SuperCar : private Car // 상속 접근 지정자
{
public:
    void PushRemoteController(){ RunEngine(); }
};

class TestSuperCar : SuperCar
{
    void Test()
    {
        DisassembleCar(); // Cannot access
    }
};

class SuperSuperCar : private Car
{
public:
    void PushRemoteController(){ RunEngine(); }
};


class TestSuperSuperCar : public SuperSuperCar
{
public:
    void Test() { /* .. Can't call DisassembleCar(); */}
};

이유중에 2번째: 다른 경로로 접근 이라는게 있다. 이거에 대한 예를 들어보자. 아래의 코드를 보면, main 함수에서 버서커를 instantiate 한다음에, hp를 바꾸는데, 버서커모드의 출력창이 안나온다. 그 이유는 일단 _hp 를 접근해서 바꾸는건 위험하고 또 다른건 클래스는 그냥 틀에 불과 하기 때문에 설계를 잘못했다고 말을 할수 있다. 그래서 이 부분에서 encapsulation 에 대한 이야기를 할까 싶다. 캡슐화는 한마디로 연관된 데이터와 함수를 논리적으로 묶어 놓은것이라고 볼수 있다.

class Berserker
{
public:
    void SetBerserkerMode(){ cout << "Getting Stronger" << endl; }
public:
    int _hp = 100;
};

int main()
{
    Berserker berserker;
    berserker._hp = 10;
}

Encapsulate 된 클래스 구조를 코드로 확인 해보자. 일단 _hp 를 쉽게 건들수 없게 private 으로 막아놓으면, 일단 외부에서는 접근을 못하게 막아 놓았다. 주로 member variable 을 가지고 나올때는 getter 와 setter 을 쓰기 때문에, GetHp()SetHp() 를 만들어준다. 그래서 일단 우리가 만들고 싶은거는 뭔가 hp 가 세팅이 됬을때, 버서커모드로 될지 안될지를 체크를 해주면 된다. 또 외부에서 버서커 모드를 키게 하면 안되니까 private 으로 막아줬다.

class Berserker
{
public:
    int GetHp() { return _hp; }
    void SetHp(int hp)
    {
        _hp = hp;
        if (_hp <= 50) SetBerserkerMode();
    }

private:
    int _hp = 100;
    void SetBerserkerMode(){ cout << "Getting Stronger" << endl; }
};

int main()
{
    Berserker berserker;
    berserker.SetHp(20);
    return 0;
}

Polymorphism

Polymorphism 이라는건 결국 다양한 형태로 존재 한다 라고 생각하면 된다. 즉 쉽게 말해서 겉은 똑같은데, 기능이 다르게 동작한다고 말할수 있다. 두가지로 대표적으로 2 가지를 말을 할수 있다.

  1. 오버로딩(Overloading) = 함수 중복 정의 = 함수 이름의 재사용
  2. 오버라이딩(Overriding) = 재정의 = 부모 class method 를 사용해서 자식클래스에서 재정의

잠깐 오버로딩에 대해서 이야기를 해보자. 바로 코드를 보겠다. 아래는 Move() 라는 함수를 이용해서 같은 이름이지만 signature 이 다른 함수인 Move(int) 로 함수를 중복 정의 한걸 볼수 있다. 이게 바로 대표적인 오버로딩에 대한 예이다. 이와같이 오버로딩은 되게 간단하다고 볼수있다.

class Player
{
public:
    Player() {_hp = 100;}
    void Move(){cout << "Move()" << endl;}
    void Move(int step){cout << "Move(int)" << endl;}
};

int main()
{
    Player player;
    player.Move();
    player.Move(20);
    return 0;
}

오버라이딩 같은 경우가 굉장히 polymorphism 에서 중요한 부분인데 한번 알아보자. 아래와 같이 오버라이딩에 대한 간단한 예라고 볼수 있다 부모 클래스인 Player() 에서 상속받은 KnightMage 같은 경우 Move() 함수를 재정의 해서 사용한걸 볼수있다.


class Player()
{
public:
    void Move(){ cout << "Move() " << endl; }
public:
    int _hp;
};

class Knight : public Player
{
public:
    Knight() {_stamina = 100; }
    void Move(){ cout << "Knight Move()" << endl; }

public:
    int _stamina;
};

class Mage : public Player
{
public;
    void Move() {cout << "Mage Move()" << endl; }
public:
    int _mp;
};

int main()
{
    Knight k;
    k.Move();

    Mage m;
    m.Move();
    return 0;
}

상속받아서 하는 클래스를 설계하는건 아주 중요한 스킬중에 하나인데, 그중에 또하나의 장점이 있다. 아래의 코드를 봐보자. Move 라는 기능의 함수를 클래스 별로 만들었다. 여기에서 만약에 MoveKnight() 안에 player 의 주소값을 넣어주면 어떻게 될까? 호환이 되지 않는다. 일단 기존에 PlayerMove() 안에 넣었던걸 번역?을 하자면, 플레이어는 플레이어다라고 말을 할수 있는데, 플레이어가 기사냐라고 물어봤을때, 지금 계층 구조에서는 의미가 맞지 않는다. 즉 플레이어는 Mage 일수도 있고, Knight 도 될수도 있다. 그러면 그 반대의 케이스로 MovePlayer() 안에 Knight 의 주소값을 넣어줬다고 하면 어떨까? 앞의 해석에 의해서 빌드가 된다. 그렇다면 해석을 구지 하자면, Knight 는 Player 가 맞다. 바로 이 점을 사용해서, MoveKnight() 나 MoveMage() 함수를 따로 안만들어주고, MovePlayer() 로 관리 할수 있게된다. 상속관계를 잘알게 된다면, 확실히 코드가 간결해진다.

그렇다면, 여기서 더나아가서 MovePlayer() function 만 사용한다고 했을때, 과연 어떤 클래스에서 Move() method 를 사용할까? 라는 의문점이든다. 실행을 해보면, 부모안에 있는 Move() 가 실행되는걸 확인 할수 있다. 그러면 이게 문제가 된다. overriding 을 사용해서 Knight Move() 를 만들었는데.. 라고 물을수 있다. 그 이유는 바로 Binding(바인딩) 이라는 개념과 연관된다.

Binding(바인딩) 은 결국 어떤걸 묶는다라는 걸 볼수 있는데, 정적 바인딩과 동적 바인딩이 있다. 아래의 정의를 잠깐 봐보자

  • Static Binding(정적 바인딩) : compiler 시점에 결정
  • Dynamic Binding(동적 바인딩) : run time 에 결정

주로 일반함수는 정적바인딩에 속한다. 즉 MovePlayer() 가 compiler 에서 봤을때는 Player 의 주소값을 받는 타입이 있으니까, 원본데이터가 Knight 였을지여도, Player 의 Move() 함수가 실행되는거다. 그렇다면 이걸 어떻게 해결할까? 는 예상대로 동적바인딩을 사용하면 된다. 그렇다면 동적 바인딩을 사용하려면 조건이 필요하다. virtual(가상) 함수가 필요하다.

class Player()
{
public:
    void Move(){ cout << "Move() " << endl; }
public:
    int _hp;
};

class Knight : public Player
{
public:
    Knight() {_stamina = 100; }
    void Move(){ cout << "Knight Move()" << endl; }

public:
    int _stamina;
};

class Mage : public Player
{
public;
    void Move() {cout << "Mage Move()" << endl; }
public:
    int _mp;
};

void MovePlayer(Player* player)
{
    player->Move();
}

void MoveKnight(Knight* knight)
{
    knight->Move();
}

void MoveMage(Mage* mage)
{
    mage->Move();
}

int main()
{
    Player p;
    p.Move(&p);
    Kngiht k;
    k.Move(&k);
    return 0;
}

가상함수를 사용하려면 어떻게 사용해야할까는 method 앞에 keyword 를 사용하면 된다. 그렇다면 위의 코드를 조금 정리해서 봐보자. 일단 동적바인딩을 사용해서, VMove() 그리고 VAttack() 을 만들었다. 상속관계에서 virtual function 을 사용하면, virtual 함수인거다.

class Player
{
public:
    Player() {_hp =100 ;}
    virtual void VMove() { cout << "VMove" << endl;}
    virtual void VAttack();
public:
    int _hp;
};

class Knight : public Player
{
public:
    Knight() {_stamina = 100; }
    virtual void VMove() { cout << "VMove Knight" << endl;}
    virtual void VAttack(){cout << "VAttack Knight" << endl;}
public:
    int _stamina;
};

void MovePlayer(Player* player)
{
    player->VMove();
}

int main()
{
    Knight k;
    MovePlayer(&k);
    return 0;
}

앞에서 본건 구현방법이였다. 하지만 더 자세하게 보려면 어셈블리를 까봐서 어떻게 구현되어있는지 정확하게 필요가 있다. 일단 break point 를 걸어서 실행을 해보면 Knight 앞 메모리에 뭔가가 추가 되었다는게 보인다. 즉 이 추가된게 가상함수의 어떤 플래그라는걸 알수있다. 즉 실제 객체가 어떤 타입읹지 어떻게 알고 있어서 가상함수를 호출하는지를 찾아보면, 바로 가상함수 테이블(vftable) 이라는게 존재해서 그렇다. 그러면 가상함수 테이블에서 잠깐 보면 가상함수 테이블(.vftable) 는 32 bit 에서는 4 바이트를 차지하고, 64 bit 에서는 8 바이트를 차지한다. 메모리 구조에서는 [VMove][] 즉 테이블 주소로 되어있다는 거다. 즉 가상함수 테이블을 통해서 가상함수들을 관리 한다는걸 확인할수 있다. 그렇다면 가상함수를 쓰는 주체는 누구인가? 라는 질문도 할수 있다. 정답은 생성자에서 한다. 생성자의 선처리 영역에서 vftable 을 채워넣는다.

signature 만 가지고 있는 가상함수를 순수 가상함수라고 하는데, 이 가상함수는 구현은 없고 interface 만 전달하는 용도로 사용하고 싶을때 사용된다 순수 가상함수를 만들었을 경우, 빌드를 시켰을때, 그 method 가 있는 클래스 abstract class 가 된다. 여기서 abstract class 가 뭐냐고 묻는다면, 순수 가상함수가 1 개이상 존재하거나 포함되면 바로 추상클래스로 간주되고, 직접적으로 객체를 instantiate 하지 못하게 된다.

순수 가상함수일경우에는 그 가상함수를 표현할때 virtual void Attack() = 0 이런식으로 표현한다. 모던 c++ 에서는 virtual void Attack() abstract 라고 표현된다. 이렇게 순수가상함수가 표현되면, 거의 무조건 상속받는 친구들은 무조건 재정의가 필요해 이렇게 말을하는거다.

Initializing the List

초기화를 하는 이유는 여러가지가 있다. 일단 초기화를 통해서 디버깅도 쉬어지고, 또한 초기화를 함에 따라서 어떤 값이 들어갔는지 확인이 가능하다. 즉 버그를 예방을 할수 있고, 포인트나 주소값이 연루 되어있다고 한다면 더더욱 중요시 생각해야한다. 아래의 코드를 한번 봐보자. 일단 k._hp 를 출력 한다고 가정하면, 엉뚱한 메모리값을 가지고 있다는걸 확인 할수 있다. 이렇게 초기화를 안한 상태에서, if statement 로 넘어간다면, 이제 Knight 가 죽었다는 사실을 들고 있다. 이런 실수를 방지 하고자 Initializing 을 할 필요가 있다.

#include <iostream>
using namespace std;
class Knight
{
public:
    int _hp;
};

int main()
{
    Knight k;
    cout << k._hp << endl;
    if (k._hp < 0 )
    {
        cout << "Knight is Dead" << endl;
    }
    return 0;
}

초기화 방법은 여러가지가 있지만, Object Oriented Programming 관점에서의 초기화는 일단 생성자 안에서 초기화를 하는 방법이 있고, 그리고 초기화 리스트가 있으며, c++11 에서 추가된 문법이 있다. 이거에 대해서 더 상세 하게 이야기를 할려고 한다. 일단 아래의 코드를 보자. Knight 클래스는 Player 클래스로 부터 상속을 받았고, 생성자에서 부모 클래스의 초기화를 했고, 또한 Knight 클래스의 member 변수인 _hp 를 100 으로 초기화 한걸 볼수있다. 또한 Knight 생성자 내에서 멤버 변수인 _hp_hp = 100 이런식으로 초기화가 가능하다.

C++11 에서는 바로 class 내부에서 int _hp = 100 으로 설정이 가능하다.

class Player
{
public:
    Player(){}
    Player(int id){}
};

class Knight : public Player
{
public:
    Knight() : Player(1), _hp(100)
    // 선처리 영역 // 
    {

    }

public:
    int _hp;
};

일단 초기화 리스트 같은경우, 상속 관계에서 원하는 부모를 생성자 호출할때 필요하다. 더나아가서 생성자내에서 초기화를 하는게 있고, 초기화 리스트의 비교를 따로 해보자. 일단 일반 변수 같은경우는 별 차이가 없으며, 만약 멤버 타입이 클래스인 경우 차이가 난다.

일단 이걸 더 판별하기 위해서, Is-A(Knight Is-A Player?)Has-A(Knight Has-A Inventory) 스스로에게 질문을 해보면 된다. 아래의 코드를 한번 봐보면 Is-A 같은 경우는 기사는 플레이어다 라고 생각해서 맞다고 하면, 상속관계 이다. 또 다른 예를 찾아보면 Is-A 를 사용해서 기사는 인벤토리냐 라고 묻는다면, 상속관계가 아닌 포함관계라는걸 볼수 있다. 큰그림을 그려보자면, 이렇게 생각하는 이유는 처음에 코드를 설계할때의 유용하기 때문이다.

또 여기서 문제가 될게 있다. 만약 생성자 내부안에서 Inventory 를 만들어서 초기화를 할 경우, 각 다른 생성자를 한번씩 한번씩 호출이되고, 소멸자는 두번이 호출된거를 볼수 있는데, 멘붕이 올수 있다. 이부분은 선처리 영역에서 Inventory 를 만들었는데 생성자에 들어와서, 기존에 있던 기본 생성자를 날려버리고 Inventory(int) 의 생성자로 덮어씌우는 동시에 소멸자가 호출되면서, 코드가 끝나게 되면, 다시 소멸자가 호출이된다. 이럴 경우에는 애초에 선처리영역에서 아래와 같이 초기화를 하는걸 볼수 있다.

class Inventory
{
public:
    Inventory(){ cout << "inventory()" << endl; }
    Inventory(int size){ _size = size; }
    ~Inventory(){ cout << "~Inventory()" << endl; }
public:
    int _size = 10;
};

class Player
{
public:
    Player(){}
    Player(int id){}
};

class Knight : public Player
{
public:
    Knight() : Player(1), _hp(100), _inventory(20)
    // 선처리 영역 // 
    // inventory() // 
    {

    }

public:
    int _hp;
    Inventory _inventory;
};

또 정의함과 동시에 초기화가 필요한 경우가 있다. 주로 참조 타입이나 const 타입일 경우가 있다. 아래의 코드를 봐보자. 일단 테스트를 위해서, _hpRef,_hpConst 가 있다해보자. Knight 클래스 내부에서 생성자에서 저 두 멤버변수를 바꾼다고 해도 의미가 없어진다. Const 같은 경우는 바꿀수 없는거고, Reference 는 누군가 하나를 가르키고 있어야 하는건데, 이미 Knight 클래스가 생성이 될시 즉 선처리영역에서 이미 했기때문에 다른값으로 수정이 불가능하다. 그래서 하고 싶을 경우에는 마찬가지로 선처리 영역에서 하면된다.

class Inventory
{
public:
    Inventory(){ cout << "inventory()" << endl; }
    Inventory(int size){ _size = size; }
    ~Inventory(){ cout << "~Inventory()" << endl; }
public:
    int _size = 10;
};

class Player
{
public:
    Player(){}
    Player(int id){}
};

class Knight : public Player
{
public:
    Knight() : Player(1), _hp(100), _inventory(20), _hpRef(_hp), _hpConst(100)
    // 선처리 영역 // 
    // inventory() // 
    {
        _hpRef = _hp; // 안됨
        _hpConst = 100; // 안됨
    }

public:
    int _hp;
    Inventory _inventory;
    int& _hpRef;
    const _hpConst;
};

Operation Overloading

Operation Overloading 이란 큰틀로 보면 객체와 객체끼리의 연산을 할수 있게 만들어주는 역활을 말한다. 아래의 코드로 예를 들어보자. 우리는 두 가지의 객체를 만들어서 더했을때, pos2 로 저장하려고 했을때 각각의 객체들의 멤버 variable 이 더해졌으면 좋겠다는 바램에 실행을 해보면, 빌드에 실패하게 되어있다. 이럴때 필요한게 Operation Overloading 이라고 한다.

class Position
{
public:
    int _x;
    int _y;
};

int main()
{
    Position pos;
    pos._x = 3;
    pos._y = 4;

    Position pos1;
    pos1._x = 7;
    pos1._y = 3;

    Position pos2 = pos + pos1;
    return 0;
}

그렇다면 어떻게 만들까? 그리고 어떤 문법이 있을까를 알아보자. 어떻게 보면 일단 오버로딩 같은경우 한개의 같은 Naming 에 각 각 다른 signature 을 들고 있었다. 연산자도 똑깥이 사용하면 돼고, 그리고 그렇게 생각하다보면 그냥 보통 함수와 연산자 와는 무슨 차이가 있을까? 일단 연산자 같은경우는 피연산자의 개수/타입이 고정되어있다.

그렇다면 바로 넘어가서 연산자 오버로딩에 대해서 더 설명 하려고 한다. 일단 연산자 함수를 정의를 해야하는데, 그 함수를 정의하는것도 멤버함수와 전역함수로 일반함수와 비슷하게 할수 있다. 일반 함수의 signature 을 생각을 해보자. 처음에는 Return type 이 존재 하고 그 다음 함수 이름에 argument 가 따라왔었다. 연산자 오버로딩도 똑같다. RET operator+(ARG_LIST) 이런식으로 하면 된다. 그래서 위의 코드를 서포트 하긴 아래의 코드를 한번봐보자. 아래와 같이 reference 의 값을 받아서 Position 을 + 연산을 한 이후에 Position 을 return 하는거로 보인다. 즉 a op b 이런 형태일때, 왼쪽으로 기준으로 실행이 된걸 확인 할수 있다. 즉 a 를 피연산자라고 생각 하면된다. 근데 만약 a 가 클래스가 아니면, 연산이 안된다. 즉 1 + pos 가 된다면 안된다. 이걸 해결 할수 있는 방법은 전역 연산자 함수로 만들면 된다. 즉 a op b 라고 한다면, a, b 모두를 연산자 함수의 피연산자로 만들면 된다는 뜻이다.

class Position
{
public:
    Position operator+(const Position& arg)
    {
        Position pos;
        pos._x = _x + arg._x;
        pos._y = _y + arg._y;
        return pos;
    }

public:
    int _x;
    int _y;
}

Position operator+(int a, const Position& b)
{
    Position ret;
    ret._x = b._x + a;
    ret._y = b._y + a;
}

int main()
{
    Position pos;
    pos._x = 3;
    pos._y = 4;

    Position pos1;
    pos1._x = 7;
    pos1._y = 3;

    Position pos2 = pos + pos1;
    return 0;
}

그렇다면 어떤 연산자가 있을까? 그리고 전역 연산자 함수와 멤버 연산자의 함수 둘중에 어떤게 좋은걸까? 분명 전역으로 만들면 좋다는걸 말을 했었다. 하지만 둘다 사실 알아야 할 필요가 있다. 주로 대입 연산자 같은 경우(a=b)는 전역 연산자 함수로 만들어주는게 맞다. 조금은 Tricky 하지만 대입연산자같은 경우는 자기 자신의 참조값을 return 하는 경우가 많기 때문에 Position& operator=(int arg) 이런식으로 된다. 그래서 자기 자신의 포인터를 표현하기 위해서 *this return 하면 된다.

class Position
{
public:
    Position operator+(const Position& arg)
    {
        Position pos;
        pos._x = _x + arg._x;
        pos._y = _y + arg._y;
        return pos;
    }
    Position operator+(int arg)
    {
        Position pos;
        pos._x = _x + arg;
        pos._y = _y + arg;
    }

    bool operator==(const Position &arg)
    {
        return _x == arg._x && _y == arg._y;
    }

    Position& operator=(int arg)
    {
        _x = arg;
        _y = arg;
        return *this;
    }

    Position& operator=(const Position& arg)
    {
        _x = arg._x;
        _y = arg._y;
        return *this;
    }

    Position& operator++()
    {
        _x++;
        _y++;
        return *this;
    }

    Position operator++(int)
    {
        Position ret = *this;
        _x++;
        _y++;
        return ret;
    }

public:
    int _x;
    int _y;
};

Position operator+(int a, Position &b)
{
    Position ret;
    ret._x = b._x + a;
    ret._y = b._y + a;
    return ret;
}

위의 코드를 보면 복사 대입연산자의 signature 들이 보인다. 일단 말부터 어렵다 복사하긴 하는데 대입? 이라고 하는데, 이걸 잘풀어 보면 대입 연산자는 연산자인데, 자기 자신의 참조 타입을 인자로 받는것을 복사 대입 연산자라고 한다. 위의 코드를 봤을때, Position& operator=(int arg) 이 member function 인데, arguments 를 Position& arg 로 받는것이다. 그래서 표현을 하면 Position& operator=(const Position& arg) 이런 signature 로 된다는 것이다. 앞에서 봤듯이 복사 생성자와 복사 대입연산자 같은 경우는 Deep Copy 와 Shallow Copy 에서 다룰거지만 Memory 를 그대로 복사한다는거에대해서 되게 큰 의미가 있는 작업이다.

위에서 봤듯이 모든 연산자를 오버로딩을 할수 있는건 아니다. (::, . .*) 등 안된다. 그리고 단항 연산자도 가능하다. 그렇다면 증감 연산자도 되기 때문에 위의 코드를 참고 하면 된다. 전위형 같은경우는 operator++() 이런 형태와, 후위형은 operator++(int) 이런식으로 되어있다는걸 확인할수 있다.

Etc

조금 디테일한거에 대해서 포커스를 해보자 한다. 일단 C 에서 사용하는 struct 와 class 에 대한 차이를 알아보자 하는데, 결론은 종이 한 장 차이이다. 주로 struct 를 사용할때는 Data 의 묶음? 이라고 했던 점이 있었는데, 클래스에도 똑같이 사용할 수 있다. 그리고 Operator 나 function 을 넣을수 있다. 근데 다른점 이라고 한다면, 접근을 할 수 있느냐 없느냐의 차이이다. 아래의 코드를 실행시켜보면 알 수 있다. 즉 클래스에서 접근 지정자를 선언하지 않으면, private 으로 default 로 생성이 되기 때문에 미묘한 차이로 접근 할 수 없게 된다. 그리고 Struct 같은 경우는 기본 접근 지정자가 public 이다.

이렇게 설계한 느낌은 결국은 C 의 호환성이라고 한다.

struct TestStruct
{
    int _a;
    int _b;
};
class TestClass
{
    int _a;
    int _b;
};
int main()
{
    TestStruct ts;
    ts._a = 1;
    TestStruct tc;
    tc._a = 3;
    return 0;
}

결국은 Struct 는 그냥 구조체 (데이터 묶음)을 표현하는 용도로 사용하면 되고, class 같은 경우는 객체 지향 프로그래밍을 나타내는 정도의 용도로 사용하면 될 것 같다.

그 다음에 static 변수와 static 함수에 대해서 설명하고자 한다. static 은 정적이라는 의미를 가지고 있다. 아래의 코드 상태에서 스타크래프트의 아카데미 역활을 함수로 표현 하고자 할때 생각을 해보면, 모든 마린은 attack 이라는 변수를 가지고 있는데, 설계할때 클래스의 멤버 변수로 가지고 있기 때문에, 모든 마린의 공격력을 증가 시킬때, Instantiate 한 모든 마린의 공격력을 무식하게 다 바꿔줘야 해야 해서 불편할 수 있다. 그래서 static 을 사용해서 하면 해결 할 수 있다.

즉 이렇게 하는 이유는 Marine class 의 설계도에 종속적으로 가져갈 수 있기 때문이다. (그 의미는 Marine 을 instatiate 할 때 마다 각 각 다른 객체이지만, 공통적으로 가지고 있는 것을 만들고 싶을 때 이렇게 사용된다.)

class Marine
{
  public:
    int _hp;
    static int _attack;
};
int Marine::s_attack = 0;
int main()
{
    Marine m1;
    m1._hp = 40;
    m1._attack = 6;
    
    Marine m2;
    m2._hp = 40;
    m2._attack = 6;
    
    // Academy
    Marine::s_attack = 7; // 모든 마린의 공격력을 7 로 바꾼다.
}

일반 static 이 변수에 쓰는 경우를 알아봤다. 이렇다면 함수에 쓰이는 경우는 어떤 경우가 있을까? 생각이 든다. 아래의 코드를 한번 봐보자. 아까의 위의 클래스를 생각을 해보자. 아래와 같이 보면, 모든 Marine 에 대해서 SetAttack() 함수가 정의 되어있는걸 보인다. static 변수는 어떤 메모리에 올라갈까? 일단 초기화를 한다면 .data 에 올라갈거고, 안하면 .bss 영역에 올라갈것이다.

class Marine
{
public:
    void TakeDamage(int damage)
    {
        _hp -= damage;
    }

    static void SetAttack()
    {
        _attack = 100; 
    }
public:
    int _hp;
    static int _attack;
};
int Marine::s_attack = 0;
int main()
{
    Marine m1;
    m1._hp = 40;
    m1._attack = 6;
    
    Marine m2;
    m2._hp = 40;
    m2._attack = 6;
    
    // Academy
    Marine::_attack = 7; // 모든 마린의 공격력을 7 로 바꾼다.
    Marine::SetAttack();
}

static 의 생명주기는 프로그램의 시작과 종료, 즉 메모리에 항상 올라가 있다. 즉 id 같은 경우는 계속 저기 어딘가에 메모리주소에 있기때문에 내부에서 선언이 되었더라도, 함수 스택메모리에 벗어나더라도, id 는 계속적으로 바뀌어있는게 계속 있을거다.

class Player
{
public:
    int _id;
};

int GenerateId()
{
    // 1, 2, 3, 4
    // 정적 지역 객체
    static int id = 1;
    return id++;
}

Resource

Source Code

Dynamic Allocation

동적 할당은 진짜 C++ 에서 정말 중요도가 너무 높다. 동적 할당을 알려면 메모리의 구조도 알아야 할 필요가 있다. 메모리 구조에대해서 잠깐 복습을 해보자.

전체적인 그림에서 주로 어떤 OS 든지, 유저영역과 커널 영역(ex: Windows 핵심 코드) 나누어져있다. 유저영역에서는 어플리케이션들이 유저영역에서 실행된다. 만약에 유저 애플리케이션이 서로가 서로의 메모리를 공유해서 쓰거나, 서로의 영역을 침범한다면 정말 망할것이다. 유저 애플리케이션은 서로 서로 독립적으로 메모리를 커널영역에 요청을 한다. 즉 유저영역에서, 운영체제에서 제공하는 API 를 호출한다. 그런 다음 커널영역에서 메모리를 넘겨준다.

일단 샐행할 코드가 저장되는 영역은 코드 영역 이라고 한다. 전역(global) / 정적 (static) 변수는 데이터 영역에 저장이되고 마지막으로 지역 변수 / 매개 변수는 스택영역에 사용이된다. 하지만 위에 이미지를 보면, 다른 영역들도 보인다. 그러면 왜 구지 다른 영역들을 사용할까? 라는 질문이 떠오르긴 한다.

만약에 실제 상황에서 MMORPG 에서 동시 접속이 1 만명 더많게는 10 만명이 있고, 몬스터도 500만 마리가 될수 있다. 아래의 코드를 실행해보면, stack overflow 라고 하면서 에러를 내뱉는걸 볼수 있다. 근데 이게 항상 최대 상한선에서의 이야기이였던거 였다. 실제 게임에서는 한번에 만드는게 아니라, 가끔씩 시간에 지남에 따라서 생성되고 죽음을 맞이 해야할것이다. 그렇다면 메모리 구조에서 조금만 더 생각해보자.

스택 영역에서는 함수가 끝나면 같이 정리되서, 조금 불안정한 메모리라고 볼수 있ㄷ다. 그래서 잠시 함수에 매개변수를 넘긴다거나, 하는 용도로는 좋다. 그리고 그에 따른 메모리영역은 무조건 사용이된다고 볼수 있다. 그렇다면 우리가 위에서 하고 있었던 목적으로 메모리를 필요할때만 사용하고, 필요없을때 반납할수 있는 그런 메모리와 스택과는 다르게 우리가 생성 / 소멸 시점을 관리할 수 있는 그런 아름다운 메모리를 바로 힙영역이다.

그렇다면, C++ 에서 이 힙영역을 건들려면 어떤 Keyword 를 살펴보아야 하나면 malloc, calloc, realoc, free, new, delete, new[], delete[] 이 있다. 그렇다면 아까 잠깐 언급한 유저영역과 커널영역에서의 관점을 보자면 C++ 에서는 어떻게 힙영역을 사용할까? C++ 에서는 기본적으로 CRT(C RunTime Library)의 힙관리자를 통해서 힙영역을 사용한다. 단, 정말 원한다면 우리가 직접 API 를 통해 힙을 생성하고 관리할 수도 있다. 예를 들어서 MMORPG 서버 메모리 풀링을 사용할수 있다.

class Monster
{
public:
    int _hp;
    int _x;
    int _y;
    int _z;
};

int main()
{
    Monster monster[500 * 10000000];
    return 0;
}

일단 malloc 의 signature 을 봐보면 size_t 를 볼수 있는데 F11 을 타서 봐보면 typedef 로 unsigned int 를 size_t 로 되어있는걸 볼수 있다. 그리고 return 값을 보면 pointer 로 보인다. 그래서 void* pointer = malloc(1000) 볼수 있다. 이때 1000 byte 만큼을 메모리 관리자로부터 요청해서, 메모리를 할당 후 시작 주소를 가르키는 포인터를 반환해준다. 잠깐 확인 해야 할 필요가 있는게, void* pointer 라는거라는게 뭘까? 라는걸 확인해야 할 필요가 있다. 앞서 포인터를 타고 가면 아무것도 없다라는게 아니라, 타고 가면 패ㅑㅇ 뭐가 있는지 모르겠으니까 너가 적당히 반환해서 사용해라라는 느낌으로 void 라는게 붙어져있다. 그래서 위의 Monster 를 사용하자면, Monster* m1 = (Monster*)pointer; 이렇게 변환해서 사용할 수 있다. 그래서 m1 에다가 _hp 나 세팅이 가능하게 되는것이다. 근데 1000 byte 는 너무 과하고 딱 Monster 객체만큼 받고 싶다면 sizeof(Monster) 사용하면 될것이다.

그렇다고 한다면 우리가 빌려쓰고 이제 반환하는 과정을 보려고 한다. 일단 결론적으로 malloc 이라는 키워드를 사용한다면 free 라는 keyword 같이 따라오는데, 이게 반환하는 과정이라고 생각하면 된다. freemalloc, calloc, realoc 메모리로 요청한것들을 반환하거나 풀어주는거라고 생각하면된다. 조금 궁금한점이 있다면, 메모리를 요청 할떄는 얼마만큼 요청했는지를 알수 있는데, 왜 풀어줄때는 메모리를 얼마나 풀어주는지를 따로 요청 하지 않는다. 이는 메모리를 요청할때 Header 라는게 있는데 이게 얼마만큼 요청했는지를 Tracking 할수 있어서 free 를 할때 이걸 보고 메모리를 풀어주는것이다.

class Monster
{
public:
    int _hp;
    int _x;
    int _y;
    int _z;
};

int main()
{
    void* pointer = mallocs(sizeof(Monster));
    Monster* m1 = (Monster*)pointer;
    m1._hp = 100;
    m1._x = 0;
    m1._y = 0;
    m1._z = 0;

    free(pointer);
    return 0;
}

메모리를 건들다 보면, 이제 stackoverflow 처럼, Heap 영역에서 요구한만큼을 더 사용했을 경우에 heapoverflow 가 생긴다. 즉 유효환 힙 범위를 초과했을때 corruption 이 생긴다. 만약에 free() 를 사용하지 않고, malloc 을하면 live(RunTime) 일때 Memory Lick(메모리 누수)가 날수 있다. 계속 요청만하고 풀어주지 않아서 문제가 생기는것이다. 즉 반납을해야 지속적으로 사용하거나 재사용을 할수 있다. 그렇다면 free 를 두번 하면 될까? 라는 질문이 생길수 있다. 첫번쨰 free 를 했을때, 그 Header 에 얼마나 할당받았는지를 알수 있는데 free 를 할때 그정보도 날려주기 때문에 두번째에 free 를 했을때 엉뚱한값을 free 하려고 해서 문제가 생긴다.

그렇다면 어떤게 정말 큰 잘못일까? 라고 생각을 하면, 바로 use-after-free 라는 것이다. 아래의 코드를 보면 이 케이스를 볼수 있다. free 를 이미 했으면 그 메모리를 건들면 안되는데, m1 이라는 메모리를 건드는걸 볼 수 있다. pointer 는 살아있기때문에, 건드릴수 있는데 crash 가 날때가 있고 없을때도 있다. 근데 없을때가 정말 큰일이나는 거다. 즉 엉뚱한 메모리를 건들게되면 위험하기때문에, 동적할당을 사용할때는 사용자가 잘사용해야된다는것이다. 그래서 일단 방지 할수 있는거는 pointer=null 을 할수 있다.

class Monster
{
public:
    int _hp;
    int _x;
    int _y;
    int _z;
};

int main()
{
    void* pointer = mallocs(sizeof(Monster));
    Monster* m1 = (Monster*)pointer;
    m1._hp = 100;
    m1._x = 0;
    m1._y = 0;
    m1._z = 0;

    free(pointer);
    // m1._hp = 100;
    // m1._x = 0;
    // m1._y = 0;
    // m1._z = 0;
    pointer = null;     
    return 0;
}

사실 malloc, calloc, realoc 은 C 에서 사용되는 부분이였다. 그래서, C++ 에서 자주 사용되는 newdelete 를 알아보자. 일단 넘어가기전에 malloc, calloc, realoc 이들은 함수였다. 하지만 C++ 에서 동적할당을하는 newdelete 연산자(operator)이다.

아래의 코드를 봐보자. new 를 했을때 타입을 넣어준다. 이때 타입의 크기만큼을 동적할당을 해준다. 앞에서 mallocfree 가 같이 있듯이, newdelete 같이 묶여 다니는걸 볼수 있다. 버그의 케이스도 위의 malloc 과 같다. 그러면 우리가 Monster 를 여러마리를 만들고 싶을때는 아래의 코드를 보면 new Monster[5] 를 사용해서 5 마리의 몬스터를 만든것을 볼수 있다.

class Monster
{
public:
    int _hp;
    int _x;
    int _y;
    int _z;
};

int main()
{
    Monster* m1 = new Monster;
    m1._hp = 100;
    m1._x = 0;
    m1._y = 0;
    m1._z = 0;
    delete m1;

    Monster* m2 = new Monster[5];
    delete[] m2;
    return 0;
}

그렇다면 malloc / free 그리고 new / delete 의 Use-Case 같은경우는 사용 편의성에서는 확실이 new / delete 이게 좋지만, 타입에 상관없이 특정한 크기의 메모리 영역을 할당받으려면 malloc / free 이 좋다. 정말 가장 중요한 근본적인 차이는 또 new / delete 는 생성타입이 클래스일 경우, 생성자 / 소멸자를 호출해준다. 즉 위의 코드에서 Monster 를 여러개 만들때 생성자와 소멸자가 5번 호출을 해주고, 만약에 delete 를 한번 했었을때, 에러를 뱉어내는걸 확인할수 있다.

class Monster
{
public:
    Monster(){ cout << "Monster()" << endl;}
    ~Monster(){ cout << "~Monster()" << endl;}
    int _hp;
    int _x;
    int _y;
    int _z;
};

int main()
{
    Monster m1 = new Monster;
    delete m1;
    return 0;
}

마지막 예를 보자. 아래의 코드를 봐보면, 실제 stack 에서 Item 을 instantiate 했을때는, 생성자가 호출이 되는걸 확인 할수 있는데 pointer 로 Item 이라는걸 생성할때는 생성자가 호출이 될수도 있고 안될수도 있다. 아래의 같이 pointer 로 배열을 만들경우 타입만 선언을 했기때문에 그리고 초기에는 아무것도 없기 때문에 생성자가 없을 수 도 있다. 그래서 아래와 같이 객체생성을 하기위해서 loop 을 돌면서 생성을 하고 그 다음에 소멸자도 마찬가지로 소멸을 시키면 소멸자가 호출이된다.

class Item
{
public:
    Item(){cout << "Item()" << endl;}
    Item(const Item& item){cout << "Item(const: item&" << endl;}
    ~Item(){ cout << "~Item()" << endl;}
public:
    int _itemType = 0;
    int _itemObid = 0;

    char _dummy[4096] = {};
};

void TestItem(Item item){ /* 복사생성자 호출 */ }

void TestItemPtr(Item* item){ /*원본을 건드리기때문에 원격*/}

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

    TestItem(item);
    TestItem(*item2);

    TestItemPtr(&item);
    TestItemPtr(item2);

    Item item3[100] = {};

    // 실질적으로 아무것도 없을수 있음
    Item* item4[100] = {};

    for(int i=0; i<100; i++)
        Item4[i] = new Item();

    for(int i=0; i<100; i++)
        delete Item4[i];
    delete item2;
    return 0;
}

Type Conversion

위에서 보았듯이 malloc / free 를 사용했을때 void 값으로 설정을 해준 이후 타입을 변환한걸 확인 할수 있었다.

일단 타입 변환에도 유형(비트열 재구성 여부)이 있다.

  1. 값 타입 변환
    1. 의미를 유지하기 위해서, 원본 객체의 다른 비트열 재구성.
  2. 참조 타입 변환
    1. 비트열을 재구성하지 않고 관점만 바꾸는 것
int main()
{
    {
        // 값 타입 변환
        int a = 123456789;
        float b = (float)a;
    }
    {
        // 참조 타입 변환
        int a = 123456789;
        float b = (float&)a;
    }
    return 0;
}

타입 변환에서는 변환이 안전하게 변하게 되는 경향과 불안전하게 변환되는게 있다. 예를 들어서 Upcasting 즉 작은 메모리를 타입을 가지고 있는걸, 큰거에다가 변환시켰을때에 안전하게 변환되는걸 확인할수 있다. 그렇다면 불완전하게 되는 경향은 이거에 반대되는 상황일다. 이럴 경우 데이터의 손실을 불러 일으킨다.

또 프로그래머의 암시적변환과 명시적 변환이존재한다. 암시적 변환 같은 경우는 컴파일러에게 알아서 변환 인거고, 명시적인거는 프로그래머가 따로 괜찮으니까 타입캐스팅을 해줘라는 느낌이다.

그렇다면 객체 클래스가 나온다고 했을때는 어떻게 타입변환을 어떻게 해야할까? 라는 질문을 할 수 있다. 일반적으로는당연히 타입변환이 되지 않는다. 하지만 예외적인 케이스는 있다. 타입 변환생성자를 만들어주게 되면, 타입변환이 가능하다. 그리고 타입 변환 연산자로 타입변환이 가능하다. 아래의 코드를 참고하자.

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

class Dog
{
public:
    Dog(){}
    // 타입 변환 생성자
    Dog(const Knight& knight){ _age = knight._hp;}

    // 타입 변환 연산자 (return type 이 없음)
    operator Knight()
    {
        return (Knight)(*this);
    }
public:
    int _age = 1;
    int _cuteness = 2;
};

int main()
{
    Knight knight;
    Dog = dog (Dog)knight; 
}

그리고 또 연관없는 클래스 사이의 참조 타입변환을 알아보자. 아래의 코드를 봐보면 (Dog&) 이게 knight 앞에 없으면 에러를 내뱉는다. 이건 일단 기본적으로 서로 타입이 안맞기 때문이다. 그리고 문법적으로 봤을때는 일단 참조기 때문에 주소값을 타고 가면 Dog 가 있을꺼야라고하는게 사실을 문법적으로 맞다. 하지만 사실적으로는 Knight 이 있는거다. 즉 문법적으로 통과할지라도, 사실의 값이 다르다는건 명시적으로는 괜찮다.

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

class Dog
{
public:
    Dog(){}
    // 타입 변환 생성자
    Dog(const Knight& knight){ _age = knight._hp;}

    // 타입 변환 연산자 (return type 이 없음)
    operator Knight()
    {
        return (Knight)(*this);
    }
public:
    int _age = 1;
    int _cuteness = 2;
};

int main()
{
    Knight knight;
    Dog& dog = (Dog&)knight; 
}

그렇다면 마지막으로 볼수 있는게 클래스에서 중요했던 상속관계에 있는 클래스 사이의 변환은 어떻게 될까? 첫번째는 상속 관계 클래스의 값타입변환이 있다. 아래의 코드를 보면 bulldog 은 dog 를 상속 받고 있기때문에, 말에 일리가 있다. 즉 자식의 타입변환을 해서 부모님에게 저장하는건 가능하다 라는 말이다.

class Dog
{
public:
    Dog(){}
    // 타입 변환 생성자
    Dog(const Knight& knight){ _age = knight._hp;}

    // 타입 변환 연산자 (return type 이 없음)
    operator Knight()
    {
        return (Knight)(*this);
    }
public:
    int _age = 1;
    int _cuteness = 2;
};

class BullDog : public Dog
{
public:
    bool IsFrench;
};

int main()
{
    BullDog bulldog;
    Dog dog = bulldog;
}

마지막으로는 상속관계 클래스의 참조 타입 변환이다. 아래의 코드를 확인했을때 자식에서 부모의 타입변환은 Ok 지만, 부모에서 자식으로 할때 암시적으로는 안돼지만, 명시적으로는 Ok 한다.

class Dog
{
public:
    Dog(){}
    // 타입 변환 생성자
    Dog(const Knight& knight){ _age = knight._hp;}

    // 타입 변환 연산자 (return type 이 없음)
    operator Knight()
    {
        return (Knight)(*this);
    }
public:
    int _age = 1;
    int _cuteness = 2;
};

class BullDog : public Dog
{
public:
    bool IsFrench;
};

int main()
{
    Dog dog;
    BullDog& bulldog = (BullDog&)dog; 
}

Pointer Type Conversion

위와같이 Type Conversion 을 연이어 해보자. 일단 연관성이 없는 클래스 사이의 포인터 변환을 해보자. 아래의 코드에서 보면 명시적으로는 Ok 지만 암시적인것은 컴파일러에서 에러를 내뱉는걸 확인할수있다. 이걸 해석해보자면 item 의 주소를 타고 가면 Item 이 있다라는걸 명시해주는데, 사실상 틀린거라고 확인할수있다. 그런데 여기서 문제점은 실제 Knight() 안에 _hp 가 4 byte 일텐데 item->_ItemType 을 했을때까지는 괜찮다. 왜냐하면 같은 4 byte 일테니까. 하지만 두번째 _ItemDbId 를 넣을경우 엉뚱한곳에다가 메모리의 값을 수정하다보니 메모리오염이 있을수가 있다. 또 이건 에러로 내뱉지도 않으니, 그냥 지나칠수 있는 치명적인 메모리의 오염의 주범이 될거다.

class Knight
{
public:
    int _hp;
};

int main()
{
    Knight* knight = new Knight();
    // Item* item = knight; 암시적 NO
    // 명시적 OK
    Item* item = (Item*)knight;
    item->_ItemType = 2;
    item->_ItemDbId = 1;
    delete knight;
    return 0;
}

그렇다면 상속관계에서의 포인터 타입 변환관계를 알아보자. 여기에서도 명시적으로 하면 Ok 지만, 사실 엉뚱한 메모리를 바꿀수 있는 위험이 있다. 하지만, 논리적으로 생각했을때 자식에서 부모 변환테스트는 암시적으로는 된다. 당연히 Weapon 은 Item 이 맞기 때문이다. 즉 명시적으로 타입변환을 할때는 항상 조심해야한다.

그렇다면 항상모든게 명시적으로 하는게 좋지 않느냐라는 질문을 할수 있지만 아래의 코드를 보면, 자식에서 부모로 가는건 설계적인 면에서 많은 이득을 볼수 있기때문에, Inventory 라는 pointer array 를 사용해서 추가할수 있다. 이 코드에서 사실 제일 중요한 부분은 Okay. 분명 포인터 타입변환을해서 새로운 객체 생성도 했어. 하지만 제일 중요한건 메모리를 빌렸으면 깔끔하게 반납하는게 사실 제일 중요하다. 생성할때는 Item 으로 관리를해서, loop 을 돌면서 item 을 지운다고 하면 어떻게 될까? 일단 Item 만 삭제 하려고 하면 안되고, weapon 이나 armor 의 소멸자를 호출해야 제일 깔끔하게 지워준다.

class Item
{
public:
    Item(){cout << "Item()" << endl;}
    Item(int itemType) : _itemType(_itemType) {};
    Item(const Item& item){cout << "Item(const: item&)" << endl;}
    ~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; _damage = rand() % 100 + 1;} 
    ~Weapon(){ cout << "~Weapon()" << endl; }
public:
    int _damage = 0;
};

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

int main()
{
    // Parent -> child
    Item* item = new Item();
    // item 은 무기냐? --> 아니다. 다른거일수도 있잖아!
    Weapon* weapon = item;

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

    delete item;
    delete weapon;
    

    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;
        }
    }


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

        if (item->_itemType == IT_WEAPON)
        {
            Weapon* weapon = (Weapon*)item;
            cout << "Weapon Damage: " << weapon->_damage << endl;
        }

        if (item->_itemType == IT_ARMOR)
        {
            Armor* armor = (Armor*)item;
            cout << "Armor " << armor->_defence << endl; 
        }
    }

    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;
        }

        if (item->_itemType == IT_ARMOR)
        {
            Armor* armor = (Armor*)item;
            delete armor;
        }
    }

    return 0;
}

오케이 여기까지 해봤는데, 뭔가 쉬운 방법이 없을까? 라는 생각이든다. 즉 위의 코드 처럼 타입별로 지우는 방법도 있지만, virtual 이라는 keyword 를 사용해서 자식이 어떤 타입이든 상관하지 않고 지울수 있는 방법이 있다. 아래의 코드를 보면 확실히 코드가 깔끔해지는걸 볼수 있다. 가상함수의 개념을 사용해서, virtual keyword 소멸자

class Item
{
public:
    Item(){cout << "Item()" << endl;}
    Item(int itemType) : _itemType(_itemType) {};
    Item(const Item& item){cout << "Item(const: item&)" << endl;}
    virtual ~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; _damage = rand() % 100 + 1;} 
    virtual ~Weapon(){ cout << "~Weapon()" << endl; }
public:
    int _damage = 0;
};

class Armor : public Item
{
public:
    Armor() : Item(IT_ARMOR){ cout << " Armor() " << endl;}
    virtual ~Armor(){ cout << " ~Armor() " << endl;}
public:
    int _defence = 0;
};

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

        delete item;
    }
    return 0;
}

결론적으로 포인터나 일반적인 타입의 생성자 호출이 중요했으며, 포인터 사이의 타입변환(캐스팅)을 할 떄는 매우 매우 조심해야한다. 부모-자식 관계에서 부모 클래스의 소멸자에는 까먹지 말고 virtual 을 붙이는게 굉장히 중요하다라는걸 알아보았다.

Shallow Copy vs Deep Copy

가끔식은 객체를 우리가 복사해서 사용을 할수 있다. 원본 데이터는 그대로 내두고, 복사를 통해서 복사한 값으로 이리 저리 사용한다음에 테스팅만 할수 도 있다. 그렇면 복사에 대해서 잠깐 알아보자.

아래의 코드를 봐보자. 아래의 main() 쪽을 봐보자. 일단 복사 생성자와 복사 대입연산자가 없어도 컴파일러가 암시적으로 만들어주는걸 확인 할수 있다. 컴파일러가 기본적으로 만들어주는건, 물론 편하지만, 가끔씩은 커스텀을 해야할 필요가 있다. 즉 그런 케이스는 참조와 포인트를 사용할 경우가 있다.

class Knight
{
public:
    Knight(){};
    ~Knight(){};

public:
    int _hp = 100; // c++11
};

int main()
{
    Knight knight;
    knight._hp = 200;

    Knight knight2 = knight; // 복사 생성자
    Knight knight3(knight);

    Knight knight4; // 기본생성자
    knight4 = knight; // 복사 대입 연산자
}

위의 코드에서는 Knight 의 기본 class 가 있었다. 하지만 기사들이 만약에 pet 이라는 것을 들고 다닌다고 생각해보자. 근데 아래 처럼 Knight 에 Pet 이 속해 있게끔 Knight 클래스 안에 Pet 을 넣었다고 생각을 해보자. 근데 여기 설계에서 안좋은 점은 Pet 의 객체의 생명주기를 tracking 하기 힘들다는 것이다. 그리고 Pet class 안에 정말 큰 이상한 데이터가 들어간다고 생각해보면, Knight 를 instantiate 할때마다 엄청난 큰 데이터를 들고 있는건 메모리 측면에서도 비효율 적이다. 그리고 만약 지금은 괜찮겠지만 Pet 을 상속을 받는 클래스가 있다고 하면 Knight 에서는 상속받는 클래스를 지정해주기가 어렵다. 그래서 Pet* _pet; 이런식 으로 만들면 된다.

다시 복사에대해서 생각을 해보자. 아래와 같이 기본 복사 대입 연산자나 복사 생성자를 통해서 만든다고 했을때, knight 가 들고 있는 Pet 을 그대로 들고 있다는 걸 확인 할 수 있다. 즉 한 펫을 공유하고 있다. 이런 공유의 개념은 사실 얕은 복사(Shallow Copy)라고 생각을 하면 된다. 어떤 하나의 객체를 복사를 하려고 했을떄, 그 복사되는 객체가 다른 객체의 주소값을 그대로 가지고 있어서, 공유가 되는 현상이다.

class Pet
{
public:
    Pet(){ cout << "Pet()" << endl; }
    Pet(const Pet& pet){ cout << "Pet(const&)" << endl; } // 복사 생성자
    ~Pet(){ cout << "~Pet()" << endl; }
};

class Knight
{
public:
    Knight(){};
    ~Knight(){};

public:
    int _hp = 100; // c++11
    //Pet _pet;
    Pet* _pet;    
};

int main()
{
    Pet* pet = new Pet();
    Knight knight;
    knight._hp = 200;
    knight._pet = pet;

    Knight k2 = knight;
    Knight k3;
    k3 = knight;
    return 0;
}

이거만 봤을때 얕은 복사의 역활은 알겠지만 문제가 되는점이 뭘까?라고 생각을 해보자. 얕은 복사가 문제가 될경우는 이거다. 만약 Pet 의 생명주기가 knight 의 생명주기가 같다고 아래의 코드와 같이 생각 해보자. 세개의 Knight 의 객체가 하나의 Pet 을 바라보기 때문에, 소멸자를 날려줄때, 한번삭제는 가능하지만, 나머지는 아예 삭제된 객체를 보기 떄문에 문제가 생긴다. 즉 double free 문제가 생긴다.

class Pet
{
public:
    Pet(){ cout << "Pet()" << endl; }
    Pet(const Pet& pet){ cout << "Pet(const&)" << endl; } // 복사 생성자
    ~Pet(){ cout << "~Pet()" << endl; }
};

class Knight
{
public:
    Knight()
    {
        _pet = new Pet;
    };
    ~Knight()
    {
        delete _pet;
    };

public:
    int _hp = 100; // c++11
    Pet* _pet;    
};

int main()
{
    Knight knight;
    knight._hp = 200;
 
    Knight k2 = knight;
    Knight k3;
    k3 = knight;
    return 0;
}

그래서 이 Shallow Copy 를 안하기 위해서, Deep Copy(깊은 복사)를 하면 된다. 즉 위의 예제와 같이 Knight 들은 각자 자기들만의 Pet 의 객체를 들고 싶어한다. 포인터는 주소값이 있다면, 주소를 그대로 복사하는게 아니라 새로운 객체를 생성하고 상이한 객체를 가르키는 상태가 되게 할 수 있다.

다시 말해서 깊은 복사를 하려면, Compiler 에서 제공되는 기본 복사 생성자나 복사 대입 연산자를 사용하면 안되고, 명시적인 표현이 필요하다. 아래의 코드를 봐보자. 아래의 코드를 보면 복사 대입연산자와 복사 생성자를 명시적으로 표현한게 보인다. 일단 Pet 을 새롭게 만들어야하는것도 맞지만 Pet 의 복사 생성자를 이용해서 knight._pet 을 인자로 준게 보인다. 이건 Knight 에 해당되는 _pet 에 속한다라고도 생각하면 된다. 이것이 깊은 복사라고 한다.

class Pet
{
public:
    Pet(){ cout << "Pet()" << endl; }
    Pet(const Pet& pet){ cout << "Pet(const&)" << endl; } // 복사 생성자
    ~Pet(){ cout << "~Pet()" << endl; }
};

class Knight
{
public:
    Knight()
    {
        _pet = new Pet;
    };
    Knight(const Knight& knight) // 복사 생성자
    {
        _hp = knight._hp;
        _pet = new Pet(*(knight._pet));
    }
    Knight& operator=(const Knight& knight) // 복사 대입 연산자
    {
        _hp = knight._hp;
        _pet = new Pet(*(knight._pet));
        return *this;
    }
    ~Knight()
    {
        delete _pet;
    };

public:
    int _hp = 100; // c++11
    Pet* _pet;    
};

int main()
{
    Knight knight;
    knight._hp = 200;
 
    Knight k2 = knight;
    Knight k3;
    k3 = knight;
    return 0;
}

암시적으로 생성되는 복사 생성자와 복사 대입 연산자를 알아보자, 그리고 그 스텝과 명시적과의 차이를 알아보자. 일단 결론적으로 말하자면, 암시적 복사 생성자의 step 은 이렇다.

  1. 암시적 복사 생성자 Steps
    1. 부모 클래스의 복사 생성자 호출
    2. 멤버 클래스(pointer x)의 복사 생성자 호출
    3. 멤버가 기본 타입일 경우 메모리 복사 (shallow copy)
  2. 명시적 복사 생성자 Steps
    1. 부모클래스의 기본 생성자 호출
    2. 멤버 클래스의 기본 생성자 호출

아래의 코드를 한번 봐보자. 아래의 코드는 암시적으로 복사 생성자와 복사 대입생성자의 흐름을 알수 있다.

class Pet
{
public:
    Pet(){ cout << "Pet()" << endl; }
    Pet(const Pet& pet){ cout << "Pet(const&)" << endl; } // 복사 생성자
    ~Pet(){ cout << "~Pet()" << endl; }
};

class Player
{
public:
    Player(){ cout << "Player()" << endl;}
    Player(const Player& player) 
    { 
        cout << "Player(const Player&)" << endl; 
        _lvl = player._lvl; 
    }
    Player& operator=(const Player& player)
    { 
        cout << "Player operator=()" << endl; 
        _lvl = player._lvl;
        return *this; 
    }
    ~Player(){cout << "~Player()" << endl; }
public:
    int _lvl = 0;
};

class Knight : public Player
{
public:
    Knight()
    {
    };
    ~Knight()
    {
    };

public:
    int _hp = 100; // c++11
    Pet _pet;    
};

int main()
{
    Knight knight;
    knight._hp = 200;
 
    Knight k2 = knight; // 복사 생성자
    Knight k3;
    k3 = knight; // 복사 대입 연산자
    return 0;
}

명시적인걸 한번 봐보자. Inherit 에서 명시적으로 복사 생성자를 했을 경우 주의점은 부모의 복사생성자가 call 이 됬는지, 기본 생성자가 생성이 되어있는지를 확인해보아야 한다.

class Pet
{
public:
    Pet(){ cout << "Pet()" << endl; }
    Pet(const Pet& pet){ cout << "Pet(const&)" << endl; } // 복사 생성자
    ~Pet(){ cout << "~Pet()" << endl; }
};

class Player
{
public:
    Player(){ cout << "Player()" << endl;}
    Player(const Player& player) 
    { 
        cout << "Player(const Player&)" << endl; 
        _lvl = player._lvl; 
    }
    Player& operator=(const Player& player)
    { 
        cout << "Player operator=()" << endl; 
        _lvl = player._lvl;
        return *this; 
    }
    ~Player(){cout << "~Player()" << endl; }
public:
    int _lvl = 0;
};

class Knight : public Player
{
public:
    Knight()
    {
    };
    Knight(const Knight& knight) : Player(knight), _pet(knight._pet)
    {
        _hp = knight._hp;
    }
    ~Knight()
    {
    };

public:
    int _hp = 100; // c++11
    Pet _pet;    
};

int main()
{
    Knight knight;
    knight._hp = 200;
 
    Knight k2 = knight;
    Knight k3;
    k3 = knight;
    return 0;
}

그 다음에는 암시적 복사 대입연산자의 step 을 알아보자.

  1. 암시적 복사 대입 연산자 step
    1. 부모 클래스의 복사 대입 연산자 호출
    2. 멤버 클래스의 복사 대입 연산자 호출
    3. 멤버가 기본 타입일 경우 메모리 복사 (얕은 복사 shallow copy)
  2. 명시적 복사 대입 연산자 step
    1. 알아서 잘해라!

암시적인 복사 대입연산자도 암시적 복사 생성자와 같다.

class Pet
{
public:
    Pet(){ cout << "Pet()" << endl; }
    Pet(const Pet& pet){ cout << "Pet(const&)" << endl; } // 복사 생성자
    ~Pet(){ cout << "~Pet()" << endl; }
    Pet& operator=(const Pet& pet){ cout << "Pet& operator()="<< endl; return *this; }
};

class Player
{
public:
    Player(){ cout << "Player()" << endl;}
    Player(const Player& player) 
    { 
        cout << "Player(const Player&)" << endl; 
        _lvl = player._lvl; 
    }
    Player& operator=(const Player& player)
    { 
        cout << "Player operator=()" << endl; 
        _lvl = player._lvl;
        return *this; 
    }
    ~Player(){cout << "~Player()" << endl; }
public:
    int _lvl = 0;
};

class Knight : public Player
{
public:
    Knight()
    {
    };
    Knight(const Knight& knight) : Player(knight), _pet(knight._pet)
    {
        _hp = knight._hp;
    }
    Knight& operator=(const Knight& knight)
    {
        cout << "Knight operator=()" << endl;
        _hp = knight._hp;
        return *this;
    }
    ~Knight()
    {
    };

public:
    int _hp = 100; // c++11
    Pet _pet;    
};

int main()
{
    Knight knight;
    knight._hp = 200;
 
    Knight k2 = knight;
    Knight k3;
    k3 = knight;
    return 0;
}

하지만, 명시적인것은 얕은복사를 피하기 위해서 Knight 의 복사 대입연산자를 호출 했을경우 Pet 과 Player 에 대한 정보가 없으므로 초기 세팅이 필요하다.

class Pet
{
public:
    Pet(){ cout << "Pet()" << endl; }
    Pet(const Pet& pet){ cout << "Pet(const&)" << endl; } // 복사 생성자
    ~Pet(){ cout << "~Pet()" << endl; }
    Pet& operator=(const Pet& pet){ cout << "Pet& operator()="<< endl; return *this; }
};

class Player
{
public:
    Player(){ cout << "Player()" << endl;}
    Player(const Player& player) 
    { 
        cout << "Player(const Player&)" << endl; 
        _lvl = player._lvl; 
    }
    Player& operator=(const Player& player)
    { 
        cout << "Player operator=()" << endl; 
        _lvl = player._lvl;
        return *this; 
    }
    ~Player(){cout << "~Player()" << endl; }
public:
    int _lvl = 0;
};

class Knight : public Player
{
public:
    Knight()
    {
    };
    Knight(const Knight& knight) : Player(knight), _pet(knight._pet)
    {
        _hp = knight._hp;
    }
    Knight& operator=(const Knight& knight)
    {
        cout << "Knight operator=()" << endl;
        Player::operator=(knight);
        _hp = knight._hp;
        _pet = knight._pet;
        return *this;
    }
    ~Knight()
    {
    };

public:
    int _hp = 100; // c++11
    Pet _pet;    
};

int main()
{
    Knight knight;
    knight._hp = 200;
 
    Knight k2 = knight;
    Knight k3;
    k3 = knight;
    return 0;
}

Casting

또 C++ 에서 casting 에 관련된 함수들이 존재한다. 한번 알아보자.

  1. static_cast
  2. dynamic_cast
  3. const_cast
  4. reinterpret_cast

static_cast 같은경우, 타입 원칙에 비춰서 볼때, 상식적인 캐스팅만 허용해준다. (예 int <-> float). 그리고 다운 캐스팅도 허락이된다 (예 Player* -> Knight*). 이것도 마찬가지로 뭔가 타입 캐스팅을 할때 프로그래머가 객체의 구조를 확인하고, 명시적으로 할수 있는지 없는지에 따라서 결정을 해야한다. 아래의 코드를 확인해보자.

int hp = 100;
int maxHP = 200;
float ratio = static_cast<float>(hp) / maxHP;
class Player
{

};

class Knight : public Player
{

};

class Archer : public Player
{

};

int main()
{
    Player* player = new Player(); // 예외의 케이스
    Knight* k1 = static_cast<Knight*> player;

    Knight* k2 = new Knight();
    Player* p2 = static_cast<Player*>(k2);
    return 0;
}

그 다음에는 dynamic_cast 를 확인해보자. 결국 dynamic_cast 같은 경우 static_cast 의 단점을 살짝 보완해주는 느낌이다. 이게 어떻게 작동하는지는 RTTI(RunTime Time Information) 라는 거에 결정이 되는데, 이 개념은 사실 virtual 과 비슷하다. .vftable 에서 run time 에서 타입을 확인 할수 있다. 즉 virtual keyword 가 있어야 dynamic_cast 를 사용할수 있다라는 것이다. 그리고 만약에 잘못된 타입으로 캐스팅을 했으면, nullptr 로 반환을 한다. 근데 이렇게 .vftable 를 확인해서, 그 타입이 한번 맞는지 더 체크하는게 performance 에서는 않좋을수 있으니 static 과 같이 사용하는게 좋다. 아래의 코드를 보면 정의하는 방법이있다.

Knight* k4 = dynamic_cast<Knight*>(player)

const_cast 의 경우는 const 를 떄고 붙이고 하는 역활을 한다. 아래와 같이 "Nick" 같은 경우는 const char pointer 이기 때문에 안될수 밖에 없다. 그래서 const_cast 를 사용해서 const 를 빼고 넘겨주기 떄문에 PrintName 의 signature 에 맞아서 작동 하게 된다.

PrintName(char *str)
{

}

int main()
{
    PrintName(const_cast<char*>("Nick"));
}

그리고 마지막 reinterpret_cast 를 알아보자. 이 친구는 강력한 형태의 캐스팅이다. 예를 들어서 포인터와 전혀 관계업는 다른타입 변환이 있다.

Knight* k2;
__int64 address = reinterpret_cast<__int64> k2;

Resource

Source Code

What is Pointer ?

포인터는 한마디로 해서, 주소를 저장하고 있는 바구니라고 생각 하면 된다, 주로 포인터 사이즈는 64 비트 기준으로 8 바이트이다. 일단 사용하는 방법은 TYPE* VAR_NAME 이런 식으로 사용하면 된다. 그러면 딱 코드를 봤을때 어떻게 생각하면 되냐? 뭔가 *(Asterisk) 이게 등장했다하면 포인터 = 주소라고 생각하면된다. 아래와 같이 number 라는 variable 은 Stack Memory 에 저장이 되어있는데, 이 Stack Memory 에 주소값을 ptr 로 받아주는 느낌이라고 생각하면 된다. 그렇다면 주소의 값은 어떻게 받냐? 라고 하냐면 & (ampersand) 이렇게 받으면된다. 다시말하자면 *ptr 이라는건 주소를 저장하는 형식의 바구니라는거라고 생각하면 된다.

int number = 1;
int *ptr = &number;

그렇다면 이 주소를 가지고 있는 바구니가지고 뭘할수 있을까 라고 생각이든다. 값을 가지고 오는 방법은 뭐가있을까? 일단 그러기전에 변수선언(variable declartion)을 한상태에서, 사용할때는 마치 포틀을 타고 순간이동을 한거나 마찬가지라고 생각을 하면된다. 왜 갑자기 포탈이라고 생각할수 있는데. 만약 메모리를 까보면 이런식으로 되어있다. 위의 코드를 봤을때 ptr 의 값을 &ptr 이런식으로 가게되면 주소값이 저장된걸 확인할수 있고, 그 값을 통해서 가면 number 의 주소로 향하고 있다는걸 확인 할수 있다. 또한 아래의 코드를 보자면, ptr 을 타고 들어가서 그 값은 주소값이니까, 그주소로 가면 2로 변경된걸 확인 할수 있다. 즉 포탈을 타고 가서 값을 변경하는것이다. 이걸 타입면에서 생각을 해보았을때 * 가 있다면 ptr 을 가면 int 가 있다고 생각해도 된다. 그런데, 타입 캐스팅이 가능하기 때문에, 메모리 오염을 시킬수 있다.

int value1 = *ptr;
*ptr = 2

// but let's think about this way
// what if type is differnet
__int64* ptr2 = (__int64*)&number;
*ptr = 0x0000AABBCCDDEEFF; // this wil contaminate the memory since it will allocate extra space.

아래와 같은 예제는 함수에 사용될때의 예시인데, main() 안에 stack memory 에 올라간 hp 를 SetHp로 인자를 넘겨줬을때, 값은 변화하지 않는다. 값을 변화시킬려면 stack memory 안에 있는 hp 의 주소를 던져줘서 hp value 를 바꿀수 있는 예제이다.

void SetHp(int *hp)
{
    *hp = 100;
}

int main()
{
    int hp = 1;
    SetHp(&hp) // give the address.
    return 0;
}

Pointer Operation

포인터 연산에는 아래와 같이 나누어진다.

  1. 주소 연산자 : ampersand(&)
  2. 산술 연산자
  3. 간접 연산자
  4. 간접 멤버 연산자

주소 연산자 같은 경우는 & 사용해서 주소값을 가지고 올때 사용한다. 즉 해당 변수 타입에 따라서 TYPE* 을 return 한다. 산술 연산자 같은 경우 +- 를 사용한다. 다만 나눗셈과 곱셈 연산은 사용되지 않는다. +- 를 했을때 그 타입의 사이즈를 따라서 그 주소 뒤에 가거나 앞으로 간다고 생각하면 된다. 즉 타입만큼의 크기만큼 이동하는거라고 생각하면 된다. 간접 연산자 같은경우는, 우리가 포인터를 생성할때 * 사용했었다. 포인터란 다시말해서, 포탈을 타고가라 라고 생각하면 됬었다 했었다. 그렇다면 간접 멤버 연산자는 뭘까? 정담은 이 친구 -> 이다. 아래의 예제 코드를 보면, player 의 객체의 주소를 playerPtr 향하게 했었다. 그말은 playerPtr 에는 player 의 주소가 담겨져 있다 라고 생각하면 된다. 그래서 player 의 멤버 변수인 _hp_damage 를 바꾼다고 가정했을때 아래와 같이 할수 있다. 그럼 간접연산자를 어떻게 사용할수 있을까는 그 다음 코드 segment 로 사용해도 똑같다. 즉 * 와 . 을 합친게 -> 라고 생각하면 쉽다.

class Player()
{
public:
    int _hp;
    int _damage;
}

int main()
{
    int number = 1;
    int* ptr = &number;

    Player player;
    player._hp = 100;
    player._damage = 30;

    Player* playerPtr = &player;

    //pointer
    (*playerPtr)._hp = 200;
    (*playerPtr)._damage = 40;

    // indirect member op
    playerPtr->_hp = 300;
    playerPtr->_damage = 50;
    return 0;
}

예제 코드는 아래, resource 에 있다.

Reference

이걸 말하기전에 바로 코드로 넘어가보자. StatInfo 라는 struct 를 만들어서 pointer 값을 CreateMonster 함수 에서 parameter 로 받았을때, hp, attack, and defence 는 각각 4 바이트씩 차지하고 있을거다. 그래서 CreateMonster 함수에서 인자로 넘겨줄때는 주소값(address)를 넘겨주는걸 확인할수 있다. 하지만 만약 CreateMonster 함수의 paramater 를 StatInfo info 이런 식으로 signature 가 저장이 되어있다면 어떻게 될까? 라고 생각을 하자면, 원본 데이터 즉 main 에 있는 info 가 인자값으로 돌아와 그 매개의 복사값을 생성하면서 값을 채워넣는다. 즉 CreateMonster 안에 있는 함수의 info 와 main 에 있는 함수는 별개의 것이며, info 라는 매개변수를 새로 생성해서 복사를 하는 형태가 된다. 즉 원본데이터를 수정하기 위해선 주소값을 넘겨주고, 인자선언은 pointer 로 받으면 된다는 뜻이다. 근데 결과물은 똑같다. 하지만 performance 에서 봤을땐 복사값을 만들어서 붙여넣기 하는 형태이고, 주소값을 보내는 친구는 그냥 딱 야 이거해 하는 느낌인거다.

struct StatInfo()
{
    int hp;
    int attack;
    int defence;
};

void CreateMonster(StatInfo* info)
{
    info->hp = 100;
    info->attack = 9;
    info->defence = 5;
}

int main()
{
    StatInfo info;
    CreateMonster(&info);
    return 0;
}

reference 놈을 알아보자. reference 는 c 에 없고 c++ 에 있는 친구인데. 일단은 low level (assembly) 단에서보면 pointer 와 동일하게 작동한다. 일단 정의 부터 알아보자. reference 를 하고 싶다면, 아래와 같이 하면되는데. 이렇게 생까하면 된다 number 라는 바구니에 reference 라는 다른 이름을 지어줄께. 라고 생각하면 된다. 뭐야? reference 가 더쉽잖아. 왜 구지 포인터를 쓰면서 까지 저렇게 해 라고 되물을 수 있다. 근데 여기서 중요한건 pass_by_referencepass_by_pointer 라고 생각하면 된다. 즉 함수에서 문법이 달라진다.

int number = 3;
int& reference = number;

아래의 코드와 같이 문법이 살짝 달라진다고 알수 있다. pass_by_reference 같은경우 pass_by_pointer 와 달리 info 값을 넣어준걸 확인 할수 있고, pass_by_pointerinfo 의 주소값을 던져주는걸로 알수 있다. 즉 결론을 말하자면 pass_by_referencepass_by_pointer 를 assembly 언어로 까보면 동작은 똑같다. 즉 performance 측면에서는 똑같다. 하지만 문법이 다른것으로 알수있으며 pass_by_reference 는 약간 야매로 pass_by_valuepass_by_pointer 의 중간지점이라고 생각하면 편할것같다.

struct StatInfo()
{
    int hp;
    int attack;
    int defence;
}

void PrintInfoByValue(StatInfo info)
{
  cout << "_________________" << endl;
  cout << info.hp << endl;
  cout << info.attack << endl;
  cout << info.defence << endl;
}

void PrintInfoByPtr(StatInfo* info)
{
  cout << "_________________" << endl;
  cout << info->hp << endl;
  cout << info->attack << endl;
  cout << info->defence << endl;
}

void PrintInfoByRef(StatInfo& info)
{
  cout << "_________________" << endl;
  cout << info.hp << endl;
  cout << info.attack << endl;
  cout << info.defence << endl;
}

int main()
{
    // ...
    StatInfo info;
    PrintInfoByValue(info);
    PrintInfoByPtr(&info);
    PrintInfoByRef(info);
    return 0;
}

Pointer vs Reference

위에서 봤듯이 low level 에서는 pointerreference 가 동일하다는 걸 체크를 했었다. 동일하다는 의미는 Performance 적으로는 확실히 동일하고, 편리성은 reference 가 더 편할수 있다는 말이다. 근데 편하다는건 항상 단점을 가지고 있다. 포인터는 주소를 넘기니 확실하게 원본을 넘긴다는 힌트를 주는데 예를 들어서, & 사용해서 넘겨준다. 하지만 참존는 자연스럼게 모르고 지나칠수 있다는 말이다. 예시를 들자면, 위의 코드에서 만약 함수의 이름이 다 동일한 케이스일 경우 들수 있다.

// 어떤 여러개 선언


int main()
{
    PrintInfo(&info)
    PrintInfo(info) // --> 이럴 경우에는 실제 원본을 건들이는건지, 복사를 하는건지 명확하지 않다.
    return 0;
}

즉 실제 원본을 건들이는건지, 아니면 복사를 하는건지 모를수도 있고, 원본을 훼손할 가능성이 티가 안날수 있다는것도 맞다. 그러면 어떻게 이걸 막을수 있는 방법이 뭘까? 라고 고민할수 있다. (즉 절대 수정하지마라!) 라는걸 어떻게 표현할까. 아래의 코드 처럼 const 를 쓰게 되면 info 가 read-only 로 만들수 있다.

void PrintInfo(SztatInfo *){};
void PrintInfo(const StatInfo& info){};

그렇다면 여기서 또 질문할수 있는게, pointer 앞에 const keyword 를 사용하면 어떤의미를 가지고 있을까도 생각할수 있다. pointer 에서 const 를 쓸때 앞에다가 붙이느냐, 뒤에다가 붙이느냐의 의미는 서로 다르다.

void PrintInfo(const StatInfo* info){}
void PrintInfo(StatInfo)

만약 앞에 const 라는 keyword 가 붙었더라면, info 가 가르키고 있는 메모리에 저장되어있는[바구니] 값을 바꿀수 없는 형태이며, 반대로 뒤에 const 가 붙인다면, info 라는 주소(바구니의 내용물)을 못바꾸는 형식으로 된다. 아래의 예제 코드를 봐보자.

즉 다시말해서, 원격 바구닌를 못바꾸냐, 직접적인 바구니를 못바꾸냐의 차이이다. 아래의 코드를 보면 info 안에 들고 있는 struct 의 member variable? 을 access 할수 없게 되는거며, 뒤에 const 가 바뀔시에는 info 라는 값을 못바꾸는 것이라고 생각하면된다. 즉 안정성을 보안할수 있다는게 중요하다. 그럼 reference 와 pointer 를 비교했을때, nullptr 을 체크해서 사용을 한다면 가끔씩 유용할수 있다는것과 쉽게 바구니의 다른 네이밍을 넘겨주는것도 편리성에서는 좋다는 것이다.

struct StatInfo()
{
    int hp;
    int attack;
    int defence;
};

StatInfo globalInfo;
void ConstantBehind(StatInfo* const info)
{
    info = &globalInfo; // redline (error)
}

void ConstantFront(const StatInfo* info)
{
    info -> hp = 1000; // redline
}

StatInfo* FindMonster()
{
    // TODO: HEAP 영역에서 뭔가를 찾아봄
    // 찾았다
    // return monster
    // if not 
    return nullptr
}

int main()
{
    StatInfo info;
    return 0;
}


또 넘어가보자. 오케이. 알겠어 reference 와 pointer 의 차이점을 이해가간다고 생각을한다면, 사실 맨처음에 다뤄야할게 이제 나온다. 바로 initialization 을 어떻게 할것인가가 문제이다.

참조 타입은 바구니의 2번째의 이름이라고 생각하면 된다. 포인터같은경우는 가르키는 바구니를 말을 하면 되는거다(어떤 ~ 주소 라는 의미). 즉 참조 타입 같은 경우 참조하는 대상이 없으면 안된다. 하지만 포인터 같은경우 가르키는 대상이 실존하지 않을수도 있다. 즉 Null 로 정의 될수 있다는 것이다. Intuitively 생각하자면, 참조 타입 같은 경우, NULL 이나 nullptr 로 initialization 이 없다라고 생각하면된다.

The Lecturer told that it’s case-by-case to use reference or pointer. For example, google uses the pointer, and Unreal Engine prefer the reference.

결국엔 선호에 따라서 달라져있는데, nullptr 처럼 유용하게 쓸수 있다고 고려한다면, pointer 를 사용하고. 바뀌지 않고 읽는 용도로만 사용한다면 const ref& 이런식으로 하고. 그 외 일반적으로 ref (명시적으로) 호출할때 OUT 을 붙여준다. 아래의 코드를 봐보면 될것이다.

// Unreal Engine
#define OUT
void ChangeInfo(OUT Statinfo &info)
{
    info.hp = 1000;
}

int main()
{
    ChangeInfo(OUT info);
    return 0;
}

참고) 포인터로 사용하던것을 reference 로 사용하려면 어떻게 해야되냐. 아래의 코드를 보면된다. 여기에서 ptr 을 그냥 주게 되면 에러가뜨게 된다 그말은 가르키는 주소를 던져준다라고 생각하면 되는데, 우리는 info 의 값을 바꾸고 싶으니까, info 로 가라 라는건 * 연산자를 통해서 가면 된다. 그리고 만약 이걸 반대로 생각해서 우리가 reference 값을 pointer 로 넘기는 방법은, & 를 사용해서 주소값을 넘겨줘라 라고 생각하면 된다.

struct StatInfo()
{
    int hp;
    int attack;
    int defence;
}

void PrintInfoByPtr(StatInfo* info)
{
  cout << "_________________" << endl;
  cout << info->hp << endl;
  cout << info->attack << endl;
  cout << info->defence << endl;
}

void PrintInfoByRef(StatInfo& info)
{
  cout << "_________________" << endl;
  cout << info.hp << endl;
  cout << info.attack << endl;
  cout << info.defence << endl;
}

int main()
{
    StatInfo = info
    StatInfo* ptr = nullptr;
    ptr = &info;
    PrintInfoByRef(*ptr);

    StatInfo &ref = info;
    PrintInfoByPtr(&ref)
    return 0;
}

Basic Array

배열이란 결국 우리가 1 층 아파트를 생각하면될것이다. 1 층 건물에 누구를 할당시킬것인가가 라고 생각하면 편하다 아래의 코드를 보자. 아래의 코드를 보자면 사실 comment 로 다 작성을 했었다. Pointer 로 작성 했었을때, 일단 포인터는 주소값이고 array 라고 생각하면, 제일 앞에를 가르키고 있을것이다. 그래서 pointer operation 을 통해서 + 를 사용해서 그다음 데이터를 고칠수 있을것이다. 그리고 initialization 같은 경우는 코드를 보면 될것이다.

struct StatInfo
{
    int hp;
    int attack;
    int defence;
};

int main()
{
    const int mCount = 10; // mosnter count
    StatInfo monsters[monsterCount] // how many monster will be in the array

    // Access Array with pointer
    StatInfo* ptr = monsters; // [monster 0] [monster 1] [monster 2] ... [monster 9]
    // ptr is initially in monster 0 location
    ptr->hp = 100;
    ptr->attack = 10;
    ptr->defence = 10;

    // what if we do the operation + 1 to pointer, that is going to be the next one
    StatInfo* ptr1 = ptr + 1;
    ptr1->hp = 200;
    ptr1->attack = 20;
    ptr1->defence = 10;

    // then what if I want to get the reference data using pointer, then this is going to be the second one
    StatInfo& ref = *(ptr + 2);
    ref.hp = 300;
    ref.attack = 30;
    ref.defence = 30;

    // what if I want to go full round
    for (int i=0; i < mCount; i++)
    {
        monsters[i].hp = 100 * (i+1);
        monsters[i].attack = 10 * (i+1);
        monsters[i].defence = (i+1);
    }

    // Array Initialization
    int arr[5] = {}; // the size is going to be 5(0-4), and all variable set to 0
    int arr1[10] = {1, 2, 3, 4}; // [1][2][3][4][0][0].. rest are going to be 0
    int arr2[] = {1, 2, 3, 4, 5, 6}; // depending on the elements in array, it will set to the number of elements

    return 0;
}

MultiPointer

이를 다중 포인터라고 하는데 예를 들어서 이런 예제 코드를 봐보자. 아래의 코드를 실행 시켰을때, 이상하게도 type 은 정확하게 넣었는데, 값이 “Hi” 로 안바뀐걸로 보이는다. 왜그럴까? 라고 생각해보자. 일단 stack 안에 있는 msg 가 parameter 로 SetMessage 로 들어가는데, 이때 a 값이 변하지 않는 이유는 SetMessage 안에 있는 a 의 값은 변경이되고 사라진다는 것이다. 그래서 a 를 고치려면 *a 식으로 가야한다. 근데 막상 돌려보면 이건 const char 이다. 그래서 그 아래의 코드로 변경을 해야 된다. 어 ** 이렇게 한다고 어이가 없을수 밖에 없다.

그러면 이거에 대해서 설명을 하겠다. msg 도 pointer 이다. msgHello 라는 값을 pointing 하고 있다. 그렇다면 이값을 변경하려면 msg 의 주소값을 들고 와야한다. 그러면 다시 pointer 의 개념으로 돌아가는것이다. 그 의미는 pointer 를 다시사용해서, 즉 두번사용해서 msg 를 가르키는 놈이 필요하다는것이다. 여기서 dpointer 가 하는 역활이 msg 의 주소값을 가지고 있어서 두번 와프를 타라는 것이다. 왜냐하면 거기에 hello 라는 놈이 있을테니. 그래서 **dpointermsg 의 주소값을 가져오게 되면, msghello 를 가르키고 있기때문에 가지고 올수 있는것이다. 그 이후에 parameter 로 SetMessageDiff 에 넘겨주게 된다면, a 라는 놈을 한번 타고 들어가서 값을 변경시키면 될것이다.


void SetMessage(const char *a)
{
    a = "Hi";
}

void SetMessageDiff(const char **a)
{
    *a = "Bye";
}

int main()
{
    const char* msg = "Hello";
    SetMessage(msg);

    const char **dpointer = &msg;

    SetMessageDiff(&msg);
    cout << msg << endl;
    return 0;
}

MultiDimension Array

Array section 에서 본것 같이 multi-Dimension Array 란 이제 층수가 달라진다는것이다. 예를 들어 코드를 봐보자

int main()
{
    for (int floor = 0; floor < 2; floor++) {
        for (int room = 0; room < 5; room++) {
            int num = apartment[floor][room];
        }
    }
    return 0;
}

이런식으로 배열이 되어있다면, unit number 은 5 개라는거고 층의 개수는 2개라고 생각하면 될것이다. 그래서 이렇게 배열을 indexing 할수 있는거다. 그렇다고 하면 pointer 로 어떻게 access 를 할수 있을까가 질문인것이다.

2 차원 배열과 다중포인터로 어떻게 사용할수 있을까? 라는 생각이든다. 이렇게 생긴 코드를 한번 보자. pp [ 주소1 ] –> 주소1[주소2] –> 주소2[] 이렇게 하면 될까? 라는 생각이든다. 하지만, 실제로 보면, *pp 만 해도 덩그러니 1 이라는 값이 있는걸 생각할수 있다. 즉 pp[ 주소1 ] 을 타고 들어갔더니 주소1[ value ] 가 있는것이다. 그래서 프로그램이 뻗어버린다. 즉 다중 포인터와 다중 배열은 완전 다른 타입이다.

int **pp = arr2;

그래서 이 코드를 보면 된다, 그러기전에 꼭 타입을 한번 확인해보자. 그러면 int(*)[2] 라고 확인할수 있다. 그러면 이걸 어떻게 해석하면 되냐. p2 로 타고 들어갔더니 int()[2] 2차원 배열이 있다고 생각하면 된다. 근데 이렇게 구지 할필요는 없을거다. 그냥 arr2[0][0] indexing 을 해도 충분하다.

int (*p2)[2] = arr2;
cout << (*p2)[0] << end;
cout << (*p2)[1] << endl;
cout << (*(p2 + 1))[0] << endl;
cout << (*(p2 + 1))[1] << endl;

Resoure

Source Code

Black Panther - Wakanda Forever

Black Panther : Wakanda Forever

사실 그렇게 Marble 영화를 좋아한다고 하기에는 굉장히 논란거리가 많다고 생각한다. 나는 아이언맨 1, 2, 헐크, 토르1, 2 만 보았고 전체적인 스토리에대해서 전혀 모른다. 마블의 광팬이라면, 뭔가 내가 해리포터를 굉장한 팬처럼 생각하는것 처럼 그렇겠구나라는 생각이든다. 항상 마블하면 생각하는건 미국에 있었을때 였다. 뭔가 엔드게임을 보자는 친구들이 많아서, 나를 대신해서 티케팅을 해줘서 그냥 따라 갔던 기억이 난다. 그때는 뭔가 영어를 알아들어도, 무슨 스토리인지 감이 안와서 생각없이 보았던 영화였는데 하면서 봤던건데 기억나는 장면은 갑자기 모든 주인공이 나타나 싸우질 않나, 갑자기 캡틴 아메리카가 토르의 망치를 가지고 오면서 싸우는 장면이 띄어졌을때, 온 사람이 “WoW! YEAH! OMG!” 이런 반응에 뭐지? 한것도 기억이 난다. 그리고 마지막은 정말 슬펐던 장면, 아이언맨이 죽고 나서, 아이언맨의 딸에게 최애의 음식이 치즈버거 였다는 말까지 밖에 기억이 안났었다. 그게 나의 마지막 마블영화였던것 같다.

블랙팬서 1도 비행기 타면서 봤었는데 너무 지루하고, 무슨 이야기가 돌아가는지 몰라서 잤었던것 밖에 기억이 안났는데, 이걸 어찌보냐 이런생각 밖에 안들었었다. 그런데, 주인공이 죽었다라는 소식을 듣기는 들었었어서 마냥 완전 새로운 스토리겠지 하면서 봤었다. 스포를 싫어하는 사람들을 위해서 말을 하자면, 결론은 딱이거다. 새로운 블랙팬서가 나타났다. 그게 여동생이다라는 말을 할수 밖에 없는것 같다. 뭔가 영화를 보면서, 조금 음 뚱딴지 같다라는 생각이드는데, 나중에 나의 최애 캐릭터가 있었던거 같다. 의리와 재치가 넘치는 음바쿠! 음바쿠라는 캐릭터 설정이 진짜 재밌어 보였는데, 사실 내가 영어를 많이 않쓰다보니 영어 듣기 실력이 줄었겠지 하지만, 이 친구가 말하는건 웬지 너무 잘들리고, 뭔가 내 이미지와 맞는것 같아서 계속 그냥 들렸던것 같다.

하.. 딱히 영화리뷰라고 할것도 없다. 아무 생각없이 보기엔 정말 좋은 영화였다. 스토리 라인은 그냥 조금 그랬었던것 같았다. 극적인 변환

Sid Meier

Sid Meyer

사실 이 책을 읽었을때, 한국에 온지 한 1년이 되지 않은차에 천천히 읽게 되었다. 가끔씩은 내가 어떤 문장을 번역을 한다고 했을때, 정확한 의미 전달이 안될수도 있다는 생각이 자주들지만, 가끔씩 이런 미국에서 책을 쓴게 원작이 되어서 한국어로 번역되어서 나온 책들은 늘 부자연스럽게 느껴진다. 그리고 뭔가 게임 개발자로서 통하는 용어들은 확실히 와닿지 않았다.

나는 항상 어떤 product 를 만드는것도 좋아하며, 그 만든것을 내가 소유하는것을 굉장히 좋아한다. 그래서 게임을 만들기전 과거의 사람들이 게임을 만들때, 임했던 자세나 어떤 고생과 땀을 가지고 게임을 만들었는지가 궁금했어서 이책을 리디북스로 바로 구매 했다.

회사에 다니면서, 뭔가 Unreal Engine 에 빠져있었던 나, 그게 영향을 끼쳤던 Carla Simulator, ray tracing 을 통해서 보여지는 차량들, 그림자(Shader) 효과 등 realistic 한 느낌을 들었기 때문에 게임 개발을 하고 싶다는 생각이 들어서, 사실 이 책을 읽게되었다. 이 책을 읽기전, 책 이름이 어떤 게임 이름의 이름인 줄 알았다. 하지만, 읽고 나서 보니, 시드 마이어라는 게임 개발자가 게임을 만들면서 일어났던 회사의 이슈, 그리고 시드마이어의 family background(like where he grew up in sweden affects his displine for developing his personality) 또는 그가 좋아했던 바흐의 음악이 어떻게 Sims Golf 라는 게임에 영향을 끼쳤는지, 알수 있었다.

시드마이어 이 책은 내가 기억하기론 한챕터마다 마지막에 미션이 달려있다. 그리고 이 책은 약간 시드마이어 게임 개발자로서의 삶이 시간대별로 녹아져내려 있으며, 시드마이어 라는 사람이 누구인지를 알게된계기가 되었다.

읽으면서 좋았던 구문들을 밑줄치면서 읽었는데, 공유하면 좋을것 같다.

  1. “효율이 목표이고 이를 위해서는 개발주기의 횟수를 최대한 늘리는 것이 좋으나 각 개발 주기에 최대한 많은 정보를 얻는 것도 중요하다. 내 원칙은 “2배로 늘리거나 절반으로 잘라라 이다.”
  2. “변화가 과한 것 같다면 올바른 방향으로 진행하고 있는 것을 확인했으니 변화의 정도를 적정 수준으로 조절하면 된다.”
  3. “여러분의 재능을 누군가 발견해준다는 보장은 없다. 하지만 아무것도 만들지 않는 사람에게는 확실히 그런 일이 일어나지 않는다. 자신의 아이디어가 좋다는 것을 증명하는 최고의 방법은 말이 아닌 행동이다. 처음에는 프로그래머가 되어서 플레이할 수 있는 무언가를 만들라. 다음에는 그래픽 디자이너가 되어서 이 무언가를 대충 알아볼 만한 상태로 변신 시켜라. 그리고 그 뒤에는 테스터 역활을 맡아서 재밌는 부분과 그렇지 않은 부분이 무엇인지 솔직히 평가하라. 어떤 역활에든 완벽할 필요는 없다. 자신의 아이디어를 증명하고 다른사람도 그 아이디어 구현에 동참하고 싶다고 생각하게 만들 정도의 실력만 갖추면 된다”
  4. “수학이 인기와 정면대결을 해서 이기긴 어렵다. 디자인 관점에서는 사각형보다 육각형이 나은데도 <문명>이처음 출시될 당시 평범한 컴퓨터 사용자가 보기에는 육각형은 너무 너드스럽다는 이유로 익숙한 사각형을 선택할 수 밖에 없다. ... 말했듯이 모든 일에는 대가가 따른다"
  5. “각자가 자신이 가장 좋아하는 게임의 추억 안에 나를 보존해두기 때문이다. 어떤 이는 십 대 시절 자신의 인도한 현명한 노선생으로 기억할 것이고 어떤 이는 모두가 너무 늙어서 그런 일을 할 수 없다고 말할 때 함께 해적인 척해준 엉뚱한 비밀 친구로 기억할 것이다. 사람들이 나에 대해 품고 있는 환상은 사실 내가 아니라 그들이 경험한 즐거움에 관한 환상이고 나는 그들을 위해 그 행복한 기억을 지켜주고 싶다.”

등등 글이 있는데, 정말 뜻깊고 주옥같은 이야기이다. 만약 내가 개발자 또는 게임쪽으로 개발을 전향한다면, 내 미래의 나에게 이렇게 개발을 했으면 좋겟다 라는 말을 전해주고 싶다. 제일 생각이 많이들었던 부분이라고 한다면, 어떻게 더 낳은 개발자로 성장하려면, 기본적으로 그러한 성격을 가지고 있어야하나라는 생각과 어떤게 안된다고 했을때, 그 호기심을 가지고 문제를 해결하려는 지속성이 필요한건가 라는 질문을 많이 던졌었다. 그러면서 결국 자신에게 속이려고 하지말자라는 생각을 하게 되었다.

또 이제 문명에 관한 시드 마이어의 인터뷰를 찾아보았는데, 아래와 같은 내용을 설명을 해주었다. 조금 클래식 하지만 뭔가 기본적인 지식이라고 하지만 근본있는 이야기 같아 보인다.

  • zone-based game
  • interactive story
  • turn-based game vs real-time game

그리고 2 번째의 이야기 처럼, The valley of dispair rule 를 설명하였다. The valley of dispair Rule, which is the double it or cut it in half rule. If you’re gonna make a change make it dramatic.

Look at the Youtube Below

뭔가 감상문이라고 하기에는 조금 내가 마음에 들었던 부분만 이야기한것 같지만, 지하철에서 뛰엄 뛰엄 읽느라 많이 생각들을 잃어버렸던것 같다.

The Greenhouse At The End of The Earth

The Greenhouse at the end of the earth (지구 끝의 온실)

이 책을 읽었던 동기는 사실 잘 기억이 나지 않는다. 뭔가, 전에 소설 책을 읽으면 재밌겠지. 소설이라는 책에도 감정이 충분이 녹아들을수 있겠다? 라는 기대감에 리디북스에서 전자책을 사서 읽게 시작됬다.

이 책같은 경우 SF 판타지 종류의 책이다. 사실 이 책의 Genre 를 정확하게 알게된게, 아마 마지막에 다 읽고 나서, 사람들의 리뷰를 봤을때 쯤이였다. 이 책의 내용은 지구가 ‘Dust’ 로 인해, 멸망 한 후의 이야기를 다루고 있다. 이 시점에 두가지 종류인 사람이 있었던것 같다. 하나는 Dust 에 대해서 immune 한 사람과 그렇지 못한 사람, 그리고 dust 에 immune 한 사람들은 dust 밖에 살아도 immune 한 정도에 따라서, 살수 있느냐 없느냐를 결정할수 있었다. 그리고 그들은 주로 실험체로 사용되었기 때문에 dom 에서 그들을 잡기 위해서, 사냥꾼이 존재 했었던 배경이다.

이 책이 재미있었던 부분은 나는 주인공 시점이 계속 바뀌었던 부분이다. 또 그 주인공 시점이 바뀔때 마다 장면들도 바뀌고, 주인공들의 이야기도 섹션마다 이상하게? 바뀌어서 재밌었던 부분이긴 했다. 예를 들어서 식물학자인 아영은 자기의 어렸을때, 그 나중에 지수라는 분의 창고에서 보았던 그 모스바나(식물)의 장면과 나오미라는 분이 어떻게 해서 모스바나를 통해서 더스트에 살아남았던 장면 스위칭이 되게 해깔리게 했지만, 항상 그런 스위칭은 내가 그 전 내용을 봤을때를 다시 remind 시켜주는게 좋았다.

사실 내용은 별로 다루고 싶지 않다. 그리고, 별로 다루지도 못하고. 어떤 이야기가 지나갔는지는 책을 읽어봐야 느끼는 그런게 필요하다. 특히나 이 책은 뭔가 생각보다 SF 치고, 감정이 깊다. 특히나 점점 사이보그로 변해가는 레이첼이 지수의 이기심으로 생겨난 감정들이, 레이첼한테 없었던게 아니라, 애초에 이기심이 아닌 사람의 생명을 구해주고 싶었던 지수로 인해서 생겨난거라는게 인상 깊긴 했었다.

그리고, 나오미와 아마나? 의 떠돌이 같은 삶은 뭔가 내 미국의 삶을 보여주기도 하는것 같아 보였다. 버티기 위해서, 뭔가 이뤄내고 싶다는 마음보다도, 그냥 일단 존버로 버텨보자 그런 희망을 위해서 내가 미국에서 있었던 그런 짧은 순간들이 주로 생각이 났었다.

내가 책을 읽었던 순간 순간 중에, 아영이 나오미와 아마나의 기억이 가짜가 아니라는걸 증명을 하려고 했을때? 분명 우여곡절이 있었지만 진짜 증명해나가는 모습들이 인상깊었고. 또한, 그 우여곡절, 아영이 자기가 살았던 도시로 돌아가 지수라는 사람에대해서 힌트를 찾으러 갔을때, 어렸을때 그 감성을 다시 찾아보려고 노력했을때, 아무것도 못찾아서 허탈한 기분들이 느껴졌었을때 쯤 연구소에서 모스바나 식물의 Origin 힌트를 찾았을때의 그 기쁨 및 뭔가 그리운것을 되찾으려고 했던것들이 마음에 와닿았던것 같다.

역시나 어떤 사람에 대해서 공감을 어느 정도 하느냐는 참 쉽지 않다. 내가 많이 노력해봐도 그건 natural 하게 되는것이지 관심이 있고 없고의 문제를 이미 넘어선 시점에서 일어나는것 같다. 요즘 감정이 사실 굳혀져 간다는게 느껴진다. 행복한게 행복한거지만 계속되며, 딱히 슬픈것을 못느끼는것 같다. 다만, 답답한 마음은 존재하지만, 내가 할수 없는 일이기때문에 포기하는 속도가 더 빨라져. 이제 될대로 되라 이렇게 살아가고 있다. 내 인생에서 중요한것이 무엇일까? 또는 Goal 이 뭘까? 라고 물어봐도, 한번 씨게 다쳤기 때문에, 아직 잘몰라, 인생 어떻게 될지 어떻게 알아? 라는 질문으로 아무것도 안하고 있는것 같다. 인생이 어떻게 열릴지 모르지만, 내 나름 노력해야되고 계획도 세워야되고, 내가 내 스스로를 사랑해야되는 그런 순간순간들이 점차점차 보이기 시작한다.

그리고 나는 계속 책읽기를 시작할거다. 내가 미래의 아들이나 딸에게, 저녁에 동화책을 읽어줬을때, 그 아이들이 행복한 추억으로 잘남겨질때까지 계속 이어가고 싶다.

Mangwon-Brothers

Mangwon Brothers

이 책을 읽을수 있었던 동기는, 사실 나의 논산 훈련소에 있을때, 재미있게 봤던 책이였다. 그책의 이름은 “불편한 편의점 (Inconvenient Convenient Store).” 조금 역설적이지만, 되게 나름 그 책의 분위기를 잘 살렸던 책이 였다. 사실 나는 소설을 되게 싫어했다. 그 이유는 소설이라는건 내 인생에 유익한 정보를 들고 있지 않으면서도, 되게 볼품없는 책이라고 생각했었기 때문이다. 하지만, 훈련소에서 이 책을 읽고, 소설에 대한 생각도 많이바뀌고, 요즘 따라 많이 읽게된다. 혼자서 읽을때 웃기도하고 나름 힐링이 되는 책이다. 직장인으로서 많이 힘이 들긴하다. 특히나 요즘, 뭔가 이상하게 나의 정체성도 조금 흐릿해지고, 직장인으로서 물들어간다라는 생각도 들면서, 내안에 있던 감정들이 예전처럼 매말라간다고 많이 생각한다. 한 5년전만해도, 내가 마이웨이라는 성격을 가지고 있다, 이상한 고집이 있다라는 둥 되게 많은 이야기를 들었지만, 시간이 지난지금은 좋은게 좋은거처럼 마조히스트? 같은 느낌이있다. 이 나름대로 나쁜건 없지만, 그래도 어느정도 사람이 변해가는구나 라는걸 알게되었다라는 생각이든다.

일단 이책은 망원동에서 사는 만화작가의 스토리이다. 사실 서울에 산지 오래 되지 않아서 잘모르고 시작했던 책이였다. 망원동이 어디야? 약간 조금 신림느낌이 나는곳일까? 라는 생각부터 했었고, 이야기를 들어보면 되게 가관이라고 할수 있다. 이 책에서 나오는 부주인공같은 주인공들, 기러기 아버지를 하고 있는 김부장, 주인공의 싸부였던 스토리 작가, 그리고 7급 공무원을 준비하고 있는 삼척동자. 일단 참 이름부터 뭔가 정감이 가는 그런 책이다. 서로 안좋게 시작해서 시작된 동거. 김부장은 약간? 가족들을 버려두고 한국에서 뭐라도 적응하려는 모습, 답답하지만 뭐라도 해서 돈을 벌어서, 캐나다에있는 가족들에게 뭔가 보여주려는 모습들을 많이 depict 했었다. 싸부는 이혼을 당할준비를 하면서, 술만 마시고 그런 가부장적인 모습들을 가졌으면서도 되게 말마다 웃긴 캐릭터지만 가족들에게 잘못해서 집을 나와있는 모습을 보여주었고, 7급 공무원을 준비하고 있는 삼척동자는 주인공의 후배였는데, 대학교떄 돈많은척? 아는척, 또다른 척을 해서, 삼척동자이지만, 결국엔 고시원에서 공무원을 준비하고 있는 고시생이다. 망원동에 새로 생긴 마트에서 떡뽁이 순대 시합에서 1등을하다가 만나서, 주인공이외에 같이 8층 짜리 옥탑방에서 티비보며, 술마시며, 이야기 하는 그런 모습들이 많이 담기었다.

이 책을 보면서, 되게 많이 웃기도 하면서, 공감되는 부분들이 많았다. 예를 들어서, 싸부의 아는 사람 술집에 일했던 싸부의 후배들을 만나는 주인공의 모습으로부터 시작해서, 그 친구를 통해서 주인공이 김칫국을 마시면서 초라하게 끝났던 시작. 그 부분은 약간 한국에 대한 약간 “악을 쓰면서까지 높은 자리로 올라가, 높은자리에서 어울려야겠다” 이런 느낌이였지만, 또 좋았던 부분은 부동산을 통해서 만났던 주인공의 현 여자친구의 만남은, 내가 보면서도 “와 저게되네” 라는 생각까지 할정도로 자연스러우면서도 되게 부드러웠던 만남? 이였다. 은근 나도 몰래 설레기도 했지만 너무 너무 그런 감정들을 잘 실어 첵에 진짜 집중하게 되었다.

어떻게 보면, 책 작가라는 사람들은 정말 정말 대단한것같다. 나처럼 맨날 코드보면서, 이거 조금 안되지 않을까? 라는 생각에 조마조마 하는데, 책작가들도 분명 이런저런 생각을 하겠지만, 저렇게 이야기가 thorough 하게 만들어 낼수 있을까? 참 그냥 허탈한 웃음밖에 안나온다. 물론 초반엔 조금 지루했다. 뭔가 내가 한국 배경에대해서 알아야할것 같은 느낌이 들었고, 그리고 내가 만화가라는 직업을 가져본적 없기 떄문에, 공감을 형성을 못했지만, 이건 그런거 부류가 아니라고 생각했다. 사람사는거, 생각하는거 거의 다 비슷하다고 생각했고, 행복해지는건 정말 소소한거 부터 시작한다라는거? 라는게 제일 느껴졌다.

요즘 따라 종이책들의 느낌이 좋다. 항상 종이책 무거운거 왜들고 다녀 귀찮게 이랬지만, 종이책 나름 그 넘기는 재미가 있다. 아무튼 이책 너무 재밌게 읽었고, 그 다음책도 잘읽혀나갔으면 한다는 생각에 기대된다.

You make your own boat

You Make Your Own Boat

이제 BootCamp 를 다녀온지 한달이 지나갔다. 참 시간이라는게 빠르면 빠르고 붙잡고 싶어도 붙잡아지지 않는다. 군대에 나오고 나서, 되게 사람들의 말을 끊어버리는 이상한 습관이 하나 생긴것같다. 가끔씩은 나의 그런 모습이 참 보기 싫어졌다는 생각이든다. 왜 그럴까? 내가 군대 가기전에 그렇게 여유로운 삶을 누렸는데, 왜 이제는 다를까? 라는 생각도 많이 들게 된다. 뭔가 포옹력이 사라진것 같기도 하고, 내가 지금 아직도 한국 적응중인가도 많이 생각 하게 된다. 최근 들어서 그 트라우마라고 생각하는 4월과 5월이 생각난다. 하지만 그랬던 시절도 이제는 그립기 시작했다.

내 친구와 함께 토요일에 밥을 먹었다. 원래는 술도 한잔 하려고 했었는데, 점심으로 약속이 잡히자, 그럴 생각도 금쪽같이 사라졌다. 사실 이 친구와 알기 시작한건 2016년 부터 시작해서, 우리 둘이 인생극장 을 찍을정도로 같이 붙어있었고 너무 잘맞는 친구였다. 그 친구와 친해지기까지는 생각보다 오래걸렸었고, 술을 마시고 나서 “우리 힘내보자! 할수 있잖아!”라는 말을 외치면서 미국 집앞에서 소리 친적이 있다. 그때는 되게 민폐라고 생각했지만, 우리 서로 사정이 있었고 힘들었던 시기에 만난거라 그 무엇보다도 큰 힘이 되었었다. 하지만, 이렇게 당당했던 친구와 나의 모습은 없었고, 둘다 미국이 그립다는 이야기만 엄청했었다. 그 친구와 나의 인생의 절반은 다른 대륙에서 살아갔었고, 한국에 들어오고 싶지 않았던 우리는 우리가 내렸던 결론에 끝을 잘 맺치지 못했던것 같다. 물론 과거는 과거였고, 추억은 추억으로 남겨져간다. 하지만 그 과거에 지금의 우리를 만들었던 좋은 추억들은 쉽사리 사라져가진 않는다는건 분명하다. 늘 우리는 서로의 생일을 챙겨주고, 맛있는 밥도 같이해서 유학생 다웠던 삶을 살아갔었다. 항상 웃어서 행복했고, 나의 아파트 발코니에서 나무와 고속도로를 넘어서 담배피면서, 맥주 한캔 했던 그런 여유로움을 나름 즐겼다. 하지만, 미국의 삶도 호락호락 하진 않았다. 나 나름 영어를 잘한다고 생각하지만, 막상 발표나 친구들과 이야기할때는 흐름이 비슷하고, 백인들과 흑인들 사이에서 콕 파묻힌 느낌도 들때도 많았지만, 그렇게 다양한 인종들끼리 친구가 됬었고, 차별을 당한적은 있지만, 그때는 그런가보다 하면서 넘겨갔던게 많았고, 풀리지 않은 문제들은 정말 혼자서 끊임 없이 붙잡으려고 포기 하지 않았다. 그랬던 삶들은 점차 점차 사라져가며, 이제는 밖에 나가면, 음식집 앞에서 사람들의 소리들이 들린다.

물론 한국의 삶은 다른 형들과 사람들의 말로는, 바쁘게 살아가고, 포기하며 살아가고, 여유롭게 살아가지 못하고 있는것 같다. 되게 호락호락 하지 않는 반면에 나는 최대한 이 미국에서 받아온 여유로움이 아직은 남아있는것 같다. 나와 친구가 했던 말이 생각난다. “우리의 몸은 한국에 있는데, 정신은 미국에서 살고 있다고.” 이 말이 정말 정확하게 맞다. 나는 이제 한국 산지 반년이 지나간다. 처음에는 적응하느라, 일하느라, 전화만 했었던 가족들이 진짜로 나의 인생에 들어오고, 많이 힘들었던건 사실이다. 그리고 책상에는 아직도 “좋은 개발자나 연구자가 되려면, 모든 순간에 열심히, 끊임없이 살아가야된다” 써놓은 포스트잇이 아직 붙여있다. 마음 한편으로는 내가 지금 살만하니까 이런 저런 잡생각들이 많아진건가? 라는 생각이든다. 사실 제일 힘든 사람은, 이런 글을 쓸수도 없을것이다. 아마 피곤해서 그냥 누워버리겠지 라는 생각이 들겠지. 하지만, 이런 잡생각도 쓰는게 나의 Life Log 아닌가? 라는 생각에 글을 쓰면서 뭔가 마음으로 안정이된다.

사실은 요즘 이런 생각이든다. 더 좋은 사람이 되어야지 라는 생각. 구체적이진 않다. 하지만, 이 좋은 사람이 된다는건 약간 이런 방향인것 같다. 사람의 말에 귀기울여 줄수 있고, 나의 고민거리를 먼저 다가서는게 아니라, 다른 사람 고민거리를 먼저 들어주고, 내 이야기를 잘 물들일수 있는 사람, 계속 몸에 여유로움이 남아 있는 사람 그리고 기다려줄수 있는 사람. 나의 과거의 모습에 이런 글을 본다고 한다면, 참 너 뭐하냐? 이런 생각이 들기도 하지만, 이제는 나이가 들어간다. 그리고 나이가 들면 들수록, 점점더 wise 한 사람이 되려면, 이런 저런것을 많이 해봐야된다고 생각한다. 그래서 지금 하고 싶은 것들이 많이 생각나다. 이거는 이번년의 나의 목표이자 하고 싶은것이다. 2022 년 12 월 31 이 되면 꼭 내 스스로에게 확인을 해보고 싶은 사항이다.

Check List

  1. 캐나다에 있는 Panoram Ridge에 꼭 가보기 []
  2. 한국에 있는 페러글라이딩 해보기 []
  3. 미국에 이번엔 여행객으로 방문해보기 [X]
  4. 북 유럽 여행 가보기 []

뭔가 4번은 뭔가 불문명 하지만, 꼭 부딫쳐 봐야한다. 그리고 나에게 하고 싶은 말이 있다. 너는 지금까지 잘해왔고, 분명 좋은 사람으로 거듭나기 위한 지금의 과정을 지나치고 있는거야. 책도 많이 읽어서 사람에게 너의 감정을 쉽게 들키지 말고, 슬픈것과 화나는것의 차이를 분명히 알아야될거고, 좀더 진정성있고 여유로운 사람이 될수있기를 바래. 과거는 과거일뿐이고 현재를 열심히 살아가렴. 너가 분명 이런저런 Transition 을 겪고 있는건 알지만, 이것도 하나의 step 이야, 너를 더 사랑해줄수 있는 그런 기회이며, 남들을 더 사랑을 나눠줄수있고, 귀를 열수 있는 그런 사람이 되길 바래.

아직 나는 20 대 이고, sometimes you gotta say fuck to the world. It’s fine to be that way, just kin on your instinct okay?! Also, just forgot to mention I love you Nick! Keep working hard alright!

Pagination


© 2021. All rights reserved.