¿Cómo la función REALMENTE devuelve la variable de estructura en C?

8 minutos de lectura

avatar de usuario
gitesh.tyagi

Me queda claro cómo una función devuelve el valor, solo para comenzar:

int f()
{
  int a = 2;
  return a;
}

Ahora a obtiene la memoria en la pila y su vida útil está dentro del f() para devolver el valor, copia el valor en un registro especial que lee la persona que llama, ya que sabe que la persona que llama ha colocado el valor por él. (Dado que el tamaño del registro especial del titular del valor de retorno es limitado, es por eso que no podemos devolver objetos grandes, por lo tanto, en el caso de idiomas avanzados, cuando queremos devolver la función del objeto, en realidad copia la dirección del objeto en el montón a ese registro especial)

Volvamos a C para una situación en la que quiero devolver una variable de estructura, no un puntero:

struct inventory
{
    char name[20];
    int number;
};
struct inventory function();

int main()
{
    struct inventory items;
    items=function();
    printf("\nam in main\n");
    printf("\n%s\t",items.name);
    printf(" %d\t",items.number); 
    getch();
    return 0;
}

struct inventory function()
{
    struct inventory items;
    printf(" enter the item name\n ");
    scanf(" %s ",&items.name );
    printf(" enter the number of items\n ");
    scanf("%d",&items.number );
    return items;
}

Código bifurcado de: https://stackoverflow.com/a/22952975/962545

Aquí está el trato,

Comencemos con principal, items variable declarada pero no inicializada y luego se llama a la función que devuelve la variable de estructura inicializada que se copia a la principal. Ahora estoy un poco confuso para entender cómo function() variable de estructura devuelta items que no se crea dinámicamente (técnicamente no está en el montón), por lo que la vida útil de esta variable está dentro del function() cuerpo, también tamaño de variable item puede ser lo suficientemente grande como para no caber en un registro especial, entonces, ¿por qué funcionó? (Sé que podemos asignar dinámicamente el elemento dentro de la función y devolver la dirección, pero no quiero una alternativa, estoy buscando una explicación)

Pregunta:
Aunque funciona pero como function() en realidad devolvió la variable de estructura y se copió para items variable en main cuando se supone que debe morir con function() regreso.

Seguramente me estoy perdiendo algo importante, una explicación detallada ayudaría. 🙂

EDITAR:
Otras referencias de respuesta:

  1. https://stackoverflow.com/a/2155742/962545
  2. Nombrado como optimización del valor de retorno

  • La norma no define cómo se hace esto. Diferentes compiladores pueden, y lo hacen, elegir diferentes formas de implementar valores devueltos. Debe leer el ABI para el compilador que está utilizando. Semánticamente, se pasa una copia de la persona que llama a la persona que llama.

    –David Heffernan

    9 de abril de 2014 a las 8:52

  • Sí, acepto que se aprobará la copia y estoy más interesado en profundizar en la ruta de viaje de esa copia.

    – gitesh.tyagi

    9 de abril de 2014 a las 8:56

  • Si compila el código con la opción de mantener el archivo .ASM intermedio para su máquina, puede mirar ese código para ver exactamente qué está pasando.

    – AnthonyLambert

    9 de abril de 2014 a las 8:56

  • posible duplicado de ¿Cómo implementan los compiladores de C funciones que devuelven estructuras grandes?

    – relajarse

    9 de abril de 2014 a las 8:59

  • Mire el código ensamblador generado por su compilador para tener una idea de cómo tu el compilador maneja esto. Sin embargo, otro compilador puede manejarlo de manera diferente.

    – Jabberwocky

    9 de abril de 2014 a las 9:02

Los detalles varían ampliamente según la convención de llamadas. Algunas ABI no tienen una convención de llamadas para pasar estructuras completas, en cuyo caso el compilador es libre de hacer lo que crea que tiene sentido.

Ejemplos incluyen:

  • Pasar y devolver la estructura completa como una serie de registros consecutivos (a menudo se usa con estructuras “pequeñas”)
  • Colocar toda la estructura como un bloque de argumentos en la pila
  • Asignar un argumento vacío lo suficientemente grande como para contener la estructura, para que se llene con un valor de retorno
  • Pasar la dirección (pila) de la estructura como argumento (como si la función fuera declarada void function(struct inventory *))

Cualquiera de estas implementaciones podría cumplir con la especificación C aquí. Pero, veamos una implementación específica: la salida de mi compilador cruzado GCC ARM.

Compilando el código que diste me da esto:

main:
    stmfd   sp!, {fp, lr}
    add fp, sp, #4
    sub sp, sp, #48
    sub r3, fp, #52
    mov r0, r3
    bl  function(PLT)

Los operandos de destino siempre están a la izquierda. Puede ver que el programa reserva espacio de pila, luego pasa la dirección del espacio de pila como r0 (el primer argumento en la convención de llamadas ARM EABI). function no toma argumentos, por lo que este argumento es claramente un argumento artificial agregado por nuestro compilador.

function Se ve como esto:

