1. 포인터 변수를 가리키는 이중 포인터 변수 (더블 포인터 변수)
포인터의 포인터는 포인터 변수를 가리키는 또 다른 포인터 변수를 뜻하는 의미로 흔히 "이중 포인터" 또는 "더블 포인터"라 부르며 아래와 같이 포인터 변수의 선언에 사용되는 * 연산자를 두 개 이어서 선언한다.
int** dptr //int형 이중 포인터
아래의 코드를 한번 살펴보자
int num = 0;
int* ptr = #
우리는 앞서 포인터를 공부하면서 위의 예제를 수도 없이 봤었다.
num은 변수이기 때문에 주소값이 존재하고 이 주소값을 얻어서 int형 포인터 변수인 ptr에 저장을 하였다.
여기서 변수 num과 포인터 변수 ptr의 차이점과 공통점을 정리해보자면..
공통점 : 둘 다 변수이다. 따라서 값의 저장이 가능하다.
차이점 : 저장하는 값의 종류가 다르다.
방금 정리한것처럼 ptr도 메모리 공간에 할당이 되는 "변수"이다. 따라서 마찬가지로 이를 대상으로 & 연산이 가능하다.
따라서 싱글 포인터변수(* 연산자가 하나 있는 포인터 변수) ptr을 대상으로 다음과 같은 문장구성이 가능하다.
int** dptr = &ptr;
* 연산을 하나 더 추가함으로서 해당 타입의 주소값을 저장할 수 있는 포인터 변수가 선언되었다.
위의 문장은 아래와 같이 해석하면 된다.
"dptr이라는 이름의 포인터 변수인데 여기에 저장되는 것은 int형 포인터 변수의 주소값이다."
위의 그림의 상태에서는 dptr을 대상으로 아래와 같은 방식으로 포인터 변수 ptr과 변수 num에 접근이 가능하다.
*dptr = ...; //*dptr은 포인터 변수 ptr을 의미함
*(*dptr) = ...; //*(*dptr)은 변수 num을 의미함
(괄호는 생략 가능해서 **dptr로도 표현이 가능하다.)
아래의 예제 코드를 살펴보자.
#include<stdio.h>
int main(void) {
double num = 3.14;
double* ptr = #
double** dptr = &ptr;
double* ptr2;
printf("%9p %9p\n", ptr, *dptr); //*dptr은 ptr을 의미하니 동일한 출력결과가 나올 것이다.
printf("%9g %9g\n", num, **dptr); //**dptr은 num을 의미하니 동일한 출력결과가 나올 것이다.
ptr2 = *dptr; //*dptr은 현재 num1을 가리키고 있는 상태이다. 따라서 ptr2 도 num1을 가리킬 것이다.
*ptr2 = 10.99; //ptr2는 현재 num1을 가리키고 있고 num에 접근해서 새로운 값으로 초기화 하는중이다.
printf("%9g %9g\n", num, **dptr); //따라서 num과 **dptr은 같은 값이 나올것이고 *ptr2로 새롭게 초기화 한 값
//10.99가 출력될 것이다.
return 0;
}
*dptr
dptr이라는 포인터 변수가 가지고 있는 주소값을 참조해서 그 주소값에 해당하는 변수를 참조하라는 의미이다
(dptr이 가리키는 메모리 공간을 참조하라는 말이다.)
당분간은 이중 포인터 변수가 나왔을 때 아래와 같이 보는 연습을 한번 해보자.
*[*dptr] 에서 괄호 안의 연산을 먼저 진행하면 ptr이 될 것이고 이 ptr에 * 연산자를 붙이면 num이 된다.이렇게 잘라서 보는 연습을 해보시길 바란다.
위 코드의 주석내용을 바탕으로 이들 간의 참조관계를 그림으로 설명하면 아래와 같다
이러한 상황에서 변수 num에 접근하는 방법은 4가지가 있다.
1. num = 10.1 //변수 num에 접근하는 가장 기본적인 방법
2. *ptr = 10.2 //변수 num에 10.2가 저장된다.
3. *ptr2 = 10.3 //변수 num에 10.3이 저장된다.
4. **dptr = 10.4 //변수 num에 10.4가 저장된다.
위와 같은 방법으로 변수 num에 접근하면 변수 num의 값과 동일한 결과를 보일 것이다.
그렇다면 위의 코드에서 tptr이라는 삼중 포인터변수(*연산자가 3개)를 선언한 다음 포인터 변수 ptr에 접근하는 방법은 몇 가지가 있을까?
아래에 답을 적어둘 테니 한번 생각해 보고 펼쳐보길 바란다.
1. ptr = #
2. *dptr = #
3. **tptr = #
연산의 결과가 다 같다.

tptr이 dptr을 가리키면 이들의 참조관계는 위와 같이 될 것이다.
2. 포인터 변수 대상의 Call - by - reference
저저번 포스팅에서 우리는 Swap 함수로 변수를 전달해서 main 함수 내부에 있는 num1, num2 의 값을 바꾼 적이 있었다.
즉, 외부함수를 통해서 main함수에 저장되어 있는 값을 바꿨다.
(기억이 안난다면 저저번 포스팅 "포인터와 함수에 대한 이해" 글을 참고하길 바란다)
그렇다면 이번에는 두 싱글 포인터 변수에 저장된 값을 서로 바꿔서 저장하는 함수를 정의해 보자.
위의 그림과 같이 포인터 변수의 참조관계를 바꿔보자.
두 가지 예시를 제시할 건데 첫 번째 예시는 잘못된 경우이다. 한 번 잘 생각해 보고 어떤 부분을 고쳐야 하는지 잘 파악하길 바란다.
▼잘못된 예시
#include<stdio.h>
void Swap(int* p1, int* p2) {
int* temp = p1;
p1 = p2;
p2 = temp;
}
int main(void) {
int num1 = 10, num2 = 20;
int* ptr1, * ptr2;
ptr1 = &num1, ptr2 = &num2;
printf("*ptr1, *ptr2 : %d %d\n", *ptr1, *ptr2);
Swap(ptr1, ptr2);
printf("*ptr1, *ptr2 : %d %d\n", *ptr1, *ptr2);
return 0;
}
실행결과를 보면 함수 Swap는 두 포인터 변수 ptr1과 ptr2가 가리키는 대상을 변경시키지 못했음을 알 수 있다.
그렇다면 무엇이 문제일까?
해설은 아래 접힘글에 있다. 펼쳐보기 전에 먼저 잘 생각해 보고 펼쳐보길 바란다.
위의 Swap함수 내에서 분명 매개변수로 선언된 두 포인터 변수 p1과 p2가 가리키는 대상을 서로 바꿔주고 있음에도 불구하고 포인터 변수 ptr1과 ptr2가 가리키는 대상이 바뀌지 않은 이유는 무엇일까?
먼저 Swap함수의 호출로 인자가 전달되면 다음과 같은 상황이 발생한다.(함수로 인자만 전달된 상황)

이어서 함수의 몸체 부분이 실행되면 p1과 p2에 저장된 값이 서로 뒤바뀌어 다음의 상태가 된다.

위의 그림에서 보이듯이 p1과 p2에 저장된 값은 바뀌지만 이는 ptr1과 ptr2와는 별개의 변수이기 때문에 ptr1은 여전히 num1의 주소값을, ptr2도 마찬가지로 여전히 num2의 주소 값을 저장하는 상태가 된다.
그렇다면 어떻게 해야 함수 내에서 ptr1과 ptr2가 가리키는 대상을 바꿀 수 있겠는가? 이를 위해서는 함수 내에서 포인터 변수 ptr1과 ptr2에 직접 접근이 가능해야 한다. 그래서 이 두 변수에 저장된 값을 서로 바꿔줘야 한다.
이를 위해서는 int형 더블 포인터가 매개변수로 선언되어야 한다.
우선 Swap함수로 인자를 전달할 때 싱글 포인터 변수의 주소값을 전달할 거므로 &연산자를 사용해서 주소값을
전달해 준다.
Swap(&ptr1, &ptr2);
그다음으로 싱글 포인터 변수의 주소값을 전달했으므로 해당 주소값을 전달받는 p1과 p2는 더블형 포인터 변수를 선언한다.
Swap(int** p1, int** p2)
이렇게 하면 num1, num2에 저장된 값을 성공적으로 바꿀 수 있을 것이다.
위의 내용을 수정해서 코드로 작성해 보겠다.
#include<stdio.h>
void Swap(int** dp1, int** dp2) {
int* temp = *dp1;
*dp1 = *dp2;
*dp2 = temp;
}
int main(void) {
int num1 = 10, num2 = 20;
int* ptr1, * ptr2;
ptr1 = &num1, ptr2 = &num2;
printf("*ptr1, *ptr2 : %d %d\n", *ptr1, *ptr2);
Swap(&ptr1, &ptr2); //&연산자를 사용해 ptr1과 ptr2의 주소 값 전달!
printf("*ptr1, *ptr2 : %d %d\n", *ptr1, *ptr2);
return 0;
}
실행결과▼

주소값이 우선 Swap의 매개변수로 전달이 되면 다음과 같은 형태가 된다.

이러한 상황에서 함수 내의 문장 아래의 코드가 순서대로 실행된다.
int* temp = *dp1;
*dp1 = *dp2;
*dp2 = temp;
이는 dp1이 가리키는 변수와 dp2가 가리키는 변수에 저장된 값을 서로 교환하는 코드이다.
결국 ptr1에 저장된 값과 ptr2에 저장된 값이 바뀌어서 다음과 같은 형태가 된다.

이로 인해 num1과 num2에 저장된 값이 뒤바뀌어서 출력되는 결과를 얻을 것이다.
더블 포인터의 개념을 이해했다면 이번 예제에서 보인 것과 같이 두 int형 포인터 변수를 대상으로 하는 Swap함수도 정의할 수 있어야 한다.
3. 포인터 배열과 포인터 배열의 포인터 형
다음 유형의 포인터 배열에 대해서 설명한 적이 있다
int* arr1[20]; //길이가 20인 int형 포인터 배열 arr1
double* arr2[30]; //길이가 39인 double형 포인터 배열 arr2
우리는 다음 배열이름의 포인터 형을 잘 알고 있다.
int arr[30]; //배열이름 arr은 int형 포인터
1차원 배열이름의 포인터 형을 결정짓는 것은 어렵지 않다. 배열이름이 가리키는 요소의 자료형만 고려하면 된다.
그렇다면 위에 선언된 int형 포인터 배열의 이름 arr1과 double형 포인터 배열의 이름 arr2의 포인터 형은 어떻게 되는가?
이 또한 역시 1차원 배열이기 때문에 배열이름이 가리키는 첫 번째 요소의 자료형에 따라서 포인터 형이 결정된다.
즉, int* arr1 [20]; 에서 arr1의 포인터 형은 int**
double* arr2 [30]; 에서 arr2의 포인터 형은 double**
이를 증명하는 간단한 예제를 제시하고 이번 포스팅을 마치겠다.
#include<stdio.h>
int main(void) {
int num1 = 10, num2 = 20, num3 = 30;
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]));
return 0;
}
실행결과 ▼
10 20 30
10 20 30
오늘 배운 내용은 크게 어려운 건 없다. 그저 이전에 올렸던 포스팅의 전부 연장선일 뿐이다. 만약 이전 내용이 기억이 안 난다면 앞의 내용은 정말로 중요하니 꼭 다시 보면서 참고하길 바란다. 다음포스팅은 짧게 다중 포인터 변수와 포인터의 필요성에 대해서 들고 오겠다.
'C언어' 카테고리의 다른 글
C언어 - 다차원 배열 (0) | 2023.07.26 |
---|---|
C언어 - 포인터와 함수에 대한 이해 (0) | 2023.07.21 |
C언어 - 상수 형태의 문자열을 가리키는 포인터 (0) | 2023.07.20 |
C언어 - 포인터와 배열 (0) | 2023.07.12 |
C언어 - 포인터의 이해(3) (0) | 2023.07.09 |