Skip to content

27. 포인터(Pointer)


1. 키워드

  • 포인터(Pointer)
  • 주소 연산자
  • 포인터 변수
  • 역참조(Dereference) 연산자
  • void 타입 포인터
  • 단일 포인터와 다중 포인터


2. 포인터 사용하기

  • 지금까지 값을 저장할 때 변수를 사용했다.


int num1 = 10;


  • 이렇게 사용된 변수는 컴퓨터의 메모리에 생성된다.
  • 즉, 메모리에 일정한 공간을 확보해 두고 원하는 값을 저장하거나 가져오는 방식이다.


001


  • 보통 변수는 num1과 같이 이름으로 사용하지만, 메모리의 특정 장소에 위치하고 있으므로 메모리 주소로도 표현할 수 있다.
  • 일상 생활에서 집을 구분할 때 주소를 사용하는 것과 같은 원리이다.


  • 다음은 변수의 메모리 주소를 구한다.


#include <stdio.h>

int main()
{
    int num1 = 10;

    printf("%p\n", &num1); // num1의 메모리 주소를 출력
                           // 컴퓨터마다, 실행할 때마다 달라짐

    return 0;
}

// 0x3041272c8


  • 변수의 메모리 주소를 구할 때는 변수 앞에 &(주소 연산자)를 붙이면 된다.
  • 여기서는 &num1과 같이 num1 앞에 &를 붙여서 변수 num1의 메모리 주소를 구했다.
  • 메모리 주소는 0x3041272c8과 같이 16진수 형태이며 printf에서 서식 지정자 &p(Pointer의 약어)를 사용하여 출력한다.
  • 물론 16진수로 출력하는 %x, %X를 사용해도 된다.
  • 이때 메모리 주소는 고정된 것이 아니라 컴퓨터마다, 실행할 때마다 달라진다.


002


32비트, 64비트와 메모리 주소

  • 다음과 같이 시스템이 32비트인지 64비트인지에 따라 메모리 주소의 범위도 달라진다.


1] 32비트

  • 16진수 8자리
  • 0x00000000 ~ 0xFFFFFFFF

2] 64비트

  • 16진수 16자리
  • 0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF
  • 64비트 메모리 주소는 0x0000000000000000`처럼 8자리씩 끊어서 ```를 붙이기도 한다.


리눅스, OSX에서 %p*

  • 리눅스, OSX에서 서식 지정자 %p를 사용하면 메모리 주소 008AF7FC0x8af7fc와 같이 앞에 0x가 붙고, A ~ F는 소문자로 출력된다.
  • 또한 높은 자릿수의 0은 생략된다.


3. 포인터 변수 선언하기

  • C에서 메모리 주소는 포인터 변수에 저장한다.


  • 다음과 같이 포인터 변수는 *를 사용하여 선언한다.
  • 포인터 변수는 줄여서 포인터라고 부르기도 한다.


자료형 *포인터이름;
포인터 = &변수;
#include <stdio.h>

int main()
{
    int *numPtr;   // 포인터 변수 선언
    int num1 = 10; // int 타입 변수를 선언하고 10 저장

    numPtr = &num1; // num1의 메모리 주소를 포인터 변수에 저장

    printf("%p\n", numPtr); // 포인터 변수 numPtr의 값 출력
                            // 컴퓨터마다, 실행할 때마다 달라짐

    printf("%p\n", &num1); // 변수 num1의 메모리 주소 출력
                           // 컴퓨터마다, 실행할 때마다 달라짐

    return 0;
}

// 0x3041272bc
// 0x3041272bc


  • 포인터 변수를 선언할 때는 자료형 뒤에 *(애스터리스크)를 붙인다.
  • *의 위치에 따른 차이는 없으며 모두 같은 뜻이다.


int* numPtr;  // 자료형 쪽에 *를 붙임
int * numPtr; // 자료형과 변수 가운데 *를 넣음
int *numPtr;  // 변수 쪽에 *를 붙임


  • 포인터 변수를 선언했으면 다음과 같이 &로 변수의 주소를 구해서 포인터 변수에 저장한다.


numPtr = &num1; // num1의 메모리 주소를 포인터 변수에 저장


  • 이제 printf로 포인터 numPtr의 값을 출력해 보면 변수 num1의 메모리 주소가 나온다.
  • 즉, 포인터와 메모리 주소는 같은 의미이다.


printf("%p\n", numPtr); // 포인터 변수 numPtr의 값 출력
                        // 컴퓨터마다, 실행할 때마다 달라짐

