sirven que punteros puntero para los estructuras estructura declaracion datos cadenas aritmetica apuntadores c pointers struct malloc

que - En C, ¿cómo elegiría si devolver una estructura o un puntero a una estructura?



punteros a cadenas (6)

Trabajar en mi músculo C últimamente y revisar las muchas bibliotecas con las que he estado trabajando me dio una buena idea de lo que es una buena práctica. Una cosa que NO he visto es una función que devuelve una estructura:

something_t make_something() { ... }

Por lo que he absorbido, esta es la forma "correcta" de hacer esto:

something_t *make_something() { ... } void destroy_something(something_t *object) { ... }

La arquitectura en el fragmento de código 2 es MUCHO más popular que el fragmento 1. Así que ahora pregunto, ¿por qué devolvería una estructura directamente, como en el fragmento 1? ¿Qué diferencias debo tener en cuenta al elegir entre las dos opciones?

Además, ¿cómo se compara esta opción?

void make_something(something_t *object)


A diferencia de otros lenguajes (como Python), C no tiene el concepto de una tuple . Por ejemplo, lo siguiente es legal en Python:

def foo(): return 1,2 x,y = foo() print x, y

La función foo devuelve dos valores como una tupla, que se asignan a x e y .

Como C no tiene el concepto de tupla, es inconveniente devolver múltiples valores de una función. Una forma de evitar esto es definir una estructura para contener los valores, y luego devolver la estructura, así:

typedef struct { int x, y; } stPoint; stPoint foo( void ) { stPoint point = { 1, 2 }; return point; } int main( void ) { stPoint point = foo(); printf( "%d %d/n", point.x, point.y ); }

Este es solo un ejemplo en el que puede ver que una función devuelve una estructura.


Algunas ventajas del primer enfoque:

  • Menos código para escribir.
  • Más idiomático para el caso de uso de devolver múltiples valores.
  • Funciona en sistemas que no tienen asignación dinámica.
  • Probablemente más rápido para objetos pequeños o más pequeños.
  • No hay pérdida de memoria debido a olvidarse de free .

Algunas desventajas:

  • Si el objeto es grande (digamos, un megabyte), puede provocar un desbordamiento de la pila o puede ser lento si los compiladores no lo optimizan bien.
  • Puede sorprender a las personas que aprendieron C en la década de 1970 cuando esto no era posible y no se han mantenido al día.
  • No funciona con objetos que contienen un puntero a una parte de sí mismos.

Casi siempre se trata de la estabilidad ABI. Estabilidad binaria entre versiones de la biblioteca. En los casos en que no lo es, a veces se trata de tener estructuras de tamaño dinámico. Raramente se trata de struct o rendimiento extremadamente grandes.

Es extremadamente raro que asignar una struct en el montón y devolverla sea casi tan rápido como devolverla por valor. La struct tendría que ser enorme.

Realmente, la velocidad no es la razón detrás de la técnica 2, retorno por puntero, en lugar de retorno por valor.

La técnica 2 existe para la estabilidad ABI. Si tiene una struct y su próxima versión de la biblioteca agrega otros 20 campos, los consumidores de su versión anterior de la biblioteca son compatibles con los binarios si reciben punteros preconstruidos. Los datos adicionales más allá del final de la struct que conocen es algo que no tienen que saber.

Si lo devuelve a la pila, la persona que llama está asignando la memoria y debe estar de acuerdo con usted en cuanto a su tamaño. Si su biblioteca se actualizó desde la última vez que se reconstruyó, va a la papelera.

La técnica 2 también le permite ocultar datos adicionales antes y después del puntero que devuelve (las versiones que anexan datos al final de la estructura es una variante de). Puede finalizar la estructura con una matriz de tamaño variable, o anteponer el puntero con algunos datos adicionales, o ambos.

Si desea struct asignadas a la pila en un ABI estable, casi todas las funciones que se comunican con la struct deben pasar información de versión.

Entonces

something_t make_something(unsigned library_version) { ... }

