# C언어 (4)

## 1\. 포인터

### 1-1. 포인터란?

포인터는 **다른 변수의 메모리 주소를 저장하는 변수** 다.

일반 변수가 값을 직접 담고 있다면, 포인터는 "그 값이 어디에 있는지"를 담고 있다. 비유하자면 일반 변수는 서랍 안의 물건이고, 포인터는 그 서랍의 위치를 적어둔 메모지다.

```c
int x = 10;
int *p = &x;  // p는 x의 주소를 저장

printf("x의 값: %d\n", x);       // 10
printf("x의 주소: %p\n", &x);    // 0x7ffeefbff4ac (예시)
printf("p의 값: %p\n", p);       // 0x7ffeefbff4ac (같은 주소)
printf("p가 가리키는 값: %d\n", *p);  // 10
```

여기서 두 가지 연산자가 핵심이다.

`&` 는 **주소 연산자** 로, 변수의 메모리 주소를 반환한다. `&x`는 "x가 메모리 어디에 있는지"를 알려준다.

`*` 는 **역참조(dereference) 연산자** 로, 포인터가 가리키는 주소에 저장된 값을 가져온다. `*p`는 "p가 가리키는 곳에 가서 값을 읽어라"는 뜻이다.

선언할 때의 `*`와 사용할 때의 `*`는 의미가 다르다. `int *p`에서 `*`는 "p가 포인터다"라는 선언이고, `*p`에서 `*`는 "p가 가리키는 값"이라는 역참조다. 모양은 같지만 맥락이 다르다.

포인터를 통해 원본 변수의 값을 바꿀 수도 있다.

```c
int x = 10;
int *p = &x;
*p = 50;
printf("%d\n", x);  // 50 (원본이 바뀜)
```

이전에 함수에서 call by value 때문에 원본을 수정할 수 없다고 했는데, 포인터를 사용하면 이 문제를 해결할 수 있다.

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

int main(void) {
    int x = 10, y = 20;
    swap(&x, &y);
    printf("x=%d, y=%d\n", x, y);  // x=20, y=10
    return 0;
}
```

함수에 주소를 넘기고, 함수 안에서 그 주소를 통해 원본을 직접 수정하는 것이다. `scanf`에서 `&`를 붙였던 이유도 이것과 같다.

### 1-2. 포인터의 연산

포인터에 정수를 더하거나 빼면 **자료형 크기만큼** 주소가 이동한다. 이것을 **포인터 산술(pointer arithmetic)** 이라고 한다.

```c
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;  // 배열 이름은 첫 번째 요소의 주소

printf("%d\n", *p);       // 10 (arr[0])
printf("%d\n", *(p + 1)); // 20 (arr[1])
printf("%d\n", *(p + 2)); // 30 (arr[2])
```

`p + 1`은 주소값에 1을 더하는 게 아니다. `int` 포인터이므로 **4바이트(int 크기)만큼** 주소가 이동한다. `p`가 `0x1000`이라면 `p + 1`은 `0x1004`다.

이 덕분에 포인터와 배열은 밀접하게 연결된다. `arr[i]`는 사실 `*(arr + i)`와 같은 표현이다. 배열의 인덱스 접근은 내부적으로 포인터 연산으로 변환된다.

```c
int arr[3] = {100, 200, 300};

printf("%d\n", arr[2]);       // 300
printf("%d\n", *(arr + 2));   // 300 (같은 의미)
```

포인터끼리의 뺄셈도 가능하다. 두 포인터 사이에 요소가 몇 개 있는지를 반환한다.

```c
int arr[5] = {10, 20, 30, 40, 50};
int *p1 = &arr[1];
int *p2 = &arr[4];
printf("%ld\n", p2 - p1);  // 3 (요소 3개 차이)
```

단, 포인터끼리의 덧셈은 의미가 없으므로 허용되지 않는다. 주소 + 주소는 아무런 의미를 가지지 않기 때문이다.

`++`와 `--`도 사용할 수 있다. `p++`는 다음 요소로, `p--`는 이전 요소로 이동한다.

```c
int arr[3] = {10, 20, 30};
int *p = arr;

printf("%d\n", *p);  // 10
p++;
printf("%d\n", *p);  // 20
p++;
printf("%d\n", *p);  // 30
```

### 1-3. 포인터에 여러 가지 자료형이 있는 이유

포인터는 결국 메모리 주소를 저장하는 것이고, 주소는 시스템에 따라 4바이트 또는 8바이트 정수다. 그러면 왜 `int *`, `char *`, `double *`처럼 타입을 구분해야 할까? 어차피 주소 크기는 같은데 전부 `void *` 하나로 쓰면 안 될까?

이유는 **역참조할 때 몇 바이트를 읽어야 하는지** 를 결정하기 위해서다.

`int *p`로 `*p`를 하면 그 주소에서 4바이트를 읽어서 정수로 해석한다. `char *p`로 `*p`를 하면 1바이트만 읽어서 문자로 해석한다. `double *p`로 `*p`를 하면 8바이트를 읽어서 실수로 해석한다.

만약 타입 정보가 없다면, `*p`를 했을 때 그 주소에서 몇 바이트를 읽어야 하는지, 그리고 읽은 비트를 정수로 해석할지 실수로 해석할지 알 수 없다.

```c
int x = 10;
int *ip = &x;
char *cp = (char *)&x;

printf("%d\n", *ip);  // 10 (4바이트를 int로 읽음)
printf("%d\n", *cp);  // 10일 수도, 아닐 수도 (1바이트만 읽음)
```

포인터 산술에서도 타입이 중요하다. 앞서 `p + 1`이 자료형 크기만큼 주소를 이동한다고 했다. `int *`에서 `p + 1`은 4바이트 뒤지만, `char *`에서 `p + 1`은 1바이트 뒤다. 타입이 없으면 이 계산도 불가능하다.

정리하면 포인터의 타입은 **역참조 시 읽을 크기**와 **포인터 연산의 단위** 를 결정한다.

* * *

## 2\. 배열과 포인터

배열과 포인터는 C에서 밀접하게 연결되어 있지만, **같은 것은 아니다.**

배열 이름은 첫 번째 요소의 주소를 가리키는 **상수 포인터** 처럼 동작한다. "상수"라는 것은 배열 이름 자체에 다른 주소를 대입할 수 없다는 뜻이다.

```c
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;  // OK — 배열의 시작 주소를 포인터에 대입

p++;       // OK — 포인터는 이동 가능
// arr++;  // 에러! — 배열 이름은 이동 불가 (상수)
```

배열을 포인터로 순회하는 것은 C에서 매우 흔한 패턴이다.

```c
int arr[5] = {10, 20, 30, 40, 50};

// 인덱스 방식
for (int i = 0; i < 5; i++) {
    printf("%d ", arr[i]);
}

// 포인터 방식
int *p = arr;
for (int i = 0; i < 5; i++) {
    printf("%d ", *(p + i));
}

// 포인터 증가 방식
int *q = arr;
for (int i = 0; i < 5; i++) {
    printf("%d ", *q);
    q++;
}
```

세 가지 모두 같은 결과를 출력한다. `arr[i]`, `*(arr + i)`, `*(p + i)` 는 전부 같은 의미다.

함수에 배열을 넘기면 배열이 통째로 복사되는 것이 아니라 **첫 번째 요소의 주소(포인터)만 전달** 된다. 그래서 함수 안에서 배열을 수정하면 원본이 바뀐다.

```c
void doubleAll(int *arr, int size) {
    for (int i = 0; i < size; i++) {
        arr[i] *= 2;  // 원본 배열이 수정됨
    }
}

int main(void) {
    int nums[3] = {1, 2, 3};
    doubleAll(nums, 3);
    // nums는 이제 {2, 4, 6}
    return 0;
}
```

함수 매개변수에서 `int arr[]`와 `int *arr`는 같은 의미다. 둘 다 포인터를 받는다.

* * *

## 3\. 함수 포인터

C에서는 함수도 메모리에 존재하므로 주소를 가진다. **함수 포인터** 는 함수의 주소를 저장하는 포인터다.

선언 문법이 조금 복잡하다.

```c
반환타입 (*포인터이름)(매개변수타입들);
```

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

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

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

    op = add;
    printf("%d\n", op(3, 5));  // 8

    op = subtract;
    printf("%d\n", op(3, 5));  // -2

    return 0;
}
```

`int (*op)(int, int)` 에서 `op`는 "int 두 개를 받아서 int를 반환하는 함수"를 가리킬 수 있는 포인터다. `add`든 `subtract`든, 시그니처가 같은 함수라면 어디든 가리킬 수 있다.

함수 이름 자체가 함수의 주소이므로 `op = add`에서 `&`를 붙이지 않아도 된다. 배열 이름이 배열의 주소인 것과 같은 원리다.

괄호 위치가 중요하다. `int (*op)(int, int)`는 함수 포인터이고, `int *op(int, int)`는 `int *`를 반환하는 함수 선언이다. `*op`를 괄호로 감싸야 포인터라는 뜻이 된다.

함수 포인터가 유용한 대표적인 사례는 **콜백(callback)** 패턴이다. 어떤 함수에 "실행할 함수"를 인자로 넘기는 것이다. JavaScript에서 `addEventListener`에 콜백을 넘기는 것과 같은 개념이다.

```c
void calculate(int a, int b, int (*operation)(int, int)) {
    printf("결과: %d\n", operation(a, b));
}

int main(void) {
    calculate(10, 3, add);       // 결과: 13
    calculate(10, 3, subtract);  // 결과: 7
    return 0;
}
```

`calculate` 함수는 어떤 연산을 할지 모른다. 호출할 때 연산 함수를 넘겨주면 그걸 실행할 뿐이다. 이렇게 하면 `calculate` 함수를 수정하지 않고도 새로운 연산을 추가할 수 있다.

C 표준 라이브러리의 `qsort`도 함수 포인터를 활용한 대표적인 예다. 정렬 기준을 함수 포인터로 받아서, 오름차순/내림차순 등 다양한 방식으로 정렬할 수 있게 한다.

```c
#include <stdlib.h>

int compare(const void *a, const void *b) {
    return (*(int *)a - *(int *)b);
}

int main(void) {
    int arr[5] = {50, 20, 40, 10, 30};
    qsort(arr, 5, sizeof(int), compare);
    // arr는 이제 {10, 20, 30, 40, 50}
    return 0;
}
```

* * *

## 마무리

포인터는 C언어에서 가장 중요하면서도 가장 어렵다고 느끼는 개념이다. 핵심은 `&`(주소를 구함)와 `*`(주소로 가서 값을 읽음) 이 두 연산자의 의미를 정확히 아는 것이다. 포인터의 타입은 역참조할 때 몇 바이트를 읽을지, 포인터 연산에서 몇 바이트씩 이동할지를 결정한다. 이 원리를 이해하면 배열과 포인터의 관계, 함수 포인터까지 자연스럽게 따라온다.
