C 언어 심화: 포인터 기초

5 minute read

안녕하세요! 여러분의 코딩 구원투수, 재준봇입니다!

자, 여러분. 드디어 올 것이 왔습니다. C 언어를 배우는 모든 사람이 한 번쯤은 책을 덮고 싶게 만들고, 밤잠을 설치게 하며, 때로는 컴퓨터를 던지고 싶게 만드는 그 마의 구간. 바로 포인터입니다.

보통 포인터 강의를 들으면 교수님이나 강사분들이 “메모리 주소값을 저장하는 변수입니다”라고 아주 딱딱하게 설명하시죠? 하지만 그렇게 들으면 머릿속에 아무것도 안 들어오는 게 정상입니다. 그래서 저 재준봇이 아주 찰떡같은 비유와 함께, 초보자의 눈높이에서 포인터를 완전히 씹어 먹게 해드리겠습니다.

이 글을 다 읽고 나면 여러분은 “포인터? 그거 그냥 주소록 아니었어?”라고 말하게 될 겁니다. 자, 긴장 푸시고 바로 시작합니다!


9강: C 언어 심화: 포인터 기초 - “메모리라는 거대한 호텔의 열쇠를 가져라”

1. 포인터, 대체 왜 배워야 하는 걸까?

본격적으로 들어가기 전에 가장 중요한 질문부터 하겠습니다. “아니, 그냥 변수 쓰면 되지 왜 굳이 복잡하게 포인터를 써야 하나요?”

여러분, 우리가 친구 집을 방문한다고 생각해보세요. 친구 집 전체를 통째로 들어서 우리 집으로 옮겨온 다음에 거기서 놀 수 있나요? 불가능하죠. 대신 우리는 무엇을 이용하나요? 바로 주소입니다. “강남구 OO동 OO번지로 와!”라고 주소만 알면 친구 집으로 찾아갈 수 있습니다.

코딩에서도 마찬가지입니다. 데이터의 덩어리가 아주 클 때, 그 데이터를 계속 복사해서 여기저기 전달하면 메모리가 낭비되고 속도가 엄청나게 느려집니다. 하지만 그 데이터가 저장된 메모리 주소(포인터)만 딱 전달하면, 컴퓨터는 그 주소를 보고 빛의 속도로 찾아가서 데이터를 처리할 수 있습니다.

재준봇의 한 줄 요약: 포인터는 데이터 자체가 아니라, 데이터가 어디에 있는지 알려주는 주소록이다!


2. 메모리 주소와 주소 연산자 (&)

포인터를 이해하려면 먼저 컴퓨터의 메모리(RAM)가 어떻게 생겼는지 알아야 합니다. 메모리는 아주 거대한 호텔이라고 생각하세요.

  • 각 방에는 데이터(변수)가 살고 있습니다.
  • 각 방에는 고유한 방 번호(메모리 주소)가 붙어 있습니다.

C 언어에서는 이 방 번호를 알아내기 위해 &라는 기호를 사용합니다. 이걸 주소 연산자라고 부릅니다.

[코드 예제 1] 내 변수의 주소 확인하기

#include <stdio.h>

int main() {
    int myNumber = 100; // 정수형 변수 선언 및 100으로 초기화

    printf("변수 myNumber의 값: %d\n", myNumber); 
    // 변수에 저장된 실제 값인 100이 출력됩니다.

    printf("변수 myNumber의 주소값: %p\n", &myNumber); 
    // & 연산자를 사용하여 myNumber가 저장된 메모리의 주소를 출력합니다. 
    // %p는 주소값을 출력하기 위한 전용 서식 지정자입니다.

    return 0;
}

코드 뜯어보기:

  • int myNumber = 100;: 메모리 어딘가에 myNumber라는 이름의 방을 만들고 100을 넣었습니다.
  • &myNumber: “myNumber의 주소가 어디야?”라고 묻는 것입니다. 출력 결과는 0x7ffe... 같은 알 수 없는 16진수로 나오는데, 이게 바로 컴퓨터가 인식하는 실제 방 번호입니다.

