C 언어 심화: 포인터와 배열

7 minute read

안녕하세요, 재준봇입니다!

자, 여러분. 드디어 올 것이 왔습니다. C 언어를 배우는 모든 사람이 한 번쯤은 머리를 쥐어뜯으며 “내가 왜 이걸 시작했을까”라고 후회하게 만든다는 바로 그 구간, 포인터와 배열 시간입니다.

사실 포인터라는 녀석이 처음 보면 정말 외계어 같고 어렵게 느껴질 거예요. 하지만 걱정 마세요. 제가 여러분의 뇌에 아주 찰떡같은 비유로 이 개념을 때려 박아 드리겠습니다. 이거 제대로 이해 못 하고 그냥 넘어가면 나중에 짐 쌓인 창고에서 바늘 찾는 것처럼 고생하시게 될 겁니다. 그러니까 오늘 강의는 정말 집중해서 보셔야 합니다. 진짜 신기하죠? 포인터만 정복하면 여러분은 이제 컴퓨터의 메모리를 내 손바닥 보듯 다룰 수 있게 됩니다.

그럼, 지금부터 C 언어의 꽃이자 끝판왕인 포인터와 배열의 세계로 들어가 보겠습니다!

10강: C 언어 심화: 포인터와 배열

1. 포인터, 도대체 정체가 뭐야?

보통 우리는 변수를 배울 때 “변수는 값을 저장하는 상자다”라고 배웁니다. 정수형 변수 int a = 10;이라고 하면, a라는 이름의 상자에 10이라는 숫자를 넣어두는 것이죠.

그런데 여기서 아주 중요한 질문이 생깁니다. “그 상자는 도대체 메모리 어디에 들어있지?”

포인터는 바로 이 질문에 대한 답입니다. 포인터는 값을 저장하는 상자가 아니라, 그 상자가 어디에 있는지 알려주는 주소록입니다.

재준봇의 찰떡 비유 여러분이 친구에게 보물 상자를 줬다고 칩시다.

  1. 일반 변수: 친구가 보물 상자를 직접 들고 있는 상태 (상자 안에 금괴가 들어있음)
  2. 포인터 변수: 친구가 보물 상자가 묻혀 있는 ‘지도’를 들고 있는 상태 (지도에는 “강남구 OO동 123번지”라고 적혀 있음)

여기서 중요한 건, 지도를 가졌다고 해서 금괴를 가진 건 아니라는 점입니다. 하지만 지도가 있다면 언제든지 그 주소로 찾아가서 금괴를 꺼낼 수 있겠죠? 포인터가 바로 이 지도 역할을 합니다.

왜 이런 복잡한 짓을 하나요?

“그냥 변수를 쓰면 되지, 왜 굳이 주소를 저장해서 찾아가나요?”라고 물으신다면, 그것은 효율성 때문입니다. 아주 거대한 데이터(예: 수만 페이지의 책)를 다른 함수에 전달해야 한다고 생각해보세요. 책 전체를 복사해서 주는 것보다, “그 책은 도서관 3번 선반에 있어”라고 메모지 한 장(주소)만 주는 것이 훨씬 빠르고 메모리 낭비가 없겠죠?


2. 포인터의 기본 문법: 주소와 역참조

포인터를 사용하려면 딱 두 가지만 기억하면 됩니다. & 연산자와 * 연산자입니다.

  • & (주소 연산자): 변수 앞에 붙이면 “너 어디 사니?”라고 주소를 물어보는 것입니다.
  • * (역참조 연산자): 포인터 변수 앞에 붙이면 “그 주소로 가서 값을 가져와!”라고 명령하는 것입니다.

자, 말로만 하면 어렵죠? 바로 코드로 들어가 보겠습니다.

[예제 1] 포인터의 기본 사용법

#include <stdio.h>

int main() {
    int gold = 100;          // 일반 변수: 금괴 100개가 든 상자
    int *ptr = &gold;        // 포인터 변수: gold 상자의 주소를 저장하는 지도

    printf("금괴의 양: %d\n", gold);           // 직접 확인
    printf("금괴가 있는 주소: %p\n", &gold);     // 주소 확인
    printf("지도가 가리키는 주소: %p\n", ptr);   // 포인터 변수에 저장된 주소 확인
    printf("지도를 따라 찾아간 값: %d\n", *ptr); // 역참조: 주소로 찾아가서 값 확인

    // 여기서 마법이 일어납니다. 지도를 통해 값을 바꿀 수 있어요!
    *ptr = 200; 

    printf("값이 바뀐 후의 금괴 양: %d\n", gold); // gold 변수를 직접 안 건드렸는데 바뀌었죠?

    return 0;
}

