Devuelve un `struct` de una función en C

171

Hoy les estaba enseñando a un par de amigos cómo usar C structs. Uno de ellos preguntó si podía devolver un structde una función, a lo que respondí: "¡No! En su lugar, devolvería los punteros a malloced dinámicamente struct".

Viniendo de alguien que principalmente hace C ++, esperaba no poder devolver structs por valores. En C ++ puede sobrecargar los operator =objetos y tiene mucho sentido tener una función para devolver su objeto por valor. En C, sin embargo, no tienes esa opción, por lo que me hizo pensar qué está haciendo realmente el compilador. Considera lo siguiente:

struct MyObj{
    double x, y;
};

struct MyObj foo(){
    struct MyObj a;

    a.x = 10;
    a.y = 10;

    return a;
}        

int main () {

    struct MyObj a;

    a = foo();    // This DOES work
    struct b = a; // This does not work

    return 0;
}    

Entiendo por struct b = a;qué no debería funcionar: no puede sobrecargar operator =su tipo de datos. ¿Cómo es que se a = foo();compila bien? ¿Significa algo más que struct b = a;? Tal vez la pregunta que returndebe =hacerse es: ¿Qué hace exactamente la declaración en conjunto para firmar?

[editar]: Ok, me acaban de señalar que struct b = aes un error de sintaxis, ¡eso es correcto y soy un idiota! ¡Pero eso lo hace aún más complicado! ¡Usar struct MyObj b = así funciona! ¿Que me estoy perdiendo aqui?

mmirzadeh
fuente
24
struct b = a;es un error de sintaxis ¿Y si lo intentas struct MyObj b = a;?
Greg Hewgill
2
@ GregHewgill: Tienes toda la razón. Muy interesante, sin embargo, struct MyObj b = a;parece funcionar :)
mmirzadeh

Respuestas:

200

Puede devolver una estructura desde una función (o usar el =operador) sin ningún problema. Es una parte bien definida del lenguaje. El único problema struct b = aes que no proporcionó un tipo completo. struct MyObj b = afuncionará bien. También puede pasar estructuras a funciones: una estructura es exactamente la misma que cualquier tipo incorporado para fines de paso de parámetros, valores de retorno y asignación.

Aquí hay un programa de demostración simple que hace las tres cosas: pasa una estructura como parámetro, devuelve una estructura de una función y usa estructuras en las declaraciones de asignación:

#include <stdio.h>

struct a {
   int i;
};

struct a f(struct a x)
{
   struct a r = x;
   return r;
}

int main(void)
{
   struct a x = { 12 };
   struct a y = f(x);
   printf("%d\n", y.i);
   return 0;
}

El siguiente ejemplo es casi exactamente el mismo, pero utiliza el tipo incorporado intpara fines de demostración. Los dos programas tienen el mismo comportamiento con respecto al paso por valor para el paso de parámetros, asignación, etc.

#include <stdio.h>

int f(int x) 
{
  int r = x;
  return r;
}

int main(void)
{
  int x = 12;
  int y = f(x);
  printf("%d\n", y);
  return 0;
}
Carl Norum
fuente
15
Eso es bastante interesante Siempre tuve la impresión de que necesitas punteros para esto. Estaba equivocado :)
mmirzadeh
8
Ciertamente no necesitas punteros. Dicho esto, la mayoría de las veces desearía usarlos: las copias de memoria implícitas que tienen lugar volteando estructuras por valor pueden ser una verdadera pérdida de ciclos de CPU, sin mencionar el ancho de banda de memoria.
Carl Norum
10
@CarlNorum ¿qué tan grande debe ser una estructura para que una copia cueste más que malloc + gratis?
josefx
77
@josefx, una sola copia? Probablemente enorme. La cuestión es que, normalmente, si pasa estructuras por valor, las copia mucho . De todos modos, no es tan simple como eso. Podría estar pasando por estructuras locales o globales, en cuyo caso su costo de asignación es prácticamente gratuito.
Carl Norum
77
Necesita punteros y asignación de memoria para el valor devuelto fuera del cuerpo de la función tan pronto como no se conozca la cantidad de memoria asignada para un valor en tiempo de compilación. Es para estructuras, por lo que las funciones C no tienen problemas para devolverlas.
reinierpost
33

Al realizar una llamada como a = foo();, el compilador puede insertar la dirección de la estructura de resultados en la pila y pasarla como un puntero "oculto" a la foo()función. Efectivamente, podría convertirse en algo como:

void foo(MyObj *r) {
    struct MyObj a;
    // ...
    *r = a;
}

foo(&a);

Sin embargo, la implementación exacta de esto depende del compilador y / o plataforma. Como señala Carl Norum, si la estructura es lo suficientemente pequeña, incluso podría devolverse completamente en un registro.

Greg Hewgill
fuente
11
Eso depende totalmente de la implementación. Por ejemplo, armcc pasará estructuras suficientemente pequeñas en el parámetro regular que pasa (o devuelve el valor) registros.
Carl Norum
¿No sería eso devolver un puntero a una variable local? La memoria para la estructura devuelta no puede ser parte del foomarco de la pila. Tiene que estar en un lugar que sobreviva más allá del regreso de foo.
Anders Abel
@AndersAbel: Creo que lo que Greg quiere decir es que el compilador toma un puntero a la variable en la función principal y la pasa a la función foo. Dentro de la función foo, solo haces la tarea
mmirzadeh
44
@AndersAbel: Al *r = afinal (efectivamente) haría una copia de la variable local a la variable del llamante. Digo "efectivamente" porque el compilador podría implementar RVO y eliminar la variable local por acompleto.
Greg Hewgill
3
Aunque esto no responde directamente a la pregunta, esta es la razón por la cual muchas personas caerán aquí a través de google c return struct: saben que en cdecl eaxse devuelve por valor y que las estructuras en general no encajan dentro eax. Esto es lo que estaba buscando.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
14

La struct blínea no funciona porque es un error de sintaxis. Si lo expande para incluir el tipo, funcionará bien

struct MyObj b = a;  // Runs fine

Lo que C está haciendo aquí es esencialmente una memcpyestructura de origen al destino. Esto es cierto tanto para la asignación como para el retorno de structvalores (y realmente cualquier otro valor en C)

JaredPar
fuente
+1, de hecho, muchos compiladores en realidad emitirán una llamada literal memcpyen este caso, al menos, si la estructura es razonablemente grande.
Carl Norum
Entonces, durante la inicialización de un tipo de datos, ¿funciona la función memcpy?
bhuwansahni
1
@bhuwansahni No estoy muy seguro de lo que estás preguntando aquí. Podrías elaborar un poco?
JaredPar
44
@JaredPar: los compiladores a menudo llaman literalmente a la memcpyfunción para las situaciones de estructura. Puede hacer un programa de prueba rápida y ver GCC hacerlo, por ejemplo. Para los tipos integrados que no sucederán, no son lo suficientemente grandes como para activar ese tipo de optimización.
Carl Norum
3
Definitivamente es posible hacer que suceda: el proyecto en el que estoy trabajando no tiene un memcpysímbolo definido, por lo que a menudo nos encontramos con errores de enlace de "símbolo indefinido" cuando el compilador decide escupir uno solo.
Carl Norum
9

Sí, es posible que podamos pasar la estructura y devolver la estructura también. Tenía razón, pero en realidad no pasó el tipo de datos que debería ser como esta estructura MyObj b = a.

En realidad, también llegué a saber cuándo estaba tratando de encontrar una mejor solución para devolver más de un valor para la función sin usar puntero o variable global.

Ahora a continuación se muestra el ejemplo de lo mismo, que calcula la desviación de las calificaciones de un estudiante sobre el promedio.

#include<stdio.h>
struct marks{
    int maths;
    int physics;
    int chem;
};

struct marks deviation(struct marks student1 , struct marks student2 );

int main(){

    struct marks student;
    student.maths= 87;
    student.chem = 67;
    student.physics=96;

    struct marks avg;
    avg.maths= 55;
    avg.chem = 45;
    avg.physics=34;
    //struct marks dev;
    struct marks dev= deviation(student, avg );
    printf("%d %d %d" ,dev.maths,dev.chem,dev.physics);

    return 0;
 }

struct marks deviation(struct marks student , struct marks student2 ){
    struct marks dev;

    dev.maths = student.maths-student2.maths;
    dev.chem = student.chem-student2.chem;
    dev.physics = student.physics-student2.physics; 

    return dev;
}
Un hombre
fuente
5

Hasta donde puedo recordar, las primeras versiones de C solo permitían devolver un valor que podría caber en un registro de procesador, lo que significa que solo podía devolver un puntero a una estructura. La misma restricción aplicada a los argumentos de la función.

Las versiones más recientes permiten pasar objetos de datos más grandes como estructuras. Creo que esta característica ya era común durante los años ochenta o principios de los noventa.

Sin embargo, las matrices todavía se pueden pasar y devolver solo como punteros.

Giorgio
fuente
Puede devolver una matriz por valor si la coloca dentro de una estructura. Lo que no puede devolver por valor es una matriz de longitud variable.
Han
1
Sí, puedo poner una matriz dentro de una estructura, pero no puedo, por ejemplo, escribir typedef char arr [100]; arr foo () {...} No se puede devolver una matriz, incluso si se conoce el tamaño.
Giorgio
¿Podría el votante explicar la razón del voto negativo? Si mi respuesta contiene información incorrecta, me complacería solucionarlo.
Giorgio el
4

Usted puede asignar estructuras en C a = b; es una sintaxis válida.

Simplemente dejó parte del tipo, la etiqueta de estructura, en su línea que no funciona.

DigitalRoss
fuente
4

No hay problema en devolver una estructura. Será pasado por valor

Pero, ¿qué pasa si la estructura contiene algún miembro que tenga una dirección de una variable local?

struct emp {
    int id;
    char *name;
};

struct emp get() {
    char *name = "John";

    struct emp e1 = {100, name};

    return (e1);
}

int main() {

    struct emp e2 = get();

    printf("%s\n", e2.name);
}

Ahora, aquí e1.name contiene una dirección de memoria local para la función get (). Una vez que get () regresa, la dirección local para el nombre se habría liberado. Por lo tanto, en la persona que llama si intentamos acceder a esa dirección, puede causar un error de segmentación, ya que estamos intentando una dirección liberada. Eso es malo..

Donde como e1.id será perfectamente válido ya que su valor se copiará a e2.id

Por lo tanto, siempre debemos tratar de evitar devolver las direcciones de memoria local de una función.

Cualquier cosa mal colocada se puede devolver cuando lo desee

Jagan
fuente
2
struct emp {
    int id;
    char *name;
};

struct emp get() {
    char *name = "John";

    struct emp e1 = {100, name};

    return (e1);
}

int main() {

    struct emp e2 = get();

    printf("%s\n", e2.name);
}

funciona bien con versiones más recientes de compiladores. Al igual que id, el contenido del nombre se copia en la variable de estructura asignada.

saroj panda
fuente
1
Aún más simple: struct emp get () {return {100, "john"}; }
Chris Reid
1

la dirección struct var e2 se empuja como arg a la pila llamada y los valores se asignan allí. De hecho, get () devuelve la dirección de e2 en eax reg. Esto funciona como llamada por referencia.

Bala
fuente