Foro de elhacker.net

Programación => Programación C/C++ => Mensaje iniciado por: prosebas en 19 Abril 2021, 06:56 am



Título: cast Void pointer en c
Publicado por: prosebas en 19 Abril 2021, 06:56 am
Buenas noches comunidad, estoy realizando un arbol general en c que cuenta con una lista génerica para guardar los descendientes(hijos) que hice  en c de forma génerica usando void pointer pero estoy teniendo problemas  :-(.

El problema que me surge ya lo tengo identificado, sin embargo, no se como solucionarlo, lo que pasa es que cuando quiero imprimir un dato del nodo general que se encuentra dentro de la lista me esta saliendo basura, el problema debe ser que estoy realizando mal el cast pero ya intente de varias formas y sigue sin funcionar.

Estructura de la lista
Código
  1. typedef struct node
  2. {
  3.    void *data;
  4.    struct node *next;
  5. } node, Node;
  6. typedef struct list
  7. {
  8.    struct node *head;
  9.  
  10. } list, List;
  11.  
  12.  

Estructura del arbol
Código
  1. typedef struct NodeGeneral
  2. {
  3.    void *data;
  4.    list *dec;
  5. } GeneralNode;
  6. typedef struct GeneralTree
  7. {
  8.    GeneralNode *root;
  9. } GeneralTree;
  10.  



main
En el siguiente código se encuentra el fragmento donde estoy realizando el cast(Linea 12).
Código
  1. int main(void)
  2. {
  3.  
  4.  List *list;
  5.  GeneralNode *proof;
  6.  int x = 5;
  7.  proof = init_GeneralNode((int *)&x);
  8.  init_list(&list, &proof);
  9.  Node *aux = list->head;
  10.  while (aux != NULL)
  11.  {
  12.    GeneralNode *tmp =(GeneralNode *)aux->data; //Aqui estoy realizando el casteo
  13.    printf("::%d\n", *((int *)tmp->data));
  14.    aux = aux->next;
  15.  
  16.  }
  17.  
  18. }
  19. //Las funciones init crean la memoria y establecen los datos
  20.  
Anexó las funciones con las que inicialice la lista y establezco la cabeza(Ya la he probado con diferentes tipos de datos(int,float... datos nativos) y funciona bien la lista)
Código
  1. node *newNode(void *value)
  2. {
  3.    node *newNode = (node *)malloc(sizeof(node));
  4.    newNode->data = value;
  5.    newNode->next = NULL;
  6.    return newNode;
  7. }
  8. void init_list(list **lista, void *value)
  9. {
  10.    list *aux = *lista;
  11.    aux = (list *)malloc(sizeof(list));
  12.    aux->head = newNode(value);
  13.    *lista = aux;
  14. }
  15. //************************************
  16. GeneralNode *init_GeneralNode(void *data)
  17. {
  18.    GeneralNode *newNode = (GeneralNode *)malloc(sizeof(GeneralNode));
  19.    newNode->data = data;
  20.    newNode->dec = NULL;
  21.    return newNode;
  22. }
  23.  

En el NodoGeneral que tengo,en el caso de la lista de descendiente cada dato de la lista representa un NodoGeneral.El next, el puntero al siguiente hijo.

Les agradezco si me pueden ayudar.


Título: Re: cast Void pointer en c
Publicado por: Usuario887 en 19 Abril 2021, 11:33 am
Código
  1. GeneralNode *tmp =(GeneralNode *)*((size_t*)aux->data);

Intenta recordar a que exactamente apuntan tus punteros en cada momento dado mientras programas. Si no te toparas con problemas.
Saludos.



me esta saliendo basura,

PD: Si fuera basura luego de cierta cantidad de intentos el programa se hubiese detenido. Sin embargo a medida que compilas te das cuenta de que la cantidad que esta retornando varia en un rango especifico. Esas suelen ser las direcciones que reserva Windows para con la memoria.
Lo que esta imprimiendo printf en tu programa original no es basura, era la direccion de tu variable proof en memoria, con menos precision a causa del casting que hiciste al tipo GeneralNode. size_t es adecuado para estos casos porque se identifica como palabra, es decir, puede contener una direccion a cualquier parte de la memoria (direccionable dentro de tu programa).


Título: Re: cast Void pointer en c
Publicado por: prosebas en 19 Abril 2021, 16:14 pm
Código
  1. GeneralNode *tmp =(GeneralNode *)*((size_t*)aux->data);

Intenta recordar a que exactamente apuntan tus punteros en cada momento dado mientras programas. Si no te toparas con problemas.
Saludos.

Gracias ya esta solucionado, ese era el error, sin embargo, no entiendo porque es (GeneralNode *)*((size_t*)aux->data especificamente del size_t.



Título: Re: cast Void pointer en c
Publicado por: MinusFour en 19 Abril 2021, 19:43 pm
Tu variable proof es de tipo GeneralNode*. Si haces &proof, este valor es GeneralNode**. Básicamente estás poniendo GeneralNode** en node->data.

Simplemente pasa:

Código
  1. init_list(&list, proof);

O si no vas a tener que dereferenciar el puntero dos veces prácticamente.

Quizás sea buena idea considerar lo mismo para lista (en lugar de usar list** usar list* y no usar &list. Yo también no le daría el mismo nombre que tus tipos.


Título: Re: cast Void pointer en c
Publicado por: Usuario887 en 20 Abril 2021, 11:25 am
Gracias ya esta solucionado, ese era el error, sin embargo, no entiendo porque es (GeneralNode *)*((size_t*)aux->data especificamente del size_t.

Lo que "es" size_t no es aux->data. Lo que es de tamaño size_t es la palabra de tu computador.
https://es.wikipedia.org/wiki/Palabra_(inform%C3%A1tica) (https://es.wikipedia.org/wiki/Palabra_(inform%C3%A1tica))

Te equivocaste en dos cosas:

1. Accediste a la direccion de un dato, pensando que era el dato mismo. Esto te ha pasado por no mantener un registro mental o escrito de tus punteros. Aqui estas usando una variedad de punteros, innecesarios en cierta manera como añadio MinusFour, sin embargo entiendo que estas experimentando... cosa que yo veo imprescindible para hacerse habil con este tema.
2. Hiciste un casting equivocado segun la logica de tu programa, por lo que aunque hubieses accedido al dato correcto, habrias estado desalineado [https://en.wikipedia.org/wiki/Data_structure_alignment (https://en.wikipedia.org/wiki/Data_structure_alignment)] y habrias accedido a un dato, pero incorrecto.

Con size_t se puede arreglar el problema del alineamiento. ¿Por que? Porque no size_t en si, sino (size_t *) es algo asi como un puntero (void *) pero accesible. Si tu haces esto:

Código
  1. int x;
  2. void *p;
  3. p=(void *)&x;
  4. *p=1;

El compilador te va a dar un error:
Citar
error: 'void*' is not a pointer-to-object type

Sin embargo, puedes hacerlo con size_t:

Código
  1. int x;
  2. size_t *p;
  3. p=(size_t *)&x;
  4. *p=1;

Y lo compila perfectamente.

size_t digamos que lo uso como un intermediario, no como un tipo.
Lee los archivos que he enlazado y te daras cuenta de que hay varias formas de llegar a eso.

Saludos.





Título: Re: cast Void pointer en c
Publicado por: RayR en 20 Abril 2021, 19:49 pm
El problema concreto que tenías se arregla como te dijo MinusFour. El cast de la línea 12 tal como lo tenías ya estaba bien. El size_t es innecesario e incorrecto. Jamás vas a tener problemas de alineación si usas punteros al mismo tipo del dato al que deseas referenciar. aux->data se refiere a proof, que apunta a un bloque reservado mediante malloc(sizeof(GeneralNode)); por lo que (GeneralNode *) es lo único que necesitas. Ese size_t no tiene ninguna razón para estar ahí.

Más aún, esto:

Código
  1.    int x;
  2.    size_t *p;
  3.    p=(size_t *)&x;
  4.    *p=1;
  5.  

viola las reglas de C. Simplificando un poco, sólo es válido acceder a un objeto o variable de tipo A mediante un puntero al mismo tipo A (o a un tipo compatible), o bien, mediante un puntero char. size_t, por supuesto, no es un tipo compatible con int, y mucho menos con GeneralNode. En un ejemplo tan sencillo, seguramente no habría problemas con ese código, pero independientemente de eso, es una clara violación a las reglas del lenguaje; en concreto, a la llamada "strict aliasing rule", por lo que, aunque compile, es un error, y el compilador es libre de realizar optimizaciones que hagan que tu programa funcione de forma incorrecta. A veces puede ser válido saltarse algunas reglas, y de hecho los compiladores a veces ofrecen opciones para estos casos, tipo -fno-strict-aliasing, pero esto sólo se debería hacer si es 100% necesario y siendo conscientes de que nos salimos del estándar y nuestro código ya no es no portable.

Las reglas que definen qué tipos son compatibles con otros pueden ser algo complejas, pero como consejo y regla general, a menos que sepamos muy bien lo que hacemos y tengamos amplia experiencia, sólo deberíamos usar punteros de exactamente el mismo tipo que la variable a referenciar (o punteros char, que es la excepción a la regla).


Título: Re: cast Void pointer en c
Publicado por: Usuario887 en 21 Abril 2021, 12:36 pm
RayR, creo que no entendiste el problema del hombre. El quería hacer un casting correcto de un valor que definió como (void *). Este tipo de dato no puede ser accedido directamente, así que use (size_t *) (Ojo, size_t* y no size_t) como un intermediario al dato.

¿Al menos sabes por qué la solución que le di le funcionó?:
Código
  1. GeneralNode *tmp =(GeneralNode *)*((size_t*)aux->data);



PD: Si el dato es void* como en este caso y sin embargo se insiste en un tipo especifico que puede variar con el tiempo (pues esa es la razon por la que existen punteros void*) se puede incluso indexar un array en un sentido general:

Código
  1. _t *x=(_t*)((size_t*)addr)+sizeof(_t)*i;



PD 2: Esta claro que pudo haber hecho su programa mucho mas eficientemente, pero la pregunta que tenia era como hacer el casting correctamente. Esa es la forma. Luego se dara cuenta de por que es mejor algo mas simple, mientras tanto no creo que preste atencion a eso.


Título: Re: cast Void pointer en c
Publicado por: MinusFour en 21 Abril 2021, 15:57 pm
En el dado caso que quisiera usar un puntero de ese tipo... porque no usar:

Código
  1. GeneralNode *tmp = *(GeneralNode **)aux->data;

En lugar de:

Código
  1. GeneralNode *tmp =(GeneralNode *)*((size_t*)aux->data);


Título: Re: cast Void pointer en c
Publicado por: RayR en 21 Abril 2021, 16:40 pm
Claro que lo entendí. Y su problema más que con el cast, era con lo que le explicó MinusFour. Estás mezclando y confundiendo varias cosas. Por ejemplo, la alineación no tiene nada que ver en este caso. Le fallaba el programa porque aux->data tenía un puntero a puntero a GeneralNode, pero en su código lo trataba como puntero a GeneralNode. Al desreferenciarlo, estaba sólo haciéndolo con el primer puntero. En todo caso, el cast correcto debería haber sido:

Código
  1. GeneralNode* tmp = *(GeneralNode**)aux->data;

Pero de ninguna manera el puntero a size_t era correcto. Esta línea

    
Código
  1. GeneralNode *tmp =(GeneralNode *)*((size_t*)aux->data);

aunque compile y funcione, no es correcta. En ningún lugar donde se dediquen a esto lo aceptarían.

Y por supuesto que entendí por qué tu solución funcionó. Llevo muchos años trabajando en esto profesionalmente, casi todos ellos en C o C++, como para no entender algo tan básico. Además, sí que estabas usando un size_t, aunque de forma implícita. Cuando haces esto

Código
  1. *((size_t*)aux->data)

el asterisco más a la izquierda desreferencia el puntero y convierte su valor a size_t. Ya desde ahí tu código está equivocado. Estás invocando UB (undefined behavior, comportamiento indefinido). Para empezar, size_t no necesariamente tiene el mismo tamaño que un puntero y, de hecho, en algunas plataformas es diferente, por lo que, en teoría, se podría perder precisión con esa conversión intermedia. Ahora, en realidad en todas las plataformas actuales, size_t sí que ocupa lo mismo que la mayoría de punteros, pero aún así, eso es incorrecto, es UB. No lo digo yo, lo dice el estándar del lenguaje. Sí, es un error algo común pensar que size_t (o sus punteros) es apto para punteros "genéricos". No lo es. Como ya había mencionado, a veces sí se usa, pero sólo en casos muy concretos y a  sabiendas de que no es algo estándar. El ejemplo de este tema de ninguna manera justifica su uso. En todo caso, para eso existe uintptr_t. Aún así, y esta es la verdadera cuestión del asunto, cuando tenemos un GeneralNode**, como prosebas en su código original, lo que se hace es convertirlo a GeneralNode**; nada más.

Ya perdí más tiempo del que hubiera querido, pero, una última cosa. Te recomiendo que leas más sobre punteros. En tus comentarios de este hilo noto más o menos lo mismo que en tus dudas sobre ensamblador: tienes una cierta idea del tema, pero confundes y mezclas muchas cosas. Lo peor que puede uno hacer es creerse un experto cuando apenas empieza (por el tipo de dudas que recuerdo que has planteado, y por lo que escribes en este tema, no parece que tengas mucha experiencia) porque ahí se deja de aprender. Si aún piensas que estás en lo correcto, ¿por qué no pones el código con tu línea en stackoverflow y preguntas si es correcta? Te garantizo que te van a responder que no, y sus respuestas no van a variar mucho de lo que yo te dije. No te tomaría ni 5 minutos copiar y pegar, y saldrías de dudas. O te puedes quedar así, convencido de que tienes razón porque sí. Cada quién.

Editado: veo que MinusFour ya te había respondido. Y sí, de nuevo, el código que él pone es el correcto.


Título: Re: cast Void pointer en c
Publicado por: Usuario887 en 22 Abril 2021, 10:57 am
En el dado caso que quisiera usar un puntero de ese tipo... porque no usar:

Código
  1. GeneralNode *tmp = *(GeneralNode **)aux->data;

Esta si que es la forma correcta.

Lo peor que puede uno hacer es creerse un experto

Perdoname hacer uso de tu precioso tiempo respondiendo esta cita:

Tienes razon pero no me creo un experto. Lo que crei fue que tu le estabas demostrando otra forma de hacerlo intentando menospreciar la mia, solo otra, y no otra correcta. Hay muchos arrogantes por aqui.

PD: Me jode mas el ego que alguien crea que me la estoy tirando de experto, a que me demuestren que no soy un experto.


Título: Re: cast Void pointer en c
Publicado por: RayR en 22 Abril 2021, 15:35 pm
Eso de experto lo dije porque esta frase:

Código
  1. ¿Al menos sabes por qué la solución que le di le funcionó?:

me había sonado un poco condescendiente, como si fuera demasiado complicado para que alguien (yo) lo entendiera. En fin.

Sólo puntualizar una cosa. No es que almacenar el valor de un puntero en un entero como size_t esté mal en sí. Como había dicho antes, a veces se hace; la propia API de Windows es un ejemplo. Pero aunque en todos los sistemas actuales size_t es lo suficientemente grande para guardar direcciones, no hay garantía de que siempre sea así, y en los casos donde no lo sea, el resultado quedaría indefinido (por ejemplo, en DOS de 16 bits, el programa seguramente no funcionaría correctamente, al menos si se compila para modelo compact, large, etc.) por lo que, si no es estrictamente necesario no se debería hacer.


Título: Re: cast Void pointer en c
Publicado por: Usuario887 en 22 Abril 2021, 18:38 pm
Código
  1. ¿Al menos sabes por qué la solución que le di le funcionó?:

Quise decir: Si contestaste a mi explicacion antes de probar/depurar o al menos echarle un vistazo a la logica de mi planteamiento, entonces...
Nada mas.

Sólo puntualizar una cosa. No es que almacenar el valor de un puntero en un entero como size_t esté mal en sí. Como había dicho antes, a veces se hace; la propia API de Windows es un ejemplo. Pero aunque en todos los sistemas actuales size_t es lo suficientemente grande para guardar direcciones, no hay garantía de que siempre sea así, y en los casos donde no lo sea, el resultado quedaría indefinido (por ejemplo, en DOS de 16 bits, el programa seguramente no funcionaría correctamente, al menos si se compila para modelo compact, large, etc.) por lo que, si no es estrictamente necesario no se debería hacer.

Claro. Por eso le habia dicho al hombre esto:

Aqui estas usando una variedad de punteros, innecesarios en cierta manera como añadio MinusFour, sin embargo entiendo que estas experimentando... cosa que yo veo imprescindible para hacerse habil con este tema

El no esta haciendo nada serio. Tu has dicho que eres un habil experto en C. Me imagino que has jugado con el lenguaje de esta forma en el pasado, y eso ha jugado a su vez un papel importante en tu desarrollo. Si no, si eres mas ortodoxo, y en verdad valoras tu tiempo, olvida este hilo porque nunca nos vamos a entender.

Respecto al direccionamiento, eso tambien se lo mencione...

Lo que esta imprimiendo printf en tu programa original no es basura, era la direccion de tu variable proof en memoria, con menos precision a causa del casting que hiciste al tipo GeneralNode. size_t es adecuado para estos casos porque se identifica como palabra, es decir, puede contener una direccion a cualquier parte de la memoria (direccionable dentro de tu programa).
(direccionable dentro de tu programa)

De hecho, lo que si que no se es hasta que punto size_t* puede direccionar la memoria. ¿Hablas del caso en que una direccion contenga ademas un segmento?


Título: Re: cast Void pointer en c
Publicado por: RayR en 24 Abril 2021, 04:41 am
No dije ser experto, sólo que algo de experiencia sí que tengo. Estoy de acuerdo en que experimentar y equivocarse es indispensable para aprender, aunque normalmente yo trato de señalar cuando algo no es del todo correcto, sobre todo en cuestiones relacionadas con punteros, que puede ser un tema confuso.

Respecto al direccionamiento, eso tambien se lo mencione...
(direccionable dentro de tu programa)

El problema no es si es direccionable o no por el programa. Como había mencionado en otro comentario, aunque es una idea muy extendida, el objetivo de size_t no es almacenar direcciones. Nunca fue creado para eso, y de hecho, existen plataformas donde tiene un tamaño menor a un puntero, por lo que es totalmente posible, y probable, tener variables cuyas direcciones no pueden ser almacenadas en un size_t. Eso es válido, ya que la especificación de C dice expresamente que aunque un puntero puede convertirse a cualquiera de los tipos enteros (incluyendo size_t), puede no caber, y que en ese caso, el comportamiento del programa queda indefinido. En la práctica, en casi todas las arquitecturas actuales sí tiene el tamaño suficiente, y por eso no es tan raro almacenar direcciones en variables enteras (aunque normalmente se usa directamente el long o unsigned long), sin embargo, no hay garantías. Por eso se incorporaron a C los tipos opcionales intptr_t y uintptr_t, los únicos con los que es seguro almacenar punteros (siempre que no sean punteros a funciones). Por cierto, tampoco se requiere que size_t tenga el tamaño de una palabra, aunque eso es lo habitual.

De hecho, lo que si que no se es hasta que punto size_t* puede direccionar la memoria. ¿Hablas del caso en que una direccion contenga ademas un segmento?

size_t* puede direccionar la memoria, como el resto de punteros, aunque aquí también, C dice que punteros a diferentes tipos pueden tener tamaños distintos, por lo que en teoría sólo deberían apuntar a memoria donde haya un valor size_t. Claro que en la realidad es prácticamente seguro que todos los punteros tengan el mismo tamaño, exceptuando, otra vez, los punteros a funciones. Ni siquiera void* es válido para apuntar a funciones. Para eso únicamente se deben usar punteros a funciones. Y por supuesto, desreferenciar un puntero que apunta a un dato cuyo tipo no es el mismo que el del puntero es arriesgado. No sólo puede haber problemas de alineación o tamaño, sino que a veces los compiladores, apegándose a lo que dicen las reglas, hacen optimizaciones que rompen código que accede a direcciones por medio de punteros a tipos distintos. Según su rígida interpretación, dado que un puntero de tipo A "no puede" apuntar a un valor de un tipo distinto a A, el compilador no está obligado asegurar que ese acceso se produzca correctamente, y si se rompe algo, toda la culpa es del programador. Eso ha pasado con el kernel Linux, por ejemplo, y hay mucho debate al respecto.

Lo de memorias con segmentos es un ejemplo. En el caso de DOS de 16 bits, para modelo large, por ejemplo, típicamente size_t es de 16 bits, pero los punteros ocupan 32. En este caso específico (x86) sí sería porque contienen segmentos, pero aún en ausencia de segmentación, otras arquitecturas pueden tener punteros más grandes que size_t por motivos diferentes.


Título: Re: cast Void pointer en c
Publicado por: MAFUS en 26 Abril 2021, 16:04 pm
Si se me permite un inciso, y por lo que sé, todos los punteros en C (repito: punteros) tienen el mismo tamaño: un tamaño capaz de guardar una dirección de memoria.
Otra cosa es el tipo del puntero: éste le dice al compilador el tamaño del dato al que se apunta para que cuándo se dereferencíe tome tantos bytes de la memoria a partir de la dirección del puntero para trabajar con él.
Es decir, un puntero en sí da igual que sea int, long, unsigned, o una función de muchos parámetros, es solo una dirección de memoria.
Por eso el void* puede convertirse en cualquier cosa, porqué únicamente guarda la información de la dirección, no del tamaño del dato y por eso el compilador necesita el cast, para saber cuántos bytes toma el dato que se guarda allí.
En los sistemas en modo real un puntero puede apuntar a cualquier sitio: a la memoria gráfica, a alguna parte del sistema operativo, a otros programas... y lo peor es que dejaba escribir allí. Por eso durante tanto tiempo los punteros eran una cosa muy temida, no académicamente, sino porqué podían causar auténticos desastres. También los virus tenían más acceso a la máquina.
Ahora con las protecciones propias del procesador y del sistema operativo un puntero mal dirigido lo único que puede conseguir es que el sistema cierre el programa.


Título: Re: cast Void pointer en c
Publicado por: MinusFour en 26 Abril 2021, 21:51 pm
Si se me permite un inciso, y por lo que sé, todos los punteros en C (repito: punteros) tienen el mismo tamaño: un tamaño capaz de guardar una dirección de memoria.
Otra cosa es el tipo del puntero: éste le dice al compilador el tamaño del dato al que se apunta para que cuándo se dereferencíe tome tantos bytes de la memoria a partir de la dirección del puntero para trabajar con él.
Es decir, un puntero en sí da igual que sea int, long, unsigned, o una función de muchos parámetros, es solo una dirección de memoria.
Por eso el void* puede convertirse en cualquier cosa, porqué únicamente guarda la información de la dirección, no del tamaño del dato y por eso el compilador necesita el cast, para saber cuántos bytes toma el dato que se guarda allí.
En los sistemas en modo real un puntero puede apuntar a cualquier sitio: a la memoria gráfica, a alguna parte del sistema operativo, a otros programas... y lo peor es que dejaba escribir allí. Por eso durante tanto tiempo los punteros eran una cosa muy temida, no académicamente, sino porqué podían causar auténticos desastres. También los virus tenían más acceso a la máquina.
Ahora con las protecciones propias del procesador y del sistema operativo un puntero mal dirigido lo único que puede conseguir es que el sistema cierre el programa.

Hasta donde yo se no hay ninguna garantía que un programa en C pueda usar un puntero void* para almacenar punteros a funciones (los punteros de funciones pueden usar otro tipo de direccionamiento). Había un apartado en la especificación en la que dejaba esto en claro. Hay otros estándares y extensiones que quizás puedan agregar esa limitación pero al menos el estándar en C no ofrece esa garantía.

Esto ya se sale fuera del tema.


Título: Re: cast Void pointer en c
Publicado por: RayR en 26 Abril 2021, 22:57 pm
Es tal como dijo MinusFour. Y C++ va aún más lejos. Los punteros a funciones miembro normalmente son totalmente distintos a los otros, incluyendo los punteros a funciones "normales", no miembro. Por ejemplo, en Visual C++, cuando se usa herencia múltiple, estos punteros ocupan el doble de espacio que los normales (128 bits en programas de 64 bits). La explicación se saldría mucho del tema, pero tiene que ver con que los objetos con varias clases padre tienen más de un puntero this. En GCC (al menos en x86 y x86-64) incluso para objetos sin superclases, los punteros a sus funciones miembro ocupan el doble.