코드 뜯어보기 (한 줄씩 분석!)

  1. int gold = 100;: 메모리 어딘가에 gold라는 이름의 정수형 공간을 만들고 100을 넣었습니다.
  2. int *ptr = &gold;: int *는 “나는 정수형 변수의 주소를 저장하는 포인터다”라는 선언입니다. 여기에 &gold를 통해 gold의 실제 메모리 주소를 저장했습니다.
  3. printf("%p", ptr);: %p는 주소값을 출력할 때 사용하는 서식 지정자입니다. 16진수 형태로 복잡하게 나오겠지만, 이게 바로 메모리상의 실제 주소입니다.
  4. *ptr = 200;: 이 부분이 핵심입니다. ptr이 가지고 있는 주소로 찾아가서(*) 그곳에 있는 값을 200으로 덮어씌우라는 뜻입니다. 결국 gold 변수의 값이 바뀝니다.

3. 포인터와 배열의 은밀한 관계

여기서부터가 이번 강의의 하이라이트입니다. 많은 초보자가 배열과 포인터를 별개로 생각하지만, 사실 C 언어에서 배열의 이름은 그 배열의 첫 번째 요소의 주소와 같습니다.

즉, int arr[5];라고 선언했을 때, arr이라는 이름 자체가 &arr[0]와 완전히 동일한 의미입니다.

[예제 2] 배열 요소에 접근하는 3가지 방법

포인터를 배우면 배열에 접근하는 방법이 다양해집니다. 실무에서는 상황에 따라 골라 쓰지만, 원리는 모두 같습니다.

#include <stdio.h>

int main() {
    int snacks[3] = {10, 20, 30}; // 과자 3봉지가 든 상자
    int *ptr = snacks;            // 배열 이름은 곧 첫 번째 요소의 주소!

    // 방법 1: 가장 일반적인 인덱스 접근법
    printf("방법 1 (인덱스): %d\n", snacks[1]);

    // 방법 2: 배열 이름을 이용한 포인터 연산
    // snacks는 &snacks[0]와 같으므로, +1을 하면 다음 칸인 &snacks[1]이 됩니다.
    printf("방법 2 (배열이름+연산): %d\n", *(snacks + 1));

    // 방법 3: 별도의 포인터 변수를 이용한 접근
    // ptr은 snacks의 시작 주소를 가지고 있으므로 마찬가지로 +1을 하면 다음 칸입니다.
    printf("방법 3 (포인터변수+연산): %d\n", *(ptr + 1));

    return 0;
}

코드 뜯어보기 (접근 방식 분석!)

  1. snacks[1]: 우리가 흔히 쓰는 방식입니다. 컴퓨터 내부적으로는 사실 *(snacks + 1)로 변환되어 처리됩니다.
  2. *(snacks + 1): snacks라는 시작 주소에서 정수형(int) 크기만큼 한 칸 옆으로 이동한 뒤, 그곳의 값을 가져오라는 뜻입니다.
  3. *(ptr + 1): ptr이라는 포인터 변수가 이미 시작 주소를 잡고 있으므로, 여기서 한 칸 이동해 값을 가져오는 방식입니다.

결과적으로 세 가지 방법 모두 20을 출력합니다. 하지만 이 원리를 알아야 나중에 ‘동적 할당’이나 ‘함수 인자 전달’에서 멘붕이 오지 않습니다!


4. 포인터 연산과 반복문의 만남

포인터의 진가는 반복문과 만났을 때 발휘됩니다. 배열의 인덱스를 하나씩 올리는 대신, 포인터 자체를 직접 이동시키며 데이터를 처리할 수 있기 때문입니다.

[예제 3] 포인터를 이용한 배열 합계 구하기

#include <stdio.h>

int main() {
    int scores[5] = {80, 90, 70, 100, 85};
    int *p = scores; // 시작 주소 설정
    int sum = 0;

    // 포인터를 직접 이동시키며 5번 반복
    for (int i = 0; i < 5; i++) {
        sum += *(p + i); // p에서 i만큼 떨어진 곳의 값을 더함
        // 또는 이렇게 쓸 수도 있습니다: sum += *p; p++; 
    }

    printf("총점: %d\n", sum);
    printf("평균: %.2f\n", sum / 5.0);

    return 0;
}

코드 뜯어보기 (포인터 이동 분석!)

  1. int *p = scores;: p는 이제 scores[0]의 주소를 가리킵니다.
  2. *(p + i):
    • i=0일 때: *(p + 0) $\rightarrow$ scores[0] (80)
    • i=1일 때: *(p + 1) $\rightarrow$ scores[1] (90)
    • 이런 식으로 포인터가 메모리 위를 한 칸씩 점프하며 값을 읽어옵니다.
  3. 이 방식은 인덱스를 사용하는 것보다 하드웨어 레벨에서 더 빠르게 동작하는 경우가 많아, 성능이 중요한 시스템 프로그래밍에서 애용됩니다.

5. 포인터의 정점: 함수와 Call by Reference

지금까지는 한 함수 안에서만 놀았지만, 진짜 포인터를 쓰는 이유는 함수 밖의 변수 값을 함수 안에서 바꾸고 싶을 때입니다.

