포인터 기초 #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;
에서&number
는number
변수의 주소값을 의미- 해당 변수 타입에 따라서
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;
의 형식으로 사용
- 변수
number
에reference
라는 또다른 이름을 부여 - 로우레벨(어셈블리) 관점에서 보면 실제 작동 방식은 포인터와 같음
- 원본 데이터를 사용하면서도 사용성을 향상시키기 위해 참조 전달을 사용
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 |
댓글