Skip to content

52. 포인터 연산


1. 키워드

  • 포인터 연산


2. 포인터 연산 사용하기

  • 포인터로 선언한 변수에는 메모리 주소가 들어있다.
  • 이 메모리 주소에 일정 숫자를 더하거나 빼면 메모리 주소가 증가, 감소한다.
  • 즉, 포인터 연산을 하면 다른 메모리 주소에 접근할 수 있으며 메모리 주소를 손쉽게 옮겨 다니기 위해 사용한다.


001


  • 여기서 메모리 주소가 커지는 상황을 순방향으로 이동(Forward), 메모리 주소가 작아지는 상황을 역방향으로 이동(Backward)이라고 한다.


3. 포인터 연산으로 메모리 주소 조작하기

  • 포인터 연산은 포인터 변수에 +, - 연산자를 사용하여 값을 더하거나 뺀다.
  • 또는, ++, -- 연산자를 사용하여 값을 증가, 감소시킨다.
  • 단, *, / 연산자와 실수 값은 사용할 수 없다.


포인터 + 
포인터 - 
#include <stdio.h>

int main()
{
    int numArr[5] = {11, 22, 33, 44, 55};
    int *numPtrA;
    int *numPtrB;
    int *numPtrC;

    numPtrA = numArr; // 배열 첫 번째 요소의 메모리 주소를 포인터에 저장

    numPtrB = numPtrA + 1; // 포인터 연산
    numPtrC = numPtrA + 2; // 포인터 연산

    printf("%p\n", numPtrA); // 메모리 주소. 컴퓨터마다, 실행할 때마다 달라짐
    printf("%p\n", numPtrA); // sizeof(int) * 1이므로 numPtrA에서 4가 증가함
    printf("%p\n", numPtrA); // sizeof(int) * 2이므로 numPtrA에서 8이 증가함

    return 0;
}

// 0x00a3fc00 (메모리 주소. 컴퓨터마다, 실행할 때마다 달라짐)
// 0x00a3fc04
// 0x00a3fc08


  • 먼저 정수형 배열을 선언하고 값을 초기화했다.
  • 그리고 배열을 포인터에 할당했다.
  • 배열 이름 자체는 배열의 첫 번째 요소에 대한 포인터라 할 수 있다.


  • 포인터 연산은 포인터 변수에 정수 값을 더하거나 빼면 된다.


numPtrB = numPtrA + 1; // 포인터 연산
numPtrC = numPtrA + 2; // 포인터 연산


  • numPtrA에 저장된 메모리 주소 0x00a3fc001을 더하면 1이 증가한 0x00a3fc01이 나와야 할 것 같은데 0x00a3fc04가 나왔다.
  • 또한 0x00a3fc002를 더하면 2가 증가한 0x00a3fc02가 나와야 할 것 같지만 0x00a3fc08이 나온다.
  • 즉, 포인터 연산은 포인터 자료형의 크기만큼 더하거나 뺀다.


printf("%p\n", numPtrA); // 메모리 주소. 컴퓨터마다, 실행할 때마다 달라짐
printf("%p\n", numPtrA); // sizeof(int) * 1이므로 numPtrA에서 4가 증가함
printf("%p\n", numPtrA); // sizeof(int) * 2이므로 numPtrA에서 8이 증가함


  • 여기서는 numPtrA가 4바이트 크기의 int 타입이다.
  • 따라서 numPtrA + 1은 메모리 주소에서 4바이트만큼 1번 순방향으로 이동한다는 뜻이다.
  • 즉, 계산식은 sizeof(자료형) * 더하거나 빼는 값이 된다.


002


  • 이번에는 포인터에서 뺄셈을 해보자.


#include <stdio.h>

int main()
{
    int numArr[5] = {11, 22, 33, 44, 55};
    int *numPtrA;
    int *numPtrB;
    int *numPtrC;

    numPtrA = &numArr[2]; // 배열 세 번째 요소의 메모리 주소를 포인터에 저장

    numPtrB = numPtrA - 1; // 포인터 연산
    numPtrC = numPtrA - 2; // 포인터 연산

    printf("%p\n", numPtrA); // 메모리 주소. 컴퓨터마다, 실행할 때마다 달라짐
    printf("%p\n", numPtrB); // sizeof(int) * -1이므로 numPtrA에서 4가 감소함
    printf("%p\n", numPtrC); // sizeof(int) * -2이므로 numPtrA에서 8이 감소함

    return 0;
}

// 0x00a3fc08 (메모리 주소. 컴퓨터마다, 실행할 때마다 달라짐)
// 0x00a3fc04
// 0x00a3fc00


  • numPtrA = &numArr[2];와 같이 배열에 [](대괄호)를 사용하여 요소에 접근한 뒤 &(주소 연산자)를 사용하면 해당 요소의 메모리 주소를 구할 수 있다.
  • 여기서는 numArr에서 세 번째 요소의 메모리 주소를 구해서 numPtrA에 저장했다.


  • 포인터 연산으로 numPtrA에서 1, 2를 뺀다.


numPtrB = numPtrA - 1; // 포인터 연산
numPtrC = numPtrA - 2; // 포인터 연산


  • numPtrBnumPtrA에서 4가 감소한 값이 나오고, numPtrCnumPtrA에서 8이 감소한 값이 나온다.
  • 즉, sizeof(int) * -1이므로 메모리 주소에서 4바이트만큼 역방향으로 이동하고, sizeof(int) * -2이므로 메모리 주소에서 8바이트만큼 역방향으로 이동한다.


printf("%p\n", numPtrA); // 메모리 주소. 컴퓨터마다, 실행할 때마다 달라짐
printf("%p\n", numPtrB); // sizeof(int) * -1이므로 numPtrA에서 4가 감소함
printf("%p\n", numPtrC); // sizeof(int) * -2이므로 numPtrA에서 8이 감소함


003


char, short, long long 타입 포인터를 연산하면?

  • 포인터 연산은 sizeof(자료형) * 더하거나 빼는 값이므로 char 타입은 1바이트, short 타입은 2바이트, long long 타입은 8바이트만큼 메모리 주소에서 순방향, 역방향으로 이동한다.


#include <stdio.h>

int main()
{
    char *cPtr1 = NULL;
    short *numPtr1 = NULL;
    long long *numPtr2 = NULL;

    printf("%p\n", cPtr1 + 1);   // 0x000000에서 1바이트만큼 순방향으로 이동
    printf("%p\n", numPtr1 + 1); // 0x000000에서 2바이트만큼 순방향으로 이동
    printf("%p\n", numPtr2 + 1); // 0x000000에서 8바이트만큼 순방향으로 이동

    return 0;
}

// 0x00000001
// 0x00000002
// 0x00000008


  • 이번에는 증감 연산자를 사용해 보자.


포인터++
포인터--
++포인터
--포인터
#include <stdio.h>

int main()
{
    int numArr[5] = {11, 22, 33, 44, 55};
    int *numPtrA;
    int *numPtrB;
    int *numPtrC;

    numPtrA = &numArr[2]; // 배열 세 번째 요소의 주소를 포인터에 저장

    numPtrB = numPtrA;
    numPtrC = numPtrA;

    numPtrB++; // 포인터 연산
    numPtrC--; // 포인터 연산

    printf("%p\n", numPtrA); // 메모리 주소. 컴퓨터마다, 실행할 때마다 달라짐
    printf("%p\n", numPtrB); // sizeof(int) * 1이므로 numPtrA에서 4가 증가함
    printf("%p\n", numPtrC); // sizeof(int) * -1이므로 numPtrA에서 4가 감소함

    return 0;
}

// 0x00a3fc08
// 0x00a3fc0c
// 0x00a3fc04


  • 먼저 numPtrBnumPtrCnumPtrA의 메모리 주소를 저장했다.
  • 그리고 numPtrB에는 증가 연산자를 사용하고, numPtrC에는 감소 연산자를 사용했다.
  • 증가 연산자는 + 1과 같기 때문에 sizeof(int) * 1이 되므로 numPtrA에서 4가 증가한다.
  • 마찬가지로 감소 연산자는 - 1과 같기 때문에 sizeof(int) * -1이 되므로 numPtrB에서 4가 감소한다.


numPtrB++; // 포인터 연산
numPtrC--; // 포인터 연산

