본문 바로가기

Miscellaneous

2차원 배열과 더블포인터

2차원 배열과 더블포인터

int a[4];

in *p = a;

일때 a[0] == p[0], a[1] == p[1] 이라고 하는 것을 보고



in a[3][4]; 일 때

int **p =a;

이고 a[0][0] == p[0][0], a[1][2]== p[1][2]로 알고 계시는 분들이 있으십니다.

혹은 왜 이게 안되는지 궁급해 하십니다.



그래서 이런 글을 올립니다. 참고하세요



배열의 등가포인터 의미와 생성 그리고 활용에 대하여 다룹니다.



자 이런 배열이 있습니다

int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};



이때 배열의 이름 a는 배열의 정의상 배열의 첫요소의 주소입니다.



그러면 이는 배열의 첫요소 즉, a[0][0]의 주소 즉, &a[0][0] 일까요?

아니면 &a[0] 일까요? 값만 놓고 보면 둘 다 같은 값이 나옵니다. 

그러면 의미상 정확하게 맞는 표현은 무엇일까요?



int a[3][4] 배열은 요소 12개 짜리 1차원 배열이 아닙니다. 

메모리에는 12개의 숫자가 연속해서 존재하기는 하나 이를 

편의를 위하여 2차원 배열처럼 생각하는 거지요



고로 int a[3][4]는 요소수 3개 짜리 배열입니다. 즉, 3칸짜리 배열이지요



그런데 이 배열은 각 칸칸마다 뭐가 들어 있냐면 정수가 있는것이 아니라

int [4] 즉, int 4개짜리 배열이 들어있는 겁니다.



따라서 이 배열의 요소수는 3개이고 이 배열의 첫요소는 a[0] 가 됩니다.

고로 배열명 a == &a[0]라고 해야합니다.



그러면 어차피 값은 &a[0][0]나 &a[0]이나 같은데 왜 꼭 &a[0]가 맞다고 할까요?

자 다음을 보시지요



int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12}; 

int *p = a;

int (*q)[4] = a;

int **r = a;



세개의 포인터에 배열명 a를 담았습니다. 어느것이 옳을까요?

여기서 옳다는 것은 단지 대입이 되느냐 아니냐가 아니라

컴파일리 에러, 경고 없는 경우입니다.



사실 대부분 컴파일러는 int *p = a; 는 경고를 내거나 엄격한 컴파일러는 

에러를 냅니다. 고로 에러, 경고를 막으려면 casting을 써서

int *p = (int *)a; 라고 해야 합니다.



그러나 두번째 int (*q)[4] = a; 는 에러, 경고 나지 않습니다.

즉, 두개는 완벽히 일치하는 데이터 타입이라는 뜻이지요...



세번째 2중 포인터는 int **r = p;는 역시 첫번째와 마찬가지로

타입이 다른 주소가 대입되므로 에러 혹은 경고가 납니다.

정상이려면 int **r = (int **)a; 하셔야 합니다.



자 왜 두번째 포인터만 정상일까요?

위에서 얘기 했듯이 int a[3][4] 일때 배열명 a는 &a[0]의 주소인데

a[0]은 a배열의 첫요소 즉, 3칸 짜리 배열에는 int [4] 

즉, int 4개 짜리 배열이 들어 있으므로

a[0]는 그 속의 int [4] 배열의 이름이 됩니다.



고로 &a[0]는 바꿔 말하면 int [4] 배열의 주소이므로

타입은 int (*)[4] 라고 쓸수 있습니다.

그런데 a == &a[0] 이니 결국 배열 이름은 int (*)[4] 와 같은 타입입니다.



그러므로 이러한 주소를 받는 포인터는 int (*q)[4] 로 선언하게되고

int (*q)[4] = a; 가 성립하는 것입니다.



그럼 다시 다음에서 봅시다.



int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12}; 

int *p = (int *)a;

int (*q)[4] = a;

int **r = (int **)a;



이렇게 강제로라도 a를 대입했다면 이 포인터들로

배열에 있는 a[1][2] 즉, 7을 억세스 하려면 어떻게 할까요?



int *p는 현재 값이 정수 즉, 1의 주소를 갖으므로

그리고 정수(int)의 포인터 이므로

printf("%d\n", *(p+6)); 하거나 혹은 [ ] 연산자를 이용하여

printf("%d\n", p[6]); 으로 해야 합니다.



그러나 두번째는 int (*q)[4]의 현재값이 {1,2;3,4} 배열의 주소이므로

a[1][2]와 동일한 표현은 q[1][2] 가 됩니다.

이것이 왜 이렇게 되는지는 다차원 배열을 공부하시면 됩니다.



즉, 

int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12}; 

에서 a[1][2] 와 같은 값을 억세스 하기 위하여 



int *p = (int *)a; 로 하면 p[6] 혹은 *(p+6)

int (*q)[4] = a; 로 하면 q[1][2] 로 됩니다.

두 방법중 어떤것이 이해가 쉽습니까? 



이와같이 배열의 이름을 대입받을 때 에러, 경고, 캐스팅 없이 대입 가능한 

포인터를 배열의 등가포인터라고 하고 등가포인터가 배열의 이름을 받으면

그때부터는 포인터가 배열과 표현식이 동일하게 사용가능합니다.



물론 그렇다고 이것이 완전히 동일해 지는것은 아닙니다.

배열은 배열이고 포인터는 포인터입니다. 단지 배열명을 갖는 포인터이지요



단, 억세스는 간접연산(포인터로 인하여)이므로 억세스 속도는 배열보다 느립니다.



그럼 1차원 함수에 넘기는 경우를 보지요...



void main(void)

{

int a[4] = {1,2,3,4};

func(a);

}

라고 할때 func 함수는 넘겨받은 배열의 이름을 이용하여 

배열내의 a[2]인 3을 인쇄한다고 합시다.



그럼 어떻게 하나요?

void func(int *p)

{

printf("%d\n", p[2]);



라고 할것입니다.



이 상황을 잘보면 



(1) main에서 func를 호출하면서 넘긴것은 배열의 이름 a입니다.

1차원 배열이므로 이 이름은 첫요소의 주소 즉, &a[0] 이고 a[0]는 정수 1이므로 

int * 타입입니다.



(2) 고로 func에서는 이런 배열의 이름과 등가인 int *p로 받는것입니다.

즉, int *p = a; 가 가능하므로 int *로 한것이지요

고로 p는 표현식에서 a 배열과 동일하므로 a[1] ==p[1]이 됩니다.



즉, 배열은 전체를 넘기지 못하고 그 첫요소의 주소(배열명)를 넘기므로



이를 등가포인터로 받고 활용은 마치 배열인듯이 하는 겁니다.



이것이 등가포인터의 활용이지요...



그럼 2차원 배열을 넘기면 어떻게 되나요?



int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12}; 

func(a);

에서 a[1][2] 와 값을 인쇄하는 함수는 어떻게 만드나요?



(1) int func(int *p)

(2) int func(int (*p)[4]);

(3) int func(**p); 어느것이 옳을까요?



만약 1처럼 하면?



func(a); 하면 경고가 발생하므로 

억지로 캐스팅 하여 func((int *)a); 로 해야 합니다.

그리고 함수는 이렇게 만들지요



int func(int *p)

{

printf("%d\n", p[6]);

또는 

printf("%d\n", *(p+6));

}



그런데 2번처럼 하면 아까 얘기했듯이 int (*p)[4]는 배열명 a 와 등가이므로

int (*p)[4] = a; 가 가능합니다.



고로 활용이 마치 a 배열처럼 되므로 다음과 같이 됩니다.



int func(int (*p)[4])

{

printf("%d\n", p[1][2]);

}



3번 처럼 단순히 2중포인터라고 생각한다면? 

이거 골치아퍼집니다. 아마 세가지 방법에서 제일 골치아프겠네요

한번 직접 해보세요...



이때 p[1][2]는 절대 배열이 아닙니다. p는 엄연한 포인터입니다.

그럼 어떤것이 쉽고 정상적인 방법일까요? 



배열을 넘기고(배열명을 넘기는 거지만) 이를 등가 포인터로 받고

그리고 이를 활용할때는 마치 원래의 배열처럼 하는것 이게 등가포인터를 활용한 겁니다.



다음은 2차원 배열을 넘기고 배열요소의 전체 합을 구하는 함수를 설계합니다.

역시 등가 포인터를 활용합니다.



결국



void main(void)

{

int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12}; 

printf(""%d\n", sum(a));

}



int sum(int (*p)[4])

{

int a = 0, i, j;

for(i=0; i<3; i++)

{

for(j=0; j<3; j++)

{

a += p[i][j];



}

return a;

}



다시한번 말씀드리지만

위와 같이 배열을 다른 함수로 넘기거나 다른함수에서 리턴하거나

할때 배열명을 넘기고 받기는 포인터(등가포인터)로 받고

활용은 넘겨준 배열과 동일하게 하는것이 좋습니다.

이런 포인터를 등가포인터라고 합니다.



결국 등가포인터를 이용하면 배열, 함수를 함수로 넘기거나 함수 리턴으로 줄 때

매우 쉬워 집니다.



참고로 다음은 등가적인 포인터의 예를 보입니다.



(1) int a[4] ==> int *p

(2) int *a[4] ==> int **p

(3) int a[3][4] ==> int (*p)[4]

(4) int a [2][3][4] ==> int (*p)[3][4]

(5) int *(*a[3])(int) ==> int *(**p)(int)

...

위에서 등가포인터 만드는 공식(?) 내지는 꼼수를 찾아보세요.


반응형