2009-11-30 20 views
11

¿Cuáles serían las diferencias entre usar simplemente un vacío * en lugar de una unión? Ejemplo:union versus void pointer

struct my_struct { 
    short datatype; 
    void *data; 
} 

struct my_struct { 
    short datatype; 
    union { 
     char* c; 
     int* i; 
     long* l; 
    }; 
}; 

Tanto de los que se pueden utilizar para llevar a cabo la misma cosa, es mejor usar la unión o el vacío * sin embargo?

+5

Disculpe el cliché, pero el tamaño importa :) –

+2

@tinkertim y las probabilidades son de que sean del mismo tamaño. En un sistema Intel de 32 bits, ambos tomarán 4 bytes. técnicamente, void * podría ser más grande, pero casi nunca lo será (a menos que double * sea más grande ...) – Mikeage

+0

¿No son todos los punteros del mismo tamaño, independientemente del tipo de datos que señalan? –

Respuesta

16

Tuve exactamente el caso en nuestra biblioteca. Teníamos un módulo genérico de mapeo de cadenas que podía usar diferentes tamaños para el índice, 8, 16 o 32 bits (por razones históricas). Entonces el código estaba lleno de código así:

if(map->idxSiz == 1) 
    return ((BYTE *)map->idx)[Pos] = ...whatever 
else 
    if(map->idxSiz == 2) 
    return ((WORD *)map->idx)[Pos] = ...whatever 
    else 
    return ((LONG *)map->idx)[Pos] = ...whatever 

Había 100 líneas así. En un primer paso, lo cambié de unión y descubrí que era más legible.

switch(map->idxSiz) { 
    case 1: return map->idx.u8[Pos] = ...whatever 
    case 2: return map->idx.u16[Pos] = ...whatever 
    case 3: return map->idx.u32[Pos] = ...whatever 
} 

Esto me alowed para ver mejor lo que estaba pasando y entonces podría decidir eliminar por completo el uso de idxSiz variantes de sólo 32 bits índices. Pero esto solo fue posible una vez que el código se volvió más legible. PD: Eso fue solo una parte menor de nuestro proyecto, que se trata de varios cientos de miles de líneas de código escritas por personas que ya no existen. Entonces los cambios en el código son graduales para no romper las aplicaciones.

Conclusión: Incluso si las personas están menos acostumbradas a la variante de unión, la prefiero porque puede hacer que el código sea mucho más ligero de leer. En proyectos grandes, es extremadamente importante hacer que el código sea más legible, incluso si es usted quien lo leerá más adelante.

Editar: Se ha añadido el comentario, como comentarios no formato de código:

El cambio de interruptor de vino antes (esto es ahora el código real como lo fue)

switch(this->IdxSiz) { 
    case 2: ((uint16_t*)this->iSort)[Pos-1] = (uint16_t)this->header.nUz; break; 
    case 4: ((uint32_t*)this->iSort)[Pos-1] = this->header.nUz; break; 
} 

fue cambiado a

switch(this->IdxSiz) { 
    case 2: this->iSort.u16[Pos-1] = this->header.nUz; break; 
    case 4: this->iSort.u32[Pos-1] = this->header.nUz; break; 
} 

que no debería haber combinado las embellecimiento que hice en el código y sólo muestro ese paso.Pero publiqué mi respuesta desde mi casa donde no tuve acceso al código

+5

Creo que la legibilidad proviene de usar un conmutador en lugar de ifs anidados. – swegi

+0

El cambio para cambiar vino antes (este es ahora el código real) interruptor (this-> IdxSiz) { caso 2: (uint16_t *) this-> iSort) [Pos-1] = (uint16_t) this-> header.nUz; descanso; caso 4: ((uint32_t *) this-> iSort) [Pos-1] = this-> header.nUz; descanso; } se cambió a modificador (this-> IdxSiz) { caso 2: this-> iSort.u16 [Pos-1] = this-> header.nUz; descanso; caso 4: this-> iSort.u32 [Pos-1] = this-> header.nUz; descanso; } No debería haber combinado todo el embellecimiento que hice en el código y solo mostrar ese paso. Pero publiqué mi respuesta desde mi casa, donde no tuve acceso al código. –

