포인터도 결국 변수였다. 그리고 포인터는 메모리에 할당된 변수의 주소를 저장한다. 무슨 말인가? 포인터의 주소를 저장하는 포인터도 있을 것이다. 그리고 그 변수를 또 가르키는 포인터도 가능하다...!
포인터를 가르키는 포인터 변수, 이중 포인터 변수
포인터 변수도 변수인 이상, 주소값을 저장하고 있는 하나의 변수이다. 따라서 메모리 어딘가에 할당되는 것이다. 그러면 포인터를 가르키는 포인터는 기존 문법으로 사용하면 되는 것인가? 아니다. 포인터변수의 주소값을 저장하는 것은 그 변수의 자료형 이중 포인터를 사용해야 한다.
#include <stdio.h>
int main () {
int num = 1;
int *ptr1 = #
// int *ptr2 = &ptr1; // warning: initialization from incompatible pointer type
int **ptr2 = &ptr1; // int형 더블 포인터 변수 선언
printf("%d %d %d", ptr2, *ptr2, **ptr2);
return 0;
}
이를 통해 ptr2는 ptr1을 가르키고, ptr1은 num을 가르키는 관계가 되는 것이다. 이때 *ptr2는 ptr1이므로 num의 주소값을, **ptr2는 num에 저장된 값을 의미한다.
더블포인터를 이용한 예제를 하나 살펴보자.
#include <stdio.h>
int main () {
double num = 3.14;
double *ptr = #
double **dptr = &ptr;
double *ptr2 = NULL;
printf("%9p %9p \n", ptr, *dptr); // ptr == *dptr
printf("%9g %9g \n", num, **dptr); // num == **dptr
ptr2 = *dptr; // ptr2 = ptr;
*ptr2 = 1.414; // num = 1.414
printf("%9g %9g \n",num, **dptr);
return 0;
}
실행결과
0061FF10 0061FF10
3.14 3.14
1.414 1.414
dptr은 ptr의 주소이고, ptr은 num을 가르키는 것을 이용하여 dptr을 통해 num값을 바꾸는 것이다.
포인터 배열과 포인터 배열의 포인터 형
저번에 잠깐 언급한 포인터 배열에 관한 이야기가 나온다.
포인터 배열은 포인터 변수를 저장하는 배열이고, 배열의 이름은 상수형 포인터이다. 그렇다면, 포인터 배열의 이름은 무엇일까?
일단 다차원 배열의 이름에 관해서는 배우지 않았으므로 일단은 1차원 포인터 배열에 관해서만 이야기 해보자.
배열의 이름은, 첫번째 요소를 가르키는 포인터였다. 포인터 배열의 요소는 각각 변수를 가르키는 포인터이고, 배열의 이름은 이 포인터를 가르키고 있으므로 포인터 배열의 이름은 이중포인터인 셈이다. 그렇다면 이 이론이 맞는지 한번 체크해보자.
#include <stdio.h>
int main () {
int num1 = 1, num2 = 2, num3 = 3;
int *ptr1 = &num1;
int *ptr2 = &num2;
int *ptr3 = &num3;
int * ptrArr[] = {ptr1, ptr2, ptr3};
int **dptr = ptrArr;
printf("%d %d %d \n", *ptrArr[0], *ptrArr[1], *ptrArr[2]);
printf("%d %d %d \n", *dptr[0], *dptr[1], *dptr[2]);
}
실행결과
1 2 3
1 2 3
포인터 배열 ptrArr의 주소값을 이중포인터 dptr에 넘겨준 후에, 같은 방법으로 요소에 접근하였다. 결과가 똑같이 나오는 걸 보아 이론이 맞았음을 알 수 있다.
다중 포인터 변수
이중 포인터에 관해 알아보았다. 그러면 당연히 3중, 4중.....n중 포인터 변수가 존재할 것이다. 그리고 이는 실제로 존재하면 개념은 똑같다. 간단히만 살펴보자.
#include <stdio.h>
int main () {
int num = 1;
int *ptr = #
int **dptr = &ptr;
int ***tptr = &dptr;
printf("%d %d %d %d", num, *ptr, **dptr, ***tptr);
}
실행 결과
1 1 1 1
하지만 책의 필자는 삼중 이상의 포인터가 사용되는 예는 그리 많지 않다고 한다. 따라서 삼중 포인터가 사용된다면, 포인터를 오·남용하고 있는 것은 아닌지, 또는 잘못된 방식으로 접근하고 있는 것은 아닌지 확인해볼 필요성이 있다고 한다.
포인터의 필요성
포인터가 필요한 이유가 뭘까?
자료구조를 공부하게 되면 포인터의 필요성을 보다 확실히 알 수 있게 됩니다.
라고 책의 필자는 말한다. 하지만 아직 내가 여기까지 배운 것도 아니고, 지금 당장의 필요한 이유는 다음과 같이 설명될 것이다.
함수 내에서 함수 외부의 변수에 접근하는 방법을 제시해줍니다.
문제를 하나만 풀고 다음 챕터로 넘어가도록 하자.
문제 17-1
다음과 같이 두 개의 int형 포인터 변수와 길이가 5인 int형 배열을 선언한다.
int * maxPtr;
int * minPtr;
int arr[5];
그리고 MaxAndMin이란 이름의 함수를 정의하고 호출하면서 위의 배열과 두 포인터 변수를 전달하여 maxPtr에는 요소중에서 가장 큰 값의 주소값, minPtr에는 가장 작은 값의 주소값을 저장하라. 방법은 어떻게 해도 상관없다
시키는 대로 풀자.
#include <stdio.h>
void MaxAndMin (int arr[], int len, int * max, int * min);
int main () {
int max, min; // 최댓값, 최솟값의 위치
int * maxPtr = &max;
int * minPtr = &min;
int arr[5];
for (int i = 0; i < 5; i++) {
scanf("%d", &arr[i]);
}
MaxAndMin(arr, 5, maxPtr, minPtr);
maxPtr = &arr[*maxPtr]; // 가르키는 값을 바꾼다.
minPtr = &arr[*minPtr];
printf("max: %d (%p) \n", *maxPtr, maxPtr);
printf("min: %d (%p) \n", *minPtr, minPtr);
return 0;
}
void MaxAndMin (int arr[], int len, int * max, int * min) {
int M = (-1 * __INT32_MAX__) - 1; // int형 최솟값
int m = __INT32_MAX__; // int형 최댓값
int a,b; // 최댓값의 위치, 최솟값의 위치
for (int i = 0; i < len; i++) {
// a = arr[i] > M ? arr[i] : M;
// m = arr[i] < m ? arr[i] : m;
if (arr[i] > M) {
a = i;
M = arr[i];
}
if (arr[i] < m) {
b = i;
m = arr[i];
}
}
*max = a ; *min = b;
}
쉬울줄 알았는데 생각보다 오래 걸렸다.
일단 maxPtr과 minPtr에 최댓값, 최솟값의 위치를 저장할 변수의 주소를 저장하여, MaxAndMin함수에서 maxPtr과 minPtr이 가르키는 값에 위치를 저장하고 이후에 maxPtr과 minPtr이 가르키는 값을 배열의 최댓값과 최솟값으로 바꾸어 주는 방법으로 풀이했다.
'Language > C' 카테고리의 다른 글
포인터(8) - 下 (0) | 2021.09.05 |
---|---|
포인터(8) - 上 (0) | 2021.09.04 |
포인터(6.5) (0) | 2021.09.01 |
포인터(6) (0) | 2021.08.31 |
포인터(5) (0) | 2021.08.29 |