포인터 변환은 C 언어에서 일반적인 작업으로, void*를 통해 메모리 작업을 하거나 일반적인 데이터 구조를 구현하는 등 범용 인터페이스를 사용할 수 있게 해줍니다. 그러나 포인터 타입 변환은 특정한 위험을 동반하며 엄격한 표준에 따라야 합니다.
void***는 C에서 주소를 저장하지만, 가리키는 내용의 타입에 대해서는 알지 않습니다. 다른 모든 포인터(int*, char*, struct mytype*)는 명시적(또는 암시적)으로 void*로 변환할 수 있으며, 정보 손실 없이 다시 변환할 수 있습니다(단, "좁히기"가 발생하지 않는 경우).void*가 아닐 경우) 해당 주소에 실제로 호환 타입의 값이 존재한다는 것을 확신해야 합니다.void process(void *data) { int *arr = (int*)data; // arr를 int 배열로 사용 } int main() { double x = 10; process(&x); // 위험: double*를 int*로 변환, UB }
"모든 포인터가 안전하게 void*로 변환되고 손실 없이 다시 변환될 수 있나요?"
많은 사람들이 "예"라고 답하지만, C 표준은 모든 객체 포인터를 void*로 변환하고 다시 돌아오는 것은 손실이 없다고 보장합니다. 그러나 객체가 아닌 포인터(예: 함수 포인터)를 void*로 변환하거나 서로 다른 아키텍처를 혼합하면(포인터 크기가 함수와 데이터에 대해 다름) 정의되지 않은 동작을 경험하게 됩니다.
void foo() {} void *p = (void*)foo; // UB! 함수 포인터는 이렇게 변환할 수 없음
이야기
크로스 플랫폼 데이터 처리 서브시스템 프로젝트에서 구조체 포인터를
void*로 변환한 다음 원래 타입으로 다시 변환하는 핸들러를 사용했습니다.int*와double*의 정렬이 다른 아키텍처로 전환했을 때,void*를 잘못된 타입으로 변환하여 "bus error"(비정상 종료)가 발생했습니다.
이야기
임베디드 프로젝트에서 프로그래머가
void*에 대한 범용 인터페이스로 원형 버퍼를 구현했지만, 정렬 요구 사항을 간과하고(char배열로 메모리를 할당하고int*로 전달) 적합하지 않은 데이터로 인해 특정 플랫폼에서 "하드웨어에 이해되지 않음" 오류가 발생했습니다 — 읽기 오류와 불안정한 동작이 발생했습니다.
이야기
동적 컬렉션에서 주소를
void*로 저장했지만 함수 포인터를void*로 변환할 수 없다는 것을 잊었습니다. 이벤트 핸들러(callback)를 이런 필드를 통해 저장하고 전달하려고 시도했는데 일부 플랫폼에서만 충돌이 발생했고, 오류를 잡는 것이 매우 어려웠습니다.