ПрограммированиеC разработчик, Системный программист

Какие существуют правила приведения указателей разных типов в языке C, чем опасно приведение void* к другим типам и обратно, и как избежать ошибок работы с памятью при преобразовании указателей?

Проходите собеседования с ИИ помощником Hintsage

Ответ

Приведение указателей — частая операция в языке C, позволяющая использовать обобщённые интерфейсы, например, работать с памятью через void* или реализовывать универсальные структуры данных. Однако приведение типов указателей сопровождается определёнными рисками и подчиняется строгим стандартам.

  • void* в C хранит адрес, но не знает о типе содержимого, на который указывает. Любой другой указатель (например, int*, char*, struct mytype*) можно явно (или неявно) привести к void* и обратно без потери информации (если не происходит "сужение").
  • При этом, если вы приводите указатель на один тип к другому (не к void*), вы должны быть уверены, что по этому адресу действительно лежит значение совместимого типа.
  • Работать с адресами, выделенными под один тип, но полученными через "чужой" указатель, опасно: может возникнуть проблема выравнивания (alignment), неопределённое поведение или даже сбой выполнения.

Пример

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" (аварийному завершению).


История

В embedded-проекте программист реализовал кольцевой буфер с универсальным интерфейсом на void*, но забыл про строгие требования к выравниванию (выделял память под char массив, передавал как int*). На некоторых платформах данные стали "непонятны аппаратуре" — возникали ошибка чтения и нестабильная работа.


История

В динамической коллекции использовали хранение адресов как void*, но забыли, что указатель на функцию нельзя приводить к void*. Попытка хранить и передавать обработчик событий (callback) через такое поле привела к сбоям только на части платформ, причем отловить ошибку было крайне сложно.