donde library_version es utilizada por la biblioteca para determinar qué versión de library_version se espera que regrese y cambia la cantidad de la pila que manipula . Esto no es posible usando el estándar C, pero

void make_something(something_t* here) { ... }

es. En este caso, something_t podría tener un campo de version como primer elemento (o un campo de tamaño), y necesitaría que se rellenara antes de llamar a make_something .

Otro código de biblioteca que toma something_t algo_t luego consultaría el campo de version para determinar con qué versión de something_t están trabajando.


Como regla general, nunca debe pasar objetos de struct por valor. En la práctica, estará bien hacerlo siempre que sean más pequeños o iguales al tamaño máximo que su CPU puede manejar en una sola instrucción. Pero estilísticamente, uno típicamente lo evita incluso entonces. Si nunca pasa estructuras por valor, más tarde puede agregar miembros a la estructura y esto no afectará el rendimiento.

Creo que void make_something(something_t *object) es la forma más común de usar estructuras en C. void make_something(something_t *object) la asignación a la persona que llama. Es eficiente pero no bonito.

Sin embargo, los programas C orientados a objetos usan something_t *make_something() ya que están construidos con el concepto de tipo opaco , lo que te obliga a usar punteros. Si el puntero devuelto apunta a la memoria dinámica u otra cosa depende de la implementación. OO con tipo opaco es a menudo una de las mejores y más elegantes formas de diseñar programas en C más complejos, pero lamentablemente, pocos programadores en C lo saben / les importa.


Cuando something_t es pequeño (léase: copiarlo es tan barato como copiar un puntero) y desea que se asigne de forma predeterminada a la pila:

something_t make_something(void); something_t stack_thing = make_something(); something_t *heap_thing = malloc(sizeof *heap_thing); *heap_thing = make_something();

Cuando something_t es grande o desea que esté asignado en el montón:

something_t *make_something(void); something_t *heap_thing = make_something();

Independientemente del tamaño de something_t , y si no te importa dónde está asignado:

void make_something(something_t *); something_t stack_thing; make_something(&stack_thing); something_t *heap_thing = malloc(sizeof *heap_thing); make_something(heap_thing);


Estoy algo sorprendido

La diferencia es que el ejemplo 1 crea una estructura en la pila, el ejemplo 2 la crea en el montón. En código C o C ++ que es efectivamente C, es idiomático y conveniente crear la mayoría de los objetos en el montón. En C ++ no lo es, en su mayoría van a la pila. La razón es que si crea un objeto en la pila, el destructor se llama automáticamente, si lo crea en el montón, debe llamarse explícitamente, por lo que es mucho más fácil asegurarse de que no haya pérdidas de memoria y manejar las excepciones. todo va a la pila. En C, el destructor debe llamarse explícitamente de todos modos, y no existe el concepto de una función destructora especial (tiene destructores, por supuesto, pero son funciones normales con nombres como destroy_myobject ()).

Ahora la excepción en C ++ es para objetos de contenedor de bajo nivel, por ejemplo, vectores, árboles, mapas hash, etc. Estos retienen a los miembros del montón y tienen destructores. Ahora, la mayoría de los objetos con mucha memoria consisten en unos pocos miembros de datos inmediatos que proporcionan tamaños, identificadores, etiquetas, etc., y luego el resto de la información en estructuras STL, tal vez un vector de datos de píxeles o un mapa de pares de palabras / valores en inglés. Entonces, la mayoría de los datos están de hecho en el montón, incluso en C ++.

Y el C ++ moderno está diseñado para que este patrón

class big { std::vector<double> observations; // thousands of observations int station_x; // a bit of data associated with them int station_y; std::string station_name; } big retrieveobservations(int a, int b, int c) { big answer; // lots of code to fill in the structure here return answer; } void high_level() { big myobservations = retriveobservations(1, 2, 3); }

Se compilará en un código bastante eficiente. El miembro de observación grande no generará copias de trabajo innecesarias.