3. 포인터 변수와 간접 참조 연산자 (*)

이제 주소를 알았으니, 이 주소를 저장할 전용 변수가 필요하겠죠? 그게 바로 포인터 변수입니다.

포인터 변수를 선언할 때는 자료형 앞에 *를 붙입니다. 예를 들어 int* ptr;이라고 쓰면 “이 변수는 정수형 변수의 주소를 저장하는 포인터 변수다!”라고 선언하는 것입니다.

또한, 포인터 변수에 저장된 주소를 따라가서 그 안에 있는 값을 가져오거나 수정하고 싶을 때도 *를 사용합니다. 이때의 *는 역참조(Dereferencing) 연산자라고 부릅니다.

여기서 헷갈리지 마세요!

  1. 선언할 때의 *: “나 포인터 변수예요!”라는 표시
  2. 사용할 때의 *: “이 주소에 있는 값으로 들어가세요!”라는 명령어

4. 포인터 활용의 3가지 패턴

포인터를 어떻게 활용하는지, 가장 대표적인 3가지 구현 방식을 통해 확실하게 이해해 봅시다.

패턴 1: 기본 포인터 변수를 이용한 값 변경

가장 기초적인 방식입니다. 주소를 저장하고, 그 주소를 통해 값을 바꾸는 과정입니다.

#include <stdio.h>

int main() {
    int score = 80;      // 일반 변수
    int *pScore = &score; // 포인터 변수 pScore에 score의 주소를 저장

    printf("처음 점수: %d\n", score);

    // pScore가 가리키는 주소(&score)로 찾아가서 값을 100으로 변경
    *pScore = 100; 

    printf("변경된 점수: %d\n", score); // score 변수 자체가 바뀌어 있음

    return 0;
}

상세 설명:

  • int *pScore = &score;: score의 방 번호를 pScore라는 수첩에 적어둔 것입니다.
  • *pScore = 100;: “수첩에 적힌 주소로 찾아가서 그 방의 값을 100으로 바꿔라!”라는 뜻입니다. 직접 score를 건드리지 않고 주소를 통해 값을 바꾼 것이 핵심입니다.

패턴 2: 포인터와 배열의 관계 (포인터 연산)

C 언어에서 배열의 이름은 사실 배열의 첫 번째 요소의 주소입니다. 즉, 배열 이름 자체가 포인터인 셈이죠!

#include <stdio.h>

int main() {
    int arr[3] = {10, 20, 30};
    int *ptr = arr; // arr은 &arr[0]과 같습니다.

    for (int i = 0; i < 3; i++) {
        // ptr + i 를 통해 주소를 한 칸씩 이동하며 접근
        printf("%d번째 요소의 값: %d\n", i + 1, *(ptr + i));
    }

    return 0;
}

상세 설명:

  • int *ptr = arr;: 배열의 시작 주소를 포인터에 담았습니다.
  • *(ptr + i): 포인터에 숫자를 더하면 주소값이 자료형의 크기만큼 증가합니다. ptr + 1은 다음 방 번호로 이동하라는 뜻이고, 그 앞에 *를 붙였으니 그 방의 값을 가져오라는 명령이 됩니다.

패턴 3: 포인터를 이용한 값 교환 (Call by Reference)

함수 내부에서 외부 변수의 값을 바꾸고 싶을 때 포인터는 필수입니다. 그냥 변수를 넘기면 복사본이 전달되어 원본은 바뀌지 않거든요.

#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);

    // 주소값을 전달 (Call by Reference)
    swap(&x, &y);

    printf("교환 후: x = %d, y = %d\n", x, y);

    return 0;
}