function:
    stmfd   sp!, {r4, fp, lr}
    add fp, sp, #8
    sub sp, sp, #36
    str r0, [fp, #-40]
    ldr r3, .L6

        ...
    add r2, pc, r2
    mov r0, r2
    mov r1, r3
    bl  scanf(PLT)
    ldr r3, [fp, #-40]
    mov ip, r3
    sub r4, fp, #36
    ldmia   r4!, {r0, r1, r2, r3}
    stmia   ip!, {r0, r1, r2, r3}
    ldmia   r4, {r0, r1}
    stmia   ip, {r0, r1}
    ldr r0, [fp, #-40]
    sub sp, fp, #8
    ldmfd   sp!, {r4, fp, pc}

Este código básicamente oculta el único argumento en [fp, #-40], luego lo carga y comienza a almacenar datos en la dirección a la que apunta. Al final, devuelve este valor de puntero en r0 otra vez. Efectivamente, el compilador ha convertido la firma de la función en

struct inventory *function(struct inventory *)

donde la estructura devuelta es asignada en la pila por la persona que llama, pasada y luego devuelta.

  • Yo también estoy de acuerdo con tu explicación. 🙂

    – gitesh.tyagi

    9 de abril de 2014 a las 9:25

  • +1 es curioso cómo, cuando estaba escribiendo mi respuesta explicando cómo siempre supuse que funcionaba, en realidad te molestaste en verificar y probar mis sospechas. Haría +1 nuevamente si pudiera, por el esfuerzo y la integridad.

    – Elías Van Ootegem

    09/04/2014 a las 11:33

  • Entonces… ¿Esto es básicamente RVO?

    – VF1

    11 de mayo de 2014 a las 2:21

avatar de usuario
Elias Van Ootegem

Te estás perdiendo lo más obvio que hay en la forma en que C pasa/devuelve las cosas: todo se pasa por valoro al menos: se comporta de esa manera.

Es decir:

struct foo some_f( void )
{
    struct foo local = {
       .member = 123,
       .bar = 2.0
    };
    //some awsome code
    return local;
}

Funcionará, muy bien. Si la estructura es pequeña, entonces es posible que este código cree una variable de estructura local y devuelva un Copiar de esa estructura a la persona que llama.
En otros casos, sin embargo, este código se traducirá aproximadamente a:

void caller()
{
    struct foo hidden_stack_space;
    struct foo your_var = *(some_f(&hidden_stack_space));
}
//and the some_f function will behave as:
struct foo * some_f(struct foo * local)
{
    //works on local and
    return local;
}

Bueno, esto no es Exactamente lo que sucede todo el tiempo, pero se reduce a esto, más o menos. El resultado será el mismo, pero los compiladores pueden comportarse de manera diferente en este caso.

La conclusión es: C devuelve por valor, por lo que su código funciona bien.
Sin embargo, hay trampas:

struct foo
{
    int member1;
    char *str;
};
struct foo some_f()
{
    char bar[] = "foobar";
    struct foo local = {
        .member1 = 123,
        .str = &bar[0]
    };
    return local;
}

Es peligroso: el puntero asignado a local.str apunta a la memoria que voluntad se liberará una vez que se devuelva la estructura. En ese caso, los problemas que esperaba con este código son ciertos: esa memoria ya no existe (o ya no es válida).
Simplemente porque un puntero es una variable cuya valor es la dirección mem, y ese valor se devuelve/asigna.

  • necesita desreferenciar el valor para la asignación en la estructura de línea foo your_var = * some_f(&hidden_stack_space); Sí, estoy de acuerdo con el último escenario.

    – gitesh.tyagi

    9 de abril de 2014 a las 9:15


  • @gitesh.tyagi: actualizado, tenga en cuenta que el código en su pregunta contiene un main función que no devuelve un inty no necesitas los paréntesis alrededor items en (items).member =P

    – Elías Van Ootegem

    9 abr 2014 a las 9:30


  • el código no es mío, lo copié y también pegué la referencia. ¡actualizado!

    – gitesh.tyagi

    09/04/2014 a las 10:59


  • @gitesh.tyagi: OK, veo que también aceptaste la respuesta más completa (que entiendo, y yo haría lo mismo si fuera tú). Solo un último detalle: se podría argumentar que int main() estaría mejor escrito como int main (void). En realidad, ambos funcionarán bien, pero si una función no acepta argumentos, es mejor que lo diga explícitamente en su código. Motivos: código autodocumentado, confusión sobre el estándar y todo eso…

    – Elías Van Ootegem

    09/04/2014 a las 11:30

  • Sí, tiene razón, en realidad se molestó en probar el concepto, por eso cambió la respuesta aceptada.

    – gitesh.tyagi

    09/04/2014 a las 17:31

Se asignará una estructura, al menos una grande, y se devolverá a la pila, y la persona que llama la sacará de la pila (si es que lo hace). El compilador intentará ubicarlo en el mismo lugar donde la persona que llama espera encontrarlo, pero hará una copia si eso no es posible. Es posible, pero no necesario, que haya además un puntero a la estructura, devuelto a través de registros.

Por supuesto, los detalles variarán dependiendo de la arquitectura.

  • Nunca he oído hablar de leer el valor devuelto de la pila, ¿estás seguro? la referencia ayudaría.

    – gitesh.tyagi

    9 de abril de 2014 a las 8:54

  • @gitesh Ver stackoverflow.com/questions/5366121/…

    – Klas Lindbäck

    9 de abril de 2014 a las 8:58


  • Eliminé mi comentario sobre amd64 ABI porque al volver a leer encontré que en esa plataforma, el llamador asigna la Memoria. Recomiendo la respuesta de @ nnenneo.

    – Adrián Ratnapala

    9 de abril de 2014 a las 9:11

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad