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

CodeSandbox로 이어서 실습하기

아래 샌드박스는 CodeSandbox의 Universal starter입니다. C는 터미널에서 직접 컴파일하고 다시 실행하는 흐름이 중요하니, 이번 글의 코드를 파일로 만들고 빌드-실행 사이클을 다시 따라가 보세요.

Live Practice

C Practice Sandbox

CodeSandbox

Run the starter project in CodeSandbox, compare it with the lesson code, and keep experimenting.

Universal starterCterminal
  1. starter를 fork한 뒤 hello.c 같은 실습 파일을 만든다
  2. 본문 코드를 붙여 넣고 clang 또는 gcc가 있으면 직접 컴파일한다
  3. 코드를 고친 뒤 다시 빌드하고 실행 결과를 비교한다

C는 브라우저 미리보기보다 터미널 빌드 흐름이 핵심입니다. Universal starter의 컴파일러 구성이 환경에 따라 다를 수 있으니, 먼저 clang이나 gcc 사용 가능 여부부터 확인하세요.

실습

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

마무리

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

💬 댓글

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