printf("%p\n", &num1); // 변수 num1의 메모리 주소 출력
                       // 컴퓨터마다, 실행할 때마다 달라짐


  • 포인터 변수를 선언할 때는 자료형을 알려주고 *를 붙이는 방식을 사용한다.
  • 만약 변수가 int 타입이면 이 변수의 메모리 주소를 저장하는 포인터는 int *이어야 한다.
  • 여기서 int *는 영어로 pointer to int라고 읽는데 int 타입 공간을 가리키는 포인터라는 뜻이다.
  • 간단하게 int 타입 포인터라고도 부른다.


003


  • 즉, 다음과 같이 포인터는 메모리의 특정 위치를 가리킬 때 사용한다.


004


  • 이제 다음 코드를 그림으로 표현해 보자.


int *numPtr; // 포인터 변수 선언
int num1 = 10;

numPtr = &num1; // num1의 메모리 주소를 포인터 변수에 저장


  • numPtr10이 저장된 메모리 공간을 가리킨다.
  • 즉, 변수 num1이 있는 공간을 가리키게 되는 것이다.


005


32비트와 64비트와 포인터 크기

  • 다음과 같이 시스템이 32비트인지 64비트인지에 따라 포인터의 크기가 달라진다.


1] 32비트

  • 16진수 8자리
  • 0x00000000 ~ 0xFFFFFFFF

2] 64비트

  • 16진수 16자리
  • 0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF


  • sizeof로 포인터의 크기를 구해 보면 32비트에서는 4바이트, 64비트에서는 8바이트가 나온다.


sizeof(포인터)
sizeof(자료형 *)
int *numPtr;

printf("%u\n", sizeof(numPtr)); // 32비트에서 int 타입 포인터는 4바이트

printf("%u\n", sizeof(char *)); // 32비트에서 char 타입 포인터는 4바이트
int *numPtr;

printf("%lu\n", sizeof(numPtr)); // 64비트에서 int 타입 포인터는 8바이트

printf("%lu\n", sizeof(char *)); // 64비트에서 char 타입 포인터는 8바이트


4. 역참조 연산자 사용하기

  • 포인터 변수에는 메모리 주소가 저장되어 있다.
  • 이때 메모리 주소가 있는 곳으로 이동해서 값을 가져오고 싶다면 역참조 연산자 *를 사용한다.


*포인터
#include <stdio.h>

int main()
{
    int *numPtr;   // 포인터 변수 선언
    int num1 = 10; // 정수형 변수를 선언하고 10 저장

    numPtr = &num1; // num1의 메모리 주소를 포인터 변수에 저장

    printf("%d\n", *numPtr); // 역참조 연산자로 num1의 메모리 주소에 접근하여 값을 가져옴

    return 0;
}

// 10


  • 다음과 같이 numPtr 앞에 *를 붙이면 numPtr에 저장된 메모리 주소로 가서 값을 가져온다.
  • 여기서는 numPtrnum1의 메모리 주소를 저장하고 있으므로 num1의 값인 10이 출력된다.


printf("%d\n", *numPtr); // 역참조 연산자로 num1의 메모리 주소에 접근하여 값을 가져옴


  • 즉, 포인터는 변수의 주소만 가리키며, 역참조는 주소에 접근하여 값을 가져온다.


006


포인터 선언과 역참조

  • 포인터를 선언할 때도 *를 사용하고 역참조를 할 때도 *를 사용한다.
  • 같은 * 기호를 사용해서 헷갈리기 쉽지만 선언과 사용을 구분해서 생각하면 된다.
  • 즉, 포인터를 선언할 때 *는 "이 변수는 포인터이다"라고 알려주는 역할이고, 포인터에 사용할 때 *는 "포인터의 메모리 주소를 역참조한다"라는 뜻이다.


int *numPtr;             // 포인터. 포인터를 선언할 때 *
printf("%d\n", *numPtr); // 역참조. 포인터에 사용할 때 *


  • 이번에는 포인터 변수에 역참조 연산자를 사용한 뒤 값을 저장(할당)해 보자.


*포인터 = ;
#include <stdio.h>

int main()
{
    int *numPtr;   // 포인터 변수 선언
    int num1 = 10; // 정수형 변수를 선언하고 10 저장

    numPtr = &num1; // num1의 메모리 주소를 포인터 변수에 저장

    *numPtr = 20; // 역참조 연산자로 메모리 주소에 접근하여 20을 저장

    printf("%d\n", *numPtr); // 역참조 연산자로 메모리 주소에 접근하여 값을 가져옴
    printf("%d\n", num1);    // 실제 num1의 값도 바뀜

    return 0;
}

// 20
// 20


  • 역참조 연산자는 값을 가져올 수도 있고 값을 저장할 수도 있다.
  • 여기서는 *numPtr = 20;과 같이 numPtr에 저장된 메모리 주소에 접근하여 20을 저장했다.
  • 따라서 printf*numPtr을 출력해 보면 20이 나온다.
  • 또 한 가지 중요한 점은 *numPtr = 20;으로 20을 저장한 뒤 printf로 변수 num1의 값을 출력해 보면 20이 나온다는 것이다.
  • 왜냐하면 numPtr에는 num1의 메모리 주소가 저장되어 있으므로 역참조 연산자로 값을 저장하면 결국 num1에 저장하게 된다.


007


  • 역참조 연산자는 자료형을 바꾸는 효과를 낸다.
  • 즉, int *numPtr;에서 *numPtr처럼 역참조하면 pointer to int에서 pointer to를 제거하여 그냥 int로 만든다.


  • 만약 포인터 numPtr에 변수 num1을 할당한다면 역참조 연산자로 자료형을 맞춰주면 된다.


int *numPtr;
int num1 = 10;

numPtr = num1; // 컴파일 경고. numPtr은 int 타입 포인터 타입이고, num1은 int 타입이라 자료형이 일치하지 않음

*numPtr = num1; // *numPtr은 int 타입이고, num1도 int 타입이라 자료형이 일치함


  • 물론 주소 연산자 &도 자료형을 맞춰주는 역할을 한다.


int *numPtr;
int num1;

numPtr = &num1; // numPtr은 int 타입 포인터 타입이고, &num1은 int 타입 변수의 주소이므로 자료형이 일치함
                // numPtr은 pointer to int, &num1은 address of int이므로 자료형이 일치함


  • 즉, pointer to intaddress of int는 자료형이 같다.


008


  • 이제 변수, 주소 연산자, 역참조 연산자, 포인터의 차이를 정리해 보자.


009


1] 변수

  • 메모리 주소를 몰라도 값을 가져오거나 저장할 수 있다.
  • 그냥 변수에 값을 할당하거나 그대로 출력하면 된다.

2] 주소 연산자 &

  • 변수의 메모리 주소를 구한다.
  • 위의 그림에서 10을 감싸고 있는 상자는 메모리 공간을 뜻하는데, 주소 연산자는 메모리 공간이 어디에 있는지 위치만 알아낼 수 있다.

3] 역참조 연산자 *

  • 위의 그림에서 보면 상자 안까지 들어가서 값을 가져오거나 저장한다.
  • 즉, 메모리 주소를 알고 있으므로 메모리 주소를 거쳐서 그 안에 있는 값을 가져오거나 저장한다.

4] 포인터

  • 변수의 메모리 주소만 가리킨다.
  • 따라서 포인터는 메모리 공간이 어디에 있는지 위치만 알고 있다.


5. 다양한 자료형의 포인터 선언하기

  • 이번에는 다양한 자료형의 포인터를 선언해 보자.


#include <stdio.h>

int main()
{
    long long *numPtr1; // long long 타입 포인터 선언
    float *numPtr2;     // float 타입 포인터 선언
    char *cPtr1;        // char 타입 포인터 선언

    long long num1 = 10;
    float num2 = 3.5f;
    char c1 = 'a';

    numPtr1 = &num1; // num1의 메모리 주소 저장
    numPtr2 = &num2; // num2의 메모리 주소 저장
    cPtr1 = &c1;     // c1의 메모리 주소 저장

    printf("%lld\n", *numPtr1);
    printf("%f\n", *numPtr2);
    printf("%c\n", *cPtr1);
}

// 10
// 3.500000
// a


  • C에서는 사용할 수 있는 모든 자료형은 포인터로 만들 수 있다.
  • 포인터에 저장되는 메모리 주솟값은 정수형으로 동일하지만 선언하는 자료형에 따라 메모리에 접근하는 방법이 달라진다.
  • 즉, 다음과 같이 포인터를 역참조하면 선언한 자료형의 크기에 맞춰서 값을 가져오거나 저장하게 된다.


010


  • 즉, long long 타입 포인터는 8바이트 크기만큼 값을 가져오거나 저장하고, char 타입 포인터는 1바이트 크기만큼 값을 가져오거나 저장한다.


상수와 포인터

  • 포인터에도 const 키워드를 붙일 수 있는데, const의 위치에 따라 특성이 달라진다.


  • 먼저 상수를 가리키는 포인터(pointer to constant)이다.


const int num1 = 10; // int 타입 상수
const int *numPtr;   // int 타입 상수를 가리키는 포인터. int const *numPtr도 같음

numPtr = &num1;

*numPtr = 20; // 컴파일 에러. num1이 상수이므로 역참조로 값을 변경할 수 없음


  • 먼저 num1const int이므로 이 변수의 주소를 넣을 수 있는 포인터는 const int *로 선언해야 한다.
  • 그리고 num1의 주소를 numPtr에 넣은 뒤 역참조로 값을 변경하려고 해도 num1은 상수이므로 컴파일 에러가 발생한다.
  • 즉, pointer to constant는 메모리 주소에 저장된 값을 변경할 수 없다는 뜻이다.


  • 이번에는 포인터 자체가 상수인 상황(constant pointer)이다.
  • 이때는 * 뒤에 const를 붙인다.


int num1 = 10;             // int 타입 변수
int num2 = 20;             // int 타입 변수
int *const numPtr = &num1; // int 타입 포인터 상수

numPtr = &num2; // 컴파일 에러. 포인터(메모리 주소)를 변경할 수 없음


  • numPtrnum1의 주소가 들어가 있는 상태에서 다시 num2의 주소를 넣으려고 하면 컴파일 에러가 발생한다.
  • numPtr은 포인터 자체가 상수이므로 다른 포인터(메모리 주소)를 할당할 수 없다.
  • 즉, constant pointer는 메모리 주소를 변경할 수 없다는 뜻이다.


  • 마지막으로 포인터가 상수이면서 상수를 가리키는 상황(constant pointer to constant)이다.
  • 이때는 포인터를 선언하는 자료형에도 const를 붙이고 * 뒤에도 const를 붙인다.


const int num1 = 10;             // int 타입 상수
const int num2 = 20;             // int 타입 상수
const int *const numPtr = &num1; // int 타입 상수를 가리키는 포인터 상수
                                 // int const * const numPtr도 같음

*numPtr = 30;   // 컴파일 에러. num1이 상수이므로 역참조로 값을 변경할 수 없음
numPtr = &num2; // 컴파일 에러. 포인터(메모리 주소)를 변경할 수 없음


  • 여기서는 numPtr을 역참조한 뒤 값을 변경하려고 해도 num1은 상수이므로 컴파일 에러가 발생한다.
  • 그리고 numPtr 자체도 상수이므로 num2의 주소를 넣으려고 하면 컴파일 에러가 발생한다.
  • 즉, constant pointer to constant는 메모리 주소도 변경할 수 없고 메모리 주소에 저장된 값도 변경할 수 없다는 뜻이다.


6. void 타입 포인터 선언하기

  • long long *numPtr1;이나 float *numPtr2;는 자료형이 정해진 포인터이다.
  • 하지만 C에서는 자료형이 정해지지 않은 포인터도 있다.
  • void 타입 포인터라는 포인터인데 다음과 같이 void 키워드와 *로 선언한다.


void *포인터이름;
#include <stdio.h>

int main()
{
    int num1 = 10;
    char c1 = 'a';
    int *numPtr1 = &num1;
    char *cPtr1 = &c1;

    void *ptr; // void 타입 포인터 선언

    // 포인터 자료형이 달라도 컴파일 경고가 발생하지 않음
    ptr = numPtr1; // void 타입 포인터에 int 타입 포인터 저장
    ptr = cPtr1;   // void 타입 포인터에 char 타입 포인터 저장

    // 포인터 자료형이 달라도 컴파일 경고가 발생하지 않음
    numPtr1 = ptr; // int 타입 포인터에 void 타입 포인터 저장
    cPtr1 = ptr;   // char 타입 포인터에 void 타입 포인터 저장

    return 0;
}


  • 기본적으로 C는 자료형이 다른 포인터끼리 메모리 주소를 저장하면 컴파일 경고가 발생한다.
  • 하지만 void 타입 포인터는 자료형이 정해지지 않은 특성 때문에 어떤 자료형으로 된 포인터든 모두 저장할 수 있다.
  • 반대로 다양한 자료형으로 된 포인터에도 void 타입 포인터를 저장할 수도 있다.
  • 이런 특성 때문에 void 타입 포인터는 범용 포인터라고 한다.
  • 즉, 직접 자료형을 변환하지 않아도 암시적으로 자료형이 변환되는 방식이다.


// 포인터 자료형이 달라도 컴파일 경고가 발생하지 않음
ptr = numPtr1; // void 타입 포인터에 int 타입 포인터 저장
ptr = cPtr1;   // void 타입 포인터에 char 타입 포인터 저장

// 포인터 자료형이 달라도 컴파일 경고가 발생하지 않음
numPtr1 = ptr; // int 타입 포인터에 void 타입 포인터 저장
cPtr1 = ptr;   // char 타입 포인터에 void 타입 포인터 저장


011


  • 단, void 타입 포인터는 자료형이 정해지지 않았으므로 값을 가져오거나 저장할 크기도 정해지지 않는다.
  • 따라서 void 타입 포인터는 역참조를 할 수 없다.


ptr = numPtr1;      // void 타입 포인터에 int 타입 포인터 저장
printf("%d", *ptr); // 컴파일 에러. void 타입 포인터는 역참조할 수 없음

ptr = cPtr1;        // void 타입 포인터에 char 타입 포인터 저장
printf("%c", *ptr); // 컴파일 에러. void 타입 포인터는 역참조할 수 없음


  • void 키워드로는 변수를 선언할 수도 없다.


void v1; // 컴파일 에러. void 타입으로는 변수를 선언할 수 없음


  • void 타입 포인터는 되는 게 별로 없어 보이지만 실제로 C에서는 다양한 형태로 사용되고 있다.
  • 예를 들자면 함수에서 다양한 자료형을 받아들일 때, 함수의 반환 포인터를 다양한 자료형으로 된 포인터에 저장할 때, 자료형을 숨기고 싶을 때 사용한다.


7. 다중 포인터 사용하기

  • 이번에는 포인터의 메모리 주소를 저장하는 포인터의 포인터(이중 포인터)를 선언해 보자.


자료형 **포인터이름;
#include <stdio.h>

int main()
{
    int *numPtr1;  // 단일 포인터 선언
    int **numPtr2; // 이중 포인터 선언
    int num1 = 10;

    numPtr1 = &num1; // num1의 메모리 주소 저장

    numPtr2 = &numPtr1; // numPtr1의 메모리 주소 저장

    printf("%d\n", **numPtr2); // 포인터를 두 번 역참조하여 num1의 메모리 주소에 접근

    return 0;
}

// 10


  • 포인터도 실제로는 변수이기 때문에 메모리 주소를 구할 수 있다.
  • 하지만 포인터의 메모리 주소는 일반 포인터에 저장할 수 없고, int **numPtr2;처럼 이중 포인터에 저장해야 한다.
  • int **numPtr2;를 영어로 읽으면 pointer to pointer to int가 된다.
  • 여기서 이중 포인터 numPtr2를 끝까지 따라가서 실제 값을 가져오려면 **numPtr2처럼 변수 앞에 역참조 연산자를 두 번 사용하면 된다.
  • 즉, 역참조를 두 번 하므로 numPtr2numPtr1num1과 같은 모양이 되고 num1의 값을 가져올 수 있다.


012


  • 포인터를 선언할 때 *의 개수에 따라서 삼중 포인터, 사중 포인터 그 이상도 만들 수 있다.
  • 마찬가지로 역참조를 할 때도 *를 세 번, 네 번 또는 그 이상 사용할 수 있다.


8. 잘못된 포인터 사용

  • 포인터는 메모리 주소를 저장하는 용도이므로 다음과 같이 값을 직접 저장하면 안 된다.


#include <stdio.h>

int main()
{
    int *numPtr = 0x100; // 포인터에 0x100을 직접 저장

    printf("%d\n", *numPtr); // 0x100은 잘못된 메모리 주소이므로 실행 에러

    return 0;
}


  • 소스 코드를 컴파일해서 실행해 보면 제대로 실행이 되지 않는다.
  • 왜냐하면 int *numPtr = 0x100;과 같이 포인터에 직접 0x100을 저장했을 때 메모리에서 0x100은 잘못된 주솟값이기 때문이다.


013


  • 이 상태에서 numPtr을 역참조하여 메모리 주소에 접근해 봐야 에러만 발생한다.


  • 만약 실제로 존재하는 메모리 주소라면 포인터에 직접 저장할 수 있다.


int *numPtr = 0x00CCFC2C; // 실제로 존재하는 메모리 주소라면 저장할 수 있음


  • 보통 임베디드 시스템이나 마이크로 프로세서에서 제공하는 메모리 주소를 사용할 때 포인터에 직접 저장하기도 한다.

References