C, C++ 언어

C언어 기초 14. 포인터 기초

게임플밍마스터 2025. 10. 20. 17:29

 

안녕하세요. c언어를 열심히 공부하고 있는 여러분, 게임플밍마스터 입니다. 

앞에서 배열이라는 중요한 데이터 구조를 익혔으니, 이제 C언어의 가장 강력하고 핵심적인 기능이자 많은 초심자들이 가장 어려워하는 '포인터'의 세계로 조심스럽게 첫발을 내디뎌 보겠습니다.

포인터는 어렵다는 악명이 높지만, 그 개념을 차근차근 이해하면 C언어가 메모리를 얼마나 효율적으로 다루는지, 그리고 배열과 함수가 내부적으로 어떻게 동작하는지에 대한 깊은 통찰력을 얻게 됩니다. 두려워하지 마세요. 아파트와 집 주소 비유를 통해 최대한 쉽게 설명해 하겠습니다.


지금까지 우리는 int num = 10; 과 같은 코드를 통해 num이라는 변수에 10이라는 값을 저장해 왔습니다. 하지만 컴퓨터 내부에서는 정확히 어떤 일이 일어날까요? 컴퓨터는 이 num 변수를 위해 메모리의 특정 공간을 할당합니다. 마치 아파트 단지에 num이라는 이름의 입주자가 '101호'에 이사 온 것과 같습니다.

이때 '101호'에 해당하는, 메모리 상의 고유한 위치 값을 바로 주소(Address) 라고 합니다. 그리고 포인터(Pointer) 란, 10과 같은 일반적인 값이 아닌, 이 메모리 주소 자체를 값으로 저장하는 매우 특별한 변수입니다. 즉, 포인터는 '101호'라는 주소 정보를 담고 있는 '주소록'과 같습니다.

1. 모든 변수에게는 집 주소가 있다 - 메모리와 주소

C언어에서는 & 연산자(주소 연산자, Address-of operator)를 사용하여 특정 변수의 메모리 주소 값을 알아낼 수 있습니다. 이 연산자는 scanf를 사용할 때 이미 만나본 적이 있죠?

#include <stdio.h>

int main(void) 
{
    int num = 10;

    printf("변수 num의 값: %d\\n", num);
    printf("변수 num의 메모리 주소: %p\\n", &num); // %p는 주소 값을 16진수로 출력하는 서식 지정자

    return 0;
}

 

실행 결과 예시: (주소 값은 컴퓨터마다, 실행할 때마다 다릅니다)

변수 num의 값: 10
변수 num의 메모리 주소: 0x7ffee1b5d8ac

&num을 통해 num 변수가 실제로 거주하는 집 주소(메모리 주소)를 확인할 수 있습니다.

 

2. 주소를 저장하는 특별한 변수, 포인터

포인터는 이 메모리 주소 값을 저장하기 위한 변수입니다. 포인터 변수를 선언할 때는, 이 포인터가 어떤 자료형의 주소를 저장할 것인지를 함께 알려줘야 합니다.

포인터 선언:

자료형 * 포인터_변수명;

 

(애스터리스크) * 기호가 바로 이 변수가 포인터임을 나타내는 표시입니다.

int* pNum; // int형 변수의 주소를 저장할 포인터 변수 pNum
double* pHeight; // double형 변수의 주소를 저장할 포인터 변수 pHeight

*pNum은 'pointer to num'과 같이, 포인터 변수 이름 앞에 p 를 붙이는 것은 흔한 코딩 관례입니다.*

이제 pNum 포인터에 num 변수의 주소를 저장해 봅시다.

int num = 10;
int* pNum = NULL; // 포인터는 선언 시 NULL로 초기화하는 것이 안전합니다. (아래에서 설명)

pNum = &num; // num의 주소(&num)를 포인터 pNum에 대입

 

이 코드가 실행된 후 메모리 상태를 그림으로 나타내면 다음과 같습니다.

  • num 변수: 값으로 10을 가짐 (예: 101호에 거주)
  • pNum 변수: 값으로 num의 주소, 즉 0x...ac와 같은 주소 값 자체를 가짐 (주소록에 '101호'라고 적어둠)

 

3. 주소를 통해 값에 접근하기 - 역참조 연산자

포인터의 진짜 힘은 주소를 통해 원본 데이터에 접근할 수 있다는 데 있습니다. 이때 사용하는 것이 바로 역참조 연산자(Dereference/Indirection operator) * 입니다.

[경고!] * 기호의 두 가지 얼굴

*는 상황에 따라 완전히 다른 두 가지 의미로 사용되어 초보자를 혼란스럽게 합니다.

  1. 선언 시: int *  p; → "p는 int형 데이터를 가리키는 포인터 변수다." (타입을 명시)
  2. 사용 시: *p = 20; → "p가 가리키고 있는 주소로 찾아가서 그 공간의 값을 20으로 바꿔라." (값을 읽거나 쓰는 행위)

 

예제: 포인터로 값 읽고 쓰기

