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


© 2021. All rights reserved.