대체로 C/C++에서 배열과 포인터 부분이 어려운 부분이라는 점은 대부분 공감하는 사실이다.
나 또한 이 부분이 어렵고, 이해 안 되고, 쓰기 싫어서 대체로 외면하곤 한다. 속도 최적화와는 거리가 좀 있는 분야를 다루고 있기도 하고.
헌데 이런저런 사정 – 물론 쓰잘데기 없는 것이긴 하지만 – 이 좀 있어서 배열과 포인터에 관련된 내용을 찾아보았다.
일단 VC++ 2005에서 돌려본 것이니, 적어도 틀린 것은 아닐 것이다.
1. 문제의 시작
펼쳐!
int a[3];
(…)
a[i] = 3;
*(a+i) = 3;
위 두 코드는, 디스어셈블 뜯어보면 똑같은 코드가 나온다는 이야기.
이제 형식상의 문제인데, C 수업을 착실히 들은 사람은 알겠지만 a[i]라는 배열에서 a는 첫 번째 원소의 주소를 나타내는 포인터 상수[footnote]int * const. 틀리면 댓글주셈.[/footnote]라고 하였다.
자 그렇다면, &a 는 무엇을 나타낼까?
컴파일을 해 봐도, &a는 틀린거야! 라면서 오류를 뿜어내지는 않는다. 그 값을 출력해도 a와 다를 것은 없다.
그런데, 이상하게도
int a[3];
int *p1 = a;
int *p2 = &a;
라는 코드를 실행하면, 세 번째 줄에서 오류를 내뿜는다.
그렇다면, & 연산자는 주소 연산자니까 * 를 하나 더 붙이면 될까…??
int a[3];
int *p1 = a;
int **p2 = &a;
한 번 돌려보자. 역시나 오류를 내뿜는다. 아ㅅㅂ 뭐가 문제야.
접어!
펼쳐!
“& 연산자가 반환하는 주소는 형태가 좀 지랄같다.”
이다. 정확하게는
“& 연산자는, 피연산자를 하나의 원소로 갖는 가상의 배열에서, 피연산자가 위치하는 부분의 시작주소”
를 반환한다. 아ㅅㅂ 거 조낸 복잡하네.
예를 들어서…
int data;
&data; // 형식이 뭔데?
를 생각해 보자.
data는 그냥 int형이 되겠다. 따라서 &data는
{ int, int, int, …, int, int data, int, …, int, int, int }
라는 가상의 배열에서 data의 위치이다.
자, 이런 배열을 만들려면 뭘 써야 하는가? 당연히 int * 지.
이제 좀 더 가보자.
int a[3]; // 이번엔 배열이다.
&a[0]; // 이건 뭐야…
a; // 이건 왠지 알거같아…
&a; // 이건 또 뭐고.
자 우선. &a[0]. 연산자 우선순위상 [] 가 먼저 연산되니까, &의 피연산자는 int형이다.
또 쓰기 귀찮다. 결국 위에서 본 data와 경우가 같다. 형태는 int *.
다음은 a. 이건 수업시간에 들은 것처럼 첫 번째 원소의 주소, 그러니까 &a[0] 인 것은 확실하다…
그런데 뭔가 미묘하게 다르다. 이놈의 자료형은 int [3] 이다. 응?
이 말은 무슨 말이냐면, “int *와 비슷하긴 한데[footnote]실제로는 int * const 에 해당한다. 배열 주소가 바뀔 수는 없으니까…[/footnote], 길이가 3으로 명백해!” 라는 의미이다. 아ㅅㅂ 머리아프다.
여하튼 여기에서 체크해 두어야 할 것은, 배열도 결국에는 유도 자료형이라는 말이다.
int a[3] 은 “int형 3개를 원소로 갖는 배열 a를 만들어!” 가 아니고, “int형 3개를 이어붙인 배열구조를 갖는 변수 a를 만들어!” 라는 의미다.
아아… C 배운지 3년찍고 4년만에 겨우 이해했다. 변수가 배열이 아니라, 배열 형태를 갖는 변수다. ㅅㅂ 무슨소리래.
어쨌든 마지막, &a. &의 피연산자 형태는? int [3]인 배열이다. 그렇다면 다음과 같이 되겠다.
{ {int, int, int}, {int, int, int}, …, {int, int, int}, {a[0], a[1], a[2]}, {int, int, int}, …, {int, int, int}, {int, int, int} }
라는 배열에서 a 배열의 위치란 말이지. 반환형은? “int 3개를 이어붙인 구조의 배열” 형태의 변수를 가리키는 포인터, 즉 int (*)[3] 이다.
아ㅅㅂ 복잡하다… 이건 또 뭐여… 하겠지만, 그냥 납득해라. 글쓰는 나도 뭔소린지 제대로 모르겠다.
일단은, int 3개짜리 배열을 통짜로 가리키는 포인터라고만 알아두면 되겠다. 이 말을 이해했다면 이 글을 읽을 필요도 없는거고…
참고로, 포인터를 그 맴버로 갖는 배열과, 배열 자체를 가리키는 포인터 하나를 구분하기 위하여 다음과 같은 방법을 사용한다.
포인터를 맴버로 갖는 배열 : int *p[n] : int형 포인터 n개 모인 새로운 자료형을 갖는 변수 p.
배열 자체를 가리키는 포인터 : int (*p)[n] : int형 n개가 모인 자료형을 가리키는 포인터 p.
구분 참 지랄같지만, 여튼 둘이 다르다는 것만 이해해 두자. 참고로 이런 구분방식은 함수 포인터에서도 나온다. 젭라…
그렇담 &&a 도 있을 수 있지 않을까? 미안하지만 그딴건 없다 -.- 대신 int (**)[3] 은 있다.
접어!
펼쳐!
int a[2][3];
이제부터 &a, a, &a[0], a[0], &a[0][0], a[0][0] 여섯 개를 다 살펴보아야 한다.
불친절하게도 그림이 없으니, 알아서 메모지에 그림 그리면서 판단하면 되겠다.
일단 배열구조는 다음과 같겠지.
{ {n00, n01, n02} {n10 n11 n12} }
1. a[0][0]
이건 간단하다. n00 자체를 가리킨다.
2. &a[0][0]
&의 피연산자는? int 형이다. 따라서 다음과 같겠다.
{ int, int, …, int, n00, n01, n02, n10, n11, n12, int, …, int, int }
하여 형태는 int * 되시겠다.
3. a[0]
a[0]은 무엇을 가리키는가…?? a 배열의 첫 번째 원소일 것이다.
그렇다면 a 배열의 첫 번째 원소는 무엇인가? 이중 배열의 첫 번째 원소이니, 그냥 1차원 배열이다.
가리키는 대상은 {n00, n01, n02} 이고, 그 형태는 int [3]이다.
4. &a[0]
&의 피연산자는? 위에서 본 것처럼 int [3]형이다. 따라서…
{ {int, int, int}, {int, int, int}, …, {n00, n01, n02}, {n10, n11, n12}, {int, int, int}, …, {int, int, int} }
가 된다. 반환형은? 당연히 int (*)[3].
5. a
a는 위에서 살펴본 바, 이중배열 전체를 다 가리키는 것이 분명하다.
이제는 좀 보이겠지. 형태도 int [2][3] 이다.
6. &a
그리기 귀찮다. 이쯤 되면 보일게다.
형태는 int (*)[2][3].
접어!
물론 나도 제대로 이해한 것은 아니지만, 어쨌든 무언가 다르게 보이기 시작했다. 그리고 머리가 좀 더 아파지기 시작했고.
어쨌든 본문 내용을 한 줄로 줄이면, “포인터 ㅅㅂ 조낸 어려워.”
… 쓰고 나서 보니까 내용이 영 지저분하다. 나중에 시간나면 정리해야지.