profile image

L o a d i n g . . .

Unit 34. 포인터 사용하기

변수는 컴퓨터의 메모리에 생성된다. 메모리에 일정한 공간을 확보해두고 원하는 값을 저장하거나 가져오는 방식이다. 보통 변수는 메모리의 특정 장소에 위치하고 있으므로 메모리 주소로도 표현할 수 있다.

 

변수의 메모리 주소를 구할 때는 변수 앞에 & (주소 연산자)를 붙이면 된다. 

#include <stdio.h>

int main()
{
    int num1 = 10;

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

    return 0;
}

메모리 주소는 008AF7FC과 같이 16진수 형태이며 printf에서 서식 지정자 %p를 사용하여 출력한다.

물론 16진수로 출력하는 %x, %X를 사용해도 된다.

 

34.1 포인터 변수 선언하기

C 언어에서 메모리 주소는 포인터(pointer) 변수에 저장한다.

포인터 변수는 *를 사용하여 선언한다.

 - 자료형 *포인터이름;

 - 포인터 = &변수;

#include <stdio.h>

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

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

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

    return 0;
}

포인터 변수를 선언할 때는 자료형 뒤에 * (Asterisk, 애스터리스크)를 붙인다. 포인터 변수를 선언했으면 위와 같이 &로 변수의 주소를 구해서 포인터 변수에 저장한다.

 

포인터 변수를 선언할 때는 자료형을 알려주고 *를 붙이는 방식을 사용한다. 이때 자료형은 변수의 자료형을 따른다.

즉, int *는 영어로 pointer to int라고 읽는데 int형 공간을 가리키는 포인터라는 뜻이다.

 

 

포인터에 변수의 메모리 주소 할당

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

 

 

34.2 역참조 연산자 사용하기

포인터 변수에는 메모리 주소가 저장되어 있다.

이때 메모리 주소가 있는 곳으로 이동해서 값을 가져오고 싶다면 역참조(dereference) 연산자 *를 사용한다.

 - *포인터

#include <stdio.h>

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

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

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

    return 0;
}

역참조 연산자 *는 포인터 앞에 붙인다. 다음과 같이 numPtr 앞에 *를 붙이면 numPtr에 저장된 메모리 주소로 가서 값을 가져온다. 

 

* 포인터 선언과 역참조

포인터를 선언할 때도 *를 사용하고 역참조를 할 때도 *를 사용한다.

포인터를 선언할 때 * "이 변수가 포인터다"라고 알려주는 역할이고, 포인터에 사용할 때 * "포인터의 메모리 주소를 역참조하겠다"라는 뜻이다.

 

 

포인터 변수에 역참조 연산자를 사용한 뒤 값을 저장(할당)할 수 도 있다.

- *포인터 = 값;

#include <stdio.h>

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

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

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

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

    return 0;
}

 

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

 

변수, 주소 연산자, 역참조 연산자, 포인터의 관계

 

 

34.3 디버거에서 포인터 확인하기

디버거를 사용하면 변수의 메모리 주소, 포인터, 역참조를 손쉽게 확인할 수 있다.

 

*numPtr = 20; 이 있는 줄에서 F9 키를 눌러 중단점을 설정하고 F5 키를 눌러 디버깅을 시작한다.

 

numPtr = &num1; 까지만 실행한 상태이고 밑의 로컬 탭을 보면 현재 포인터 numPtr 에 변수 num1의 주소가 저장되어 있다.

 

메뉴에서 메모리 탭을 연 모습이다. numPtr에 저장된 메모리 주소를 복사해서 메모리 창에 붙일 것이다.

 

그 결과, 메모리주소 0x0099F7AC의 내용이 보인다. 

내용이 0a 00 00 00라고 나오는데 메모리의 내용은 보통 16진수로 표현하므로 0a를 10진수로 변환하면 10이다. 즉, 이 메모리 공간이 변수 num1의 위치이다.

 

특히 num1 int 자료형이므로 4바이트이다. 따라서 0a 00 00 00과 같이 숫자 4개를 차지한다. 또한, 우리가 사용하는 x86(x86-64) 계열 CPU는 리틀 엔디언 방식이라 값이 거꾸로 저장된다. 그래서 0a 00 00 00은 원래 00 00 00 0a이며 0a 값을 갖는다는 뜻이다.

 

 이제 다시 F10 키를 눌러  *numPtr = 20; 이 있는 줄을 실행한다.

 

 

메모리 1 창을 보면  0a 가 14로 바뀌었다.

16진수 14는 10진수로 20이므로 역참조 후 20을 할당하는 코드 *numPtr = 20;이 실행되어 메모리의 내용이 바뀐 것을 확인할 수 있다.

 

 

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

#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);    // 10
    printf("%f\n", *numPtr2);      // 3.500000
    printf("%c\n", *cPtr1);        // a

    return 0;
}

다음은 다양한 자료형의 포인터를 선언한 것이다.

 

C 언어에서 사용할 수 있는 모든 자료형은 포인터로 만들 수 있다. 그런데 왜 자료형마다 포인터를 선언하도록 만들었을까?

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

포인터의 자료형과 역참조 크기

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

 

 

34.5 void 포인터 선언하기

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 언어는 자료형이 다른 포인터끼리 메모리 주소를 저장하면 컴파일 경고(warning)가 발생한다. 하지만 void 포인터는 자료형이 정해지지 않은 특성 때문에 어떤 자료형으로 된 포인터든 모두 저장할 수 있다.

반대로 다양한 자료형으로 된 포인터에도 void 포인터를 저장할 수도 있다.

이런 특성 때문에 void 포인터는 범용 포인터라고 한다.

 

직접 자료형을 변환하지 않아도 암시적으로 자료형이 변환되는 방식이다.

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

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

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

->  void 포인터는 실제로 C 언어에서 다양한 형태로 사용되고 있다. 예를 들자면 함수에서 다양한 자료형을 받아들일 때, 함수의 반환 포인터를 다양한 자료형으로 된 포인터에 저장할 때, 자료형을 숨기고 싶을 때 사용한다.

 

 

34.6 이중 포인터 사용하기

포인터를 선언할 때 *를 두 번 사용하면 포인터의 포인터(이중 포인터)를 선언한다.

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

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

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

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

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

    return 0;
}

포인터의 메모리 주소는 일반 포인터에 저장할 수 없고, int **numPtr2;처럼 이중 포인터에 저장해야 한다.

int **numPtr2;를 영어로 읽으면 pointer to pointer to int 이다(numPtr2  numPtr1  num1).

 

여기서 이중 포인터 numPtr2를 끝까지 따라가서 실제 값을 가져오려면 **numPtr2처럼 변수 앞에 역참조 연산자를 두 번 사용하면 된다. 즉, 역참조를 두 번 하므로 numPtr2  numPtr1  num1과 같은 모양이 된다.

복사했습니다!