다중 포인터
포인터를 통해서 우리는 어떤 데이터의 원본을 수정하거나 가져올 수 있었다.
하지만 모든 경우에 그런것은 아니다. 만약 다음과 같은 코드가 있다고 해보자.
#include <iostream>
using namespace std;
void SetMessage(const char* a)
{
a = "Bye";
}
int main()
{
const char* msg = "Hello";
SetMessage(msg);
cout << msg << endl;
return 0;
}
이 코드를 실행해보면 결과가 전혀 바꾸지 않는 것을 확인할 수 있다.
메모리 구조를 잘 생각해보자.
main 함수의 영역 SetMessage의 영역
[매개변수][RET][지역변수(msg, Hello의 주소)] [매개변수(a, Hello의 주소)][RET][지역변수]
이런식으로 스택 메모리에 쌓일 것이다. 그리고 SetMessage 함수에서 a의 주소를 Bye로 바꾸게 된다.
하지만 함수가 종료되면 스택 메모리가 전부 날아가게되고 msg는 아무런 영향을 주지 않게된다.
이것을 해결하기 위해서는 다중 포인터가 필요하다.
#include <iostream>
using namespace std;
void SetMessage(const char* a)
{
//실패
a = "Bye";
}
void SetMessage2(const char** a)
{
*a = "Bye";
}
void SetMessage3(const char*& a)
{
a = "Wow";
}
int main()
{
const char* msg = "Hello";
SetMessage(msg);
// pp 는 주소를 담는 바구니이다. << 8바이트
// 그리고 그 바구니 안에는 주소가 들어있다. << 8바이트
// 그리고 그 주소를 따라가면 1바이트 char가 있다. << 1바이트
const char** pp = &msg;
SetMessage2(&msg);
cout << msg << endl;
SetMessage3(msg);
cout << msg << endl;
return 0;
}
// 다중 포인터 : 혼동스럽다
// 그냥 양파까기라고 생각하자.
// *를 하나씩 까면서 접근하자
이렇게 한다면
main 함수의 영역 SetMessage2의 영역
[매개변수][RET][지역변수(msg, Hello의 주소)] [매개변수(a, msg의 주소)][RET][지역변수]
가 될거고 *a를 통해 msg가 가리키고 있던 hello의 주소를 bye의 주소로 바꿔줌으로써 해결할 수 있다.
참조와 포인터는 동일하게 동작하기 때문에 SetMessage3을 사용해도 문제될 것 없다.
다중 포인터는 실제로 엄청 자주나오진 않는다. 이런 상황이 아니고서든 3차원, 4차원 등등 고차원까지 가는 경우는 코드에 문제가 있다고 생각할 수 있을 정도이다.
참고로 Hello와 Bye의 문자열은 스택 영역에 생기는 것이 아닌 .rdata(Read Only Data)영역에 생성이 되고 msg가 그 주소를 가리키고 있는 것이다.
다차원 배열
다중 포인터는 자주 나오지는 않지만 다차원 배열중 2차원 배열은 정말 많이 마주치게 될 것이다.
만약 아파트의 사람 수를 조사하는 프로그램을 개발한다고 해보자.
1층의 5채의 집에 사람이 4, 2, 3, 4, 1 있다고 하면 우리는
int first[5] = { 4, 2, 3, 4, 1}; 이라고 표현할 수 있다.
하지만 아파트는 1층만 있지 않다. 엄청 많은 층이 있을텐데 이를 변수를 계속 생성하는 건 옳지 않는 방법일 것이다.
그래서 이걸 표현하기 위해 2차원 배열을 사용하는 것이다.
2층까지 있다고 해보면 다음과 같이 표현할 수 있다.
#include <iostream>
using namespace std;
int main()
{
int apt[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++)
{
cout << apt[floor][room] << " ";
}
cout << endl;
}
return 0;
}
2차원 배열이라고 해도 특별한 것은 없다. 스택 메모리를 확인해보면 그냥 1차원 배열처럼 데이터들이 줄 지어 그대로 들어가 있는 모습을 확인할 수 있다.
즉, int apt[10] = { 4, 2, 3, 4, 1, 1, 1, 5, 2, 2 }; 와 동일하게 작동하는 것이다.
단순히 C++ 관점에서 프로그래밍을 도와주기 위해 사용하는 것이다.
포인터 마무리
포인터는 단순히 주소를 담는 바구니이고 진짜 데이터는 저 멀리 어딘가에 있다. 포인터는 단지 그 곳으로 워프하는 포탈이나 다름없다.
배열은 원조 데이터들이 묶여있는 것이다.
그런데 많은 사람들이 배열 = 포인터 라고 착각하는 경향이 있다.
그 이유는 배열의 이름은 배열의 시작 주소값을 가리키는 type* 포인터로 변환이 가능하기 때문이다.
또 type형 1차원 배열과 type*형 포인터는 완전히 호환이 된다.
그렇다면 2차원 배열과 다중 포인터도 똑같을까??
#include <iostream>
using namespace std;
int main()
{
int arr[2][2] = { {1, 2},{3, 4} };
//강제 형 변환
int** pp = (int**)arr;
// pp 라는 주소 바구니가 있고
// 그 주소를 타고 가면 어떤 주소가 있고
// 다시 그 주소를 타면 데이터가 있다고 생각.
cout << (**pp) << endl;
return 0;
}
다음과 같은 코드에서 에러가 난 것을 확인할 수 있다.
그 이유는 pp의 주소를 타고 가면(*pp) 바로 arr의 첫 번째 값 1이 나온 것이다.
#include <iostream>
using namespace std;
int main()
{
int arr[2][2] = { {1, 2},{3, 4} };
// 포인터를 타고 가면 어떤 값이 있는데
// 그 값이 [][] 2개로 이루어진 어떤 값이다.
int(*p2)[2] = arr;
cout << (*p2)[0] << endl;
cout << (*p2)[1] << endl;
cout << (*(p2+1))[0] << endl;
cout << (*(p2+1))[1] << endl;
cout << p2[0][0] << endl;
// ....
return 0;
}
이런 식으로 사용할 수 있다. 하지만 이는 정말 이런게 있다는 정도만 알고 있으면 된다.
여기서 말하고 싶은건 배열은 포인터가 아니라는 점이다.
다차원 배열은 포인터로 변환하는게 1차원이랑 아예 다르다는 점인 것이다.
'C++ > 기초' 카테고리의 다른 글
[C++] 객체 지향 - 2 (1) | 2023.11.27 |
---|---|
[C++] 객체 지향 - 1 (3) | 2023.11.27 |
[C++] 포인터 - 3 (배열) (1) | 2023.11.25 |
[C++] 포인터 - 2 (참조) (1) | 2023.11.25 |
[C++] 포인터 - 1 (0) | 2023.11.24 |