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
ptr을 int 포인터로 변환하여 포인터 연산을 합니다.
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
먼저 ptr을 int 포인터로 변환하여 포인터 연산을 합니다. 그리고 포인터 연산 부분을 괄호로 묶은 뒤 역참조를 하면 됩니다.
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이므로 값을 할당할 수 있음(저장할 메모리 공간이 있음)