포인터 맛보기
#include <iostream>
using namespace std;
// 오늘의 주제 : 포인터
void SetHp(int* hp)
{
*hp = 100;
}
int main()
{
int hp = 1;
SetHp(&hp);
// SetHp의 매개변수를 int* hp로 하고. 이렇게 하면 main에 있는 함수에 hp로 접근하여 값을 바꾸어준다.
//주소로 들어가서 값을 바꾸어주는거니까.
//지금까지 사용한 방식
// number라는 이름의 4바이트 정수 타입의 바구니를 만든다.
// number라는 변수 스택 메모리에 할당
// number = 1 이라 함은, number 바구니에 1이라는 숫자를 넣으라는 의미.
// 따라서 스텍 메모리에 있는 특정 주소 (number 바구니) 에 우리가 원하는 값을 넣은 셈
//number는 비유하자면 메모리에 이름을 붙인 것
// 나쁘지 않고 편리한데, 단점은 원본 수정.
// 전역변수를 사용하면 아무곳이서나 수정이 가능한데
// 지역변수 예를 들어 함수안에 있는 변수는 함수 안에서만 유효하고 밖에서는 유효하지 않다.
//지역변수는 함수 밖에서 수정 불가능한데, 포인터로는 그것을 가능하게 해준다.
//포인터 : TYPE* 변수이름;
// 바구니는 바구니인데..포인터는
// 주소를 저장하는 바구니다!
// 변수 선언할 때 * 등장했다 -> 포인터 = 주소
int number = 1;
int* ptr = &number;
// 디버깅해서 바구니를 보면 ptr 안에 number의 주소가 저장되있는 것을 확인할 수 있다.
// 포인터는 주소를 상자안에 들고 있다.
// 포인터라는 바구니는 4바이트 (32비트 환경) or 8바이트(64비트 환경) 고정크기
//디버깅시 메모리에 &ptr, &number 이런 식으로 검색하면 찾을 수 있다.
//근데 남의 주소를 가지고 뭐를 하라는건가?
// 추가 문법 : [주소를 저장하는 바구니]가 가리키는 주소를 가서 무엇을 해라.
// *변수이름 = 값;
//* 포탈을 타고 순간이동한다고 생각해보자.
//* 이 여러번 등장하니 헷갈리는데, 사용 시점에 따라서 구분하여 기억하자.
// - 변수 선언(주소를 저장하는 바구니다!)
// * 사용할 때 (포탈을 타고 순간이동)
int value1 = *ptr;
*ptr = 2;
//TYPE은 왜 붙여줄까? 어차피 주소만 보여주는데~
// * = 포인터의 의미 = 주소를 저장하는 바구니
//포인터에 대한 추가정보를 준다고 생각하면 된다.
//주소에 가면 어떤 TYPE이 있는지...
// 시험 삼아 타입 불일치하게 해서 형변환으로 한번 컴퓨터를 이해시켜보려고 한다음 실행해보자.
__int64* ptr2 = (__int64*) & number;
*ptr2 = 0xAABBCCDDEEFF;
//이렇게 짜면 디버깅해서 메모리영역 보면 int 메모리 허용범위를 초과해서 그 다음칸에 AABB가 따로 저장되어있는 것을 확인할 수 있다.
//버그 발생
return 0;
}
포인터 연산
#include <iostream>
using namespace std;
// 오늘의 주제 : 포인터 연산
// 1) 주소연산자 (&)
// 2) 산술 연산자 ( + - )
// 3) 간접 연산자
// 4) 간접 멤버 연산자
struct Player
{
int hp; //객체 생성해서 디버깅해보면 hp바로 밑이 damage가 메모리 자리를 차지하고 있는 걸 확인할 수 있다.
int damage;
};
int main()
{
int number = 1;
// 1) 주소 연산자 (&)
// - 해당 변수의 주소를 알려주세요.
// - 더 정확히 말하면 해당 변수 타입(TYPE)에 따라서 TYPE* 반환
int* pointer = &number;
// 2) 산술 연산자 ( + - )
number += 1;
//&number 0x012FFC98 000000002 &pointer 0x012FFC8C 012ffc98
//
// int*
// * : 포인터 타입이네! (8바이트) 주소를 담는 바구니
// int : 주소를 따라가면 int(41ㅏ이트 정수형 바구니)가 있다고 가정해라.
// 포인터에서 + 나 - 등 산술 연산으로 1을 더하거나 빼면
// 정말 '그 숫자'를 더하고 뺴라는 의미가 아니다.
// 한번더 TYPE의 크기만큼을 이동하라! 라는 의미
// 다음/ 이전 바구니로 이동하고 싶다 << [바구니 단위] 의 이동으로
// 즉, 1을 더하면 = 바구니 1개 이동시켜라
// 3을 더하면 - 바구니 3개를 이동시켜라. TYPE이 곱해져서 이동이 된다.
pointer += 1; // 디버깅해보니 4증가했다(?) 왜>? 다음주소로 넘어간다. +4 해서 넘어간 주소를 보니 number가 저장된 주소 다음 주소를 가리킨다.
//pointer에 &number인 012ffc98 이 저장되어있었는데 거기서 4를 더 해준게 012ffc9c 를 가리키고 이 주소는 number 바로 다음 메모리 주소를 가리킨다.
//TYPE만큼 더해지고 빼진다.TYPE만큼 연산이 된다.
//배열에서 이런거를 종종 쓴다.
//3) 간접 연산자 (*)
// * 포탈을 타고 해당 주소로 슝 이동~
number = 3;
*pointer = 4;
// 4) 간접멤버 연산자
Player player;
player.hp = 100;
player.damage = 10;
Player* playerPtr = &player;
(*playerPtr).hp = 200; // playerPtr이라는 주소로 이동한 다음에 hp를 200 으로 바꾸어준다.
(*playerPtr).damage = 200; //playerPtr 이라는 주소로 이동을 한 다음에 damage를 200으로 바꾸어주는거. hp주소에서 +4만큼 이동한 damage가 저장된 곳에 저장해준다.
// 4) 간접 멤버 연산자 (->)
// * 간접 연산자 (포탈 타고 해당 주소로 gogo)
// . 구조체의 특정 멤버를 다룰 때 사용
// -> 는 *와 . 을 한 방에 처리한다.
playerPtr->hp = 200;
playerPtr->damage = 200;
return 0;
}
포인터 실습
#include <iostream>
using namespace std;
// 오늘의 주제 : 포인터 실습
//playerInfo와 MonsterInfo로 매개변수 포인터 이용해서 하는 차이점을 알아보자.
struct StatInfo
{
int hp; // +0
int attack; // +4
int defence; // +8
};
void EnterLobby();
StatInfo CreatePlayer(); // 포인터 배우기 이전버전
void CreateMonster(StatInfo* info); // 포인터 배운 이후 버전
bool StartBattle(StatInfo* player, StatInfo* monster);
// 플레이어 승리시 true, 아니면 false
int main()
{
EnterLobby();
return 0;
}
void EnterLobby()
{
cout << "로비에 입장했습니다." << endl;
StatInfo player; // 디버깅 할 때 변수값을 보기 편하게 0xbbbbbbb로 채워놓아서 이렇게 밑에 주소로 해놓았따.
player.hp = 0xbbbbbbbb;
player.attack = 0xbbbbbbbb;
player.defence = 0xbbbbbbbb;
player = CreatePlayer(); // 이렇게 하면 만약 데이터가 커진다면성능적인 부하를 야기할 수 있다.
//[매개변수][RET][지역변수 (temp(100,10,2), player(100,10,2))] [매개변수][&temp][지역변수 (ret(100,10,2))]
//디버깅해서 어셈블리를 보면 이동 복사 붙여넣기가 여러번 일어난다.
//ret라는 중간저장소를 이용해서 다시 복사 이동되는 형식으로 데이터 복사 이동이 일어난다.
StatInfo monster;
monster.hp = 0xbbbbbbbb;
monster.attack = 0xbbbbbbbb;
monster.defence = 0xbbbbbbbb;
CreateMonster(&monster);// 반면에 위의 CreatePlayer에 비해 코드가 많이 짧아지고
//위와 다르게 temp 없이 주소를 이용해 바로 데이터가 들어간다.
// 번외편1)
// 구조체끼리 복사할 때 무슨 일이 벌어질까?
// player = monster; // 한 줄이라도 빠르다고 생각하면 안된다. 데이터 복사하는 것도 데이터가 커지면 조심해서 복사를 해야한다.
bool victory = StartBattle(&player, &monster);
if (victory)
cout << "승리!" << endl;
else
cout << "패배!" << endl;
}
StatInfo CreatePlayer()
{
StatInfo ret;
cout << "플레이어 생성" << endl;
ret.hp = 100;
ret.attack = 10;
ret.defence = 2;
return ret;
}
void CreateMonster(StatInfo* info)
{
cout << "몬스터 생성" << endl;
info->hp = 40;
info->attack = 8;
info->defence = 1;
}
bool StartBattle(StatInfo* player, StatInfo* monster)
{
while (true)
{
int damage = player->attack - monster->defence;
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->attack - player->defence;
if (damage < 0)
damage = 0;
cout << "플레이어 HP : " << player->hp << endl;
player->hp -= damage;
if (player->hp < 0)
player->hp = 0;
if (player->hp == 0)
return false;
}
}
핵심 : 포인터로 넘길때와 반환값으로 복사해서 넘길떄의 차이점
구조체 자체를 덮어주는 형식으로 만들어서 반환하는 경우 복사가 여러번 일어날 수 있다.
포인터를 넘겨주는 경우에는 실제 원본을 대상으로 접근해서 작업하기 때문에 작업이 더 빨리 수행될 수 있다.
참조 기초
#include <iostream>
using namespace std;
// 오늘의 주제 : 참조
//playerInfo와 MonsterInfo로 매개변수 포인터 이용해서 하는 차이점을 알아보자.
struct StatInfo
{
int hp; // +0
int attack; // +4
int defence; // +8 이렇게 주소에 저장이 된다.
};
//[매개변수][RET][지역변수(info)] -> [매개변수(&info)][RET][지역변수]
void CreateMonster(StatInfo* info)
{
info->hp = 100;
info->attack = 8;
info->defence = 5;
}
//포인터를 이용하지 않고 하면 값의 이동 복사가 일어나서 데이터처리부하가 늘어난다.
//[매개변수][RET][지역변수(info)] -> [매개변수(info(100,8,5))][RET][지역변수]
// 값의 저장이 temp에 되고 이 값들이 매개변수로 복사가 되어 다시 이 함수 사용하는 곳에 매개변수로 들어가서 복사와 저장이 된다.?? 뭐 이런식? 암튼 복잡하다.
void CreateMonster(StatInfo info)
{
info.hp = 100;
info.attack = 8;
info.defence = 5;
}
// 값을 수정하지 않는다면, 둘 다 문제가 없다.
// 1) 값 전달 방식
void PrintInfoByCopy(StatInfo info)
{
cout << "------------------------" << endl;
cout << "HP: " << info.hp << endl;
cout << "ATT: " << info.attack << endl;
cout << "DEF: " << info.defence << endl;
cout << "------------------------" << endl;
}
// 2) 주소 전달 방식
void PrintInfoByPtr(StatInfo* info)
{
cout << "------------------------" << endl;
cout << "HP: " << info->hp << endl;
cout << "ATT: " << info->attack << endl;
cout << "DEF: " << info->defence << endl;
cout << "------------------------" << endl;
}
// 1) 2) 둘다 결과는 같은데, 내부적으로 수행방법은 다르다.
// StatInfo 구조체가 1000바이트 짜리 대형 구조체라면?
// - (값 전달) StatInfo 로 넘기면 1000바이트가 복사되는 구조.
// - (주소 전달) StatInfo* 은 8바이트 주소용량(64비트환경)이 복사되는 구조. 데이터복사에 대한 부담이 없다.
// - (참조 전달) StatInfo& 8바이트.
// 3) 참조 전달 방식
//값 전달처럼 편리하게 사용하고!
//주소 전달처럼 주소값을 이용해 진퉁을 건드리는 일석 이조의 방식.
void PrintInfoByRef(StatInfo& info)
{
cout << "------------------------" << endl;
cout << "HP: " << info.hp << endl;
cout << "ATT: " << info.attack << endl;
cout << "DEF: " << info.defence << endl;
cout << "------------------------" << endl;
}
int main()
{
int number = 1;
// 4바이트 정수형 바구니를 사용할거야.
// 앞으로 그 바구니 이름을 number라고 할게.
// 그러니까 number에서 뭘 꺼내거나, number에 뭘 넣는다고 하면
// 찰떡같이 알아듣고 해당 주소(data, stack, heap)에 1을 넣어주면 된다.
int* pointer = &number;
// * 주소를 담는 바구니
// int 그 바구니를 따라가면 int 데이터 (바구니)가 있음
*pointer = 2;
//pointer 바구니에 있는 주소를 타고 이동해서, 그 멀리 있는 바구니에 2를 넣는다.
int& reference = number; //두번째 이름을 지어줌.
//로우레벨(어셈블리) 관점에서 실제 작동 방식은 int*와 똑같음
//C++ 관점에서는 number라는 바구니에 또 다른 이름을 부여한 것.
//number라는 바구니에 reference라는 다른 이름을 지어줄께~
// 앞으로 reference바구니에다가 뭘 꺼내거나 넣으면,
// 실제 number 바구니(진퉁에다가) 그 값을 꺼내거나 넣으면 됨.
//사용하는 방식은 위의 위와 같은데 작동방식은 포인터와 같다.
reference = 3; // 이라고 하면 reference는 number의 주소값을 들고 있고 number에 값이 3이 저장된다.
// 포인터와 같네;
//어셈블리로 디버깅해보면 포인터와 작동방식이 똑같다는 것을 확인할 수 있다.
//그런데 귀찮게 또 다른 이름을 짓는 이유는?
// 1. 포인터를 사용하자니 -> 이게 마음에 안듬.
// 2. 위에처럼 포인터를 사용안하자니 자료복사 등 데이터의 과부하 줄이기 힘듬.
//참조 전달 때문에 장점이 있다.
StatInfo info;
CreateMonster(&info);
PrintInfoByCopy(info);
PrintInfoByPtr(&info);
PrintInfoByRef(info);
return 0;
}
참조 : 작동방식은 pointer처럼 주소를 전달해주는 방식이지만 사용interface는 일반적인 변수사용과 같다.
포인터 vs 참조
#include <iostream>
using namespace std;
// 오늘의 주제 : 포인터 vs 참조
struct StatInfo
{
int hp; // +0
int attack; // +4
int defence; // +8 이렇게 주소에 저장이 된다.
};
//[매개변수][RET][지역변수(info)] -> [매개변수(&info)][RET][지역변수]
void CreateMonster(StatInfo* info)
{
info->hp = 100;
info->attack = 8;
info->defence = 5;
}
//[매개변수][RET][지역변수(info)] -> [매개변수(info(100,8,5))][RET][지역변수]
void CreateMonster(StatInfo info)
{
info.hp = 100;
info.attack = 8;
info.defence = 5;
}
// 값을 수정하지 않는다면, 둘 다 문제가 없다.
// 1) 값 전달 방식
void PrintInfoByCopy(StatInfo info)
{
cout << "------------------------" << endl;
cout << "HP: " << info.hp << endl;
cout << "ATT: " << info.attack << endl;
cout << "DEF: " << info.defence << endl;
cout << "------------------------" << endl;
}
StatInfo* FindMonster()
{
// TODO : Heap 영역에서 뭔가를 찾아봄
// 찾았다!
// return monster~;
return nullptr; //못찾았을 때. 이런 식으로 참조는 이렇게 표현 못하지만 포인터는 가능.
}
StatInfo globalInfo;
// 2) 주소 전달 방식
void PrintInfo(StatInfo* info)
{
if (info == nullptr)
return;
//info 가 nullPtr일 수도 있으니까 이렇게 처리를 해주도록 한다.
cout << "------------------------" << endl;
cout << "HP: " << info->hp << endl;
cout << "ATT: " << info->attack << endl;
cout << "DEF: " << info->defence << endl;
cout << "------------------------" << endl;
//const를 어디에다 붙이느냐에 따라 달라질 수 있다.
// 별 뒤에 붙인다면??
// StatInfo* const info
// info라는 바구니의 내용물[주소]을 바꿀 수 없음.
// info는 주소값을 갖는 바구니 -> 이 주소값이 고정이다!
// 예를 들어 info = &globalInfo; 라고 하면 컴파일에러가 난다.
// 별 이전에 붙인다면?
// const StatInfo* info
// info가 '가리키고 있는' 바구니의 내용물을 바꿀 수 없음
// '원격' 바구니의 내용물을 바꿀 수 없음.
// 예를 들어 info-> hp = 10000; 에러 확인가능하다.
// 이 경우를 정말 많이 쓴다.
// const를 붙임으로서 안정성이 향상된다.
// info는 [주소값]을 들고 있고, 주소값에 가면 [ 데이터 ]가 있다.
info = &globalInfo;
info->hp = 10000;
//위의 변수 수정으로 const 선언해보면 컴파일에러 나는 것을 확인해볼수 있다.
}
void PrintInfo(const StatInfo& info) //const를 붙여주면 읽기만 가능하고 쓰기는 할 수 없다.
{
cout << "------------------------" << endl;
cout << "HP: " << info.hp << endl;
cout << "ATT: " << info.attack << endl;
cout << "DEF: " << info.defence << endl;
cout << "------------------------" << endl;
// info.hp = 10000; //error 발생.
//참조같은 경우는 const와 정말 빈번하게 쓰인다.
}
#define OUT // 아무 의미 없는데 바뀔 수 있다는 의미를 주기 위해서 OUT을 넣어준다.
void ChangeInfo(OUT StatInfo& info)
{
info.hp = 10000;
}
int main()
{
StatInfo info;
//포인터 vs 참조 세기의 대결
// 성능 : 똑같음!
// 편의성 : 참조 승!
// 1) 편의성 관련
// 편의성이 좋다는 게 꼭 장점만은 아니다.
// 포인터는 주소를 넘기니까 확실하게 원본을 넘긴다는 힌트를 줄 수 있다.
// 참조는 자연스럽게 모르고 지나칠 수도 있다.
// ex) 마음대로 고친다면?
// const를 사용하여 마음대로 고치지 않게
// 참고로 포인터도 const를 사용가능
// * 기준으로 앞에 붙이느냐, 뒤에 붙이느냐에 따라 의미가 달라진다.
// 2) 초기화 여부
// 참조 타입은 바구니의 2번째 이름 (별칭?)
// -> 참조하는 대상이 없으면 안된다.
// 포인터처럼 주소 연산이 안된다.
// 반면 포인터는 어떤 주소 라는 의미
// -> 대상이 실존하지 않아도 선언 가능하다.
// 포인터에서 '없다'라는 의미로는 어떻게 선언하나?
// StatInfo* pointer = nullptr;
// StatInfo* pointer = null;
// StatInfo* pointer = 0;
// 셋 다 같은 의미인데 가장 위에꺼를 주로 사용한다.
//어떠한 주소도 가리키지 않는 상태라는 의미
// 참조 타입은 이런 nullptr 개념이 없다.
StatInfo* pointer;
pointer = &info;
PrintInfo(pointer);
// StatInfo& reference;
// reference = info; // 이렇게 선언하면 컴파일 에러가 난다.
StatInfo& reference = info; // 이렇게 선언해야 한다.
// reference++; // 이것 역시 컴파일 에러가 난다.
PrintInfo(reference);
CreateMonster(&info);
PrintInfoByCopy(info);
PrintInfo(&info);
PrintInfo(info);
//그래서 결론은?
// 사실 Team By Team ... 정해진 답은 없다.
// Ex) 구글에서 만든 오픈소스를 보면 거의 무조건 포인터를 사용한다.
// Ex) 언리언 엔진에선 reference도 애용한다.
// return 값이 없는 것도 고려해야 한다면 pointer (null 체크 필수)
// 바뀌지 않고 읽는 용도(reading only) 로만 사용한다면 참조도 좋다. const ref&
// 그 외 일반적으로 ref (명시적으로 호출할 때 OUT을 붙인다.)
// -- 단, 다른 사람이 pointer를 만들어놓은 걸 이어서 만든다면, 계속 pointer를 사용해준다. 섞어서 사용하지 않는다.
ChangeInfo(OUT info); // 참조지만 바뀔 수 있다는 것을 명시해주기 위해 OUT을 명시해주었다.
// Bonus : 포인터로 사용하던 걸 참조로 넘겨주려면?
// pointer [주소(&info)] ref.info[데이터]
// PrintInfoByRef(*pointer);
// Bonus : 참조로 사용하던 걸 포인터로 넘겨주려면?
// pointer[주소] reference, info[데이터]
// PrintInfoByPtr(&reference);
//이런 식으로 해준다.
return 0;
}
//단축키 ctrl r + r 로 함수 지정한 상태에서 단축키 누르면 한번에 다 바꿀 수 있다.
포인터는 함수에 넣어서 그 값을 바꾸어주는데 의의가 있어보인다.
배열 기초
#include <iostream>
using namespace std;
// 오늘의 주제 : 배열
struct StatInfo
{
int hp = 0xAAAAAAAA;
int attack = 0xBBBBBBBBB;
int defence = 0xDDDDDDDD;
};
int main()
{
// 몬스터가 정해진 숫자가 아닌 몬스터 최대 1만마리라면 이렇게 선언해서 만들수 있나?
StatInfo monsters[10];
// 배열의 크기는 상수여야 한다. (VC 컴파일러 기준)
const int monsterCount = 10;
StatInfo monsters[monsterCount];
//여태껏 변수들의 [이름]은 바구니였음
int a = 10;
int b = a;
//그런데 배열의 [이름]은 조금 다르게 동작한다.
// 배열의 이름은 배열의 시작 주소다.
// 정확히는 시작 위치를 가리키는 TYPE* 포인터
//주소이기 때문에 디버깅시 메모리검색할 떄 바로 monsters 엔터 해도 검색이 되는 것이다.
auto WhoAmI = monsters; //auto 는 카멜레온같은 녀석, 변수에 따라서 타입이 자동으로 입력된다..
// 주소[(100,10,1)] StatInfo[ ] StatInfo [ ] StatInfo[ ] ...
// monster_0[ 주소 ]
StatInfo* monster_0 = monsters;
monster_0->hp = 100;
monster_0->attack = 10;
monster_0->defence = 1;
// 포인터의 덧셈! 진짜 1을 더하라는 게 아니라, StatInfo 타입 바구니 한개씩 이동하라는 의미
// StatInfo [ ] 주소[(200,20,2)] StatInfo[ ] StatInfo [ ] StatInfo[ ]
// monster_1 [ 주소 ]
StatInfo* monster_1 = monsters + 1;
monster_1->hp = 200;
monster_1->attack = 20;
monster_1->defence = 2;
// 이번에는 참조를 써보자
// 포인터와 참조는 자유자재로 변환이 가능하다.
// StatInfo [ ] StatInfo[ ] monster_2 주소 [ ] StatInfo[ ]
// monster_2[ 주소 ]
StatInfo& monster_2 = *(monsters + 2);
monster_2.hp = 300;
monster_2.attack = 30;
monster_2.defence = 3;
//[주의] 이거는 완전 다른 의미다.
// 이거는 temp에 monsters+2 주소에 있는 거를 가져와서 저장하는 방식.
StatInfo temp;
temp = *(monsters + 2);
temp.hp = 400;
temp.attack = 40;
temp.defence = 4;
// 이를 조금 더 자동화한다!
for (int i = 0; i < 10; i++)
{
StatInfo& monster = *(monsters + i);
monster.hp = 100 * (i + 1);
monster.attack = 10*(i+1);
monster.defence = (i+1);
}
// 근데 *(monsters + i ) 너무 불편하고 가독성이 떨어진다... 더 편한 방법?
// 인덱스!
// 배열은 0번부터 시작. N번째 인덱스에 해당하는 데이터에 접근하려면 배열이름[N]
// *(monsters + i) = monsters[i]
return 0;
monsters[0].hp = 100;
monsters[0].attack = 10;
monsters[0].defence = 1;
for (int i = 0; i < 10; i++)
{
monsters[i].hp = 100 * (i + 1);
monsters[i].attack = 10 * (i + 1);
monsters[i].defence = (i + 1);
}
//배열 초기화 방법 몇 가지
int numbers[5] = {}; // 다 0으로 입력된다.
int numbers1[10] = { 1,2,3,4,5 }; // 설정한 애들은 설정한 값으로. 나머지 값들은 0으로 초기화된다.
int numbers2[] = { 1,2,3,4,11,24,124,14,1 }; // 데이터 개수만큼의 크기에 해당하는 배열로 만들어진다.
char helloStr[] = { 'H','e','l','l','o','\0' };
}
포인터 vs 배열
#include <iostream>
using namespace std;
// 오늘의 주제 : 포인터 vs 배열
void Test(int a)
{
a++;
}
//배열은 함수 인자로 넘기면, 컴파일러가 알아서 포인터로 치환한다.(char[] -> char*
// 즉 배열의 내용 전체를 넘긴게 아니라, 시작 주소 즉 포인터만 넘긴 것.
void Test(char a[])
{
a[0] = 'x';
}
int main()
{
"Hello World"; // 여기에 마우스 커서 올려보면 const char 볼 수 있다.\0 이 합쳐져 12개짜리 문자열 배열을 확인할 수 있다.
cout << "Hello WOrld" << endl;
const char msg[] = { 'H','e','l','l','o','\0' }; // char 문자열은 끝났다는 표시로 \0 (null 의 의미) 를 입력해줘야 한다.
//const가 붙으면 수정할 수가 없다.
// .data에 저장됨. [주소][H][e][l][l][o][][W][o][r][l][d][\0]
// test1 [ 주소 ] << 8바이트 (64비트 환경)
const char* test1 = "Hello World";
// [H][e][l][l][o][][W][o][r][l][d][\0]
// test2 = 주소
char test2[12] = "Hello World";
//포인터와 배열의 차이
//포인터는 주소를 담는 바구니가 따로 있다. 원격으로 주소가 가리키고 있다.
//첫번째 [H] 이게 있는 거 자체가 주소값 test2 이다.
// - [배열 이름]은 '바구니 모음'의 [시작 주소], 주소값만 넘겨주는 역할을 한다.
int a = 0;
// [매개변수][RET][지역변수(a=0)] [매개변수(a=0)][RET][지역변수(a=0]
Test(a);
cout << a << endl;
//a는 여전히 0 왜냐? 1이라는 값이 들어간거지 아무것도 변화는 없다.
Test(test2);
cout << test2 << endl;
//test2 가 바뀔까 안바뀔까? 바뀐다.
return 0;
}
const char* test1 = "Hello World";
Hello World가 메모리 어딘가에 생성되고
test1은 그저 Hello World가 어디있는지만 알려줌
포인터이기 때문에 4바이트 or 8바이트
-> 그 메모리 '어딘가' = 데이터 영역입니다.
char test2[] = "Hello World";
Hello World가 메모리 어딘가에 일단 생성됨
-> 이번엔 메모리 '어딘가' = 스택입니다.
Hello World가 있는곳을 찾아서 4바이트씩 끊어서 계속
test2 배열에 넣어줌 그래서 문자열의 크기가 커지면 크기가같이 커짐
-> 아닙니다. 4바이트가 아니라 char은 1바이트 이기 때문에
'H' 'e' 'l' ... 등을 각각 복사해주고 각 1바이트입니다.
사실상 test3 케이스와 유사하게 동작합니다.
char test3[] = { 'h','e','l','l','o' }; 는
hello가 메모리 어딘가에 생성되는게라 그냥 하나식 test3 배열에 옮겨짐
-> 네 어셈블리를 까보면 스택에 'h' 'e' .. 한 글짜씩 복사됩니다.
그리고 의문이 생길 땐 어셈블리로 까보는걸 추천 드립니다.
그래야 공부도 되고 오래 남습니다.
로또 번호 생성기
#include <iostream>
using namespace std;
// 오늘의 주제 : 로또 번호 생성기
void Swap(int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}
void Sort(int numbers[], int count)
{
for (int i = 0; i < count; i++)
{
// i 번째 값이 제일 좋은 후보라고 가정
int best = i;
// 다른 후보와 비교를 통해 제일 좋은 후보를 찾아나선다.
for (int j = i + 1; j < count; j++)
{
if (numbers[j] < numbers[best])
best = j;
}
// 제일 좋은 후보와 교체하는 과정
if (i != best)
Swap(numbers[i], numbers[best]);
}
}
// 3) 로또 번호 생성
void ChooseLotto(int numbers[])
{
srand((unsigned)time(0)); // 랜덤 시드 설정
int count = 0;
while (count != 6)
{
int randValue = 1 + (rand() % 45); // 1-45
// 이미 찾은 값인지?
bool found = false;
for (int i = 0; i < count; i++)
{
//이미 찾은 값
if (numbers[i] == randValue)
{
found = true;
break;
}
}
//못찾았으면 추가!
if (found == false)
{
numbers[count] = randValue;
count++;
//TODO : 랜덤으로 1~45 사이의 숫자를 6개를 골라주세요! (단, 중복이 없어야 함)
}
}
Sort(numbers, 6);
}
int main()
{
// 1) Swap 함수 만들기
int a = 1;
int b = 2;
Swap(a, b); // a=2, b = 1
cout << a << " " << b << endl;
// 2) 정렬 함수 만들기 (작은 숫자가 먼저 오도록 정렬)
int numbers[6] = { 1,42,3,15,5,6 };
//{ 1,3,5,6,15,42 }
//배열의 크기를 일일이 쓰기 힘드니까. sizeof(numbers) / sizeof(int) 를 활용해준다.
int size1 = sizeof(numbers); // 6*4 = 24
int size2 = sizeof(int); // 4
//Sort(numbers, 6);
//위에 것보다
Sort(numbers, sizeof(numbers) / sizeof(int));
// 3) 로또 번호 생성기
ChooseLotto(numbers);
for (int i = 0; i < 6; i++)
cout << numbers[i] << endl;
return 0;
}
다중포인터
#include <iostream>
using namespace std;
// 오늘의 주제 : 다중 포인터
void SetNumber(int* a)
{
*a = 1;
}
void SetMessage(const char* a)
{
a = "Bye"; //바구니의 내용만 고칠 뿐이지. int main에 있는 지역변수를 건드린 게 아니다.
// 함수 종료되면 그냥 그 함수 안에서만 바뀌고 끝.
}
void SetMessage(const char** a)
{
*a = "Bye";
}
void SetMessage2(const char*& a)
{
a = "Wow";
}
int main()
{
int a = 0;
SetNumber(&a);
cout << a << endl;
//main에 있는 a가 매개변수 포인터를 활용한 방법으로 수정할 수 있다.
// msg [주소] 가 들어가있는 바구니 << 8바이트(64비트환경)
//Hello는 어디있느냐? .rdata[H][e][l][l][o][\0]
const char* msg = "Hello";
//[매개변수][리턴주소][지역변수(msg(Hello주소))] [매개변수(a(Hello주소))][리턴주소][지역변수]
SetMessage(msg);
cout << msg << endl;
// .rdata Hello주소[H][e][l][l][o][\0]
// 주소1 [ Hello주소 ] << 8바이트
// pp [&msg] << 8바이트
const char** pp = &msg;
*pp = "Bye";
cout << msg << endl;
SetMessage(pp); //SetMessage(&msg); 와 동일
cout << msg << endl;
// 다중포인터 뭔가 혼동스럽다?
// 다중포인터 는 그냥 양파까기라고 생각하면 된다.
// * 을 하나씩 까면서 간다고 생각하면 된다.
SetMessage2(msg); // msg의 주소를 보낸거..
cout << msg << endl;
return 0;
}
다차원배열
#include <iostream>
using namespace std;
// 오늘의 주제 : 다차원 배열
int main()
{
int a[10] = { 1,2,3 };
// [4] [2] [3] [4] [1] << 0층
// [1] [1] [5] [2] [2] << 1층
int apartment2D[2][5] = { {4,2,3,4,1}, {1,1,5,2,2} };
for (int floor = 0; floor < 2; floor++)
{
for (int room = 0; room < 5; room++)
{
// apartment2D + (floor * 20) +4 *room을 한 주소. 디버깅후 디셈블리어로 확인가능.
int num = apartment2D[floor][room];
cout << num << " ";
}
cout << endl;
// 디버깅을 해보면 1차원배열이 쭉 나열되어있는 것처럼 2차원 배열이 메모리에 저장되어있다.
int apartment1D[10] = { 4,2,3,4,1,1,1,5,2,2 };
//디버깅해보면 메모리에는 위에꺼와 이것과 같은 방식으로 저장되어 있다.
for (int floor = 0; floor < 2; floor++)
{
for (int room = 0; room < 5; room++)
{
int index = (floor * 5) + room;
// apartment1D + (floor * 20) +4 *room을 한 주소. 디버깅후 디셈블리어로 확인가능.
int num = apartment1D[index];
cout << num << " ";
}
cout << endl;
}
// 2차 배열은 언제 사용할까? 대표적으로 2D 로그라이크 맵 사용할 떄.
int map[5][5] = // 갈 수 있는 경우를 1, 갈 수 없는 경우를 0으로 표시해줌
{
{1,1,1,1,1},
{1,0,1,1,1},
{1,0,0,0,0},
{1,0,1,1,1},
{1,1,1,1,1},
};
return 0;
}
포인터마무리
#include <iostream>
using namespace std;
// 오늘의 주제 : 포인터 마무리
// 1) 포인터 vs 배열 2탄
// 2) 주의사항 (마음가짐?)
int& TestRef()
{
int a = 1;
return a;
}
int* TestPointer() //함수 안에서만 유효한 주소값을 외부로 넘겨주는 행위는 절대로 하면 안된다.
{ //왜냐면 함수는 한번 실행되고 사라져야 하는데, 그 메모리주소가 그대로 남아있으면
int a = 1; // 앞으로 crash가 날 수 있다.
return &a;
}
void TestWrong(int* ptr)
{
int a[100] = {};
*ptr = 0x12341234;
}
int main()
{
//p는 주소를 담는 바구니
// 진퉁은 저 멀리 어딘가에 있음
//p는 단지 그 곳으로 워프하는 포탈
int* p;
//진짜배기 원조 데이터가 저장됨.
//닭장처럼 데이터의 묶음 (엄청 많고 거대함)
int arr[10];
//그런데 상당히 많은 사람들이 [배열 = 포인터]라 착각하는 경향이 있다.
// - [배열의 이름]은 배열의 시작 주소값을 가리키는 TYPE* 포인터로 변환가능
p = arr;
//-[TYPE형 1차원 배열]과 [TYPE*형 포인터]는 완전히 호환이 된다.
cout << p[0] << endl;
cout << arr[0] << endl;
cout << p[5] << endl;
cout << arr[5] << endl;
cout << *p << endl; // p[0]
cout << *arr << endl; // arr[0]
cout << *(p + 3) << endl;
cout << *(arr + 3) << endl;
// 지옥을 보여드리겠습니다. (2차원 배열 vs 다중포인터)
// [1][2][3][4]
int arr2[2][2] = { {1,2}, {3,4} };
//int** pp = (int**)arr2; // 예외발생한다.타입형변환도 원래 이렇게 하면 안됨.
//cout << (**pp) << endl;
//아래 내용은 별로 중요한게 아니다. 패스해도 됨.
int(*p2)[2] = arr2; //연습문제할때나 하는거지 이런코드는 평생 쓸 일 없을거라고 한다;
cout << (*p2)[0] << endl;
cout << (*p2)[1] << endl;
cout << (*(p2 + 1))[0] << endl;
cout << (*(p2 + 1))[1] << endl;
cout << p2[0][0] << endl;
cout << p2[0][1] << endl;
cout << p2[1][0] << endl;
cout << p2[1][1] << endl;
//C++의 무서운 점은 알게 모르게 포인터를 잘못 다루면 크래쉬가 나는 경우가 있다.
int* pointer = TestPointer();
*pointer = 123;
//위의 두 줄이 문제가 없어보이는데 사실은 아주 큰 문제를 가지고 있다.
//스텍프레임에서 TestPointer()를 실행시키면 pointer 안에 a의 주소값이 저장되게 된다.
//하지만 TestPointer()는 한 번 실행되고 나면 사라지는 것인데, 사라져야 할 것의 주소값을 계속
// 가지고 있는 것은 앞으로 큰 문제를 야기하게 된다는 거다.
//그리고 그 사라져야 할 주소값에 123을 저장하게 되면.. 나중에 crash를 야기할 수 있게 된다.
TestWrong(pointer);
//이거를 쓰면 TestWrong 함수와는 관계없는 메모리영역을 수정하는 꼴이 된다.
//이거 실행하면 crash가 난다.
//디버깅모드에서는 문제를 잡아주는데
//release모드로 하면 문제 없이 컴파일하고 끝나게 된다. 디버깅하는 습관을 갖자.
return 0;
}

*ptr이 매개변수로 받을때부터 TestPtr에서 &a를 받았는데 어째서 그값이 a[100] 주소를 가르키고있고 그 상태에서*ptr=0x12341234를 넣었을떄 어떻게 크래쉬가 나는지 이해가 되지않아 이렇게 질문을드립니다
일단 TestPtr()이라는 함수가 굉장히 문제가 많은 함수고
스택 영역의 로컬 주소를 저렇게 밖으로 반환하는 것 자체가 절대 해서는 안 되는 행동입니다.

스택 영역은 함수가 종료되고 다른 함수가 호출되면 재사용하는데
운 나쁘게 저렇게 영역이 겹치고 있다면 엉뚱한 메모리를 건드릴 수 있는 것이죠.
[스택 프레임] 구조는 안 보고도 그릴 수 있을 정도로
의외로 중요한 부분이니 완전히 이해를 해야 합니다.
TestPtr로인해 로컬주소가 밖에 노출되었고 TestWrong 스택프레임 영역에 겹쳐져서 그안에 메모리를 건드려가지고 오류가 발생했단거군요. 답변감사합니다
TextRPG #3
#include <iostream>
using namespace std;
// 오늘의 주제 : TextRPG2
// main
// - EnterLobby (PlayerInfo)
// -- CreatePlayer
// -- EnterGame (MonsterInfo)
// ---CreateMonsters
// ---EnterBattle
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 attack = 0;
int defence = 0;
};
void EnterLobby();
void PrintMessage(const char* msg);
void CreatePlayer(StatInfo* playerInfo);
void PrintStatInfo(const char* name, const StatInfo& info);
//PrintStatInfo( const StatInfo info) 이렇게 해줘도 되긴 된다. 그런데 이렇게 하면 데이터의 복사가 일어나서 StatInfo 구조체 데이터가 많아지게 되면 성능적으로 안좋은 영향을 주게 된다.
//그리고 출력으로만 사용하기 때문에 * 대신에 &을 사용했다.
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 EnterLobby()
{
while (true)
{
PrintMessage("로비에 입장했습니다.");
StatInfo playerInfo;
CreatePlayer(&playerInfo);
PrintStatInfo("player", playerInfo);
EnterGame(&playerInfo);
}
}
void PrintMessage(const char* msg)
{
cout << "***************************" << endl;
cout << msg << endl;
cout << "***************************" << endl;
}
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).hp = 100; 이거랑 같다.
playerInfo->attack = 10;
playerInfo->defence = 5;
ready = true;
break;
case PT_Archer:
playerInfo->hp = 80;
playerInfo->attack = 15;
playerInfo->defence = 3;
ready = true;
break;
case PT_Mage:
playerInfo->hp = 50;
playerInfo->attack = 25;
playerInfo->defence = 1;
ready = true;
break; // case문에서 break대신 return; 을 사용해주면 함수를 나간다.
//만약 이게 보기 싫다면 bool 설정해줘서 나가는 식으로 해준다.
}
}
}
void PrintStatInfo(const char* name, const StatInfo& info)
{
cout << "***********************" << endl;
cout << name << "HP = " << info.hp << " ATT= " << info.attack << " DEF= " << info.defence << 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].attack = 5;
monsterInfo[i].defence = 1;
break;
case MT_Orc:
monsterInfo[i].hp = 40;
monsterInfo[i].attack = 8;
monsterInfo[i].defence = 2;
break;
case MT_Skeleton:
monsterInfo[i].hp = 50;
monsterInfo[i].attack = 15;
monsterInfo[i].defence = 3;
break;
}
}
}
bool EnterBattle(StatInfo* playerInfo, StatInfo* monsterInfo)
{
while (true)
{
int damage = playerInfo->attack - monsterInfo->defence;
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->attack - playerInfo->defence;
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;
}
}
}
포인터 연습문제 : strcmp, strlen etc
#include <iostream>
using namespace std;
// 오늘의 주제 : 연습 문제
// 문제 1) 문자열 길이를 출력하는 함수
int StrLen(const char* str)
{
// str이라는 문자열의 길이를 반환하는 함수 만들어보자.
int i = 0;
while (str[i] != '\0')
i++;
return i;
}
// 문제2) 문자열 복사 함수
char* StrCpy(char* dest, char* src)
{
/*int i = 0;
while (src[i] != '\0')
{
dest[i] = src[i];
i++;
}
dest[i] = '\0';
return dest;*/
//위 방법도 있고 아래방법도 해볼 수 있다.
char* ret = dest;
while (*src != '\0')
{
*dest = *src;
dest++;
src++;
}
*dest = '\0';
return ret;
}
//문제 3) 문자열 덧붙이는 함수.
//주소1[H][e][l][l][o][W][o][r][l][d][\0][][][][][][][][][]
//주소2[W][o][r][l][d][\0][][][][][][][][][][][][]
char* StrCat(char* dest, char* src)
{
/*int len = StrLen(dest);
int i = 0;
while (src[i] != '\0')
{
dest[len + i] = src[i];
i++;
}
dest[len + i] = '\0';
return dest;*/
// 위 말고 포인터 이용한 다른 방법
char* ret = dest;
while (*dest)
dest++;
while (*src)
*dest++ = *src++;
*dest = '\0';
return ret;
}
// 문제4) 두 문자열을 비교하는 함수
int StrCmp(char* a, char* b)
{
int i = 0;
while (a[i] != '\0' || b[i] != '\0')
{
if (a[i] > b[i])
return 1;
if (a[i] < b[i])
return -1;
i++;
}
return 0;
}
// 문제5) 문자열을 뒤집는 함수
void ReverseStr(char* str)
{
int len = StrLen(str);
for (int i = 0; i < len / 2; i++)
{
int temp = str[i];
str[i] = str[len - 1 - i];
str[len - 1 - i] = temp;
}
}
#pragma warning(disable: 4996) // 오류 발생 넘어가준다.
int main()
{
const int BUF_SIZE = 100;
//[H][e][l][l][o][\0][][][][[][][][][][][][][][]
char a[BUF_SIZE] = "Hello";
int len = sizeof(a); // BUF_SIZE 크기를 출력해준다. 내용물 크기 X
cout << len << endl;
len=strlen(a);
cout << len << endl; // 문자열의 크기가 나온다.
len = StrLen(a);
cout << len << endl; // 문자열의 크기가 나온다.
char b[BUF_SIZE];
//strcpy(b, a); // #pragma warning(disable: 4996) 이거 없으면 오류발생한다.
//StrCpy(b, a);
//strcat(a, b);
//StrCat(a, b);
char c[BUF_SIZE] = "Hello";
if (a == c)
cout << "same" << endl;
else
cout << "different" << endl;
//바구니 값 자체는 주소값만 가지고 있기 때문에 다르다고 나온다.
int value = strcmp(a, b);
cout << value << endl;
value = StrCmp(a, b);
cout << value << endl;
ReverseStr(a);
cout << a << endl;
return 0;
}
달팽이문제
#include <iostream>
using namespace std;
#include <iomanip>
// 오늘의 주제 : 달팽이 문제
const int MAX = 100;
int board[MAX][MAX] = {};
int N;
void PrintBoard()
{
for (int y = 0; y < N; y++)
{
for (int x = 0; x < N; x++)
{
cout << setfill('0') << setw(2) << board[y][x] << " ";
}
cout << endl;
}
}
enum DIR
{
RIGHT = 0,
DOWN = 1,
LEFT = 2,
UP = 3,
};
bool CanGo(int y, int x)
{
if (y < 0 || y >= N)
return false;
if (x < 0 || x >= N)
return false;
if (board[y][x] != 0)
return false;
return true;
}
void SetBoard()
{
int dir = RIGHT;
int num = 1;
int y = 0;
int x = 0;
while (true)
{
board[y][x] = num;
if (num == N * N)
break;
int nextY;
int nextX;
switch (dir)
{
case RIGHT:
nextY = y;
nextX = x + 1;
break;
case DOWN:
nextY = y + 1;
nextX = x;
break;
case LEFT:
nextY = y;
nextX = x - 1;
break;
case UP:
nextY = y - 1;
nextX = x;
break;
}
if (CanGo(nextY, nextX))
{
y = nextY;
x = nextX;
num++;
}
else
{
switch (dir)
{
case RIGHT:
dir = DOWN;
break;
case DOWN:
dir = LEFT;
break;
case LEFT:
dir = UP;
break;
case UP:
dir = RIGHT;
break;
}
}
}
}
void SetBoard1() //위에 SetBaord()를 더 간략하게 만들 수 있다.
{
int dir = RIGHT;
int num = 1;
int y = 0;
int x = 0;
int dy[] = {0, 1, 0, -1};
int dx[] = { 1, 0, -1, 0 };
while (true)
{
board[y][x] = num;
if (num == N * N)
break;
int nextY = y + dy[dir];
int nextX = x + dx[dir];
if (CanGo(nextY, nextX))
{
y = nextY;
x = nextX;
num++;
}
else
{
dir = (dir + 1) % 4;
}
}
}
int main()
{
cin >> N;
SetBoard();
PrintBoard();
return 0;
}