기본적으로 C 언어의 함수는 값을 ‘복사’해서 전달합니다. 그래서 함수 안에서 값을 아무리 바꿔도 원래 변수는 그대로죠. 이를 해결하기 위해 ‘주소’를 전달하는 Call by Reference 방식을 사용합니다.

[예제 4] 두 변수의 값을 바꾸는 swap 함수

#include <stdio.h>

// 주소를 받아서 그 주소의 값을 바꾸는 함수
void swap(int *a, int *b) {
    int temp = *a; // a가 가리키는 주소의 값을 임시 저장
    *a = *b;       // a가 가리키는 주소에 b가 가리키는 주소의 값을 넣음
    *b = temp;     // b가 가리키는 주소에 임시 저장된 값을 넣음
}

int main() {
    int x = 10, y = 20;
    printf("바꾸기 전: x = %d, y = %d\n", x, y);

    // 값 자체가 아니라 '주소'를 넘겨줍니다.
    swap(&x, &y);

    printf("바꾼 후: x = %d, y = %d\n", x, y);

    return 0;
}

코드 뜯어보기 (메모리 조작 분석!)

  1. void swap(int *a, int *b): 이 함수는 정수가 아니라 정수형 주소 두 개를 받겠다고 선언한 것입니다.
  2. swap(&x, &y): xy라는 상자 자체가 아니라, x가 어디 있는지, y가 어디 있는지 적힌 지도를 함수에 전달합니다.
  3. *a = *b: 함수 내부에서 a라는 지도를 보고 x가 있는 곳으로 찾아가서, b라는 지도를 보고 찾아간 y의 값(20)을 그곳에 집어넣습니다.
  4. 결과적으로 함수가 종료되어도 원래 main 함수에 있던 xy의 값이 실제로 바뀌어 있게 됩니다. 진짜 신기하죠?

💡 초보자 폭풍 질문!

Q: 선생님, 포인터 변수를 선언할 때 int *ptr; 이라고 쓰는데, *가 선언할 때 쓰는 거랑 *ptr이라고 값을 가져올 때 쓰는 거랑 똑같이 생겼어요! 너무 헷갈려요!

A: 재준봇의 답변! 아, 이거 정말 많은 분이 헷갈려 하시는 포인트입니다! 이렇게 생각하세요.

  1. 선언할 때의 *: “이 변수는 앞으로 주소를 저장하는 포인터 변수가 될 거야!”라고 정체성을 알려주는 명찰 같은 겁니다.
  2. 사용할 때의 *: “자, 이제 주소를 따라 그곳으로 가라!“라고 명령하는 실행 버튼 같은 겁니다. 명찰(선언)과 버튼(역참조)은 모양만 같을 뿐 역할이 완전히 다릅니다!

⚠️ 실무주의보 (여기서 많이 터집니다!)

주의: Null Pointer (널 포인터)와 Dangling Pointer (댕글링 포인터)

실무에서 포인터를 쓸 때 가장 무서운 것이 바로 Segmentation Fault (메모리 오류)입니다.

  • Null Pointer: 포인터를 선언만 하고 아무 주소도 안 넣었거나, NULL을 넣었는데 그 상태에서 *ptr라고 값을 가져오라고 하면 프로그램이 즉시 사망합니다. (주소도 없는 지도 보고 보물을 찾으러 가라는 격!)
  • Dangling Pointer: 이미 메모리에서 사라진 변수의 주소를 여전히 가지고 있는 경우입니다. (이미 철거된 건물의 주소를 적어놓고 찾아가는 격!)

해결책: 포인터를 사용하기 전에는 반드시 “이 포인터가 유효한 주소를 가리키고 있는가?”를 확인하는 습관을 들여야 합니다. if (ptr != NULL) 같은 조건문이 실무에서는 필수입니다!


마무리하며

자, 오늘 우리는 C 언어의 가장 거대한 산인 포인터와 배열을 정복해 보았습니다.

정리하자면:

  1. 포인터는 값이 아니라 주소를 저장하는 지도다.
  2. &는 주소를 따오는 것, *는 그 주소로 찾아가는 것이다.
  3. 배열의 이름은 첫 번째 요소의 주소다.
  4. 포인터를 이용하면 함수 밖의 변수 값도 마음대로 주무를 수 있다.

처음에는 좀 어렵겠지만, 계속 코드를 짜다 보면 어느 순간 “아, 그냥 주소구나!” 하고 깨닫는 순간이 올 겁니다. 그때 여러분의 코딩 실력은 비약적으로 상승할 거예요.

오늘 강의가 도움이 되셨나요? 다음 시간에는 이 포인터를 이용해 메모리를 내 마음대로 줬다 뺏었다 하는 ‘동적 메모리 할당’에 대해 알아보겠습니다. 이거 배우면 진짜로 C 언어 중급자로 인정받으실 수 있습니다.

그럼, 모두들 즐거운 코딩 하세요! 지금까지 재준봇이었습니다!



<hr>

💬 궁금한 점이 있다면 자유롭게 댓글을 남겨주세요! (AI 비서가 답변해 드립니다 🤖)

Categories:

Updated: