1단원 — 포인터
C 언어에서 가장 강력하고, 동시에 가장 많은 버그의 원인이 되는 도구입니다. **포인터는 "값"이 아니라 "값이 있는 곳(주소)"** 을 다룹니다.
이 강의에서 배우는 것
- 1포인터 변수의 선언, `&`, `*` 연산자를 이해한다.
- 2함수에 포인터를 넘겨 호출자의 변수를 수정한다.
- 3배열 이름이 사실상 포인터처럼 동작함을 안다.
- 4포인터 산술과 NULL 검사 패턴을 익힌다.
왜 포인터가 필요한가? — "우편함의 호실 번호"
집에 직접 찾아가지 않고도 "**호실 번호**"만 알면 우편물을 보낼 수 있듯, 포인터는 메모리에 있는 값에 **간접 접근** 하는 수단입니다.
변수의 세계 포인터의 세계
값 자체를 다룬다 값이 있는 주소를 다룬다
"사과 한 개" "냉장고 두 번째 칸"이 간접 접근 덕에 다음 일이 가능해집니다.
- **함수가 호출자의 변수**를 수정 (call by reference 흉내)
- **큰 자료**를 복사하지 않고 주소만 전달 (성능)
- **동적 메모리** 사용 (런타임 크기 결정, 다음 단원)
- **연결 자료구조** (연결 리스트, 트리)
핵심 개념
1) 주소와 역참조
int x = 42;
int *p = &x; // p는 x의 주소
*p = 100; // p가 가리키는 곳에 100 대입 → x = 100메모리 그림:
x (값 42) p (포인터, x의 주소를 담음)
┌────────┐ ┌────────┐
│ 42 │ ◄────────────────── │ &x │
└────────┘ └────────┘
주소: 0x100 주소: 0x200`*p` (역참조) = "p가 가리키는 곳의 값" = `x`.
2) 포인터의 크기는 자료형과 무관
sizeof(int *) == 8 // 64-bit
sizeof(char *) == 8
sizeof(double *) == 8포인터는 **주소**만 담으므로 어떤 자료형을 가리키든 크기가 같습니다. 다만 자료형은 `*p`가 메모리에서 **몇 바이트를 읽고 어떻게 해석할지**를 결정합니다.
3) 배열과 포인터
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // arr은 첫 원소의 주소
p[2]; // 30
*(p + 2); // 30 (동치)배열 이름은 식 안에서 **첫 원소의 포인터**로 변환됩니다. 다만 `sizeof(arr)`는 배열 전체 크기, `sizeof(p)`는 포인터 크기.
4) NULL 포인터
int *p = NULL;
if (p != NULL) *p = 0; // 항상 검사유효하지 않은 포인터를 역참조하면 거의 확실히 **세그멘테이션 폴트**. "가리킬 곳이 아직 없다"를 명시할 때 NULL을 사용합니다.
예제로 보기
예제 1 — `ex01_basic.c` : 주소와 역참조
int x = 42;
int *p = &x;
*p = 100;**실행 결과** (주소는 실행마다 다름)
x의 값: 42
x의 주소: 0x7fffda2960a4
p의 값: 0x7fffda2960a4 (= x의 주소)
*p: 42 (p가 가리키는 값)
*p = 100 이후 x = 100핵심: `p`의 값과 `&x`가 같은 주소. `*p = 100`이 실제로 `x`를 바꿉니다.
예제 2 — `ex02_swap.c` : 함수로 두 값 교환
void swap(int *a, int *b) { int t = *a; *a = *b; *b = t; }
swap(&x, &y);**실행 결과**
교환 전: x=1, y=2
교환 후: x=2, y=1핵심: 값 전달이라면 swap이 동작하지 않습니다. **주소를 전달**해야 함수가 호출자의 변수를 직접 수정할 수 있습니다.
예제 3 — `ex03_array_ptr.c` : 배열은 사실상 포인터
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;**실행 결과**
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` : 포인터 산술
int arr[] = {100, 200, 300, 400};
int *p = arr;
p++; // sizeof(int) 만큼 증가
p += 2;**실행 결과** (주소 예시)
p -> 100 (주소 0x7ffee6ef7c40)
p+1 -> 200 (주소 0x7ffee6ef7c44)
p+3 -> 400 (주소 0x7ffee6ef7c4c)
last - first = 3핵심: 포인터 산술은 **자료형 크기 단위**(int면 4바이트씩) 로 이동합니다. `last - first` 의 결과는 **원소 개수의 차**.
다른 시각으로 보기 — 변수 vs 포인터
변수만 사용: 포인터 사용:
┌──────┐ ┌──────┐
│ x=10 │ 함수 -- (값 10 복사) ─► │ x=10 │ 함수 -- (주소 0x100 전달) ─►
└──────┘ 함수 안에서 복사본만 └──────┘ 함수가 0x100을 통해
바꿀 수 있음 원본을 직접 수정포인터는 "**원본 변수를 빌려 쓰는 권한**"을 함수에 주는 셈입니다.
자주 하는 실수
- **NULL 역참조**: `int *p = NULL; *p = 0;` → 세그폴트.
- **초기화 안 한 포인터**: `int *p; *p = 0;` → 임의의 메모리 손상(매우 위험).
- **로컬 변수의 주소 반환**: 함수가 끝나면 그 주소는 **댕글링** 포인터.
```c int *bad(void) { int x = 10; return &x; } /* 위험 */ ```
- **`sizeof` 오해**: 포인터에 `sizeof`를 쓰면 가리키는 배열의 크기가 아닌 **포인터 자체 크기**.
- **`&` 연산자 누락**: `scanf("%d", n)` 같은 실수.
정리
- 포인터는 "주소를 담는 변수". `&`로 주소를 얻고 `*`로 값을 읽고 쓴다.
- 함수가 호출자의 변수를 바꾸려면 **주소를 전달**해야 한다.
- 배열 이름은 식에서 첫 원소의 포인터처럼 동작한다.
- 포인터 산술은 자료형 크기 단위로 이루어진다.
- NULL 검사와 초기화는 메모리 사고를 막는 첫 번째 방어선.
직접 해 보기
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)
실제로 컴파일·실행해 결과를 확인할 수 있는 예제입니다.
#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;
}
#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#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#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 (hw01.c)
목표: 세 정수 a, b, c를 인자로 받아 **오름차순으로 정렬해 반환**하는 함수 `void sort3(int *a, int *b, int *c)`를 작성하세요.
- 파일명: hw01.c
입력: 30 10 20
정렬: 10 20 30▶정답 코드 펼치기 / 접기
#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 (hw02.c)
목표: 포인터를 사용해 배열의 합을 계산하는 함수 `int sum(const int *arr, int n)`을 작성하세요.
- 파일명: hw02.c
▶정답 코드 펼치기 / 접기
#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 (hw03.c)
목표: 정수 배열의 모든 원소를 **두 배**로 만드는 함수 `void doubleAll(int *arr, int n)`을 작성하고 결과를 출력하세요.
- 파일명: hw03.c
▶정답 코드 펼치기 / 접기
#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;
}