거북이처럼 천천히

이중 포인터 본문

C

이중 포인터

유로 청년 2024. 6. 19. 11:55

1. 이중 포인터란 무엇인가?

  • 이중 포인터는 포인터의 주소 값을 변수의 값으로 갖고 있는 포인터 변수이다.
  • 일반 포인터가 가르키는 주소 값에는 일반 변수가 있지만, 이중 포인터가 가르키는 주소 값에는 또 다른 포인터가 있다.
  • 따라서 이중 포인터를 통해 일반 변수에 접근하기 위해서는 *(에스터리크)를 2번 사용하여 접근해야한다.
// 일반 변수 data 
int data = 10;

// 일반 포인터 변수, p_data
int *p_data = &data;

// 이중 포인터 변수, p_p_data
int **p_p_data = &p_data;
// 일반 포인터를 통해 일반 변수 접근
printf("%d", *p_data); 


// 이중 포인터를 통해 일반 변수 접근
printf("%d", **p_p_data);

 

 

 

2. 이중 포인터의 자료형

  • 단일 포인터의 자료형은 포인터가 가르키는 일반 변수의 자료형에 맞게 포인터 변수 자료형을 선언하는 것이 일반적이다.
  • 아래와 같이 일반 변수의 자료형이 double형이라면, 이를 가르키는 포인터 변수의 자료형도 (double *)형이여야 한다.
// 일반 변수 data의 자료형은 double형이다.
double data = 10.0;


// 일반 변수 data를 가르키는 포인터 변수 p_data의 자료형도 이에 맞게 (double *)형으로 선언
double *p_data = &data;

 

  • 이중 포인터도 이에 따라 동일하게 적용된다. 즉, 이중 포인터가 가르키는 단일 포인터의 자료형에 맞게 자료형이 정하는 것이 일반적이다.
  • 아래와 같이 단일 포인터의 자료형이  (double *)형이기 때문에 이중 포인터의 자료형도 (double *)* == double ** 형을 가져야 한다.
// 일반 변수의 자료형은 double 형이다.
double data = 3.07;


// 단일 포인터의 자료형은 (double *)형이다.
double *p_data = &data;


// 이중 포인터의 자료형은 (double *)* == (double **) 형이다.
double **p_p_data = &p_data;

 

 

 

 

 

3. 이중 포인터의 활용

  • 이중 포인터의 형태를 보아서 알겠지만, 단일 포인터와 비교 했을 때, 간접 참조 연산자를 1개를 더 사용하기 때문에 덜 친숙하고, 관계가 어지럽기 때문에 어렵다고 생각하였을 것이다.
  • 따라서 이중 포인터는 도대체 어디에 사용되는지 궁금할 것이다.
  • 이번에는 이중 포인터의 활용에 대해서 살펴보자.

 

 

3 - 1. 이중 포인터의 활용 1 : Callee에서 포인터의 값을 수정해야 하는 경우

  • Calle 측에서 이중 포인터를 사용하지 않고, 포인터의 값을 받아 수정해야 하는 경우를 생각해보자.
  • Calle 측에서는 포인터 값을 받기 위해 Parameter로 단일 포인터를 받게 된다면, 중요한 질문을 갖게된다.
    Q) Parameter 값으로 받은 포인터 값은 Caller에 있는 포인터와 동일한가?
         즉, Call by value인가? Call by reference인가?
  • Callee 측에서 받은 포인터 값은 다음과 같은 과정을 거친 값이다.
    1 단계) 메모리에 Parameter를 위한 메모리 공간이 생성
    2 단계) 해당 메모리 공간에 포인트 값을 저장한다.
    3 단계) 즉, 해당 포인트 변수를 통해 포인터가 가르키는 일반 변수에 접근 및 수정이 가능하지만, 
                Callee 측에서의 포인트 변수는 Caller 측의 포인트 변수의 복사본이기 때문에 Callee측에서 
                포인트 값(메모리 주소 값)을 수정해도 Caller 측의 포인트 변수에는 영향이 없다.
  • 따라서 Callee 측에서 포인터가 가르키는 주소 값을 변경해야 할 경우에 주로 사용한다.

 

 

 

  • 아래 예시를 통해 확인하자.
  • 상황) 두 개의 문자열 상수를 가르키는 포인트 변수가 있으며, swaping 함수를 통해 두 개의 포인터
             변수가 서로 가르키는 주소 값을 바꿔줌으로서 문자열 상수를 swap하고 싶다. 