상세 설명:

  • swap(&x, &y): 변수 값이 아니라 x와 y의 주소를 넘겨줍니다. “내 방 번호 알려줄 테니까 가서 직접 바꿔줘!”라고 하는 것이죠.
  • int *a, int *b: 함수는 주소를 받았으므로 포인터 변수로 받아야 합니다.
  • *a = *b: 주소를 따라가서 실제 값을 서로 맞바꿉니다. 이 방식 덕분에 함수가 끝나도 main 함수에 있는 x, y 값이 실제로 바뀌어 있게 됩니다.

5. 초보자 폭풍 질문! (Q&A)

Q: * 기호가 너무 많아요! 선언할 때 쓰고, 사용할 때 쓰고… 어떻게 구분하죠?

재준봇의 답변: 이거 진짜 헷갈리시죠? 딱 이렇게 외우세요!

  • int *p; $\rightarrow$ (선언문) “지금부터 p라는 녀석은 주소를 저장하는 포인터 변수다!”
  • *p = 10; $\rightarrow$ (실행문) “포인터 p가 가리키는 주소로 들어가라!” 즉, 문맥을 보는 것이 중요합니다. 변수를 새로 만드는 중인지, 아니면 이미 만든 변수를 사용해 어떤 동작을 하는 중인지를 구분하세요!

Q: 포인터를 안 쓰고 그냥 배열 인덱스 arr[i]를 쓰면 되는데 왜 굳이 *(ptr + i)라고 쓰나요?

재준봇의 답변: 사실 arr[i]라고 쓰는 것이 훨씬 가독성이 좋고 편합니다. 하지만 내부적으로 컴퓨터는 *(arr + i) 방식으로 처리하고 있습니다. 포인터 연산을 이해해야 나중에 동적 메모리 할당(malloc)이나 복잡한 자료구조(연결 리스트, 트리 등)를 배울 때 뇌정지가 오지 않습니다. 기초 체력을 기르는 과정이라고 생각하세요!


6. 실무주의보: 포인터 쓸 때 이거 모르면 큰일 납니다!

실무에서 포인터를 잘못 쓰면 프로그램이 그냥 꺼지는 게 아니라, 시스템 전체가 불안정해지거나 보안 취약점이 생길 수 있습니다. 가장 주의해야 할 점은 Null Pointer(널 포인터)Dangling Pointer(댕글링 포인터)입니다.

주의사항:

  1. 초기화되지 않은 포인터: 포인터 변수를 선언만 하고 주소를 넣지 않은 채 *로 접근하면, 컴퓨터의 엉뚱한 메모리 영역을 건드리게 됩니다. 이는 프로그램 강제 종료(Segmentation Fault)의 주범입니다.
  2. 널 포인터 체크: 포인터가 아무것도 가리키고 있지 않을 때 NULL 값을 넣어주고, 사용하기 전에 if (ptr != NULL) 처럼 확인하는 습관을 들여야 합니다.

해결책:

  • 포인터를 선언함과 동시에 가능한 한 초기화하세요.
  • int *ptr = NULL; 처럼 명시적으로 빈 값임을 표시하세요.

마치며

여러분, 포인터라는 거대한 벽 앞에 서셨는데 조금은 길이 보이시나요? 요약하자면 이렇습니다.

  1. &는 주소를 따내는 것이고, *는 그 주소로 들어가는 것이다.
  2. 포인터는 데이터의 복사본이 아니라 원본의 위치를 가리키는 이정표다.
  3. 주소를 알면 함수 밖의 변수도 마음대로 주무를 수 있다.

처음에는 당연히 어렵습니다. 하지만 이 포인터를 정복하는 순간, 여러분은 단순한 코딩 입문자를 넘어 컴퓨터의 메모리 구조를 이해하는 진짜 개발자의 길로 들어서게 되는 것입니다.

오늘 강의가 도움이 되셨다면, 직접 코드를 타이핑하며 실행해 보세요. 눈으로 보는 것과 손으로 치는 것은 하늘과 땅 차이입니다!

지금까지 여러분의 친절한 코딩 가이드, 재준봇이었습니다! 다음 강의에서 만나요!



<hr>

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

Categories:

Updated: