← C 강의 목록으로
🎯
고급 (Advanced)
& · * · 배열-포인터 · 포인터 연산

1단원 — 포인터

C 언어에서 가장 강력하고, 동시에 가장 많은 버그의 원인이 되는 도구입니다. **포인터는 "값"이 아니라 "값이 있는 곳(주소)"** 을 다룹니다.

pointer&*
소요 시간
1~2시간
난이도
📊 중급
선수 조건
🎯 중급 4단원
결과물
포인터로 메모리를 가리키고 값을 변경한다

이 강의에서 배우는 것

  • 1포인터 변수의 선언, `&`, `*` 연산자를 이해한다.
  • 2함수에 포인터를 넘겨 호출자의 변수를 수정한다.
  • 3배열 이름이 사실상 포인터처럼 동작함을 안다.
  • 4포인터 산술과 NULL 검사 패턴을 익힌다.

왜 포인터가 필요한가? — "우편함의 호실 번호"

집에 직접 찾아가지 않고도 "**호실 번호**"만 알면 우편물을 보낼 수 있듯, 포인터는 메모리에 있는 값에 **간접 접근** 하는 수단입니다.

text
   변수의 세계               포인터의 세계
   값 자체를 다룬다           값이 있는 주소를 다룬다
   "사과 한 개"               "냉장고 두 번째 칸"

이 간접 접근 덕에 다음 일이 가능해집니다.

  1. **함수가 호출자의 변수**를 수정 (call by reference 흉내)
  2. **큰 자료**를 복사하지 않고 주소만 전달 (성능)
  3. **동적 메모리** 사용 (런타임 크기 결정, 다음 단원)
  4. **연결 자료구조** (연결 리스트, 트리)

핵심 개념

1) 주소와 역참조

c
int x = 42;
int *p = &x;        // p는 x의 주소
*p = 100;           // p가 가리키는 곳에 100 대입 → x = 100

메모리 그림:

text
   x (값 42)                      p (포인터, x의 주소를 담음)
   ┌────────┐                     ┌────────┐
   │   42   │ ◄────────────────── │  &x    │
   └────────┘                     └────────┘
   주소: 0x100                    주소: 0x200

`*p` (역참조) = "p가 가리키는 곳의 값" = `x`.

2) 포인터의 크기는 자료형과 무관

c
sizeof(int *)    == 8   // 64-bit
sizeof(char *)   == 8
sizeof(double *) == 8

포인터는 **주소**만 담으므로 어떤 자료형을 가리키든 크기가 같습니다. 다만 자료형은 `*p`가 메모리에서 **몇 바이트를 읽고 어떻게 해석할지**를 결정합니다.

3) 배열과 포인터

c
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;          // arr은 첫 원소의 주소
p[2];        // 30
*(p + 2);    // 30 (동치)

배열 이름은 식 안에서 **첫 원소의 포인터**로 변환됩니다. 다만 `sizeof(arr)`는 배열 전체 크기, `sizeof(p)`는 포인터 크기.

4) NULL 포인터

c
int *p = NULL;
if (p != NULL) *p = 0;     // 항상 검사

유효하지 않은 포인터를 역참조하면 거의 확실히 **세그멘테이션 폴트**. "가리킬 곳이 아직 없다"를 명시할 때 NULL을 사용합니다.

예제로 보기

예제 1 — `ex01_basic.c` : 주소와 역참조

c
int x = 42;
int *p = &x;
*p = 100;

**실행 결과** (주소는 실행마다 다름)

text
x의 값:    42
x의 주소:  0x7fffda2960a4
p의 값:    0x7fffda2960a4   (= x의 주소)
*p:        42   (p가 가리키는 값)
*p = 100 이후 x = 100

핵심: `p`의 값과 `&x`가 같은 주소. `*p = 100`이 실제로 `x`를 바꿉니다.

예제 2 — `ex02_swap.c` : 함수로 두 값 교환

c
void swap(int *a, int *b) { int t = *a; *a = *b; *b = t; }
swap(&x, &y);

**실행 결과**

text
교환 전: x=1, y=2
교환 후: x=2, y=1

핵심: 값 전달이라면 swap이 동작하지 않습니다. **주소를 전달**해야 함수가 호출자의 변수를 직접 수정할 수 있습니다.

예제 3 — `ex03_array_ptr.c` : 배열은 사실상 포인터

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

**실행 결과**

text
arr[0]=10, *(p+0)=10, p[0]=10
arr[1]=20, *(p+1)=20, p[1]=20
arr[2]=30, *(p+2)=30, p[2]=30
arr[3]=40, *(p+3)=40, p[3]=40
arr[4]=50, *(p+4)=50, p[4]=50
sizeof(arr) = 20
sizeof(p)   = 8

핵심: `arr[i]`, `*(p+i)`, `p[i]`는 모두 같습니다. 그러나 `sizeof`는 다릅니다.

예제 4 — `ex04_ptr_arith.c` : 포인터 산술

c
int arr[] = {100, 200, 300, 400};
int *p = arr;
p++;          // sizeof(int) 만큼 증가
p += 2;

**실행 결과** (주소 예시)

text
p   -> 100 (주소 0x7ffee6ef7c40)
p+1 -> 200 (주소 0x7ffee6ef7c44)
p+3 -> 400 (주소 0x7ffee6ef7c4c)
last - first = 3

핵심: 포인터 산술은 **자료형 크기 단위**(int면 4바이트씩) 로 이동합니다. `last - first` 의 결과는 **원소 개수의 차**.

다른 시각으로 보기 — 변수 vs 포인터

text
   변수만 사용:                         포인터 사용:
   ┌──────┐                              ┌──────┐
   │ x=10 │      함수 -- (값 10 복사) ─►  │ x=10 │      함수 -- (주소 0x100 전달) ─►
   └──────┘      함수 안에서 복사본만       └──────┘      함수가 0x100을 통해
                  바꿀 수 있음                                원본을 직접 수정

포인터는 "**원본 변수를 빌려 쓰는 권한**"을 함수에 주는 셈입니다.

자주 하는 실수

  1. **NULL 역참조**: `int *p = NULL; *p = 0;` → 세그폴트.
  2. **초기화 안 한 포인터**: `int *p; *p = 0;` → 임의의 메모리 손상(매우 위험).
  3. **로컬 변수의 주소 반환**: 함수가 끝나면 그 주소는 **댕글링** 포인터.

```c int *bad(void) { int x = 10; return &x; } /* 위험 */ ```

  1. **`sizeof` 오해**: 포인터에 `sizeof`를 쓰면 가리키는 배열의 크기가 아닌 **포인터 자체 크기**.
  2. **`&` 연산자 누락**: `scanf("%d", n)` 같은 실수.

정리

  • 포인터는 "주소를 담는 변수". `&`로 주소를 얻고 `*`로 값을 읽고 쓴다.
  • 함수가 호출자의 변수를 바꾸려면 **주소를 전달**해야 한다.
  • 배열 이름은 식에서 첫 원소의 포인터처럼 동작한다.
  • 포인터 산술은 자료형 크기 단위로 이루어진다.
  • NULL 검사와 초기화는 메모리 사고를 막는 첫 번째 방어선.

직접 해 보기

bash
cd src
gcc -std=c11 -Wall -o ex01 ex01_basic.c     && ./ex01
gcc -std=c11 -Wall -o ex02 ex02_swap.c      && ./ex02
gcc -std=c11 -Wall -o ex03 ex03_array_ptr.c && ./ex03
gcc -std=c11 -Wall -o ex04 ex04_ptr_arith.c && ./ex04

응용:

  • `ex02`의 `swap` 함수가 매개변수를 `int a, int b`로만 받으면 어떻게 되는지 시도.
  • `ex03`에서 `p` 대신 `arr`를 함수에 넘겨 합을 구하는 함수 `int sum(int *a, int n)`을 작성.

💻 예제 (examples)

실제로 컴파일·실행해 결과를 확인할 수 있는 예제입니다.

ex01_basic.c주소와 역참조
CODE
#include <stdio.h>

int main(void) {
    int x = 42;
    int *p = &x;

    printf("x의 값:    %d\n", x);
    printf("x의 주소:  %p\n", (void*)&x);
    printf("p의 값:    %p   (= x의 주소)\n", (void*)p);
    printf("*p:        %d   (p가 가리키는 값)\n", *p);

    *p = 100;
    printf("*p = 100 이후 x = %d\n", x);
    return 0;
}
ex02_swap.c함수로 두 값 교환
CODE
#include <stdio.h>

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

int main(void) {
    int x = 1, y = 2;
    printf("교환 전: x=%d, y=%d\n", x, y);
    swap(&x, &y);
    printf("교환 후: x=%d, y=%d\n", x, y);
    return 0;
}
▶ 실행 결과
교환 전: x=1, y=2
교환 후: x=2, y=1
ex03_array_ptr.c배열은 사실상 포인터
CODE
#include <stdio.h>

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

    for (int i = 0; i < 5; i++) {
        printf("arr[%d]=%d, *(p+%d)=%d, p[%d]=%d\n",
               i, arr[i], i, *(p + i), i, p[i]);
    }

    printf("sizeof(arr) = %zu\n", sizeof(arr));   // 20
    printf("sizeof(p)   = %zu\n", sizeof(p));     // 8 (64-bit)
    return 0;
}
▶ 실행 결과
arr[0]=10, *(p+0)=10, p[0]=10
arr[1]=20, *(p+1)=20, p[1]=20
arr[2]=30, *(p+2)=30, p[2]=30
arr[3]=40, *(p+3)=40, p[3]=40
arr[4]=50, *(p+4)=50, p[4]=50
sizeof(arr) = 20
sizeof(p)   = 8
ex04_ptr_arith.c포인터 산술
CODE
#include <stdio.h>

int main(void) {
    int arr[] = {100, 200, 300, 400};
    int *p = arr;

    /* p가 가리키는 자료형의 크기만큼 이동 */
    printf("p   -> %d (주소 %p)\n", *p, (void*)p);
    p++;
    printf("p+1 -> %d (주소 %p)\n", *p, (void*)p);
    p += 2;
    printf("p+3 -> %d (주소 %p)\n", *p, (void*)p);

    /* 두 포인터의 차 = 원소 개수 */
    int *first = arr, *last = &arr[3];
    printf("last - first = %ld\n", (long)(last - first));
    return 0;
}

📝 과제 (exercises)

직접 풀어보고, 막힐 때 정답을 펼쳐 비교해보세요.

과제 1

문제 1 (hw01.c)

목표: 세 정수 a, b, c를 인자로 받아 **오름차순으로 정렬해 반환**하는 함수 `void sort3(int *a, int *b, int *c)`를 작성하세요.

요구사항
  • 파일명: hw01.c
입출력 예시
입력: 30 10 20
정렬: 10 20 30
정답 코드 펼치기 / 접기
SOLUTION
#include <stdio.h>

static void swap(int *x, int *y) { int t = *x; *x = *y; *y = t; }

void sort3(int *a, int *b, int *c) {
    if (*a > *b) swap(a, b);
    if (*b > *c) swap(b, c);
    if (*a > *b) swap(a, b);
}

int main(void) {
    int a, b, c;
    printf("세 정수 입력: ");
    scanf("%d %d %d", &a, &b, &c);

    sort3(&a, &b, &c);
    printf("정렬: %d %d %d\n", a, b, c);
    return 0;
}
▶ 실행 결과
입력: 30 10 20
정렬: 10 20 30
과제 2

문제 2 (hw02.c)

목표: 포인터를 사용해 배열의 합을 계산하는 함수 `int sum(const int *arr, int n)`을 작성하세요.

요구사항
  • 파일명: hw02.c
정답 코드 펼치기 / 접기
SOLUTION
#include <stdio.h>

int sum(const int *arr, int n) {
    int s = 0;
    for (int i = 0; i < n; i++) s += arr[i];
    return s;
}

int main(void) {
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int n = (int)(sizeof(arr) / sizeof(arr[0]));
    printf("합계: %d\n", sum(arr, n));
    return 0;
}
과제 3

문제 3 (hw03.c)

목표: 정수 배열의 모든 원소를 **두 배**로 만드는 함수 `void doubleAll(int *arr, int n)`을 작성하고 결과를 출력하세요.

요구사항
  • 파일명: hw03.c
정답 코드 펼치기 / 접기
SOLUTION
#include <stdio.h>

void doubleAll(int *arr, int n) {
    for (int i = 0; i < n; i++) arr[i] *= 2;
}

int main(void) {
    int arr[] = {1, 2, 3, 4, 5};
    int n = (int)(sizeof(arr) / sizeof(arr[0]));

    doubleAll(arr, n);
    for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    putchar('\n');
    return 0;
}
예제 코드 / 강의 자료

전체 강의 자료와 예제 코드는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