11

En mi opinión, el puntero de vacío y el casting explícito es la mejor manera, porque es obvio para cada programador de C experimentado cuál es la intención.

Editar para aclarar: Si veo dicha unión en un programa, me preguntaría si el autor desea restringir los tipos de datos almacenados. Quizás se realizan algunos controles de cordura que solo tienen sentido en los tipos de números integrales. Pero si veo un puntero vacío, sé directamente que el autor diseñó la estructura de datos para guardar datos arbitrarios. Por lo tanto, también puedo usarlo para los tipos de estructura recién introducidos. Tenga en cuenta que podría ser que no puedo cambiar el código original, p. si es parte de una biblioteca de terceros.

+0

intención y uso son dos cosas diferentes cuando se trata de consumo de memoria. Una unión es tan grande como su miembro más grande. –

+0

pero en este caso ... todos los miembros en unión son punteros ... Entonces debería ser del mismo tamaño que un vacío * ¿no? – FatDaemon

+1

nada en la especificación requiere punteros para ser del mismo tamaño, pero nulo * tiene que ser lo suficientemente grande como para manejar cualquier otro puntero. Dicho esto, en la práctica, los punteros son generalmente del mismo tamaño – Mikeage

2

Aunque el uso de la unión no es común hoy en día, ya que la unión es más definitiva para su escenario de uso, se adapta bien. En la primera muestra de código, no se entiende el contenido de los datos.

5

El enfoque union requiere que usted conozca a priori todos los tipos que puedan ser utilizados. El enfoque void * permite almacenar tipos de datos que ni siquiera podrían existir cuando se escribe el código en cuestión (aunque haciendo mucho con un tipo de datos desconocido puede ser complicado, como requerir pasar un puntero a una función para invocar sobre esos datos en lugar de poder procesarlo directamente).

Editar: Dado que parece haber un malentendido acerca de cómo utilizar un tipo de datos desconocido: en la mayoría de los casos, proporciona algún tipo de función de "registro". En un caso típico, pasa punteros a funciones que pueden llevar a cabo todas las operaciones que necesita en un elemento que se almacena. Genera y devuelve un nuevo índice que se utilizará para el valor que identifica el tipo. Luego, cuando desea almacenar un objeto de ese tipo, establece su identificador en el valor que obtuvo del registro, y cuando el código que trabaja con los objetos necesita hacer algo con ese objeto, invoca la función apropiada a través del puntero que ingresó. En un caso típico, los punteros a las funciones estarán en un struct, y simplemente almacenará (punteros a) esas estructuras en una matriz. El valor del identificador que devuelve del registro es solo el índice en la matriz de esas estructuras donde ha almacenado este en particular.

+0

Sí, pero si no conoce el tipo de datos no tiene otra opción, entonces "¿Cuál?" la pregunta no tendrá sentido. –

2

Mi preferencia sería ir por la ruta de unión. El lanzamiento desde el vacío * es un instrumento contundente y acceder al datum a través de un puntero correctamente tipeado le da un poco de seguridad extra.

+0

no proporciona protección; si almacena un carácter * y accede como int *, el compilador estará perfectamente contento, aunque si no está alineado, tendrá problemas ... – Mikeage

+0

Tenga en cuenta que dije un "poco" de seguridad. La verdad es que C te dejará cometer el error que quieras. –

+0

¿qué bit es ese? forzándote a solo lanzar int * a char * o viceversa? – Mikeage

2

Lanza una moneda. La unión se usa más comúnmente con tipos no punteros, por lo que parece un poco extraño aquí. Sin embargo, la especificación de tipo explícita que proporciona es una documentación implícita decente. void * estaría bien siempre que siempre sepa que solo va a acceder a los punteros. No comience a poner números enteros allí y confíe en sizeof (void *) == sizeof (int).

No siento que de ninguna manera tenga ninguna ventaja sobre el otro al final.

2

