C언어 심화: 포인터와 함수
목차 24
C언어 심화는 메모리를 직접 다루는 영역이다. 포인터(
&주소·*역참조)·배열-포인터 동치·함수(Call by Value vs Reference)·변수 범위·동적 메모리를 코드로 따라가며 출력값을 도출한다. 매 회차 1~2문항 이상 나오는 4과목 최빈출 영역으로, 역참조 vs 곱셈 · sizeof(arr) vs sizeof(ptr) · Value vs Reference · free 누락 · NULL 미체크가 단골 함정이다. 39강의arr ≡ &arr[0](배열 이름 = 첫 원소 주소)와 static 저장 영역(Data)이 여기서 포인터로 본격적으로 다뤄진다.핵심 암기: 포인터 2대 연산자 ★&주*역★(
&=주소 ·*=역참조) · 배열-포인터 동치 arr ≡ &arr[0] ≡ ptr · 변수 범위 ★지전스★(지=Stack · 전=Data · 스=Data) · 동적 메모리 4단계 ★할쓰해놀★(malloc → 사용 → free → NULL) · 함정(역참조 vs 곱셈 · sizeof 8B · Value/Reference swap · 누수/dangling/이중 해제)
PART A — 포인터 기초
포인터와 ★&주*역★ ·포인터·시험 1순위·
[정의] 포인터(Pointer) = 변수의 주소를 담는 변수. 다른 변수가 사는 메모리 번지수를 적어둔 한 칸이다. 함수에서 원본을 수정하거나 동적 메모리를 다루려면 주소가 필요하다.
[표] ★&주*역★ — 2대 연산자
| 두음 | 연산자 | 의미 | 시험 함정 |
|---|---|---|---|
| & 주 | &x |
주소(address) 연산자 — 변수의 메모리 번지 | scanf("%d", &age) |
| * 역 | *p |
역참조(dereference) 연산자 — 주소가 가리키는 값 | *p++ vs (*p)++ |
[코드]
int x = 10;
int *p = &x; // p는 x의 주소를 담은 포인터
printf("%d\n", *p); // 10 — *p = p가 가리키는 값 = x
*p = 20; // p가 가리키는 곳의 값을 20으로
printf("%d\n", x); // 20 — 역참조로 원본 수정
💡 포인터 = 변수의 우편함 주소를 적어둔 메모지. &는 주소 가져오기, *는 그 주소의 값 꺼내기.
🔑 암기 "★&주*역★ — &=주소 / *=역참조. 사용 전 NULL 체크 필수"
⚠️ 함정 선언만 한 포인터(int *p;)는 쓰레기 주소(wild pointer) — 그대로 *p = 10 하면 세그먼테이션 폴트(segfault). 사용 전 if (p != NULL). / 선언의 *(포인터 표시)와 사용의 *(역참조), 변수 둘 사이 *(곱셈)는 위치로 구분.
🎯 빈출 & vs * 분별이 매 회차 1문제 이상.
포인터 크기와 연산 ·포인터·
[정의] 포인터 크기는 자료형과 무관하게 64비트 = 8바이트 고정. 포인터에 1을 더하면 자료형 크기만큼 이동한다(int *는 +4B).
[표] 자료형 크기 vs 포인터 크기
| 자료형 | 자료형 크기 | 포인터 크기 |
|---|---|---|
char |
1B | char * = 8B |
int |
4B | int * = 8B |
double |
8B | double * = 8B |
| 구조체 100B | 100B | struct * = 8B |
[코드]
int arr[3] = {10, 20, 30};
int *p = arr;
int a = *p++; // *(p++) — 10 가져오고 p는 arr[1]로 이동
p = arr; // 다시 초기화
int b = (*p)++; // *p의 값 10을 b에, 그 값을 11로 증가. p는 이동 X
💡 64비트 CPU는 주소를 8바이트로 표현 — 포인터는 주소를 담는 그릇이라 무조건 8B. 자료형은 '역참조 때 몇 바이트 읽을지'만 결정.
🔑 암기 "포인터 크기 8B 고정 · +1 = 자료형 크기만큼 이동 · *p++(값 쓰고 이동) vs (*p)++(값 자체 증가)"
⚠️ 함정 *p++와 (*p)++는 괄호 위치가 우선순위를 뒤집는다.
🎯 빈출 포인터 크기 8B · int * +1 = 4B 이동이 단골.
PART B — 배열과 포인터
배열-포인터 동치 — arr ≡ &arr[0] ≡ ptr ·배열-포인터·시험 1순위·
[정의] 배열 이름은 곧 첫 원소의 주소다. arr ≡ &arr[0] ≡ ptr, 그리고 arr[i] ≡ *(arr+i).
[흐름] 동치 등식
arr ≡ &arr[0] ≡ ptr (배열 이름 = 첫 원소 주소)
arr[i] ≡ *(arr + i) ≡ ptr[i] ≡ *(ptr + i)
[코드]
int arr[5] = {10, 20, 30, 40, 50};
int *ptr = arr; // ptr = &arr[0]
printf("%d %d\n", arr[2], *(arr+2)); // 30 30 — 동치
printf("%d %d\n", ptr[2], *(ptr+2)); // 30 30 — ptr도 인덱스 접근 가능
💡 함수에 배열을 넘기면 자동으로 포인터가 전달되고, 포인터로 배열을 순회할 수 있다. 39강의 arr ≡ &arr[0] 성질이 여기서 본격화된다.
🔑 암기 *"arr ≡ &arr[0] ≡ ptr · arr[i] ≡ (arr+i) — 두 줄이면 배열-포인터 80%"
🎯 빈출 동치 셋 비교가 매 회차. 단 sizeof만은 예외(다음 카드).
sizeof(arr) vs sizeof(ptr) ·배열-포인터·매년 단골·
[정의] arr ≡ ptr 동치지만 sizeof만은 다르다. 배열은 전체 크기, 포인터는 8B.
[코드]
int arr[5] = {10, 20, 30, 40, 50};
int *ptr = arr;
printf("%zu %zu\n", sizeof(arr), sizeof(ptr)); // 20 8
[표] sizeof 3케이스
| 케이스 | sizeof | 이유 |
|---|---|---|
sizeof(arr) 선언 함수 내 |
20 (4B × 5) | 배열 전체 크기 |
sizeof(ptr) |
8 | 포인터 크기(64비트) |
sizeof(arr) 함수 매개변수 |
8 | 배열이 포인터로 자동 변환 |
💡 배열은 함수 매개변수로 들어가는 순간 포인터로 변신 — 함수 안에선 sizeof로 길이를 못 구해 길이 매개변수를 따로 전달하는 게 관행.
🔑 암기 "sizeof(arr)=전체 / sizeof(ptr)=8B · 함수 매개변수로 가면 배열도 8B"
⚠️ 함정 "arr ≡ ptr이니 sizeof도 같다" ❌(→ 컴파일러가 배열인지 포인터인지 선언 형태로 판단해 다름).
🎯 빈출 sizeof(arr) vs sizeof(ptr)가 시험 1순위 함정.
PART C — 함수
함수 — 선언·정의·호출 ·함수·
[정의] 함수 = 입력을 받아 처리하고 결과를 돌려주는 추상화 단위. 선언(프로토타입)·정의(본문)·호출 3단계. 매개변수(Parameter) = 정의 쪽 '받는 변수', 인자(Argument) = 호출 쪽 '보내는 값'.
[코드]
int add(int a, int b); // ① 선언 — 프로토타입
int add(int a, int b) { // ② 정의
return a + b;
}
int result = add(3, 5); // ③ 호출 — result = 8
[표] 입출력 4패턴
| 패턴 | 예시 | 의미 |
|---|---|---|
| 입력 O · 출력 O | int add(int a, int b) |
받아서 반환 |
| 입력 O · 출력 X | void print(int x) |
출력만 |
| 입력 X · 출력 O | int getTime() |
인자 없이 반환 |
| 입력 X · 출력 X | void hello() |
둘 다 없음 |
💡 void=반환 없음, return=즉시 종료 + 값 전달. void가 아닌데 return이 빠지면 쓰레기 값 반환 위험.
🔑 암기 "선언·정의·호출 3단계 · 매개변수=받는 쪽(정의) / 인자=보내는 쪽(호출)"
🎯 빈출 매개변수 vs 인자 용어 분별.
Call by Value vs Reference ·함수·시험 1순위·
[정의] Call by Value = 값을 복사해 전달(원본 안 바뀜). Call by Reference = 주소(포인터)를 전달해 함수가 원본을 직접 수정. C는 기본이 Call by Value.
[코드]
void swap1(int a, int b) { int t = a; a = b; b = t; } // 값 복사 — swap 실패
void swap2(int *a, int *b) { int t = *a; *a = *b; *b = t; } // 주소 — swap 성공
int x = 10, y = 20;
swap1(x, y); // x y 그대로 10 20
swap2(&x, &y); // x y 교환 20 10
[표] Value vs Reference
| 항목 | Call by Value | Call by Reference |
|---|---|---|
| 전달 | 값 복사 | 주소 전달(포인터) |
| 원본 변경 | 불가 | 가능 |
| 비유 | 피자 사진 | 피자 가게 주소 |
| swap | 안 됨 | 됨 |
💡 값 = 사진(찢어도 원본 안전) / 주소 = 가게 주소(가서 진짜 먹어버림). 배열을 함수에 넘기면 첫 원소 주소만 가서 자동으로 Call by Reference — & 없이도 원본 수정(void f(int *arr)).
🔑 암기 "Value=사진(원본 안전) / Reference=가게 주소(원본 변경) · swap은 Reference(*,&)만 됨 · 배열은 자동 Reference"
⚠️ 함정 swap을 Call by Value(*,& 없이)로 쓰면 절대 안 바뀜.
🎯 빈출 swap 결과 · Value/Reference 분별이 매 회차 1문제 이상.
재귀 함수 ·함수·
[정의] 재귀(Recursion) = 함수가 자기 자신을 호출. 2대 필수 요소 = 종료 조건 + 자기 호출. 종료 조건 없으면 무한 재귀 → 스택 오버플로우 → 크래시.
[코드]
int factorial(int n) {
if (n <= 1) return 1; // 종료 조건
return n * factorial(n - 1); // 자기 호출
}
// factorial(5) = 5 × 4 × 3 × 2 × 1 = 120
[표] 재귀 vs 반복문
| 항목 | 재귀 | 반복문 |
|---|---|---|
| 가독성 | 수학 정의에 가까움 | 단계별 명시 |
| 메모리 | Stack에 호출 정보 쌓임 | 변수 몇 개 |
| 어울림 | 분할정복·트리 순회 | 횟수 정해진 반복 |
💡 마트료시카 — 큰 인형 안에 작은 인형, 가장 작은 인형(종료 조건)에 닿을 때까지.
🔑 암기 "재귀 2요소 = 종료 조건 + 자기 호출 · 종료 조건 없으면 스택 오버플로우"
⚠️ 함정 종료 조건 누락 = 무한 재귀.
🎯 빈출 factorial · 피보나치 추적.
PART D — 변수 범위와 메모리
메모리 4영역 — Stack · Heap · Data · Text ·메모리·
[정의] 프로그램 메모리는 4영역으로 나뉜다. Stack(지역 변수·함수 호출 정보) · Heap(동적 메모리) · Data(전역·static) · Text(실행 코드).
[흐름] 메모리 구조
높은 주소 ▲
┌─────────────┐
│ Stack │ 지역 변수 · 함수 호출 정보 (↓ 자동 반납)
├─────────────┤
│ Heap │ 동적 메모리 malloc (↑ 직접 임차/반납 free)
├─────────────┤
│ Data │ 전역 변수 · static 변수
├─────────────┤
│ Text │ 실행 코드 (읽기 전용)
└─────────────┘
낮은 주소 ▼
[표] Stack vs Heap
| 항목 | Stack | Heap |
|---|---|---|
| 할당 | 컴파일 시(자동) | 런타임(malloc) |
| 반납 | 자동(함수 종료 시) | 수동(free) |
| 크기 | 작음(~MB) | 큼(~GB 가능) |
| 비유 | 도서관 사물함 | 자가 창고 |
💡 Stack은 들어갈 때 자동으로 받고 나올 때 자동 반납, Heap은 직접 임차(malloc)·직접 반납(free). 안 정리하면 임차료 누수.
🔑 암기 "Stack(지역·자동) / Heap(동적·수동) / Data(전역·static) / Text(코드)"
🎯 빈출 영역별 저장 대상 매칭.
★지전스★ — 변수 범위 3유형 ·범위·시험 1순위·
[정의] 변수가 어디 살고 언제 사라지는지 3유형. 지역(Stack) · 전역(Data) · static(Data). 두음 ★지전스★.
[표] 변수 범위 3유형
| 두음 | 종류 | 영역 | 수명 | 초기화 | 비유 |
|---|---|---|---|---|---|
| 지 | 지역 변수 | Stack | 함수 끝나면 소멸 | 자동 X(쓰레기) | 내 방 |
| 전 | 전역 변수 | Data | 프로그램 끝까지 | 0 자동 | 거실 |
| 스 | static 변수 | Data | 프로그램 끝까지 | 첫 호출만 | 금고 |
[코드]
int global = 100; // 전 — Data·항상
void counter() {
static int s = 0; // 스 — Data·첫 호출만 초기화
int local = 0; // 지 — Stack·매번 새로
s++; local++;
printf("s=%d local=%d\n", s, local);
}
// counter() 3번 호출: s=1,2,3 / local=항상 1
💡 39강의 거실(전역)·내 방(지역)·금고(static) 비유 그대로. static의 저장 영역이 Data다.
🔑 암기 "★지전스★ — 지=Stack / 전=Data / 스=Data(첫 호출만 초기화)"
⚠️ 함정 static은 '첫 호출만 초기화'(둘째 호출부터 누적). / 지역 변수의 주소를 반환하면 안 됨 — 함수가 끝나면 Stack에서 소멸한 주소(dangling pointer). int *f() { int x = 10; return &x; } ❌.
🎯 빈출 static 누적 추적 · 지역 주소 반환(dangling)이 매 회차.
PART E — 동적 메모리
★할쓰해놀★ — 동적 메모리 4단계 ·동적 메모리·시험 1순위·
[정의] 런타임에 크기가 정해지거나, 함수 종료 후에도 살아남아야 하거나, Stack보다 큰 데이터는 Heap에서 동적 할당한다. 4단계 = 할(malloc) → 쓰(사용) → 해(free) → 놀(NULL). 두음 ★할쓰해놀★.
[표] 4단계
| 두음 | 단계 | 동작 | 비유 |
|---|---|---|---|
| 할 | 할당 | malloc/calloc/realloc |
창고 임차 |
| 쓰 | 사용 | p[i] = ..., *p = ... |
물건 넣고 빼기 |
| 해 | 해제 | free(p) |
임차 종료 |
| 놀 | NULL 대입 | p = NULL |
열쇠 버리기 |
[코드]
int *p = (int*)malloc(5 * sizeof(int)); // 할 — 임차
if (p == NULL) return 1; // 할당 실패 체크(관행)
for (int i = 0; i < 5; i++)
p[i] = i * 10; // 쓰 — 사용
free(p); // 해 — 반납
p = NULL; // 놀 — dangling 방지
💡 free한 뒤에도 p에는 옛 주소가 남아 — NULL을 넣어야 혹시 모를 재접근을 차단한다.
🔑 암기 "★할쓰해놀★ — 할(malloc) → 쓰(사용) → 해(free) → 놀(NULL)"
⚠️ 함정 malloc 직후 NULL 체크 관행 / free 뒤 p = NULL 누락.
🎯 빈출 동적 메모리 4단계가 매 회차 1~2문제.
동적 메모리 4총사 ·동적 메모리·
[정의] 동적 메모리 함수 4종. 매개변수 개수 + 초기화 여부가 분별 키.
[표] 4총사
| 함수 | 시그니처 | 인자 | 초기화 | 용도 |
|---|---|---|---|---|
| malloc | malloc(size) |
1개 | X(쓰레기) | 단순 임차 |
| calloc | calloc(n, size) |
2개 | O(모두 0) | 0 초기화 임차 |
| realloc | realloc(p, size) |
2개 | 기존 보존 | 크기 변경 |
| free | free(p) |
1개 | — | 반납 |
[코드]
int *p = (int*)malloc(5 * sizeof(int)); // p[0..4] = 쓰레기
p = (int*)calloc(5, sizeof(int)); // p[0..4] = 0
p = (int*)realloc(p, 10 * sizeof(int)); // 기존 5칸 보존 + 5칸 확장
free(p); p = NULL;
💡 malloc=전 사용자 물건이 남은 창고(쓰레기) / calloc=청소 끝난 창고(0) / realloc=물건 그대로 창고 확장·축소.
🔑 암기 "malloc 1인자·쓰레기 / calloc 2인자·0 / realloc 2인자·보존 / free 1인자"
⚠️ 함정 malloc은 0 초기화를 하지 않음(calloc과 혼동).
🎯 빈출 malloc vs calloc 인자 수·초기화 분별.
동적 메모리 3대 사고 ·동적 메모리·
[정의] ★할쓰해놀★ 4단계를 어기면 나는 3대 사고.
[표] 3대 사고
| 사고 | 위반 | 비유 |
|---|---|---|
| 메모리 누수 | free 누락 | 창고 열쇠 안 반납 |
| dangling pointer | NULL 대입 누락 | 빈집 주소로 친구 보냄 |
| 이중 해제 | free 두 번 | 반납한 열쇠 또 반납 |
[코드]
int *p = (int*)malloc(sizeof(int)); // 할
if (p == NULL) return -1; // NULL 체크
*p = 10; // 쓰
free(p); // 해
p = NULL; // 놀 — dangling + 이중 해제 동시 방지
💡 free(p) 다음 줄 p = NULL 한 줄이면 dangling + 이중 해제를 동시에 차단. free(NULL)은 표준상 안전하다.
🔑 암기 "누수=해 누락 / dangling=놀 누락 / 이중 해제=해 두 번"
⚠️ 함정 free 후 NULL을 안 넣으면 dangling + 이중 해제 위험.
🎯 빈출 3대 사고 원인 매칭.
PART F — 포인터 심화
함수 포인터와 콜백 ·심화·
[정의] 함수 포인터 = 함수의 시작 주소를 담는 포인터. 시그니처 int (*op)(int, int) — 별 + 변수명을 괄호로 묶는다. 런타임에 호출할 함수를 변수로 바꿀 수 있다.
[코드]
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int (*op)(int, int); // 함수 포인터 선언
op = add; printf("%d\n", op(3, 5)); // 8
op = sub; printf("%d\n", op(3, 5)); // -2
💡 콜백 = 내가 만든 함수를 다른 함수에 호출 권한과 함께 넘기는 것(qsort의 비교 함수). 시험 직접 출제는 드물어도 코드 분석에 자주 등장.
🔑 암기 "함수 포인터 = 함수 주소를 담는 변수 · int (*op)(int,int) · 가리키는 함수가 곧 호출되는 함수"
🎯 빈출 op가 가리키는 함수가 바뀔 때 결과.
구조체 포인터 — p->x ·심화·
[정의] 구조체 포인터로 멤버에 접근할 땐 화살표 ->. p->x ≡ (*p).x 동치. 자기참조 구조체로 연결리스트를 만든다.
[코드]
struct Point { int x; int y; };
struct Point p = {3, 4};
struct Point *pp = &p;
printf("%d %d\n", pp->x, (*pp).y); // 3 4 — 둘 다 같은 결과
struct Node { int data; struct Node *next; }; // 자기참조
struct Node n1 = {10, NULL}, n2 = {20, NULL};
n1.next = &n2; // n1 → n2 → NULL
💡 기차 칸 — 각 칸이 데이터 + 다음 칸 주소를 들고 있고, 마지막 칸의 next = NULL.
🔑 암기 "구조체 포인터 멤버 = 화살표 -> · p->x ≡ (*p).x"
⚠️ 함정 *p.x는 *(p.x)로 해석돼 오류 — p->x 또는 (*p).x를 쓴다.
🎯 빈출 p->멤버로 원본 수정 · 동치 분별.
const 포인터 ·심화·
[정의] const 위치로 보호 대상이 갈린다. * 기준 왼쪽=값 보호, 오른쪽=주소 보호.
[코드]
const int *p1 = &x; // 값 보호: *p1 = ... 금지 / p1 = ... 허용
int * const p2 = &x; // 주소 보호: *p2 = ... 허용 / p2 = ... 금지
const int * const p3 = &x; // 둘 다 보호
[표] const 위치별 보호
| 선언 | *p 변경 |
p 변경 |
|---|---|---|
const int *p |
❌ | ✅ |
int * const p |
✅ | ❌ |
const int * const p |
❌ | ❌ |
💡 함수 매개변수 void f(const int *arr, int n) — 함수 안에서 arr[i] = 0은 컴파일 오류(원본 보호 보증).
🔑 암기 "const가 * 왼쪽 = 값 보호 / * 오른쪽 = 주소 보호 (변수명에서 시계 방향으로 읽기)"
⚠️ 함정 값 보호인데 주소 변경을 막는 걸로(또는 그 반대로) 착각.
🎯 빈출 const 위치별 컴파일 오류 줄 고르기.
C 문자열 — char[] vs char* ·심화·
[정의] C 문자열 = char 배열 + 끝에 \0(null 종결). char str[]는 Stack 복사본(수정 가능), char *p는 Text 영역 리터럴(읽기 전용).
[코드]
char str1[] = "Hello"; // char 배열 — Stack에 복사. 수정 가능
str1[0] = 'h'; // OK
char *str2 = "Hello"; // char 포인터 — Text 영역(읽기 전용)
// str2[0] = 'h'; // 런타임 크래시 — 매년 단골
str2 = "World"; // 다른 리터럴 가리키기는 OK
[표] char[] vs char*
| 형태 | 메모리 | 수정 |
|---|---|---|
char str[] = "..." |
Stack(복사본) | 가능 |
char *p = "..." |
Text(읽기 전용) | 불가 |
💡 strlen은 \0 제외, sizeof는 \0 포함 — "Hello"는 strlen=5, sizeof=6.
🔑 암기 "char[]=Stack 수정 가능 / char=Text 읽기 전용 · strlen은 \0 제외 / sizeof는 \0 포함"*
⚠️ 함정 char *p = "..."의 p[0] = 'x' = 런타임 크래시.
🎯 빈출 char[] vs char* 수정 가능 여부.
기출 다지기
[기출 1 출제] 다음 C 코드의 실행 결과는? (코드 실행 결과 예측)
int x = 10;
int *p = &x;
*p = 20;
printf("%d %d", x, *p);
- ① 10 10
- ② 10 20
- ③ 20 20
- ④ 20 10
정답 및 해설 보기
정답: ③ 20 20
*p = 20은 'p가 가리키는 주소에 20 저장'이다. p가 x를 가리키므로 x도 20이 된다. 따라서 x와 *p 모두 20.
*p를 'p라는 변수 자체에 20 대입'으로 잘못 읽는 함정을 조심한다. *가 붙으면 'p가 가리키는 곳'이지 p 자신이 아니다.
🔑 암기 *p = 값 → p가 가리키는 원본이 바뀐다.
[기출 2 출제] 다음 C 코드의 실행 결과는? (코드 실행 결과 예측)
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;
printf("%d %d %d", arr[2], *(arr+2), p[2]);
- ① 30 30 30
- ② 30 40 30
- ③ 20 30 40
- ④ 컴파일 에러
정답 및 해설 보기
정답: ① 30 30 30
arr ≡ &arr[0] ≡ p 동치 + arr[i] ≡ *(arr+i) 동치다. 셋 다 인덱스 2의 값 30을 가리킨다. p도 arr와 동치이므로 p[i] 인덱스 접근이 그대로 되며 별도 변환이 필요 없다.
🔑 암기 *arr ≡ &arr[0] ≡ ptr · arr[i] ≡ (arr+i) · 배열 이름 = 첫 원소 주소.
[기출 3 출제] 다음 C 코드의 실행 결과는? (코드 실행 결과 예측)
void swap1(int a, int b) { int t = a; a = b; b = t; }
void swap2(int *a, int *b) { int t = *a; *a = *b; *b = t; }
int x = 10, y = 20;
swap1(x, y); printf("%d %d / ", x, y);
swap2(&x, &y); printf("%d %d", x, y);
- ① 10 20 / 10 20
- ② 10 20 / 20 10
- ③ 20 10 / 20 10
- ④ 20 10 / 10 20
정답 및 해설 보기
정답: ② 10 20 / 20 10
swap1은 Call by Value라 값 복사본만 교환 → 원본 x, y는 그대로 10 20(피자 사진). swap2는 Call by Reference라 주소로 원본을 직접 교환 → 20 10(피자 가게 주소).
swap1 결과를 '20 10'으로 잘못 답하는 함정이 매년 나온다. 포인터(*, &)가 없으면 swap은 절대 안 된다.
🔑 암기 swap은 Call by Reference(*, &)일 때만 원본이 바뀐다.
[기출 4 출제] 다음 C 코드의 실행 결과는? (64비트 환경 · 코드 실행 결과 예측)
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
printf("%zu %zu", sizeof(arr), sizeof(p));
- ① 20 20
- ② 20 8
- ③ 8 8
- ④ 40 8
정답 및 해설 보기
정답: ② 20 8
arr는 배열이므로 전체 크기 = 4B × 5 = 20. p는 포인터이므로 8B 고정(64비트). arr ≡ p 동치이니 sizeof도 같을 거라 잘못 답하는 함정을 조심한다 — sizeof만은 컴파일러가 '배열이냐 포인터냐'를 선언 형태로 판단해 다른 결과를 낸다.
🔑 암기 sizeof(arr) = 전체 크기 / sizeof(ptr) = 8B · 동치라도 sizeof는 예외.
[기출 5 출제] 다음 C 코드의 실행 결과는? (코드 실행 결과 예측)
int *p = (int*)calloc(3, sizeof(int));
p[0] = 10;
p[1] = 20;
printf("%d %d %d / ", p[0], p[1], p[2]);
free(p);
p = NULL;
if (p == NULL) printf("safe");
- ① 10 20 0 / safe
- ② 10 20 쓰레기 / safe
- ③ 10 20 0 (출력 없음)
- ④ 컴파일 에러
정답 및 해설 보기
정답: ① 10 20 0 / safe
calloc은 0으로 초기화하므로 건드리지 않은 p[2] = 0이다. malloc이었다면 p[2]는 쓰레기 값 — 여기가 4총사 분별 함정. 그 뒤 free → NULL 대입으로 p == NULL이 참이 되어 safe가 출력된다. ★할쓰해놀★ 4단계를 완전 준수한 코드다.
🔑 암기 calloc=0 초기화 / malloc=쓰레기 · free 뒤 p = NULL이 안전 패턴.
[기출 6 출제] 다음 C 코드의 실행 결과는? (코드 실행 결과 예측)
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
int (*op)(int, int);
op = add; printf("%d / ", op(2, 3));
op = mul; printf("%d", op(2, 3));
- ① 5 / 6
- ② 6 / 5
- ③ 5 / 5
- ④ 6 / 6
정답 및 해설 보기
정답: ① 5 / 6
op가 add를 가리킬 땐 add(2,3) = 2+3 = 5, mul을 가리킬 땐 mul(2,3) = 2×3 = 6. 같은 op(2,3) 호출이지만 op가 무엇을 가리키느냐에 따라 결과가 달라진다 — 런타임에 호출할 함수를 변수로 바꾸는 게 함수 포인터의 본질.
🔑 암기 함수 포인터 = 함수 주소를 담는 변수 · 가리키는 함수가 곧 호출되는 함수.
[기출 7 출제] 다음 C 코드의 실행 결과는? (코드 실행 결과 예측)
struct Student { int id; int score; };
struct Student s = {101, 85};
struct Student *p = &s;
p->score = 95;
printf("%d %d", p->id, (*p).score);
- ① 101 85
- ② 101 95
- ③ 95 101
- ④ 컴파일 에러
정답 및 해설 보기
정답: ② 101 95
p는 s의 주소를 담은 구조체 포인터다. p->score = 95로 원본 s.score가 95로 바뀐다. id는 그대로 101. p->id ≡ (*p).id, (*p).score도 95.
*p.x는 *(p.x)로 해석돼 오류 — 구조체 포인터 멤버 접근은 p->x 또는 괄호를 쓴 (*p).x.
🔑 암기 구조체 포인터 멤버 접근 → p->x ≡ (*p).x · 괄호 없는 *p.x는 함정.
[기출 8 출제] 다음 중 컴파일 오류가 나는 줄을 모두 고르면? (옳지 않은 것 고르기)
int x = 10, y = 20;
const int *p1 = &x;
int * const p2 = &x;
// A: *p1 = 100; B: p1 = &y;
// C: *p2 = 100; D: p2 = &y;
- ① A, B
- ② A, D
- ③ B, C
- ④ C, D
정답 및 해설 보기
정답: ② A, D
const int *p1은 값 보호 → A(*p1 = 100, 값 변경) 오류, B(p1 = &y, 주소 변경) OK.
int * const p2는 주소 보호 → C(*p2 = 100, 값 변경) OK, D(p2 = &y, 주소 변경) 오류.
시계 방향 규칙으로 읽으면 분별 끝 — const가 * 왼쪽이면 값 못 바꿈, 오른쪽이면 주소 못 바꿈.
🔑 암기 const * 왼쪽 = 값 보호 / * 오른쪽 = 주소 보호.
한 장 요약
| 주제 | 암기·핵심 | 결정 카드 |
|---|---|---|
| 포인터 | ★&주*역★ | &=주소 / *=역참조 · 사용 전 NULL 체크 |
| 포인터 크기 | 8B 고정 | +1 = 자료형 크기만큼 이동 |
| 배열-포인터 | arr ≡ &arr[0] ≡ ptr | arr[i] ≡ *(arr+i) · sizeof만 예외(전체 / 8B) |
| 함수 | 선언·정의·호출 | 매개변수=받는 쪽 / 인자=보내는 쪽 |
| Value vs Reference | 사진 vs 가게 주소 | swap은 Reference(*,&)만 · 배열은 자동 Reference |
| 재귀 | 종료 조건 + 자기 호출 | 종료 조건 없으면 스택 오버플로우 |
| 메모리 4영역 | Stack/Heap/Data/Text | 지역=Stack · 동적=Heap · 전역·static=Data |
| 변수 범위 | ★지전스★ | 지=Stack / 전=Data / 스=Data(첫 호출만) |
| 동적 메모리 | ★할쓰해놀★ | 할(malloc)→쓰→해(free)→놀(NULL) |
| 4총사 | 인자·초기화 | malloc 1·쓰레기 / calloc 2·0 / realloc 2·보존 |
| 3대 사고 | 누수·dangling·이중 해제 | free 뒤 p=NULL 한 줄로 둘 동시 방지 |
| 심화 | 함수/구조체 포인터·const·문자열 | p->x · const 왼쪽=값/오른쪽=주소 · char* 읽기 전용 |
| 함정 (매년 2~3쌍 출제) | 정답 |
|---|---|
역참조 *p vs 곱셈 |
위치로 구분 |
| sizeof(arr) vs sizeof(ptr) | 전체 / 8B |
| Value vs Reference swap | Reference에서만 바뀜 |
| free 누락 | 메모리 누수 |
| NULL 미체크 | segfault |
*p++ vs (*p)++ |
괄호가 우선순위 뒤집음 |
| 지역 변수 주소 반환 | dangling |
char *p 리터럴 수정 |
읽기 전용 크래시 |
🎯 합격 한 끗: 40강은 ★&주*역★(주소·역참조) · arr ≡ &arr[0] ≡ ptr(배열-포인터 동치) · ★할쓰해놀★(동적 메모리 4단계) 세 공식에 ★지전스★(변수 범위)까지 더하면 4과목 최고 킬러 영역의 80%를 가져온다. 깊이 파기보다 추적 한 글자가 정답을 가르는 영역 — *p=20이 원본을 바꾸고, sizeof만은 동치의 예외(20/8)이며, swap은 Reference에서만 되고, free 뒤 p=NULL 한 줄이 누수·dangling·이중 해제를 막는다. 39강의 arr ≡ &arr[0]와 static 저장 영역(Data)이 여기서 포인터·★지전스★로 회수됐다. 이로써 C언어 2강(39·40강)이 마무리되고, 41강부터 Java로 넘어간다.
