void* используется как указатель на сырые байтовые данные, не имеющие конкретного типа.
Обычно это используется…
1. В чтении-записи в файлы и на устройства, когда мы можем писать туда абсолютно любые типы.
2. В «многоликих» функциях, которые могут принимать данные разных типов (malloc/calloc, часть функций WinAPI и ODBC).
3. Как дескриптор — указатель, который запрещается разыменовывать. В Си для этого также часто используют указатель на недоопределённый тип, в Паскале с другими правилами эквивалентности типов — на пустой record. Но только пока не появится очередная многоликая функция вроде CloseHandle.
4. Для обеспечения т.н. замыкания — передачи callback’у контекста, откуда была вызвана функция, вызвавшая callback.
BOOL WINAPI EnumWindows(
_In_ WNDENUMPROC lpEnumFunc,
_In_ LPARAM lParam
);
BOOL CALLBACK EnumWindowsProc(
_In_ HWND hwnd,
_In_ LPARAM lParam
);
Вот этот LPARAM, который обычно определяется как какой-то указатель, и есть замыкание. Функция EnumWindows обещает передать его в функцию lpEnumFunc без изменений.
(В Си++ для этого также используют виртуальные интерфейсы, но такой метод, сами понимаете, языкозависим и не годится для межъязыкового API.)
Что происходит на стороне функции? Одно из двух (считаем, функция написана на ЯВУ).
1. Либо вызывается некая функция устройства, которая говорит: «записать 100 байт», и дальше уже работает железо.
2. Либо мы преобразуем void* в нужный нам тип и работаем с ним.
Типы указателям дают по трём причинам.
1. Вы забыли про операцию «разыменовать указатель». Чтобы его разыменовать, он должен иметь тип!
2. Чтобы не ошибаться и не переприсвоить несовместимые указатели.
3. Для полиморфизма — в Си++, давая delete x, мы даже можем не хранить, сколько байтов в блоке, поскольку мы знаем длину типа. (Есть ещё и виртуальные классы, но это другой вопрос.)