#include <stdio.h>

void swaping(char* p_str1, char* p_str2);

int main(void) {
	char* p_str1 = "Hello";
	char* p_str2 = "World";

	printf("str1 = %s, str2 = %s\n", p_str1, p_str2);

	swaping(p_str1, p_str2);

	printf("str1 = %s, str2 = %s\n", p_str1, p_str2);
}

void swaping(char* p_str1, char* p_str2) {
	char* temp = p_str1;
	p_str1 = p_str2;
	p_str2 = temp;
}

단일 포인터를 통해 Swap 하려고 시도한 결과, 실패

  • 위 예시는 이중 포인터 없이 단일 포인터를 가지고 swaping을 한 경우이다.
  • 위 콘솔 창에서 볼 수 있듯이 Swap이 이루어 지지 않은 것을 확인할 수 있다.
  • Q) 왜 Swap이 이루어 지지 않을 걸까?
    이에 대한 대답을 하기 전에 "과연 Caller는 Callee에게 준 포인터 변수는 원본인가? 복사본인가?"에 대한 질문을 할 필요가 있다. 이는 마치 일반 변수과 동일하다.
  • 포인터 변수를 활용하여 간접 참조 연산자를 통해 접근할 때에는 포인터 변수의 의미는 "일반 변수의 메모리 공간의 주소"를 Caller가 Calle에게 주었기 때문에 Callee 측에서 값을 수정해도 Caller 측에 영향이 미쳤다.
  • 똑같다. "Caller측에서 Calle에게 준 포인터 변수는 값인가? 주소인가?" 라는 질문에는 "포인터 값을 줬다."라고 답할 수 있고, 이는 결국 Caller가 Callee에게 준 것은 포인터 변수의 값의 복사본임을 의미한다.
  • 하지만, Callee 측의 수정이 Caller 측에 미치기 위해서는 복사본이 아닌 원본이 있는 메모리 공간의 주소가 필요하다. 따라서 이때 사용할 수 있는 것이 바로 이중 포인터 인 것이다. 
  • 즉, Caller측에서 포인터 변수의 주소 값을 전달함으로서 "포인터 변수의 메모리 공간의 주소을 전달한다."는 의미이기 때문에 Callee 측에서는 이를 이중 포인터를 받아서 이중포인터를 통해 포인터 변수와 일반 변수에 접근 및 수정을 할 수 있으며, 또한 수정한 결과값이 Caller측에도 영향이 미치게 되는 것이다.

 

 

 

  • 이러한 내용을 토대로 코드를 다시 수정하자.
#include <stdio.h>

void swaping(char** p_str1, char** p_str2);

int main(void) {
	char* p_str1 = "Hello";
	char* p_str2 = "World";

	printf("str1 = %s, str2 = %s\n", p_str1, p_str2);

	swaping(&p_str1, &p_str2);

	printf("str1 = %s, str2 = %s\n", p_str1, p_str2);
}

void swaping(char** p_str1, char** p_str2) {
	char* temp = *p_str1;
	*p_str1 = *p_str2;
	*p_str2 = temp;
}

이중 포인터를 통해 Swap 하려고 시도한 결과, 성공

  • 다시 한번 설명하자면, Caller에서 포인터 변수의 주소값을 argument 값으로 줌으로서 Calle측에서는 포인터 변수가 있는 메모리 주소 값을 받았고, 이를 저장하기 위해 이중 포인터를 사용했다.
  • Callee 측에서는 이중 포인터와 간접 참조 연산자를 통해 단일 포인터와 일반 변수의 참조 및 수정을 할 수 있다.
  • 그리고, Calle 측에서 받은 것은 Call by value가 아닌 Call by reference와 유사하게 원본이 담겨 있는 메모리 공간의 주소 값을 받았기 때문에 Callee 측에서 수정한 결과가 Caller에게도 영향을 미치게 된다.

 

 

 

 

Callee 측에서의 수정된 결과를 Caller 측에도 영향 미치도록 하고 싶으면
변수의 메모리 주소 값을 argument 값으로 줘라.

 

 

 

 

 

3 - 2. 이중 포인터의 활용 2 : 포인터 배열을 매개변수로 받는 함수

  • 이중 포인터는 "포인터 값을 Callee에서 수정하기 위한 목적" 이외에도 "포인터 배열 (2차원 배열 etc..)을 Callee에게 전달하기 위한 목적" 으로도 사용된다.
  • 예시를 통해 알아보자.
  • 문자열 배열 (char *형 배열)을 Calle에게 전달하는 경우
#include <stdio.h>

// Function prototype.
void print_the_list(char** p_list, unsigned char size);

// Main method.
int main(void) {
	// 문자열을 저장하는 배열을 만들고 싶다.
	// 문자열을 원소로 저장하기 위해서는 각 원소들은 char *형이여야 한다.
	char* p_list[4] = {"eagle", "tiger", "lion", "squirrel"};


	// Size of p_list.
	unsigned char size = sizeof(p_list) / sizeof(p_list[0]);

	// Print the list.
	print_the_list(p_list, size);
}

// Print the list
void print_the_list(char** p_list, unsigned char size) {
	for (unsigned char i = 0; i < size; i++) 
		printf("%s\n", *(p_list + i));
}

 

코드 해석)

  • Q) p_list 는 무엇인가?
  • p_list는 (char *)형 배열, 포인트 배열이다. 각각의 원소들에게 문자열을 저장하기 위해서는 char 형 포인터가 필요하며, 이를 배열 형태로 만들어서 관리하기 위해서 (char *)형 배열, 포인트 배열로 만들어 줬다.
  • Q) 왜 print_the_list 함수에서 포인트 배열인 p_list를 받기 위해서 이중 포인터를 사용했는가?
  • 배열명은 배열의 첫 번째 원소의 메모리 주소를 가리키고 있다. 
  • 만약 print_the_list 함수에서 (char *)형 배열인 p_list 을 받기 위해서 parameter의 자료형을 char *으로 선언한다면 print_the_list 함수가 받을 수 있는 값은 "eagle" 문자열 상수가 저장된 메모리 공간의 주소값 뿐이다.
  • 하지만, 우리가 원하는 건 (char *)형 배열인 p_list 을 받아서 인덱스를 통해 접근 및 수정하는 것이다.
  • 이를 위해서는 Callee 측에서는 배열의 주소 값을 받기 위해서 (char *) * == char ** 형으로 선언해줄 필요가 있다.
  • 이해하기 쉽게 일반 배열을 callee에게 전달할 상황을 비교하여 생각해보자.

 

 

 

 

 

 Calle측에 배열을 argument로 전달하고 싶다면
Calle측에서는 배열의 주소 값을 받기 위해서 
Parameter의 자료형을 배열의 포인터를 받을 준비를 해야한다.

 

 

 

 

 

 

4. 정리

  • 이중 포인터의 사용은 다음과 상황에서 사용한다.
    - Callee 측에서 포인터 변수를 수정하는 경우
    - 포인터 배열, 2차원 배열을 Callee 측에 전달하는 경우
  • Q) 왜 Callee에서 포인트 변수를 수정이 팔요할 때, 이중 포인터를 사용하는가? 
    A) Caller 측에서 포인트 변수의 주소를 전달해야 Callee 측에서 원본에 접근하여 수정이 가능하다.
              
        즉, Callee 측의 수정이 Caller에 영향을 미치도록 만들고 싶으면 변수의 주소를 전달해라.
  • Q) 왜 Callee에게 포인터 배열, 2차원 배열을 전달할 때, 이중 포인터를 사용하는가?
    A) Caller 측에서 Call by reference 처럼 받기 위해서는 Caller 에서 배열의 주소 값을 받는데,
        배열 명은 첫 번째 원소의 주소 값이기 때문에 이중 포인터로 받아야 한다.
              
        즉, Callee 측에 배열을 전달한다면 Callee 측에서는 배열의 주소를 받을 준비를 해야 한다.

'C' 카테고리의 다른 글

함수 포인터  (0) 2024.06.19
배열 포인터  (1) 2024.06.19
Pointer arry (포인터 배열)  (0) 2024.06.18
Register variable  (0) 2024.06.18
C 언어의 컴파일 과정  (0) 2024.06.06