59.3 void 포인터로 포인터 연산하기

void 포인터는 자료형의 크기가 정해져 있지 않기 때문에 +, -로 연산을 해도 얼마만큼 이동할지 알 수가 없습니다. 따라서 void 포인터는 포인터 연산을 할 수 없습니다.

void_pointer_add_error.c

#include <stdio.h>
#include <stdlib.h>    // malloc, free 함수가 선언된 헤더 파일

int main()
{
    void *ptr = malloc(100);    // 100바이트만큼 메모리 할당

    printf("%p\n", ptr);
    printf("%p\n", ptr + 1);    // 컴파일 에러. void 포인터는 포인터 연산을 할 수 없음

    free(ptr);

    return 0;
}

컴파일 결과

void_pointer_add_error.c(8): error C2036: 'void *': 알 수 없는 크기입니다.

만약 void 포인터로 포인터 연산을 하고 싶다면 다른 포인터로 변환한 뒤 연산을 하면 됩니다(Visual Studio, Windows).

  • (자료형 *)void포인터 + 값
  • (자료형 *)void포인터 - 값
  • ++(자료형 *)void포인터
  • --(자료형 *)void포인터
  • ((자료형 *)void포인터)++
  • ((자료형 *)void포인터)--

void_pointer_arithmetic.c

#include <stdio.h>
#include <stdlib.h>    // malloc, free 함수가 선언된 헤더 파일

int main()
{
    void *ptr = malloc(100);    // 100바이트만큼 메모리 할당

    printf("%p\n", ptr);               // 00FADD20: 메모리 주소. 컴퓨터마다, 실행할 때마다 달라짐
    printf("%p\n", (int *)ptr + 1);    // 00FADD24: 다른 포인터로 변환한 뒤 포인터 연산
    printf("%p\n", (int *)ptr - 1);    // 00FADD1C: 다른 포인터로 변환한 뒤 포인터 연산

    void *ptr2 = ptr;    // 메모리 주소를 변화시킬 때는 다른 포인터에 보관
    printf("%p\n", ++(int *)ptr2);     // 00FADD24: 다른 포인터로 변환한 뒤 포인터 연산
    printf("%p\n", --(int *)ptr2);     // 00FADD20: 다른 포인터로 변환한 뒤 포인터 연산

    printf("%p\n", ((int *)ptr2)++);   // 00FADD20: 다른 포인터로 변환한 뒤 포인터 연산
    printf("%p\n", ((int *)ptr2)--);   // 00FADD24: 다른 포인터로 변환한 뒤 포인터 연산

    free(ptr);

    return 0;
}

실행 결과

00FADD20
00FADD24
00FADD1C
00FADD24
00FADD20
00FADD20
00FADD24

ptrint 포인터로 변환하여 포인터 연산을 합니다.

printf("%p\n", (int *)ptr + 1);    // 00FADD24: 다른 포인터로 변환한 뒤 포인터 연산
printf("%p\n", (int *)ptr - 1);    // 00FADD1C: 다른 포인터로 변환한 뒤 포인터 연산

증가, 감소 연산자를 변수 앞에 사용할 때는 자료형 변환 앞에 연산자를 붙이면 됩니다.

printf("%p\n", ++(int *)ptr2);      // 00FADD24: 다른 포인터로 변환한 뒤 포인터 연산
printf("%p\n", --(int *)ptr2);      // 00FADD20: 다른 포인터로 변환한 뒤 포인터 연산

만약 증가, 감소 연산자를 변수 뒤에 사용하려면 ((int *)ptr2)와 같이 자료형 변환과 포인터를 모두 괄호로 묶은 뒤 연산자를 붙이면 됩니다. 단, 증가, 감소 연산자를 뒤에 붙였으므로 현재 메모리 주소를 출력한 뒤 포인터 연산을 하게 됩니다.

printf("%p\n", ((int *)ptr2)++);    // 00FADD20: 다른 포인터로 변환한 뒤 포인터 연산
printf("%p\n", ((int *)ptr2)--);    // 00FADD24: 다른 포인터로 변환한 뒤 포인터 연산
참고 | 포인터 증가, 감소 연산과 메모리 해제

동적 메모리를 할당받은 포인터를 ++, -- 연산자로 포인터 연산을 하게 되면 포인터에 저장된 메모리 주소 자체가 바뀌게 됩니다. 이때 free 함수에서 메모리 주소가 바뀐 포인터로 메모리 해제를 하면 에러가 발생하므로 주의합니다.

void *ptr = malloc(100);    // 동적 메모리 할당
*(++(int *)ptr) = 10;    // 증가 연산자를 사용했으므로 4만큼 증가한 메모리 주소가 ptr에 다시 저장됨
free(ptr);               // 메모리 주소가 바뀐 포인터로 메모리 해제를 하면 에러 발생

free 함수로 메모리 해제를 할 때는 반드시 처음에 메모리를 할당할 때 받은 주소(포인터)를 넣어주어야 합니다.

