web-dev-qa-db-fra.com

Pourquoi de nombreuses fonctions qui renvoient des structures en C renvoient-elles en fait des pointeurs vers des structures?

Quel est l'avantage de renvoyer un pointeur sur une structure par rapport au retour de la structure entière dans l'instruction return de la fonction?

Je parle de fonctions comme fopen et d'autres fonctions de bas niveau, mais il existe probablement des fonctions de niveau supérieur qui renvoient également des pointeurs vers des structures.

Je pense qu'il s'agit plus d'un choix de conception que d'une simple question de programmation et je suis curieux d'en savoir plus sur les avantages et les inconvénients des deux méthodes.

Une des raisons pour lesquelles je pensais que ce serait un avantage de retourner un pointeur vers une structure est de pouvoir dire plus facilement si la fonction a échoué en retournant le pointeur NULL.

Renvoyer une structure complète qui est NULL serait plus difficile je suppose ou moins efficace. Est-ce une raison valable?

50
yoyo_fun

Il existe plusieurs raisons pratiques pour lesquelles des fonctions comme fopen renvoient des pointeurs au lieu d'instances de types struct:

  1. Vous souhaitez masquer la représentation du type struct à l'utilisateur;
  2. Vous allouez un objet dynamiquement;
  3. Vous faites référence à une seule instance d'un objet via plusieurs références;

Dans le cas de types tels que FILE *, c'est parce que vous ne voulez pas exposer les détails de la représentation du type à l'utilisateur - un FILE * L'objet sert de poignée opaque, et vous passez simplement cette poignée à diverses routines d'E/S (et tandis que FILE est souvent implémenté en tant que type struct, il ne avoir être).

Ainsi, vous pouvez exposer un type incompletstruct dans un en-tête quelque part:

typedef struct __some_internal_stream_implementation FILE;

Bien que vous ne puissiez pas déclarer une instance d'un type incomplet, vous pouvez lui déclarer un pointeur. Je peux donc créer un FILE * et lui affecter via fopen, freopen, etc., mais je ne peux pas manipuler directement l'objet vers lequel il pointe.

Il est également probable que la fonction fopen alloue dynamiquement un objet FILE, en utilisant malloc ou similaire. Dans ce cas, il est logique de renvoyer un pointeur.

Enfin, il est possible que vous stockiez une sorte d'état dans un objet struct, et vous devez rendre cet état disponible à plusieurs endroits différents. Si vous renvoyiez des instances de type struct, ces instances seraient des objets en mémoire séparés les uns des autres et finiraient par se désynchroniser. En renvoyant un pointeur sur un seul objet, tout le monde fait référence au même objet.

62
John Bode

Il existe deux façons de "renvoyer une structure". Vous pouvez renvoyer une copie des données ou vous pouvez y renvoyer une référence (pointeur). Il est généralement préférable de renvoyer (et de faire circuler en général) un pointeur, pour plusieurs raisons.

Tout d'abord, la copie d'une structure prend beaucoup plus de temps CPU que la copie d'un pointeur. Si c'est quelque chose que votre code fait fréquemment, cela peut entraîner une différence de performance notable.

Deuxièmement, peu importe combien de fois vous copiez un pointeur, il pointe toujours vers la même structure en mémoire. Toutes ses modifications seront reflétées sur la même structure. Mais si vous copiez la structure elle-même, puis apportez une modification, la modification ne s'affiche que sur cette copie. Tout code contenant une copie différente ne verra pas la modification. Parfois, très rarement, c'est ce que vous voulez, mais la plupart du temps ce n'est pas le cas, et cela peut provoquer des bugs si vous vous trompez.

37
Mason Wheeler

En plus des autres réponses, il est parfois utile de renvoyer un petit struct par valeur. Par exemple, on pourrait renvoyer une paire de données et un code d'erreur (ou de réussite) s'y rapportant.

