Во-первых, давайте проигнорируем все определения типов;это один из тех редких случаев, когда они скрывают больше, чем освещают.
Вот определение типа данных void
из стандарта языка C (черновик n1256 ):
6.2.5 Типы
...
19 Тип void
содержит пустой набор значений;это неполный тип, который не может быть завершен.
IOW, void
- это тип данных типа «ничего из вышеперечисленного»;у него нет связанных значений.Функция с типом void
не имеет возвращаемого значения, и (несуществующий) результат выражения void
не должен использоваться.
Как и любой другой неполный тип, вы можете создавать указатели на void
.Они служат своего рода универсальным типом указателя;указатель void
может использоваться для указания на объект любого другого типа:
6.3.2.3 Указатели
1 Указатель на void
может быть преобразованк или от указателя на любой неполный или тип объекта.Указатель на любой неполный объект или тип объекта может быть преобразован в указатель на void
и обратно;результат должен сравниваться равным исходному указателю.
Так, например, я могу использовать указатель void, чтобы указывать на int, или float, или массив char и т. Д.:
int x = 1;
double y = 2.0;
char *z = "three";
void *p[3];
p[0] = &x;
printf("address of x is %p\n", p[0]);
p[1] = &y;
printf("address of y is %p\n", p[1]);
p[2] = &z;
printf("address of z is %p\n", p[2]);
Вот гипотетическая карта памятипоказывая значения каждого из них (допустим, 32 бита для целых и плавающих значений и ASCII для символьных данных):
Item Address 0x00 0x01 0x02 0x03
---- ------- ---- ---- ---- ----
"three" 0x00001000 0x74 0x68 0x72 0x65 // string literals are stored "somewhere else"
0x00001004 0x65 0x00 xxxx xxxx // xxxx indicates any random value
...
x 0x10008000 0x00 0x00 0x00 0x01
y 0x10008004 0x40 0x00 0x00 0x00 // Binary representation of 2.0 on my system
z 0x10008008 0x00 0x00 0x10 0x00 // z contains the address of the first element of the string literal
p 0x1000800C 0x10 0x00 0x80 0x00
0x10008010 0x10 0x00 0x80 0x04
0x10008014 0x10 0x00 0x80 0x08
Как видите, значение, хранящееся в p[0]
(адрес 0x10008010
), является адресомx
(0x10008000
).Аналогично, p[1]
хранит адрес y
, а p[3]
хранит адрес z
.Это свойство указателей void
дает нам возможность отделить информацию о типе от кода, хотя и не так просто, как шаблоны C ++ или обобщения Java.Таким образом, мы можем использовать массив указателей void
для создания универсального контейнера, который может содержать объекты (технически, указатели на объекты) любого типа.
Примечание. До принятия стандарта 1989 года вы бы использовали char *
в качестве "общего" типа указателя;однако вам бы пришлось явным образом привести значение указателя к целевому типу, что не нужно делать с указателями void (в C; это не так для C ++, для которого требуется приведение explcit).Это одна из причин, по которой вы все еще видите, как люди разыгрывают результат malloc/calloc/realloc
;когда-то у вас было до.
Итак, давайте посмотрим на ваши структуры данных и код, но сначала без определения типа:
struct Vector {
void **elements;
int top;
int max_size;
};
void **elements
объявляет elements
как указатель на указатель на void;в этом случае мы собираемся динамически выделить вектор из void
указателей.Это похоже на наш массив p
в примере выше.
void allocateVector(struct Vector *vector, int maxSize)
{
vector->elements = malloc(sizeof *vector->elements * maxSize);
if (vector->elements)
vector->maxSize = maxSize;
}
Теперь у нас есть 1-й массив указателей на void
, способный содержать maxSize
элементов.Теперь мы можем написать вашу Add_To_Vector
функцию:
void Add_To_Vector(struct Vector *vector, void *objectPtr, int index)
{
if (vector->elements)
{
if (index < vector->maxSize)
{
vector->elements[index] = objectPtr;
}
}
}
Однако есть две действительно большие проблемы.Прежде всего, мы не можем определить тип объекта, на который указывает указатель, из самого указателя;это просто адрес, без информации, описывающей объект, на который указывают.Если бы мы хотели, скажем, напечатать значения всех объектов в векторе, мы бы не знали, какой спецификатор преобразования использовать.Во-вторых, если мы структурируем наш код следующим образом:
while (!done)
{
scanf("%d %d", &vector_value_int, &index);
Add_To_Vector(&vector, &vector_value_int, index);
...
}
, мы будем продолжать добавлять тот же адрес (&vector_value_int
) в массив, а это не то, что нам нужно.Мы бы хотели сделать копию того, что есть в vector_value_int
, но опять же, мы не настроены на то, чтобы это сделать, потому что мы не знаем тип того, что objectPtr
указываетк.
Один из распространенных способов решения этой проблемы - передать так называемую функцию callback с информацией указателя;обратный вызов знает, как сделать копию указанного значения.Например:
void *copyIntValue(void *objectPtr)
{
int *copy = malloc(sizeof *copy);
if (copy)
*copy = *(int *) objectPtr;
return copy;
}
void Add_To_Vector(struct Vector *vector, void *objectPtr, void *(*cpyFunc)(void *), int index)
{
if (vector->elements)
{
if (index < vector->maxSize)
{
vector->elements[index] = cpyFunc(objectPtr);
}
}
}
...
while (!done)
{
scanf("%d %d", &vector_value_int, &index);
AddToVector(&vector, &vector_value_int, copyIntValue, index);
}
Мы устранили вторую проблему, но не первую;мы до сих пор не знаем, на какой тип указывает vector->elements[i]
.Так или иначе, эта информация должна быть сохранена как часть массива.
Я думаю, что собираюсь сломать программное обеспечение форума на этом этапе, поэтому я оставлю это как упражнение на потом.