void 포인터를 포인터 연산한 뒤 역참조하려면 먼저 다른 포인터로 변환하여 포인터 연산을 합니다. 그리고 포인터 연산 부분을 ( ) (괄호)로 묶어준 뒤 맨 앞에 * (역참조 연산자)를 붙이면 됩니다(Visual Studio, Windows).

  • *((자료형 *)void포인터 + 값)
  • *((자료형 *)void포인터 - 값)
  • *(++(자료형 *)void포인터)
  • *(--(자료형 *)void포인터)
  • *(((자료형 *)void포인터)++)
  • *(((자료형 *)void포인터)--)

void_pointer_arithmetic_dereference.c

#include <stdio.h>

int main()
{
    int numArr[5] = { 11, 22, 33, 44, 55 };
    void *ptr = &numArr[2];    // 두 번째 요소의 메모리 주소

    printf("%d\n", *(int *)ptr);    // 33: 포인터 연산을 하지 않은 상태에서 역참조

    // void 포인터를 다른 포인터로 변환하여 포인터 연산을 한 뒤 역참조
    printf("%d\n", *((int *)ptr + 1));    // 44
    printf("%d\n", *((int *)ptr - 1));    // 22

    printf("%d\n", *(++(int *)ptr));      // 44
    printf("%d\n", *(--(int *)ptr));      // 33

    printf("%d\n", *(((int *)ptr)++));    // 33
    printf("%d\n", *(((int *)ptr)--));    // 44

    return 0;
}

실행 결과

33
44
22
44
33
33
44

먼저 ptrint 포인터로 변환하여 포인터 연산을 합니다. 그리고 포인터 연산 부분을 괄호로 묶은 뒤 역참조를 하면 됩니다.

printf("%d\n", *((int *)ptr + 1));    // 44
printf("%d\n", *((int *)ptr - 1));    // 22

증가, 감소 연산자를 변수 앞에 사용할 때는 *(++(int *)ptr)와 같이 자료형 변환 앞에 연산자를 붙이고 증가, 감소 연산자, 자료형 변환, 포인터를 모두 괄호로 묶은 뒤 역참조를 합니다.

printf("%d\n", *(++(int *)ptr));      // 44
printf("%d\n", *(--(int *)ptr));      // 33

만약 증가, 감소 연산자를 변수 뒤에 사용하려면 먼저 ((int *)ptr)와 같이 자료형 변환과 포인터를 괄호로 묶은 뒤 연산자를 붙입니다. 그리고 *(((int *)ptr)++)와 같이 자료형 변환, 포인터, 증가, 감소 연산자를 모두 괄호로 묶은 뒤 역참조를 하면 됩니다. 단, 증가, 감소 연산자를 뒤에 붙였으므로 현재 메모리의 값을 가져온 뒤 포인터 연산을 하게 됩니다.

printf("%d\n", *(((int *)ptr)++));    // 33
printf("%d\n", *(((int *)ptr)--));    // 44
참고 | 마이크로소프트 전용 언어 확장

변수의 자료형을 다른 자료형으로 변환한 뒤 변수의 값을 변경하는 것은 마이크로소프트 전용 언어 확장입니다. 따라서 Visual Studio에서만 사용할 수 있습니다. C 언어 표준에서는 이를 허용하지 않습니다.

int num1 = 10;

// 마이크로소프트 전용 언어 확장
(char)num1 = 20;    // 변수의 자료형을 다른 자료형으로 변환한 뒤 값 할당
((char)num1)++;     // 변수의 자료형을 다른 자료형으로 변환한 뒤 값을 증가시킴
((char)num1)--;     // 변수의 자료형을 다른 자료형으로 변환한 뒤 값을 감소시킴

마찬가지로 포인터를 다른 포인터로 변환한 뒤 ++, -- 연산자를 사용하는 것도 마이크로소프트 전용 언어 확장입니다.

void *ptr = malloc(100);

void *ptr2 = ptr;
printf("%p\n", ++(int *)ptr2);    // 마이크로소프트 전용 언어 확장
printf("%p\n", --(int *)ptr2);    // 마이크로소프트 전용 언어 확장

printf("%d\n", *(++(int *)ptr2));      // 마이크로소프트 전용 언어 확장
printf("%d\n", *(--(int *)ptr2));      // 마이크로소프트 전용 언어 확장

free(ptr);

이 코드를 GCC에서 컴파일하려면 포인터를 다른 자료형의 포인터에 할당한 뒤 ++, -- 연산자를 사용해야 합니다.

void *ptr = malloc(100);

int *ptr2 = ptr;    // void 포인터를 int 포인터에 할당
printf("%p\n", ++ptr2);
printf("%p\n", --ptr2);

printf("%d\n", *(++ptr2));
printf("%d\n", *(--ptr2));

free(ptr);

C 언어 표준에서 자료형 변환은 l-value를 생성하지 않는다고 규정되어 있습니다. 참고로 l-value는 메모리 공간을 차지하는 표현식을 뜻하며 r-value는 l-value 이외의 표현식을 뜻합니다.

0 = 1;    // 0은 r-value이므로 값을 할당할 수 없음(저장할 메모리 공간이 없음)

int num1 = 1;    // num1은 l-value이므로 값을 할당할 수 있음(저장할 메모리 공간이 있음)