Está un poco oscurecido en su ejemplo, porque está utilizando punteros y por lo tanto indirecta. Pero union ciertamente tiene sus ventajas.

Imagínese:

struct my_struct { 
    short datatype; 
    union { 
     char c; 
     int i; 
     long l; 
    }; 
}; 

Ahora usted no tiene que preocuparse de que la asignación para la parte del valor viene. No hay malloc() por separado ni nada de eso. Y puede encontrar que los accesos a ->c, ->i y ->l son un poco más rápidos. (Aunque esto solo puede hacer una diferencia si hay muchos de estos accesos).

+2

tal vez esos punteros apuntan a una matriz ...? – Mikeage

+0

@Mikeage - Entonces, si la matriz tiene un tamaño máximo fijo y ese tamaño es aceptable para poner en su estructura, todavía estaría tentado de usar una unión, que no contenga punteros pero que contenga matrices. De lo contrario, usaría punteros vacíos. Como el ejemplo no incluía un miembro de tamaño, supongo que no era eso en lo que estaba pensando. – asveikau

6

Es más común usar una unión para contener objetos reales en lugar de punteros.

Creo que la mayoría de los desarrolladores de C que respeto no se molestarían en unir diferentes punteros; si se necesita un puntero de propósito general, simplemente usar void * es ciertamente "la forma C". El lenguaje sacrifica mucha seguridad para permitirte aliar deliberadamente los tipos de cosas; teniendo en cuenta lo que hemos pagado por esta función, también podríamos usarla cuando simplifique el código. Es por eso que los escapes de la tipificación estricta siempre han estado ahí.

1

Realmente depende del problema que está tratando de resolver. Sin ese contexto, es realmente imposible evaluar cuál sería mejor.

Por ejemplo, si está tratando de crear un contenedor genérico como una lista o una cola que pueda manejar tipos de datos arbitrarios, entonces es preferible utilizar el puntero void. OTOH, si te estás limitando a un pequeño conjunto de tipos de datos primitivos, entonces el enfoque de unión puede ahorrarte tiempo y esfuerzo.

2

Si construye su código con -fstrict-aliasing (gcc) u opciones similares en otros compiladores, entonces debe tener mucho cuidado con la forma en que lo hace tu casting Puede lanzar un puntero tanto como desee, pero cuando lo desreferencia, el tipo de puntero que utiliza para la desreferencia debe coincidir con el tipo original (con algunas excepciones). No se puede, por ejemplo, hacer algo como:

 
void foo(void * p) 
{ 
    short * pSubSetOfInt = (short *)p ; 
    *pSubSetOfInt = 0xFFFF ; 
} 

void goo() 
{ 
    int intValue = 0 ; 

    foo(&intValue) ; 

    printf("0x%X\n", intValue) ; 
} 

No se deje sorprendidos si esto imprime 0 (por ejemplo) en lugar de 0xFFFF o 0xFFFF0000 como se puede esperar cuando se construye con la optimización. Una forma de hacer que este código funcione es hacer lo mismo con una unión, y probablemente el código también sea más fácil de entender.

1

La unión reserva suficiente espacio para el miembro más grande, no tiene que ser el mismo, ya que el vacío * tiene un tamaño fijo, mientras que la unión se puede usar para tamaños arbitrarios.

#include <stdio.h> 
#include <stdlib.h> 

struct m1 { 
    union { 
    char c[100]; 
    }; 
}; 

struct m2 { 
    void * c; 
}; 


int 
main() 
{ 
printf("sizeof m1 is %d ",sizeof(struct m1)); 
printf("sizeof m2 is %d",sizeof(struct m2)); 
exit(EXIT_SUCCESS); 
} 

Salida: m1 sizeof es de 100 m2 sizeof es 4

EDITAR: suponiendo que sólo utiliza punteros del mismo tamaño que void *, creo que la unión es mejor, ya que ganará un poco de detección de errores al intentar establecer .c con un puntero entero, etc. ' void *, a menos que esté creando su propio asignador, definitivamente es rápido y sucio, para bien o para mal.