¿Por qué convertir el puntero en doble puntero en c?
Estoy leyendo OOP en C, y hay este encabezado:
#ifndef _NEW_H
#define _NEW_H
#include <stdarg.h>
#include <stddef.h>
#include <assert.h>
void *new (const void *type, ...);
void delete (void *item);
typedef struct
{
size_t size;
void *(*ctor)(void *self, va_list *app);
void *(*dtor)(void *self);
void *(*clone)(const void *self);
int (*differ)(const void *self, const void *x);
} class_t;
inline void *new (const void *_class, ...)
{
const class_t *class = _class;
void *p = calloc(1, class->size);
assert(p);
*(const class_t **)p = class; //why would you cast to double pointer, when you immediately dereference it?
if (class->ctor)
{
va_list args;
va_start(args, _class);
p = class->ctor(p, &args);
va_end(args);
}
return p;
}
#endif //_NEW_H
Ahora no entiendo esta expresión:
*(const class_t **)p = class;
¿Qué significa const class_t **
tipo? es como una matriz de matrices? pero si quiero tener una clase personalizada (es decir, no solo un puntero a la estructura class_t
, sino más métodos "públicos"), la clase general no es una matriz de tipos class_t
. Entonces, ¿por qué lanzaría un puntero vacío a un doble puntero e inmediatamente lo quitaría de referencia? ¿Cómo debo entenderlo?
del libro sobre esa declaración:* (const struct Class **) p = class;
p apunta al comienzo de la nueva área de memoria para el objeto. Forzamos una conversión de p que trata el comienzo del objeto como un puntero a una estructura class_t y establecemos el argumento class como el valor de este puntero.
Respuestas
La llamada a calloc()
se usa para asignar una matriz de 1 elemento de un tipo de "clase" no especificado, donde se espera que el primer miembro de ese tipo sea un class_t*
puntero (por lo tanto, class->size
debe ser al menos sizeof(class_t*)
, pero puede ser mayor). calloc()
es probable que se use en lugar de malloc()
solo para que cualquier miembro de datos adicional representado por se inicialice en cero; de lo contrario, se necesitaría class->size
un explícito .memset()
El extraño cast+dereference es solo para que el código pueda almacenar el class
puntero de entrada directamente en ese primer class_t*
miembro de ese objeto asignado.
Se puede acceder a una matriz mediante un puntero doble. La desreferenciación de dicho puntero le da la dirección del primer elemento de la matriz. Que en este caso también es la misma dirección que el class_t*
miembro.
En términos de programación orientada a objetos, el diseño de un objeto en la memoria generalmente comienza con un puntero a la vtable de la clase de objeto, que contiene una lista de punteros de función a los métodos "virtuales" de la clase. Cuando se "deriva" de una clase, los descendientes "anulan" los métodos virtuales simplemente configurando el puntero vtable del objeto en una nueva lista de punteros de función. Este concepto de programación orientada a objetos realmente no existe en C, pero es fundamental para C++. En C, tiene que implementarse manualmente, que es lo que está haciendo este código.
Básicamente, el código está asignando este diseño de memoria para el objeto asignado:
------------ -------------------- vacío *p -> | clase_t* | -> | tamaño_t tamaño | ------------ -------------------- | ... | | vacío (*ctor)() | ------------ -------------------- | vacío (*dtor)() | -------------------- | vacío (*clon)() | -------------------- | vacío (*diferente)() | --------------------
Otra forma de realizar la misma asignación sería usar un typedef para el tipo de "clase" para facilitar el acceso, por ejemplo, el código original es equivalente a esto:
typedef struct
{
class_t *vtable;
// other data members, if class->size > sizeof(class_t*) ...
} class_info_t;
inline void *new (const void *_class, ...)
{
const class_t *class = _class;
class_info_t *p = (class_info_t*) calloc(1, class->size);
assert(p);
p->vtable = class;
// other data members are implicitly zeroed by calloc() ...
...
}
Sin usar ninguna definición de tipo o conversión, memcpy()
se puede usar para lograr lo mismo, por ejemplo:
inline void *new (const void *_class, ...)
{
const class_t *class = _class;
void *p = calloc(1, class->size);
assert(p);
memcpy(p, &class, sizeof(class));
...
}
Qué significa
const class_t ** type?
Es un puntero a un puntero. Expresa que lo que sea que apunte es en realidad otro puntero. Y ese 2º apunta al objeto concreto de tipo, en este caso,class_t
[type] ---> [2nd pointer] ---> [class_t object]
¿Por qué *(const class_t **)p = class;
?
Lo que hace esta extraña construcción es colocar class
donde está el "objeto class_t". Ahora, viendo cómo new
se supone que se debe usar esta función, no es tan extraño.
new
es un constructor genérico de cualquier tipo personalizado (estructura) . Sin embargo, existe un requisito sobre el tipo , que es que la estructura debe incluir un puntero a class_t como primer miembro. Esto es para habilitar el polimorfismo y básicamente lo que hace C++ bajo el capó con un puntero a la tabla v.
Ahora, si tuviera que definir un tipo personalizado, haría:
struct Foo {
void *class_t;
// whatever members I need for my type
};
¿Cómo usarlo?
Ahora, cuando se define el tipo, hay una cosa más que hacer. Tenga en cuenta que la new
función como argumento toma un puntero inline void *new (const void *_class, ...)
que se usa como base const class_t *class = _class;
de la que se está creando. Significaría que necesita pasar algo mientras crea algo, entonces, ¿cuál es el punto?
Bueno, existe el truco para definir un puntero const al tipo en su encabezado que se puede usar para construir objetos de este tipo.
En el encabezado foo:
struct Foo {
void *class_t;
// whatever members I need for my type
};
static const struct class_t _Foo = {
sizeof (struct Foo),
// and other functions such ad ctor, dtro, etc...
};
const void *Foo = &_Foo;
Lo anterior define un tipo personalizado junto con una matriz para crear todos los objetos de este tipo. Es importante señalar que es de tipo y que se registra class_t
el tamaño del mismo .Foo
Finalmente, los nuevos objetos de Foo se crean de la siguiente manera:
void *f = new(Foo);
Sin Foo
embargo, es de tipo class_t
pero queremos que lo sea Foo
. El primer elemento de la Foo
estructura es el puntero a class_t
y, por lo tanto, para crear esta relación, se debe usar el doble puntero dentro delnew