[C 시리즈 8편] 포인터 기초와 주소 감각 익히기

English version

7편에서 배열과 문자열로 연속된 값을 다뤘다면, 이번 글에서는 그 값들이 실제로 메모리에 어디에 저장돼 있는지 살피는 포인터의 세계로 들어갑니다. 포인터는 "주소를 값처럼 다루는 변수"입니다. 어렵게 느껴지지만, 주소를 찍어 보고 값을 따라가다 보면 차근차근 감을 잡을 수 있습니다.

이번 글에서 새로 나오는 용어

  1. 포인터 (Pointer): 다른 변수의 메모리 주소를 저장하는 변수
  2. 주소 연산자 (Address-of Operator): 변수 앞에 붙이는 &, 해당 변수의 주소를 구합니다.
  3. 역참조 (Dereference): 포인터 앞에 *를 붙여 그 주소에 저장된 실제 값을 읽는 동작
  4. 널 포인터 (Null Pointer): 어떤 주소도 가리키지 않는 포인터, 보통 NULL 또는 0으로 표현
  5. 포인터 타입 (Pointer Type): 포인터가 어떤 자료형의 주소를 보관하는지 나타내는 정보 (int *, char * 등)

핵심 개념

학습 메모

  • 소요 시간: 70분 내외
  • 준비물: 배열 길이 계산과 함수 호출을 편하게 할 수 있는 상태
  • 학습 목표: 주소 출력, 역참조, 포인터 매개변수 사용 패턴 이해하기

포인터를 이해할 때는 "주소"와 "값"을 명확히 구분해야 합니다. int a = 10;이면 a는 값 10을 저장하고, &a는 10이 저장된 메모리 주소를 나타냅니다. 포인터 변수는 바로 그 주소를 저장합니다.

처음에는 아래 세 줄을 함께 비교해서 읽으면 훨씬 덜 헷갈립니다.

  • int *ptr; : 정수 주소를 저장할 포인터 변수 선언
  • ptr = &number; : number의 주소를 포인터에 저장
  • *ptr : 포인터가 가리키는 실제 값 읽기 또는 쓰기

코드로 따라하기

변수 주소 출력하기

#include <stdio.h>

int main(void) {
    int score = 90;

    printf("score 값 = %d\n", score);
    printf("score 주소 = %p\n", (void *)&score);
    return 0;
}

%p는 주소를 16진수로 출력합니다. &score 앞에 (void *)를 붙인 이유는 %pvoid *를 기대하기 때문입니다.

포인터 선언과 역참조

#include <stdio.h>

int main(void) {
    int number = 42;
    int *ptr = &number;

    printf("ptr가 가리키는 값 = %d\n", *ptr);

    *ptr = 100;
    printf("number = %d\n", number);
    return 0;
}

int *ptr는 "정수형 주소를 가리키는 포인터"라는 뜻입니다. *ptr을 사용하면 포인터가 가리키는 위치의 값을 읽거나 쓸 수 있습니다. 포인터를 통해 값을 바꾸면 원래 변수도 함께 바뀝니다.

포인터와 함수 인자

#include <stdio.h>

void set_to_zero(int *value) {
    if (value != NULL) {
        *value = 0;
    }
}

int main(void) {
    int counter = 5;
    set_to_zero(&counter);
    printf("counter = %d\n", counter);
    return 0;
}

set_to_zero는 주소를 받아 직접 값을 바꿉니다. 포인터를 사용하면 함수가 외부 변수를 수정할 수 있지만, 동시에 잘못된 주소를 건드릴 위험도 있으므로 널 포인터인지 먼저 확인하는 습관을 들입니다.

널 포인터 활용하기

#include <stdio.h>

int main(void) {
    int *ptr = NULL;

    if (ptr == NULL) {
        printf("아직 유효한 주소를 가리키지 않습니다.\n");
    }

    return 0;
}

포인터를 선언하고 나서 바로 주소를 넣지 못할 경우에는 NULL로 초기화해 두면, 나중에 사용하려 할 때 실수로 쓰지 않았는지 쉽게 판단할 수 있습니다. NULL은 "유효한 메모리 위치"가 아니라 "아직 아무것도 가리키지 않음"을 뜻하므로, 역참조하면 안 됩니다.

배열과 포인터의 첫 만남

#include <stdio.h>

int main(void) {
    int scores[3] = {10, 20, 30};
    int *p = scores; // &scores[0]와 동일

    printf("첫 번째 값 = %d\n", *p);
    printf("두 번째 값 = %d\n", *(p + 1));
    return 0;
}

배열 이름은 대부분의 표현식에서 첫 요소의 주소로 바뀝니다. 아직 포인터 산술을 깊이 다루지는 않지만, *(p + 1)scores[1]와 같은 값을 가져온다는 사실만 기억해 둡니다.

실전 예시: 사용자 입력 검증

#include <stdio.h>

int read_positive(int *out_value) {
    int temp;

    if (scanf("%d", &temp) != 1) {
        return 0;
    }

    if (temp <= 0) {
        return 0;
    }

    *out_value = temp;
    return 1;
}

int main(void) {
    int number;

    printf("양수를 입력하세요: ");
    if (read_positive(&number)) {
        printf("입력한 값: %d\n", number);
    } else {
        printf("양수만 허용됩니다.\n");
    }

    return 0;
}

함수에서 결과를 여러 개 돌려줘야 할 때 포인터 매개변수를 사용하면 편리합니다. read_positive는 성공 여부를 반환값으로 알려 주고, 실제 숫자는 포인터가 가리키는 변수에 저장합니다.

왜 중요한가

  • 포인터는 C에서 배열, 함수 인자, 동적 메모리, 구조체를 다루는 핵심 추상화입니다.
  • 주소를 출력하고 역참조하는 패턴을 익혀야 메모리 관련 버그를 읽을 수 있습니다.
  • 널 포인터 체크 습관은 프로그램이 예상치 못한 주소를 건드리는 사고를 막습니다.
  • 이후 글에서 배울 배열·포인터 관계, 동적 메모리, 구조체 등은 모두 포인터 감각 위에서 작동합니다.

실습

  • 따라 하기: ptr 예제를 직접 실행하고 number 값이 어떻게 바뀌는지 확인합니다.
  • 확장하기: 두 정수의 값을 서로 바꾸는 swap 함수를 작성하고 포인터 매개변수를 사용합니다.
  • 디버깅: 널 포인터에 역참조를 시도했을 때 어떤 런타임 오류가 발생하는지 (가능하면) 실험한 뒤, 왜 그런지 설명해 봅니다.
  • 완료 기준: &, *, 널 포인터를 설명하고, 주소를 인자로 받아 값을 수정하는 함수를 직접 만들 수 있으면 됩니다.

마무리

이번 글에서는 포인터의 기본 개념과 주소·역참조 연산을 살폈습니다. 다음 글에서는 배열과 포인터가 어떤 수학적 관계를 가지는지, 그리고 포인터 산술을 어떻게 안전하게 사용하는지 이어서 다룹니다.

💬 댓글

이 글에 대한 의견을 남겨주세요