printf("%p\n", numPtrA); // 메모리 주소. 컴퓨터마다, 실행할 때마다 달라짐
printf("%p\n", numPtrB); // sizeof(int) * 1이므로 numPtrA에서 4가 증가함
printf("%p\n", numPtrC); // sizeof(int) * -1이므로 numPtrA에서 4가 감소함


4. 포인터 연산과 역참조 사용하기

  • 포인터 연산으로 조작한 메모리 주소도 역참조 연산을 사용하여 메모리에 접근할 수 있다.


#include <stdio.h>

int main()
{
    int numArr[5] = {11, 22, 33, 44, 55};
    int *numPtrA;
    int *numPtrB;
    int *numPtrC;

    numPtrA = numArr; // 배열 첫 번째 요소의 주소를 포인터에 저장

    numPtrB = numPtrA + 1; // 포인터 연산. numPtrA + 4바이트
    numPtrC = numPtrA + 2; // 포인터 연산. numPtrA + 8바이트

    printf("%d\n", *numPtrB); // 역참조로 값을 가져옴, numArr[1]과 같음
    printf("%d\n", *numPtrC); // 역참조로 값을 가져옴, numArr[2]와 같음

    return 0;
}

// 22
// 33


  • numPtrB에는 numPtrA1을 더해서 4바이트만큼 순방향으로 이동한 메모리 주소를 저장했고, numPtrC에는 numPtrA2를 더해서 8바이트만큼 순방향으로 이동한 메모리 주소를 저장했다.


numPtrB = numPtrA + 1; // 포인터 연산. numPtrA + 4바이트
numPtrC = numPtrA + 2; // 포인터 연산. numPtrA + 8바이트


  • numPtrBnumPtrC도 일반 포인터이므로 *를 사용하여 메모리의 값을 가져올 수 있다.
  • 여기서 포인터 연산은 결과적으로 numPtrA + 1numArr[1]은 같고, numPtrA + 2numArr[2]는 같다.


printf("%d\n", *numPtrB); // 역참조로 값을 가져옴, numArr[1]과 같음
printf("%d\n", *numPtrC); // 역참조로 값을 가져옴, numArr[2]와 같음


  • 이번에는 포인터 연산과 동시에 역참조 연산을 사용하는 방법을 알아보자.
  • 다음과 같이 포인터 연산을 한 부분을 ()(괄호)로 묶어준 뒤 맨 앞에 *를 붙이면 된다.


*(포인터 + )
*(포인터 - )
#include <stdio.h>

int main()
{
    int numArr[5] = {11, 22, 33, 44, 55};
    int *numPtrA;

    numPtrA = numArr; // 배열 첫 번째 요소의 주소를 포인터에 저장

    printf("%d\n", *(numPtrA + 1)); // numPtrA에서 순방향으로 4바이트만큼 떨어진
                                    // 메모리에 주소에 접근. numArr[1]과 같음

    printf("%d\n", *(numPtrA + 2)); // numPtrA에서 순방향으로 8바이트만큼 떨어진
                                    // 메모리에 주소에 접근. numArr[2]와 같음

    return 0;
}

// 22
// 33


  • *(numPtrA + 1)과 같이 numPtrA1을 더한 뒤 ()로 묶어준다.
  • 그리고 맨 앞에 *를 붙여주면 numPtrA에서 순방향으로 4바이트만큼 떨어진 메모리 주소에서 값을 가져올 수 있다.
  • 여기서 포인터 연산과 역참조 연산 *(numPtrA + 1)은 결과적으로 numArr[1]과 같다.


printf("%d\n", *(numPtrA + 1)); // numPtrA에서 순방향으로 4바이트만큼 떨어진
                                // 메모리에 주소에 접근. numArr[1]과 같음

printf("%d\n", *(numPtrA + 2)); // numPtrA에서 순방향으로 8바이트만큼 떨어진
                                // 메모리에 주소에 접근. numArr[2]와 같음


  • 만약 포인터 연산을 ()로 묶어주지 않으면 *가 먼저 실행되어 값을 가져온 뒤 덧셈(뺄셈) 연산을 하게 된다.
  • 따라서 다음 코드는 numPtrA를 역참조하여 11을 가져온 뒤 1을 더하므로 12가 된다.


printf("%d\n", *numPtrA + 1); // numPtrA를 역참조하여 나온 값에 1을 더하므로 12


  • 즉, 역참조 연산보다 포인터 연산이 먼저 실행될 수 있도록 ()로 묶어주는 것이다.


004


  • 이번에는 증감 연산과 역참조 연산을 사용해 보자.


*(++포인터)
*(--포인터)
#include <stdio.h>

int main()
{
    int numArr[5] = {11, 22, 33, 44, 55};
    int *numPtrA;
    int *numPtrB;
    int *numPtrC;

    numPtrA = &numArr[2]; // 배열 세 번째 요소의 주소를 포인터에 저장

    numPtrB = numPtrA;
    numPtrC = numPtrA;

    printf("%d\n", *(++numPtrB)); // numPtrA에서 순방향으로 4바이트만큼 떨어진
                                  // 메모리 주소에 접근
    printf("%d\n", *(--numPtrC)); // numPtrA에서 역방향으로 4바이트만큼 떨어진
                                  // 메모리 주소에 접근

    return 0;
}

// 44
// 22


  • *(++numPtrB)와 같이 변수 앞에 ++를 사용한 뒤 ()로 묶어준다.
  • 그리고 맨 앞에 *를 붙여주면 numPtrB에 저장된 메모리 주소가 4바이트만큼 증가하고, 해당 메모리의 값을 가져온다.
  • 마찬가지로 *(--numPtrC)처럼 --*를 사용하면 numPtrC에 저장된 메모리 주소가 4바이트만큼 감소하고, 해당 메모리의 값을 가져온다.


printf("%d\n", *(++numPtrB)); // numPtrA에서 순방향으로 4바이트만큼 떨어진
                              // 메모리 주소에 접근
printf("%d\n", *(--numPtrC)); // numPtrA에서 역방향으로 4바이트만큼 떨어진
                              // 메모리 주소에 접근


  • 만약 증감 연산자를 변수 앞에 사용할 때 ()로 묶어주지 않아도 동작은 같다.
  • 왜냐하면 전위 증감 연산자와 *는 우선순위가 같고 결합방향이 오른쪽에서 왼쪽이기 때문이다.


printf("%d\n", *++numPtrB); // 44
printf("%d\n", *--numPtrC); // 22


  • 증감 연산자를 변수 뒤에 붙이고 포인터 연산을 하면 현재 메모리의 값을 가져온 뒤 포인터 연산을 하므로 주의해야 한다.


printf("%d\n", *(numPtrB++)); // 33: numPtrA의 메모리에 접근하여 값을 가져온 뒤 포인터 연산
printf("%d\n", *(numPtrC--)); // 33: numPtrA의 메모리에 접근하여 값을 가져온 뒤 포인터 연산


5. 구조체 포인터로 포인터 연산하기

  • 구조체 포인터도 포인터 연산을 할 수 있다.
  • 다음과 같이 포인터 연산을 한 부분을 ()로 묶어준 뒤 ->를 사용하여 멤버에 접근하면 된다.


(포인터 + )->멤버
(포인터 - )->멤버
#include <stdio.h>

struct Data
{
    int num1;
    int num2;
};

int main()
{
    struct Data d[3] = {{10, 20}, {30, 40}, {50, 60}}; // 구조체 배열 선언과 값 초기화
    struct Data *ptr;                                  // 구조체 포인터 선언

    ptr = d; // 구조체 배열 첫 번째 요소의 메모리 주소를 포인터에 저장

    printf("%d %d\n", (ptr + 1)->num1, (ptr + 1)->num2); // 구조체 배열에서 멤버의 값 출력
                                                         // d[1].num1, d[1].num2와 같음

    printf("%d %d\n", (ptr + 2)->num1, (ptr + 2)->num2); // 구조체 배열에서 멤버의 값 출력
                                                         // d[2].num1, d[2].num2와 같음

    return 0;
}

// 30 40
// 50 60


  • 구조체 배열 d를 선언한 뒤 첫 번째 요소의 메모리 주소를 ptr에 저장했다.
  • 구조체 포인터는 (ptr + 1)->num1과 같이 포인터 연산을 한 뒤 ()로 묶어준다.
  • 그리고 ->를 사용하여 멤버에 접근할 수 있다.
  • 결과적으로 (ptr + 1)->num1은 구조체 배열의 인덱스로 접근하는 d[1].num1과 같다.


  • 구조체 Data의 크기는 4바이트짜리 int 타입 멤버가 두 개 들어있으므로 8바이트이다.
  • 따라서 포인터 연산을 하면 8바이트씩 메모리 주소에서 더하거나 뺀다.


005


  • 이번에는 void 타입 포인터에 구조체 3개 크기만큼 동적 메모리를 할당한 뒤 포인터 연산을 해보자.


((struct 구조체이름 *)포인터 + )->멤버
((struct 구조체이름 *)포인터 - )->멤버
#include <stdio.h>
#include <stdlib.h> // malloc, free 함수가 선언된 헤더 파일
#include <string.h> // memcpy 함수가 선언된 헤더 파일

struct Data
{
    int num1;
    int num2;
};

int main()
{
    void *ptr = malloc(sizeof(struct Data) * 3); // 구조체 3개 크기만큼 동적 메모리 할당
    struct Data d[3];

    ((struct Data *)ptr)->num1 = 10; // 포인터 연산으로 메모리에 값 저장
    ((struct Data *)ptr)->num2 = 20; // 포인터 연산으로 메모리에 값 저장

    ((struct Data *)ptr + 1)->num1 = 30; // 포인터 연산으로 메모리에 값 저장
    ((struct Data *)ptr + 1)->num2 = 40; // 포인터 연산으로 메모리에 값 저장

    ((struct Data *)ptr + 2)->num1 = 50; // 포인터 연산으로 메모리에 값 저장
    ((struct Data *)ptr + 2)->num2 = 60; // 포인터 연산으로 메모리에 값 저장

    memcpy(d, ptr, sizeof(struct Data) * 3); // 동적 메모리가 구조체 배열의 형태와 같은지
                                             // 확인하기 위해 동적 메모리의 내용을 구조체 배열에 복사

    printf("%d %d\n", d[1].num1, d[1].num2);                                           // 구조체 배열의 멤버 출력
    printf("%d %d\n", ((struct Data *)ptr + 2)->num1, ((struct Data *)ptr + 2)->num2); // 포인터 연산으로 메모리의 값 출력

    free(ptr); // 동적 메모리 해제

    return 0;
}


  • ((struct Data *)ptr)->num1은 앞에서 배운 구조체 포인터로 변환하는 방법이다.
  • 이 상태에서 연산을 하려면 ((struct Data *)ptr + 1)->num1과 같이 ptr을 구조체 포인터로 변환한 뒤 값을 더해주면 된다.
  • ->를 사용하려면 반드시 ()로 묶어준다.


  • 이제 포인터 연산을 통해 메모리에 값을 저장한다.
  • 만약 (ptr + 1)->num1처럼 ptr에 포인터 연산을 하더라도 ptrvoid 타입 포인터라 Data 구조체의 형태를 모르기 때문에 멤버에 접근할 수 없고 컴파일 에러가 발생한다.


((struct Data *)ptr + 1)->num1 = 30; // 포인터 연산으로 메모리에 값 저장
((struct Data *)ptr + 1)->num2 = 40; // 포인터 연산으로 메모리에 값 저장

((struct Data *)ptr + 2)->num1 = 50; // 포인터 연산으로 메모리에 값 저장
((struct Data *)ptr + 2)->num2 = 60; // 포인터 연산으로 메모리에 값 저장


  • 그리고 포인터 연산으로 값을 저장한 결과가 Data 구조체 배열의 형태와 같은지 확인하기 위해 memcpy(d, ptr, sizeof(struct Data) * 3);처럼 동적 메모리의 내용을 구조체 배열 d에 복사했다.


  • 다음과 같이 printf로 구조체 배열 d의 멤버를 출력해 보면 포인터 연산을 통해 저장한 값이 출력되는 것을 볼 수 있다.
  • 즉, 동적 메모리에 저장된 값의 위치가 구조체 배열의 형태와 같고, 동적 메모리 내용을 그대로 복사했기 때문에 같은 값이 나온다.
  • 또한, 포인터 연산으로도 동적 메모리의 값을 출력할 수 있다.


printf("%d %d\n", d[1].num1, d[1].num2);                                           // 구조체 배열의 멤버 출력
printf("%d %d\n", ((struct Data *)ptr + 2)->num1, ((struct Data *)ptr + 2)->num2); // 포인터 연산으로 메모리의 값 출력


006


References