Pour prendre un exemple, fopen renvoie une seule donnée (le FILE* Ouvert) et en cas d'erreur, donne le code d'erreur via le errno variable pseudo-globale. Mais il serait peut-être préférable de renvoyer un struct de deux membres: le handle FILE* Et le code d'erreur (qui serait défini si le handle de fichier est NULL). Pour des raisons historiques, ce n'est pas le cas (et des erreurs sont signalées via le errno global, qui est aujourd'hui une macro).

Notez que Go language a une notation Nice pour renvoyer deux (ou quelques) valeurs.

Notez également que sous Linux/x86-64 la ABI et les conventions d'appel (voir x86-psABI page) spécifie qu'un struct de deux membres scalaires (par exemple un pointeur et un entier, ou deux pointeurs, ou deux entiers) est renvoyé par deux registres (et cela est très efficace et ne passe pas par la mémoire).

Ainsi, dans un nouveau code C, renvoyer un petit C struct peut être plus lisible, plus convivial pour les threads et plus efficace.

11

Quelque chose comme un FILE* n'est pas vraiment un pointeur vers une structure en ce qui concerne le code client, mais plutôt une forme d'identifiant opaque associé à une entité other comme un fichier. Lorsqu'un programme appelle fopen, il ne se soucie généralement pas du contenu de la structure renvoyée - tout ce qui compte c'est que d'autres fonctions comme fread feront tout ce dont elles ont besoin pour faire avec.

Si une bibliothèque standard reste dans un FILE* informations sur par exemple la position de lecture actuelle dans ce fichier, un appel à fread devrait être en mesure de mettre à jour ces informations. Faire en sorte que fread reçoive un pointeur vers FILE rend cela facile. Si fread recevait à la place un FILE, il n'aurait aucun moyen de mettre à jour l'objet FILE détenu par l'appelant.

6
supercat

Vous êtes sur la bonne voie

Les deux raisons que vous avez mentionnées sont valables:

L'une des raisons pour lesquelles je pensais que ce serait un avantage de retourner un pointeur sur une structure est de pouvoir dire plus facilement si la fonction a échoué en retournant un pointeur NULL.

Renvoyer une structure FULL qui est NULL serait plus difficile je suppose ou moins efficace. Est-ce une raison valable?

Si vous avez une texture (par exemple) quelque part en mémoire et que vous souhaitez référencer cette texture à plusieurs endroits de votre programme; il ne serait pas judicieux d'en faire une copie chaque fois que vous voudrez y faire référence. Au lieu de cela, si vous passez simplement un pointeur pour référencer la texture, votre programme s'exécutera beaucoup plus rapidement.

La principale raison est cependant l'allocation dynamique de la mémoire. Souvent, lorsqu'un programme est compilé, vous ne savez pas exactement de combien de mémoire vous avez besoin pour certaines structures de données. Dans ce cas, la quantité de mémoire que vous devez utiliser sera déterminée lors de l'exécution. Vous pouvez demander de la mémoire en utilisant "malloc", puis la libérer lorsque vous avez terminé d’utiliser "free".

Un bon exemple de cela est la lecture d'un fichier spécifié par l'utilisateur. Dans ce cas, vous n'avez aucune idée de la taille du fichier lorsque vous compilez le programme. Vous ne pouvez déterminer la quantité de mémoire dont vous avez besoin que lorsque le programme est en cours d'exécution.

Malloc et des pointeurs de retour gratuits vers des emplacements en mémoire. Les fonctions qui utilisent l'allocation dynamique de mémoire renvoient donc des pointeurs vers l'endroit où elles ont créé leurs structures en mémoire.

De plus, dans les commentaires, je vois qu'il y a une question de savoir si vous pouvez retourner une structure à partir d'une fonction. Vous pouvez en effet le faire. Les éléments suivants devraient fonctionner:

struct s1 {
   int integer;
};

struct s1 f(struct s1 input){
   struct s1 returnValue = xinput
   return returnValue;
}

int main(void){
   struct s1 a = { 42 };
   struct s1 b= f(a);

   return 0;
}
6
Ryan

Masquage des informations

Quel est l'avantage de renvoyer un pointeur sur une structure par rapport au retour de la structure entière dans l'instruction return de la fonction?

La plus courante est la dissimulation d'informations . C n'a pas, disons, la possibilité de rendre les champs d'un struct privés, et encore moins de fournir des méthodes pour y accéder.

Donc, si vous voulez empêcher de force les développeurs de voir et de falsifier le contenu d'une pointe, comme FILE, alors la seule et unique façon est de les empêcher d'être exposés à sa définition en traitant le pointeur comme opaque dont la taille et la définition de pointe ne sont pas connues du monde extérieur. La définition de FILE ne sera alors visible que pour ceux qui implémentent les opérations qui nécessitent sa définition, comme fopen, tandis que seule la déclaration de structure sera visible pour l'en-tête public.

Compatibilité binaire

Masquer la définition de la structure peut également aider à fournir une marge de manœuvre pour préserver la compatibilité binaire dans les API dylib. Il permet aux implémenteurs de la bibliothèque de modifier les champs de la structure opaque sans rompre la compatibilité binaire avec ceux qui utilisent la bibliothèque, car la nature de leur code n'a besoin que de savoir ce qu'ils peuvent faire avec la structure, pas sa taille ou quels champs il a.

À titre d'exemple, je peux actuellement exécuter certains programmes anciens construits à l'époque de Windows 95 (pas toujours parfaitement, mais étonnamment, beaucoup fonctionnent encore). Il est probable que certains codes de ces anciens binaires utilisent des pointeurs opaques vers des structures dont la taille et le contenu ont changé depuis l'ère Windows 95. Pourtant, les programmes continuent de fonctionner dans les nouvelles versions de Windows car ils n'étaient pas exposés au contenu de ces structures. Lorsque vous travaillez sur une bibliothèque où la compatibilité binaire est importante, ce à quoi le client n'est pas exposé est généralement autorisé à changer sans rompre la compatibilité descendante.

efficacité

Renvoyer une structure complète qui est NULL serait plus difficile je suppose ou moins efficace. Est-ce une raison valable?

Il est généralement moins efficace en supposant que le type peut pratiquement s'adapter et être alloué sur la pile à moins qu'il n'y ait généralement un allocateur de mémoire beaucoup moins généralisé utilisé en arrière-plan que malloc, comme un allocateur de taille fixe plutôt que de taille variable pooling mémoire déjà alloué. C'est un compromis de sécurité dans ce cas, très probablement, pour permettre aux développeurs de bibliothèque de maintenir des invariants (garanties conceptuelles) liés à FILE.

Ce n'est pas une raison valable au moins du point de vue des performances pour que fopen retourne un pointeur car la seule raison pour laquelle il retournerait NULL est l'échec de l'ouverture d'un fichier. Cela reviendrait à optimiser un scénario exceptionnel en échange d'un ralentissement de tous les chemins d'exécution courants. Dans certains cas, il peut y avoir une raison valable de productivité pour rendre les conceptions plus simples afin de les renvoyer des pointeurs pour permettre à NULL d'être renvoyé à certaines conditions.

Pour les opérations sur les fichiers, la surcharge est relativement relativement triviale par rapport aux opérations sur les fichiers elles-mêmes, et le besoin manuel de fclose ne peut pas être évité de toute façon . Ce n'est donc pas comme si nous pouvions épargner au client les tracas de libérer (fermer) la ressource en exposant la définition de FILE et en la renvoyant par valeur dans fopen ou s'attendre à une amélioration des performances étant donné la coût relatif des opérations sur les fichiers elles-mêmes pour éviter une allocation de tas.

Hotspots et correctifs

Pour d'autres cas cependant, j'ai profilé beaucoup de code C inutile dans les bases de code héritées avec des points chauds dans malloc et des échecs de cache obligatoires inutiles en raison de l'utilisation trop fréquente de cette pratique avec des pointeurs opaques et de l'allocation de trop de choses inutilement sur le tas, parfois en grosses boucles.

Une autre pratique que j'utilise à la place consiste à exposer les définitions de structure, même si le client n'est pas censé les altérer, en utilisant une norme de convention de dénomination pour communiquer que personne d'autre ne devrait toucher les champs:

struct Foo
{
   /* priv_* indicates that you shouldn't tamper with these fields! */
   int priv_internal_field;
   int priv_other_one;
};

struct Foo foo_create(void);
void foo_destroy(struct Foo* foo);
void foo_something(struct Foo* foo);

S'il y a des problèmes de compatibilité binaire à l'avenir, je l'ai trouvé suffisamment bon pour réserver simplement un espace supplémentaire à des fins futures, comme ceci:

struct Foo
{
   /* priv_* indicates that you shouldn't tamper with these fields! */
   int priv_internal_field;
   int priv_other_one;

   /* reserved for possible future uses (emergency backup plan).
     currently just set to null. */
   void* priv_reserved;
};

Cet espace réservé est un peu inutile mais peut nous sauver la vie si nous découvrons à l'avenir que nous devons ajouter un peu plus de données à Foo sans casser les binaires qui utilisent notre bibliothèque.

À mon avis l'information cachée et la compatibilité binaire est généralement la seule raison décente de n'autoriser que l'allocation de tas de structures en plus des structures de longueur variable (ce qui l'exigerait toujours , ou au moins être un peu gênant à utiliser autrement si le client devait allouer de la mémoire sur la pile de manière VLA pour allouer le VLS). Même les grandes structures sont souvent moins chères à renvoyer en valeur si cela signifie que le logiciel fonctionne beaucoup plus avec la mémoire chaude de la pile. Et même s'ils n'étaient pas moins chers à rentabiliser en valeur à la création, on pourrait simplement le faire:

int foo_create(struct Foo* foo);
...
/* In the client code: */
struct Foo foo;
if (foo_create(&foo))
{
    foo_something(&foo);
    foo_destroy(&foo);
}

... pour initialiser Foo à partir de la pile sans possibilité de copie superflue. Ou le client a même la liberté d'allouer Foo sur le tas s'il le souhaite pour une raison quelconque.

3
user204677