본문 바로가기
개인공부/Rookiss C++ 프로그래밍 입문

Chapter 5 - 포인터

by 하고싶은건많은놈 2023. 3. 29.

포인터 기초 #1

일반 변수는 함수 호출시 원본 대신 복사본이 사용됨
따라서 원본 변수를 조작할 수 있는 포인터가 필요함

TYPE* 변수명의 형태로 사용

  • 포인터 변수에는 값이 아닌 주소값이 저장됨
  • 포인터 변수의 크기는 항상 4바이트(32비트) 또는 8바이트(64비트)로 고정
  • 일반 변수의 주소값은 변수 앞에 &연산자를 사용함
  • int* ptr = &number; : ptr 포인터 변수에 number 변수의 주소를 대입

*포인터변수의 형태로 사용할 경우 해당 포인터 변수가 가리키는 주소에 저장된 값을 지칭함

  • int value = *ptr; : value 변수에 ptr 포인터 변수가 가리키는 주소에 저장된 값을 대입
  • *ptr = 2: ptr 포인터 변수가 가리키는 주소에 저장된 값을 2로 변경

포인터 기초 #2

포인터의 타입을 지정하는 것은 해당 포인터가 가리키는 주소에 어떤 형식의 데이터가 있는지를 나타내기 위함
해당 데이터가 어떤 타입으로 인식되는지에 따라 컴파일러의 작동이 달라질 수 있음

포인터 타입과 데이터 타입이 불일치할경우 에러 발생
강제 형변환 등으로 메모리 영역을 침범할시 상당한 문제가 발생할 수 있음

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

int main()
{
    int hp = 1;
    SetHP(&hp);
}

포인터

주소 연산자 &

  • 해당 변수의 주소를 나타내는 연산자
  • int* pointer = &number;에서 &numbernumber 변수의 주소값을 의미
  • 해당 변수 타입에 따라서 TYPE*형 반환

산술 연산자 +, -

  • 포인터 변수도 덧셈, 뺄셈 연산이 가능
  • 덧셈, 뺄셈 연산시 해당 포인터 변수의 자료형 크기만큼 움직임
  • 즉, int형이라면 4바이트, char형이면 1바이트 ...

간접 연산자 *

  • 포인터 변수가 가리키는 주소에 저장되어있는 값에 접근
  • *pointer = 3;

간접 멤버 연산자 ->

  • 구조체의 특정 멤버를 다룰 때 사용
  • *.을 결합하여 한번에 사용하는 연산자
  • 어셈블리 언어로 동작을 살펴보면 사실상 구조체 멤버의 자료형 크기만큼 메모리를 이동하는 덧셈연산
    struct Player
    {
      int hp;
      int damage;
    }
    

int main()
{
Player player;
player.hp = 100;
player.damage = 10;

Player* playerPtr = &player;
(*playerPtr).hp = 150;
(*playerPtr).damage = 15;
playerPtr->hp = 200;
playerPtr->damage = 20;

}


***

# 포인터 실습

```cpp
// ptr_exercise

#include <iostream>
using namespace std;

struct StatInfo
{
    int hp;
    int att;
    int def;
};

void EnterLobby();
StatInfo CreatePlayer();
void CreateMonster(StatInfo* info);
bool StartBattle(StatInfo* player, StatInfo* monster);

int main()
{
    EnterLobby();
    return 0;
}

void EnterLobby()
{
    cout << "로비에 입장했습니다." << endl;

    StatInfo player;
    player = CreatePlayer();

    StatInfo monster;
    CreateMonster(&monster);

    bool victory = StartBattle(&player, &monster);
    if (victory)
        cout << "승리!" << endl;
    else
        cout << "패배!" << endl;
}

StatInfo CreatePlayer()
{
    StatInfo pl;

    cout << "플레이어 생성" << endl;
    pl.hp = 100;
    pl.att = 10;
    pl.def = 2;

    return pl;
}

void CreateMonster(StatInfo* info)
{
    cout << "몬스터 생성" << endl;
    info->hp = 40;
    info->att = 8;
    info->def = 1;
}

bool StartBattle(StatInfo* player, StatInfo* monster)
{
    while (true)
    {
        int damage = player->att - monster->def;
        if (damage < 0)
            damage = 0;

        monster->hp -= damage;
        if (monster->hp < 0)
            monster->hp = 0;

        cout << "몬스터 HP : " << monster->hp << endl;
        if (monster->hp == 0)
            return true;

        damage = monster->att - player->def;
        if (damage < 0)
            damage = 0;

        player->hp -= damage;
        if (player->hp < 0)
            player->hp = 0;

        cout << "플레이어 HP : " << player->hp << endl;
        if (player->hp == 0)
            return false;
    }
}

값에 의한 전달보다 포인터를 사용한 주소에 의한 전달이 훨씬 효율적으로 전달함

  • 값에 의한 전달은 임시 구조체를 생성한 후 값을 대입하고, 이를 변경하고자하는 구조체에 복사함으로써 변경
  • 주소에 의한 전달은 변경하고자하는 구조체의 값을 곧바로 변경

참조 기초

int& reference = number;의 형식으로 사용

  • 변수 numberreference라는 또다른 이름을 부여
  • 로우레벨(어셈블리) 관점에서 보면 실제 작동 방식은 포인터와 같음
  • 원본 데이터를 사용하면서도 사용성을 향상시키기 위해 참조 전달을 사용
void PrintInfoByRef(Statinfo& info)
{
    cout << "-----------------" << endl;
    cout << "HP : " << info.hp << endl;
    cout << "ATT : " << info.hp << endl;
    cout << "DEF : " << info.hp << endl;
    cout << "-----------------" << endl;
}

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

포인터 vs 참조

성능적인 면에서는 포인터와 참조에 차이가 없음
편의성 면에서는 참조가 더 유용하지만, 편의성이 좋은 것이 무조건 장점은 아님

  • 포인터의 경우 주소를 전달함으로써 원본을 사용한다는 것을 명확하게 알 수 있음
  • 반면 참조는 인식하지 못하고 지나칠 가능성이 존재함
    • 이럴 때 원본을 실수로 변경하는 것을 막기 위해 const 키워드를 사용할 수 있음

포인터에도 const 키워드를 사용할 수 있음

  • * 이전에 const 사용
    • StatInfo* const info : 포인터 변수 info에 설정된 주소값을 변경하지 못함
  • * 이후에 const 사용
    • const StatInfo* info : 포인터 변수 info에 설정된 주소에 저장되어있는 값을 변경하지 못함

참조의 경우 대상이 필요하므로 반드시 초기화를 해주어야함
반면 포인터의 경우 초기화 없이 선언만 하는 것도 가능함

  • 포인터 변수의 값을 0으로 초기화하고 싶은 경우 NULL 또는 nullptr을 사용
  • 참조는 0으로 초기화할 수 없음

결론적으로 포인터와 참조에 큰 차이는 없으니 둘 중 어느것을 선택하는지는 스타일에 따라 다름

  • 없는 경우를 고려해야한다면 포인터(NULL 체크 필수), 읽는 용도로만 사용된다면 const 참조를 사용하는 것이 유용할 수 있음

  • 그 외의 일반적인 경우는 참조가 사용하기 편리함

  •   #define OUT
    
      void ChangeInfo(OUT StatInfo& info)
      {
          info.hp = 1000;
          ...
      }
    
      int main()
      {
          ...
          ChangeInfo(OUT info);
          return 0;
      }

    위 코드와 같이 내용물의 변경이 이루어지는 경우 아무런 역할도 하지 않는 OUT을 정의하여 해당 함수가 값을 변경할 수 있다는 것을 명시하는 방법도 있음

  • 포인터를 사용하던 함수를 이어서 만든다면 계속 포인터를 사용하여 섞어쓰는 것을 지양함

포인터를 참조로 넘겨주려면 PrintInfoByRef(*pointer);의 형식으로 사용
참조를 포인터로 넘겨주려면 PrintInfoByPtr(&reference);의 형식으로 사용


배열 기초

TYPE 이름[개수];의 형태로 선언

  • 배열의 크기는 반드시 상수여야함
  • 배열의 이름은 배열의 시작 위치를 가리키는 TYPE* 포인터를 나타냄
struct StatInfo
{
    int hp;
    int att;
    int def;
};

int main()
{
    const int monsterCount = 10;
    StatInfo monsters[monsterCount];

    StatInfo* monster_0 = monsters;
    Statinfo* monster_1 = monsters + 1;
    Statinfo& monster_2 = *(monsters + 2);
    StatInfo& monster_3 = monsters[3];
}

포인터의 산술 연산을 활용하여 배열의 원소에 접근함
포인터와 참조는 자유자재로 변환 가능하므로 배열 원소의 접근에도 이를 활용할 수 있음

*(monsters + i)로 접근하는 것은 가독성이 떨어지므로 monsters[i]의 형태로 인덱스를 활용할 수 있음

배열 초기화 문법

  • int numbers[5] = {}; : 모든 원소를 0으로 초기화
  • int numbers[10] = { 1,2,3,4,5 }; : 값을 지정한 원소를 제외한 나머지 원소들을 0으로 초기화
  • int numbers[] = { 1,2,3, ... };, char helloStr[] = "Hello" : 데이터 개수만큼의 크기를 가진 배열을 초기화

포인터 vs 배열

int main()
{
    const char* test1 = "Hello World";
    char test2[] = "Hello World";
}

포인터는 해당 데이터가 들어있는 주소값을 나타냄
배열은 해당 데이터 자체가 되며, 배열의 이름이 데이터들 중 첫번째 값인 시작 주소를 나타냄


void Test(char a[])
{
    a[0] = 'x';
}

int main()
{
    char test[] = "Hello World";
    Test(test);
    cout << test << endl;
}

배열을 함수의 인자로 넘겨줄시 배열 원소를 복사하여 넘겨주는 것이 아닌, 배열의 시작 주소를 전달하여 동작함
즉, 매개변수로 배열을 전달하면 컴파일러가 이를 포인터형으로 치환하여 사용함


로또 번호 생성기

// Lotto.cpp

#include <iostream>
using namespace std;

void Swap(int& a, int& b);
void Sort(int* arr, int len);
void ChooseLotto(int* arr);

int main()
{
    srand(time(0));
    int numbers[6];

    ChooseLotto(numbers);
    return 0;
}

void Swap(int& a, int& b)
{
    int temp = a;
    a = b;
    b = temp;
}

void Sort(int* arr, int len)
{
    for (int i = 0; i < len; i++)
    {
        for (int j = i + 1; j < len; j++)
        {
            if (arr[i] > arr[j])
                Swap(arr[i], arr[j]);
        }
    }
}

void ChooseLotto(int* arr)
{
    int temp;

    for (int i = 0; i < 6; i++)
    {
        temp = rand() % 45 + 1;
        for (int j = 0; j < i; j++)
        {
            if (temp == arr[j])
            {
                j = -1;
                temp = rand() % 45 + 1;
            }
        }
        arr[i] = temp;
    }
    Sort(arr, 6);

    cout << "로또 번호 생성 완료\n";
    for (int i = 0; i < 6; i++)
        cout << arr[i] << " ";
    cout << endl;
}

다중 포인터

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

int main()
{
    const char* msg = "Hello";
    SetMessage(&msg);
    std::cout << msg << endl;
    return 0; 
}

* 연산자를 하나씩 제거하면서 타고 간다고 생각
void SetMessage(const char*& a)와 같이 포인터와 참조를 섞어쓰는 것도 가능함


다차원 배열

int main()
{
    int arr2d[2][5] = { {1, 2, 3, 4, 5}, { 11, 12, 13, 14, 15} };

    for (int i = 0; i < 2; i++)
    {
        for (int j = 0; j < 5; j++)
            cout << arr2d[i][j] << " ";
        cout << endl;
    }
}

int arr[2][5]int arr[10]은 메모리상으로는 차이가 없음
배열 자료형의 크기만큼 메모리 주소를 이동해가면서 배열의 각 원소에 접근
2D배열은 대표적으로 2D 로그라이크 맵에 사용됨


포인터 마무리

포인터는 데이터의 메모리 주소를 담고있는 통로 역할로 크기가 작음
반면 배열은 실제로 데이터들의 집합이기 때문에 크기가 비대해질 수 있음
TYPE형 1차원 배열TYPE*형 포인터는 완전히 호환 가능함

int main()
{
    int arr2[2][2] = { {1, 2}, {3, 4} };
    int** pp = (int**)arr2 // 불가능
    int(*p2)[2] = arr2; // 가능
}

다차원 배열과 다차원 포인터는 아예 별개의 문법으로 사용되며, 서로 호환되지 않음
int(*)[2]의 경우 2개의 원소를 가지는 int형 배열의 포인터라는 의미로 사용됨

C++의 특성상 메모리를 다루는 작업에 주의해야함


TextRPG #3

// TextRPG3.cpp

#include <iostream>
using namespace std;

enum PlayerType
{
    PT_Knight = 1,
    PT_Archer = 2,
    PT_Mage = 3
};

enum MonsterType
{
    MT_Slime = 1,
    MT_Orc = 2,
    MT_Skeleton = 3
};

struct StatInfo
{
    int hp = 0;
    int att = 0;
    int def = 0;
};

void PrintMessage(const char* msg);
void EnterLobby();
void CreatePlayer(StatInfo* playerinfo);
void PrintStatInfo(const char* name, const StatInfo& info);
void Entergame(StatInfo* playerinfo);
void CreateMonsters(StatInfo monsterinfo[], int count);
bool EnterBattle(StatInfo* playerinfo, StatInfo* monsterinfo);

int main()
{
    srand((unsigned)time(nullptr));
    EnterLobby();
    return 0;
}

void PrintMessage(const char* msg)
{
    cout << "********************" << endl;
    cout << msg << endl;
    cout << "********************" << endl;    
}

void EnterLobby()
{
    while (true)
    {
        PrintMessage("로비에 입장했습니다.");
        StatInfo playerinfo;
        CreatePlayer(&playerinfo);
        PrintStatInfo("Player", playerinfo);
        Entergame(&playerinfo);
    }
}

void CreatePlayer(StatInfo* playerinfo)
{
    bool ready = false;

    while (ready == false)
    {
        PrintMessage("캐릭터 생성창");
        PrintMessage("[1]기사 [2]궁수 [3]법사");
        cout << "> ";

        int input;
        cin >> input;

        switch(input)
        {
            case PT_Knight:
                playerinfo->hp = 100;
                playerinfo->att = 10;
                playerinfo->def = 5;
                ready = true;
                break;
            case PT_Archer:
                playerinfo->hp = 80;
                playerinfo->att = 15;
                playerinfo->def = 3;
                ready = true;
                break;
            case PT_Mage:
                playerinfo->hp = 50;
                 playerinfo->att = 25;
                playerinfo->def = 1;
                ready = true;
                break;
        }
    }
}

void PrintStatInfo(const char* name, const StatInfo& info)
{
    cout << "********************" << endl;
    cout << name << " : HP = " << info.hp
                 << " ATT = " << info.att
                 << " DEF = " << info.def << endl;
    cout << "********************" << endl;
}

void Entergame(StatInfo* playerinfo)
{
    const int MONSTER_COUNT = 2;
    PrintMessage("게임에 입장했습니다.");

    while (true)
    {
        StatInfo monsterinfo[MONSTER_COUNT];
        CreateMonsters(monsterinfo, MONSTER_COUNT);

        for (int i = 0; i < MONSTER_COUNT; i++)
            PrintStatInfo("Monster", monsterinfo[i]);

        PrintStatInfo("Player", *playerinfo);
        PrintMessage("[1]전투 [2]전투 [3]도망");

        int input;
        cin >> input;

        if (input == 1 || input == 2)
        {
            int index = input - 1;
            bool victory = EnterBattle(playerinfo, &monsterinfo[index]);
            if (victory == false)
                break;
        }
    }
}

void CreateMonsters(StatInfo monsterinfo[], int count)
{
    for (int i = 0; i < count; i++)
    {
        int randValue = 1 + rand() % 3;

        switch(randValue)
        {
            case MT_Slime:
                monsterinfo[i].hp = 30;
                monsterinfo[i].att = 5;
                monsterinfo[i].def = 1;
                break;
            case MT_Orc:
                monsterinfo[i].hp = 40;
                monsterinfo[i].att = 8;
                monsterinfo[i].def = 2;
                break;
            case MT_Skeleton:
                monsterinfo[i].hp = 50;
                monsterinfo[i].att = 15;
                monsterinfo[i].def = 3;
                break;
        }
    }
}

bool EnterBattle(StatInfo* playerinfo, StatInfo* monsterinfo)
{
    while (true)
    {
        int damage = playerinfo->att - monsterinfo->def;
        if (damage < 0)
            damage = 0;
        monsterinfo->hp -= damage;
        if (monsterinfo->hp < 0)
            monsterinfo->hp = 0;
        PrintStatInfo("Monster", *monsterinfo);
        if (monsterinfo->hp == 0)
        {
            PrintMessage("몬스터를 처치했습니다.");
            return true;
        }

        damage = monsterinfo->att - playerinfo->def;
        if (damage < 0)
            damage = 0;
        playerinfo->hp -= damage;
        if (playerinfo->hp < 0)
            playerinfo->hp = 0;
        PrintStatInfo("Player", *playerinfo);
        if (playerinfo->hp == 0)
        {
            PrintMessage("Game Over");
            return false; 
        }
    }
}

연습 문제 (문자열) #1

// strPractice1.cpp

#include <iostream>
using namespace std;

int StrLen(const char* str)
{
    int count;

    for (count = 0; str[count]; count++)
        ;

    return count;
}

char* StrCpy(char* dest, const char* src)
{
    int i;

    for (i = 0; src[i]; i++)
        dest[i] = src[i];
    dest[i] = '\0';

    return dest;
}

int main()
{
    const int BUF_SIZE = 100;
    char a[BUF_SIZE] = "Hello";
    char b[BUF_SIZE];
    char c[BUF_SIZE];

    cout << "strlen : " << strlen(a) << endl;
    cout << "StrLen : " << StrLen(a) << endl;

    strcpy(b, a);
    cout << "strcpy : " << b << endl;
    StrCpy(c, a);
    cout << "StrCpy : " << c << endl;
    return 0;
}

연습 문제 (문자열) #2

// strPractice2.cpp

#include <iostream>
using namespace std;

char* StrCat(char* dest, const char* src)
{
    char* ret = dest;

    while (*dest)
        dest++;
    while (*src)
        *dest++ = *src++;
    *dest = '\0';

    return ret;
}

int StrCmp(const char* a, const char* b)
{
    int i = 0;
    while (a[i] && b[i])
    {
        if (a[i] > b[i] || a[i] < b[i])
            return a[i] - b[i];
        i++;
    }
    return 0;
}

void ReverseStr(char* str)
{
    int len = strlen(str) - 1;
    int i = 0;
    char temp;

    while(i <= len / 2)
    {
        temp = str[len - i];
        str[len - i] = str[i];
        str[i] = temp;
        i++;
    }
}

int main()
{
    const int BUF_SIZE = 100;
    char a[BUF_SIZE] = "Hello";
    char b[BUF_SIZE] = "Goodd";
    char c[BUF_SIZE] = "Goodd";

    cout << "strcat : " << strcat(b, a) << endl;
    cout << "StrCat : " << StrCat(c, a) << endl;

    cout << "strcmp : " << strcmp(b, c) << endl;
    cout << "StrCmp : " << StrCmp(b, c) << endl;

    ReverseStr(b);
    cout << "rev : " << b << endl;
    return 0;
}

연습 문제 (달팽이)

// snail.cpp

#include <iostream>
#include <iomanip>
using namespace std;

const int MAX = 100;
int arr[MAX][MAX] = {0, };

void print_snail(int num)
{
    cout << "****************\n";
    for (int i = 0; i < num; i++)
    {
        for (int j = 0; j < num; j++)
        {
            //if (arr[i][j] < 10)
            //    cout << "0";
            //cout << arr[i][j] << " ";
            cout << setfill('0') << setw(2) << arr[i][j] << " ";
        }
        cout << endl;
    }
    cout << "****************\n";
}

void make_snail(int num)
{
    int i = 1, row = 0, col = 0;
    enum {RIGHT, DOWN, LEFT, UP};
    int dir = RIGHT;

    while (i <= num * num)
    {
        arr[row][col] = i;
        if (dir == RIGHT)
        {
            if (col == num - 1 || arr[row][col + 1] != 0)
            {
                dir = DOWN;
                row++;
            }
            else
                col++;
        }
        else if (dir == DOWN)
        {
            if (row == num - 1 || arr[row + 1][col] != 0)
            {
                dir = LEFT;
                col--;
            }
            else
                row++;
        }
        else if (dir == LEFT)
        {
            if (col == 0 || arr[row][col - 1] != 0)
            {
                dir = UP;
                row--;
            }
            else
                col--;
        }
        else if (dir == UP)
        {
            if (row == 0 || arr[row - 1][col] != 0)
            {
                dir = RIGHT;
                col++;
            }
            else
                row--;
        }
        i++;
    }
}

int main()
{
    int num;
    cin >> num;

    make_snail(num);
    print_snail(num);
}

파일 분할 관리

헤더 파일과 소스 코드 파일을 분리하여 작업하는 것이 효율이 높음
헤더 파일에는 함수의 선언, 소스 코드 파일에는 함수의 정의를 구현
소스 코드 파일에는 #include를 이용하여 헤더 파일을 삽입함
헤더 파일에 구현부를 작성한 경우 함수의 중복에 주의해야함
헤더 파일은 최대한 간단하게 작성하는 것이 권장됨

#pragma once를 사용하면 중복 인클루드를 방지할 수 있음
공식 방법으로는 #ifndef #define #endif를 사용함

'개인공부 > Rookiss C++ 프로그래밍 입문' 카테고리의 다른 글

Chapter 7 - 동적 할당  (0) 2023.03.29
Chapter 6 - 객체지향 여행  (0) 2023.03.29
Chapter 4 - 함수  (0) 2023.03.29
Chapter 3 - 코드의 흐름 제어  (0) 2023.03.29
Chapter 2 - 데이터 갖고 놀기  (0) 2023.03.29

댓글