Renvoie une `struct` à partir d'une fonction en C

171

Aujourd'hui , j'enseignais un couple d'amis comment utiliser C structs. L' un d'eux a demandé si vous pouviez revenir un structd'une fonction, à laquelle je répondis: « Non , vous avait renvoyer des pointeurs dynamiquement malloced structs à la place. »

Venant de quelqu'un qui fait principalement du C ++, je m'attendais à ne pas pouvoir renvoyer structs par valeurs. En C ++, vous pouvez surcharger le operator =pour vos objets et il est tout à fait logique d'avoir une fonction pour renvoyer votre objet par valeur. En C, cependant, vous n'avez pas cette option et cela m'a donc fait réfléchir à ce que fait réellement le compilateur. Considérer ce qui suit:

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;
}    

Je comprends pourquoi struct b = a;ne devrait pas fonctionner - vous ne pouvez pas surcharger operator =pour votre type de données. Comment ça a = foo();compile bien? Cela signifie-t-il autre chose que struct b = a;? Peut-être que la question à se poser est la suivante: que fait exactement l' returnénoncé associé au =signe?

[modifier]: Ok, on m'a juste signalé struct b = aune erreur de syntaxe - c'est correct et je suis un idiot! Mais cela rend les choses encore plus compliquées! L'utilisation struct MyObj b = afonctionne en effet! Qu'est-ce que j'oublie ici?

mmirzadeh
la source
24
struct b = a;est une erreur de syntaxe. Et si vous essayez struct MyObj b = a;?
Greg Hewgill
2
@GregHewgill: Vous avez absolument raison. De manière assez intéressante, cependant, struct MyObj b = a;semble fonctionner :)
mmirzadeh

Réponses:

200

Vous pouvez renvoyer une structure à partir d'une fonction (ou utiliser l' =opérateur) sans aucun problème. C'est une partie bien définie de la langue. Le seul problème struct b = aest que vous n'avez pas fourni de type complet. struct MyObj b = afonctionnera très bien. Vous pouvez également transmettre des structures à des fonctions - une structure est exactement la même que tout type intégré à des fins de passage de paramètres, de valeurs de retour et d'affectation.

Voici un programme de démonstration simple qui fait les trois: passe une structure en tant que paramètre, retourne une structure à partir d'une fonction et utilise des structures dans les instructions d'assignation:

#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;
}

L'exemple suivant est à peu près exactement le même, mais utilise le type intégré à intdes fins de démonstration. Les deux programmes ont le même comportement en ce qui concerne le passage par valeur pour le passage de paramètres, l'affectation, 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
la source
15
C'est assez intéressant. J'ai toujours eu l'impression que vous aviez besoin d'indicateurs pour cela. Je me suis trompé :)
mmirzadeh
8
Vous n'avez certainement pas besoin de pointeurs. Cela dit, la plupart du temps, vous voudrez les utiliser - les copies de mémoire implicites qui ont lieu en déplaçant les structures par valeur peuvent être un véritable gaspillage de cycles CPU, sans parler de la bande passante mémoire.
Carl Norum
10
@CarlNorum Quelle est la taille d'une structure pour qu'une copie coûte plus cher que malloc + free?
josefx
7
@josefx, un seul exemplaire? Probablement énorme. Le fait est que, normalement, si vous transmettez des structures par valeur, vous les copiez beaucoup . Quoi qu'il en soit, ce n'est pas vraiment aussi simple que cela. Vous pouvez faire circuler des structures locales ou mondiales, auquel cas leur coût d'allocation est pratiquement gratuit.
Carl Norum
7
Vous avez besoin de pointeurs et d'allocation de mémoire pour la valeur renvoyée en dehors du corps de la fonction dès que la quantité de mémoire allouée pour une valeur n'est pas connue au moment de la compilation. C'est pour les structures, donc les fonctions C n'ont aucun problème à les renvoyer.
reinierpost
33

Lors d'un appel tel que a = foo();, le compilateur peut pousser l' adresse de la structure de résultat sur la pile et la passe comme un pointeur "caché" vers la foo()fonction. En effet, cela pourrait devenir quelque chose comme:

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

foo(&a);

Cependant, l'implémentation exacte de ceci dépend du compilateur et / ou de la plate-forme. Comme le note Carl Norum, si la structure est suffisamment petite, elle peut même être renvoyée complètement dans un registre.

Greg Hewgill
la source
11
Cela dépend totalement de la mise en œuvre. Par exemple, armcc passera des structures suffisamment petites dans les registres de passage de paramètres réguliers (ou de valeur de retour).
Carl Norum
Ne serait-ce pas retourner un pointeur vers une variable locale? La mémoire de la structure retournée ne peut pas faire partie du fooframe de pile. Il doit être dans un endroit qui survit après le retour de foo.
Anders Abel
@AndersAbel: Je pense que ce que Greg veut dire, c'est que le compilateur prend un pointeur vers la variable dans la fonction principale et le passe à la fonction foo. À l'intérieur de la fonction foo, vous venez de faire le devoir
mmirzadeh
4
@AndersAbel: Le *r = aà la fin ferait (effectivement) une copie de la variable locale dans la variable de l'appelant. Je dis "effectivement" parce que le compilateur pourrait implémenter RVO et éliminer acomplètement la variable locale .
Greg Hewgill
3
Bien que cela ne réponde pas directement à la question, c'est la raison pour laquelle beaucoup de gens tomberont ici via google c return struct: ils savent que dans cdecl eaxest retourné par valeur et que les structures en général ne rentrent pas à l'intérieur eax. C'est ce que je cherchais.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
14

La struct bligne ne fonctionne pas car c'est une erreur de syntaxe. Si vous le développez pour inclure le type, cela fonctionnera très bien

struct MyObj b = a;  // Runs fine

Ce que C fait ici est essentiellement un memcpyde la structure source à la destination. Ceci est vrai à la fois pour l'affectation et le retour de structvaleurs (et vraiment pour toutes les autres valeurs en C)

JaredPar
la source
+1, en fait, de nombreux compilateurs émettront réellement un appel littéral à memcpydans ce cas - du moins, si la structure est raisonnablement grande.
Carl Norum
Ainsi, lors de l'initialisation d'un type de données, la fonction memcpy fonctionne ??
bhuwansahni
1
@bhuwansahni Je ne sais pas trop ce que vous demandez ici. Pourriez-vous élaborer un peu?
JaredPar
4
@JaredPar - les compilateurs appellent souvent littéralement la memcpyfonction pour les situations de structure. Vous pouvez créer un programme de test rapide et voir GCC le faire, par exemple. Pour les types intégrés qui ne se produiront pas, ils ne sont pas assez grands pour déclencher ce type d'optimisation.
Carl Norum
3
Il est tout à fait possible d'y arriver - le projet sur memcpylequel je travaille n'a pas de symbole défini, donc nous rencontrons souvent des erreurs d'éditeur de liens "symbole indéfini" lorsque le compilateur décide d'en recracher un seul.
Carl Norum
9

oui, il est possible que nous puissions également passer la structure et la structure de retour. Vous aviez raison mais vous n'avez en fait pas passé le type de données qui devrait ressembler à cette structure MyObj b = a.

En fait, j'ai également appris à savoir quand j'essayais de trouver une meilleure solution pour renvoyer plus d'une valeur de fonction sans utiliser de pointeur ou de variable globale.

Voici maintenant l'exemple du même, qui calcule l'écart des notes d'un élève par rapport à la moyenne.

#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 homme
la source
5

Autant que je me souvienne, les premières versions de C permettaient uniquement de renvoyer une valeur qui pouvait entrer dans un registre de processeur, ce qui signifie que vous ne pouviez renvoyer qu'un pointeur vers une structure. La même restriction s'appliquait aux arguments de fonction.

Des versions plus récentes permettent de faire circuler des objets de données plus volumineux comme des structures. Je pense que cette caractéristique était déjà courante dans les années 80 ou au début des années 90.

Les tableaux, cependant, peuvent toujours être passés et renvoyés uniquement sous forme de pointeurs.

Giorgio
la source
Vous pouvez renvoyer un tableau par valeur si vous le placez dans une structure. Ce que vous ne pouvez pas renvoyer par valeur est un tableau de longueur variable.
han
1
Oui, je peux mettre un tableau dans une structure, mais je ne peux pas par exemple écrire typedef char arr [100]; arr foo () {...} Un tableau ne peut pas être retourné, même si la taille est connue.
Giorgio
L'électeur défavorable pourrait-il expliquer la raison du vote défavorable? Si ma réponse contient des informations incorrectes, je serais heureux de la corriger.
Giorgio
4

Vous pouvez affecter des structures en C. a = b; est une syntaxe valide.

Vous avez simplement laissé une partie du type - la balise struct - dans votre ligne qui ne fonctionne pas.

DigitalRoss
la source
4

Il n'y a aucun problème à renvoyer une structure. Il sera passé par valeur

Mais que se passe-t-il si la structure contient un membre qui a l'adresse d'une variable locale

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);
}

Maintenant, ici e1.name contient une adresse mémoire locale à la fonction get (). Une fois get () retourné, l'adresse locale du nom aurait été libérée. Donc, chez l'appelant, si nous essayons d'accéder à cette adresse, cela peut provoquer une erreur de segmentation, car nous essayons une adresse libérée. C'est mauvais..

Où comme e1.id sera parfaitement valide car sa valeur sera copiée dans e2.id

Donc, nous devons toujours essayer d'éviter de renvoyer les adresses de mémoire locale d'une fonction.

Tout ce qui est malléable peut être retourné au fur et à mesure

Jagan
la source
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);
}

fonctionne bien avec les nouvelles versions des compilateurs. Tout comme id, le contenu du nom est copié dans la variable de structure attribuée.

panda saroj
la source
1
Encore plus simple: struct emp get () {return {100, "john"}; }
Chris Reid
1

struct var e2 adresse poussée comme arg dans la pile de l'appelé et les valeurs y sont assignées. En fait, get () renvoie l'adresse de e2 dans eax reg. Cela fonctionne comme un appel par référence.

Bala
la source