#include <stdio.h>

int main(void) 
{
    int num = 10;
    int *pNum = &num; // pNum은 num의 주소를 가리킴

    printf("num의 주소: %p\\n", &num);
    printf("pNum에 저장된 값(주소): %p\\n", pNum);

    // 역참조 연산자(*)를 사용해 pNum이 가리키는 곳의 값을 읽기
    printf("pNum이 가리키는 곳의 값: %d\\n", *pNum);

    // 역참조 연산자(*)를 사용해 pNum이 가리키는 곳의 값을 바꾸기
    *pNum = 25;
    printf("포인터로 값을 바꾼 후...\\n");
    printf("pNum이 가리키는 곳의 값: %d\\n", *pNum);
    printf("원본 변수 num의 값: %d\\n", num); // num의 값도 25로 바뀐 것을 확인!

    return 0;
}

 

실행 결과:

num의 주소: 0x7ffee...
pNum에 저장된 값(주소): 0x7ffee...
pNum이 가리키는 곳의 값: 10
포인터로 값을 바꾼 후...
pNum이 가리키는 곳의 값: 25
원본 변수 num의 값: 25

 

*pNum = 25; 코드는 "pNum이 들고 있는 주소로 찾아가서, 그곳의 값을 25로 바꿔라"는 의미입니다. 그 결과, 원본 변수인 num의 값이 직접 바뀐 것을 볼 수 있습니다. 이것이 포인터의 핵심입니다.

 

4. 그래서 포인터는 어디에 쓸까요?

scanf 함수의 비밀이 바로 여기에 있습니다. main 함수에 있는 age 변수의 값을 바꾸기 위해, scanf 함수는 age 변수의 주소를 알아야만 했습니다. 그래서 우리는 scanf("%d", &age); 와 같이 &를 붙여 age의 주소를 인자로 넘겼던 것입니다. scanf 함수는 내부적으로 포인터를 사용해 우리가 넘겨준 주소의 값을 변경해 주었던 것이죠. (함수와 포인터의 관계는 나중에 더 자세히 배웁니다.)

이 외에도 포인터는 다음과 같은 중요한 일들을 합니다.

  • 효율적인 배열 및 문자열 관리 (다음 장에서 배웁니다)
  • 프로그램 실행 중에 필요한 만큼 메모리를 할당 받는 동적 메모리 할당
  • 복잡한 자료 구조(연결 리스트, 트리 등) 구현

 

5. 가리키는 곳이 없을 때 - NULL 포인터

아무런 주소도 담고 있지 않은, '비어 있음'을 나타내는 포인터가 필요할 때가 있습니다. 이때 사용하는 것이 바로 NULL 포인터입니다. NULL은 '아무것도 가리키고 있지 않다'는 의미의 특별한 값입니다.

선언만 하고 초기화되지 않은 포인터는 쓰레기 주소 값을 담고 있어 위험할 수 있습니다. 따라서 아직 가리킬 대상이 정해지지 않은 포인터는 NULL로 초기화하는 것이 안전한 습관입니다.

 

int *ptr = NULL;

 

6. [도전! 프로그래밍] 포인터로 변수 값 바꾸기

포인터의 기본 개념과 연산자(&, *) 사용법을 익히기 위한 연습 문제입니다.

요구 사항:

  1. int형 변수 a에 10을, double형 변수 b에 3.14를 저장합니다.
  2. int형 포인터 pa와 double형 포인터 pb를 선언합니다.
  3. pa는 a를, pb는 b를 가리키도록 만듭니다.
  4. 오직 포인터 변수 pa와 pb만을 사용해서 a와 b의 값을 화면에 출력하세요.
  5. 오직 포인터 변수 pa와 pb만을 사용해서 a의 값을 100으로, b의 값을 2.71로 변경하세요.
  6. 마지막으로, 원본 변수 a와 b의 값을 직접 출력하여 값이 정말로 바뀌었는지 확인하세요.

이번 장 정리

  • 모든 변수는 메모리 상에 고유한 주소를 가집니다.
  • 포인터는 이 메모리 주소를 값으로 저장하는 특별한 변수입니다.
  • & (주소 연산자): 변수의 주소 값을 가져옵니다.
  • *(역참조 연산자): 포인터가 가리키는 주소에 있는 값에 접근합니다.
  • *기호는 선언 시에는 '포인터 타입'을, 사용 시에는 '역참조'를 의미하는 두 얼굴을 가집니다.
  • 아무것도 가리키지 않는 포인터는 NULL로 초기화하는 것이 안전합니다.

 

포인터라는 새로운 세계에 첫발을 내디딘 것을 축하합니다! 아직은 낯설고 어렵게 느껴질 수 있습니다. 다음 장에서는 우리가 이미 익숙하게 알고 있는 '배열'이 사실은 포인터와 얼마나 가깝고 친한 사이인지 배우면서, 포인터에 대한 이해를 한층 더 높여보겠습니다.

이해가 되지 않는 부분은 댓글로 질문하세요 ~