4. 포인터의 심화

header


1. 더블 포인터 (이중 포인터)

더블 포인터는 포인터 변수를 가리키는 포인터입니다. 포인터도 변수이므로 주소를 가지며, 이 주소를 저장할 수 있습니다.

더블 포인터의 개념

일반 포인터가 변수의 주소를 저장한다면, 더블 포인터는 포인터 변수의 주소를 저장합니다.

int num = 10;
int *ptr = #       // 싱글 포인터: num의 주소 저장
int **dptr = &ptr;     // 더블 포인터: ptr의 주소 저장

메모리 구조:

num의 값   ptr의 값      dptr의 값
  10   ←   num의 주소  ←  ptr의 주소
 num        ptr          dptr
더블 포인터 선언
int **dptr;
• 애스터리스크(*) 2개 사용
• "포인터의 포인터"를 의미

더블 포인터의 사용

#include <stdio.h>

int main(void) {
    int num = 100;
    int *ptr = &num;      // num의 주소
    int **dptr = &ptr;    // ptr의 주소

    printf("=== 값 출력 ===\n");
    printf("num: %d\n", num);
    printf("*ptr: %d\n", *ptr);
    printf("**dptr: %d\n", **dptr);

    printf("\n=== 주소 출력 ===\n");
    printf("&num: %p\n", &num);
    printf("ptr: %p\n", ptr);
    printf("*dptr: %p\n", *dptr);

    printf("\n&ptr: %p\n", &ptr);
    printf("dptr: %p\n", dptr);

    return 0;
}
실행 결과 보기 (예시)
=== 값 출력 ===
num: 100
*ptr: 100
**dptr: 100

=== 주소 출력 ===
&num: 00B3FA14
ptr: 00B3FA14
*dptr: 00B3FA14

&ptr: 00B3FA08
dptr: 00B3FA08
  • **dptr은 최종적으로 num의 값(100)에 접근
  • *dptr은 ptr이 저장한 주소(num의 주소)
  • dptr은 ptr의 주소

더블 포인터로 값 변경하기

#include <stdio.h>

int main(void) {
    int num = 50;
    int *ptr = &num;
    int **dptr = &ptr;

    printf("변경 전: %d\n", num);

    **dptr = 999;  // num = 999와 동일

    printf("변경 후: %d\n", num);
    printf("*ptr: %d\n", *ptr);
    printf("**dptr: %d\n", **dptr);

    return 0;
}
실행 결과 보기
변경 전: 50
변경 후: 999
*ptr: 999
**dptr: 999
💡 더블 포인터 접근 방식
dptr: ptr의 주소
*dptr: ptr이 저장한 값 (num의 주소)
**dptr: num의 값

2. 더블 포인터와 함수

더블 포인터는 함수에서 포인터 자체를 변경할 때 유용합니다.

포인터 교환 함수

싱글 포인터만 사용하면 포인터가 가리키는 값만 교환할 수 있지만, 더블 포인터를 사용하면 포인터 자체를 교환할 수 있습니다.

#include <stdio.h>

void swapPointer(int **dptr1, int **dptr2) {
    int *temp = *dptr1;
    *dptr1 = *dptr2;
    *dptr2 = temp;
}

int main(void) {
    int num1 = 10, num2 = 20;
    int *ptr1 = &num1;
    int *ptr2 = &num2;

    printf("=== 교환 전 ===\n");
    printf("*ptr1: %d, *ptr2: %d\n", *ptr1, *ptr2);

    swapPointer(&ptr1, &ptr2);  // 포인터 자체를 교환

    printf("\n=== 교환 후 ===\n");
    printf("*ptr1: %d, *ptr2: %d\n", *ptr1, *ptr2);
    printf("num1: %d, num2: %d\n", num1, num2);

    return 0;
}
실행 결과 보기
=== 교환 전 ===
*ptr1: 10, *ptr2: 20

=== 교환 후 ===
*ptr1: 20, *ptr2: 10
num1: 10, num2: 20

num1과 num2의 값은 그대로지만, ptr1과 ptr2가 가리키는 대상이 바뀌었습니다.

메모리 상태 변화:

[교환 전]
ptr1 → num1(10)
ptr2 → num2(20)

[교환 후]
ptr1 → num2(20)
ptr2 → num1(10)

더블 포인터로 배열 접근

#include <stdio.h>

void printArray(int **dptr, int size) {
    int i;
    for (i = 0; i < size; i++) {
        printf("%d ", *(*dptr + i));
    }
    printf("\n");
}

int main(void) {
    int arr[5] = {10, 20, 30, 40, 50};
    int *ptr = arr;
    int **dptr = &ptr;

    printf("배열 출력: ");
    printArray(dptr, 5);

    return 0;
}
실행 결과 보기
배열 출력: 10 20 30 40 50

3. 다중 포인터 (삼중 포인터)

포인터는 이론적으로 무한대로 중첩할 수 있습니다. 실무에서는 드물지만, 개념 이해를 위해 삼중 포인터를 살펴보겠습니다.

삼중 포인터의 선언

int num = 100;
int *ptr = &num;        // 싱글 포인터
int **dptr = &ptr;      // 더블 포인터
int ***tptr = &dptr;    // 삼중 포인터

관계도:

num의 값  ptr의 값     dptr의 값    tptr의 값
  100  ← num의 주소 ← ptr의 주소 ← dptr의 주소
  num      ptr         dptr         tptr

삼중 포인터 예제

#include <stdio.h>

int main(void) {
    int num = 777;
    int *ptr = &num;
    int **dptr = &ptr;
    int ***tptr = &dptr;

    printf("=== 값 출력 ===\n");
    printf("num: %d\n", num);
    printf("*ptr: %d\n", *ptr);
    printf("**dptr: %d\n", **dptr);
    printf("***tptr: %d\n", ***tptr);

    // 삼중 포인터로 값 변경
    ***tptr = 999;

    printf("\n=== 변경 후 ===\n");
    printf("num: %d\n", num);

    return 0;
}
실행 결과 보기
=== 값 출력 ===
num: 777
*ptr: 777
**dptr: 777
***tptr: 777

=== 변경 후 ===
num: 999
⚠️ 다중 포인터 주의사항
• 실무에서는 삼중 포인터 이상은 거의 사용하지 않음
• 코드 가독성이 크게 떨어짐
• 더블 포인터까지만 이해해도 충분

4. 함수 포인터

함수도 메모리에 저장되므로 주소를 가집니다. 함수 포인터는 함수의 주소를 저장하는 포인터입니다.

함수 포인터의 필요성

함수를 변수처럼 다루어 동적으로 호출하거나, 함수를 다른 함수에 전달할 수 있습니다.

함수 포인터 선언

// 함수 정의
int add(int a, int b) {
    return a + b;
}

// 함수 포인터 선언
int (*funcPtr)(int, int);

// 함수 주소 저장
funcPtr = add;
함수 포인터 선언 형식
반환형 (*포인터이름)(매개변수타입1, 매개변수타입2, ...);

• 반환형과 매개변수 타입이 일치해야 함
• 괄호 (*포인터이름) 필수

함수 포인터 사용 예제

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

int multiply(int a, int b) {
    return a * b;
}

int main(void) {
    int (*funcPtr)(int, int);  // 함수 포인터 선언

    // 덧셈 함수 호출
    funcPtr = add;
    printf("10 + 5 = %d\n", funcPtr(10, 5));

    // 뺄셈 함수 호출
    funcPtr = subtract;
    printf("10 - 5 = %d\n", funcPtr(10, 5));

    // 곱셈 함수 호출
    funcPtr = multiply;
    printf("10 × 5 = %d\n", funcPtr(10, 5));

    return 0;
}
실행 결과 보기
10 + 5 = 15
10 - 5 = 5
10 × 5 = 50

함수 포인터 배열

여러 함수를 배열로 관리할 수 있습니다.

#include <stdio.h>

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) { return a / b; }

int main(void) {
    int (*calc[4])(int, int) = {add, sub, mul, div};
    char *op[4] = {"+", "-", "×", "÷"};
    int i;

    printf("=== 계산기 ===\n");
    for (i = 0; i < 4; i++) {
        printf("20 %s 4 = %d\n", op[i], calc[i](20, 4));
    }

    return 0;
}
실행 결과 보기
=== 계산기 ===
20 + 4 = 24
20 - 4 = 16
20 × 4 = 80
20 ÷ 4 = 5

함수 포인터를 매개변수로 전달

콜백 함수(Callback Function) 패턴에 활용됩니다.

#include <stdio.h>

void processArray(int arr[], int size, int (*func)(int)) {
    int i;
    for (i = 0; i < size; i++) {
        arr[i] = func(arr[i]);
    }
}

int square(int n) {
    return n * n;
}

int triple(int n) {
    return n * 3;
}

int main(void) {
    int arr1[5] = {1, 2, 3, 4, 5};
    int arr2[5] = {1, 2, 3, 4, 5};
    int i;

    processArray(arr1, 5, square);
    processArray(arr2, 5, triple);

    printf("제곱: ");
    for (i = 0; i < 5; i++) printf("%d ", arr1[i]);

    printf("\n3배: ");
    for (i = 0; i < 5; i++) printf("%d ", arr2[i]);
    printf("\n");

    return 0;
}
실행 결과 보기
제곱: 1 4 9 16 25
3배: 3 6 9 12 15

5. void 포인터

void 포인터는 자료형이 정해지지 않은 포인터입니다. 어떤 타입의 주소든 저장할 수 있습니다.

void 포인터의 특징

void *ptr;  // void 포인터 선언
  • 모든 타입의 주소를 저장 가능
  • 포인터 연산 불가
  • 역참조 전에 형 변환 필요

void 포인터 사용 예제

#include <stdio.h>

int main(void) {
    int num = 100;
    double pi = 3.14;
    char ch = 'A';

    void *ptr;  // void 포인터 선언

    // int형 주소 저장
    ptr = &num;
    printf("int: %d\n", *(int *)ptr);

    // double형 주소 저장
    ptr = &pi;
    printf("double: %.2f\n", *(double *)ptr);

    // char형 주소 저장
    ptr = &ch;
    printf("char: %c\n", *(char *)ptr);

    return 0;
}
실행 결과 보기
int: 100
double: 3.14
char: A
💡 void 포인터의 활용
• 범용 함수 구현 (예: qsort, memcpy)
• 동적 메모리 할당 (malloc의 반환 타입)
• 다양한 타입을 처리하는 구조체

6. 종합 실습

문제 1 - 더블 포인터 기초 (기초)

문제 1

다음 코드의 실행 결과는?

포인터의 심화
#include <stdio.h>

int main(void) {
    int num = 50;
    int *ptr = #
    int **dptr = &ptr;

    **dptr += 10;

    printf("%d", num);

    return 0;
}

문제 2 - 포인터 교환 (중급)

문제 2

다음 코드의 실행 결과는?

포인터의 심화
#include <stdio.h>

void swap(int **a, int **b) {
    int *temp = *a;
    *a = *b;
    *b = temp;
}

int main(void) {
    int x = 10, y = 20;
    int *p1 = &x;
    int *p2 = &y;

    swap(&p1, &p2);

    printf("%d %d", *p1, *p2);

    return 0;
}

문제 3 - 함수 포인터 (기초)

문제 3

다음 코드의 실행 결과는?

포인터의 심화
#include <stdio.h>

int calc(int a, int b) {
    return a * b + 5;
}

int main(void) {
    int (*fp)(int, int);

    fp = calc;

    printf("%d", fp(3, 4));

    return 0;
}

문제 4 - 삼중 포인터 (중급)

문제 4

다음 코드의 실행 결과는?

포인터의 심화
#include <stdio.h>

int main(void) {
    int num = 100;
    int *p = #
    int **dp = &p;
    int ***tp = &dp;

    ***tp = 500;

    printf("%d", num);

    return 0;
}

문제 5 - 함수 포인터 배열 (고급)

문제 5

다음 코드의 실행 결과는?

포인터의 심화
func[0](5, 3)은 add(5, 3) = 8, func[1](4, 2)는 mul(4, 2) = 8
#include <stdio.h>

int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }

int main(void) {
    int (*func[2])(int, int) = {add, mul};

    int result = func[0](5, 3) + func[1](4, 2);

    printf("%d", result);

    return 0;
}

문제 6 - void 포인터 (중급)

문제 6

void 포인터에 대한 설명으로 올바른 것은?

A. void 포인터는 포인터 연산이 가능하다.
B. void 포인터는 역참조할 때 형 변환이 필요하다.
C. void 포인터는 int형 주소만 저장할 수 있다.
D. void 포인터는 함수 포인터로 사용할 수 없다.
포인터의 심화

핵심 요약

1. 더블 포인터
• 선언: int **dptr;
• 포인터 변수의 주소를 저장
**dptr로 최종 값에 접근
• 포인터 자체를 변경할 때 유용

2. 다중 포인터
• 삼중 포인터: int ***tptr;
• 실무에서는 거의 사용 안 함
• 개념 이해 목적으로 학습

3. 함수 포인터
• 선언: 반환형 (*이름)(매개변수...);
• 함수의 주소를 저장
• 함수를 동적으로 호출 가능
• 콜백 함수 구현에 활용

4. void 포인터
• 선언: void *ptr;
• 모든 타입의 주소 저장 가능
• 역참조 시 형 변환 필수
• 포인터 연산 불가

5. 활용 패턴
• 더블 포인터: Call-by-reference로 포인터 변경
• 함수 포인터: 범용 함수, 콜백 구현
• void 포인터: malloc, 범용 자료구조