web-dev-qa-db-fra.com

Comment masquer certains champs de struct en C?

J'essaie d'implémenter une personne struct et j'ai besoin de cacher certains champs ou de les rendre constants. ne astuce pour créer des champs privés.

Entête:

#pragma once

#define NAME_MAX_LEN 20

typedef struct _person {
    float wage;
    int groupid;
} Person;

const char const *getName (Person *p);
int getId (Person *p);

/// OTHER FUNCTIONS

La source

#include "person.h"


struct _person
{
    int id;

    float wage;
    int groupid;

    char name[NAME_MAX_LEN];
};

/// FUNCTIONS

GCC dit que person.c:7:8: error: redefinition a 'struct _person' struct _person

Je peux écrire ceci dans un en-tête, mais après cela, je ne peux pas utiliser les champs d'une structure.

typedef struct _person Person;
18
Wootiae

C n'a pas de mécanisme pour masquer les membres individuels d'un type de structure. Cependant, en fonctionnant uniquement en termes de pointeurs vers un tel type, et sans fournir de définition, vous pouvez rendre le type entier opaque. Les utilisateurs devraient alors utiliser les fonctions que vous fournissez pour manipuler les instances de quelque manière que ce soit. C'est quelque chose qui se fait parfois.

Dans une certaine mesure, vous pourrez peut-être réaliser quelque chose comme ce que vous décrivez avec un contexte caché. Par exemple, considérez ceci:

header.h

typedef struct _person {
    float wage;
    int groupid;
} Person;

implementation.c

struct _person_real {
    Person person;  // must be first, and is a structure, not a pointer.
    int id;
    char name[NAME_MAX_LEN];
};

Vous pouvez maintenant le faire:

Person *create_person(char name[]) {
    struct _person_real *pr = malloc(sizeof(*pr));

    if (pr) {
        pr->person.wage = DEFAULT_WAGE;
        pr->person.groupid = DEFAULT_GROUPID;
        pr->id = generate_id();
        strncpy(pr->name, name, sizeof(pr->name));
        pr->name[sizeof(pr->name) - 1] = '\0';

        return &pr->person;  // <-- NOTE WELL
    } else {
        return NULL;
    }
}

Un pointeur vers le premier membre d'une structure pointe également également vers la structure entière, donc si le client vous renvoie un pointeur obtenu à partir de cette fonction, vous pouvez

struct _person_real *pr = (struct _person_real *) Person_pointer;

et travailler sur les membres dans un contexte plus large.

Sachez cependant qu'un tel schéma est risqué. Rien n'empêche un utilisateur de créer un Person sans le contexte plus large, et de lui passer un pointeur vers une fonction qui attend l'objet context être présente. Il y a d'autres problèmes.

Dans l'ensemble, les API C adoptent généralement une structure opaque ou documentent simplement soigneusement ce que les clients sont autorisés à faire avec les données auxquelles ils ont accès, ou même simplement documentent comment tout fonctionne, afin que les utilisateurs puissent faire leurs propres choix. Celles-ci, en particulier celles-ci, sont bien alignées avec les approches et les idiomes globaux de C - C ne vous tient pas la main ou ne vous protège pas de faire du mal. Il vous fait confiance pour savoir ce que vous faites et pour ne faire que ce que vous avez l'intention de faire.

18
John Bollinger

Une structure ne peut pas avoir plusieurs définitions en conflit. En tant que tel, vous ne pouvez pas créer une structure qui masque certains des champs.

Ce que vous pouvez faites cependant, il déclare que la structure existe dans l'en-tête sans la définir. Ensuite, l'appelant est limité à utiliser uniquement un pointeur vers la structure et à utiliser des fonctions dans votre implémentation pour le modifier.

Par exemple, vous pouvez définir votre en-tête comme suit:

typedef struct _person Person;

Person *init(const char *name, int id, float wage, int groupid);

const char *getName (const Person *p);
int getId (const Person *p);
float getWage (const Person *p);
int getGroupid (const Person *p);

Et votre implémentation contiendrait:

#include "person.h"

struct _person
{
    int id;

    float wage;
    int groupid;

    char name[NAME_MAX_LEN];
};

Person *init(const char *name, int id, float wage, int groupid)
{
    Person *p = malloc(sizeof *p);
    strcpy(p->name, name);
    p->id = id;
    p->wage= wage;
    p->groupid= groupid;
    return p;
}

...
24
dbush

Vous pouvez utiliser un style mixin; par exemple. écrivez dans l'en-tête:

struct person {
    float wage;
    int groupid;
};

struct person *person_new(void);
char const *getName (struct person const *p);
int getId (struct person const *p);

et dans la source

struct person_impl {
    struct person   p;
    char            name[NAME_MAX_LEN];
    int             id;
}

struct person *person_new(void)
{
    struct person_impl *p;

    p = malloc(sizeof *p);
    ...
    return &p->p;
}

chra const *getName(struct person const *p_)
{
    struct person_impl *p =
           container_of(p_, struct person_impl, p);

    return p->name;
}

Voir par ex. https://en.wikipedia.org/wiki/Offsetof pour plus de détails sur container_of().

3
ensc

Addendum à la réponse de John Bollinger:

Bien que, à mon humble avis, les types de pointeurs opaques avec des fonctions d'accesseur (init/get/set/destroy) sont l'approche la plus sécurisée, il existe une autre option qui permet aux utilisateurs de placer des objets sur la pile.

Il est possible d'allouer un seul morceau de mémoire "sans type" dans le cadre du struct et d'utiliser cette mémoire explicitement (bit par bit/octet par octet) au lieu d'utiliser des types supplémentaires.

c'est à dire.:

// public
typedef struct {
    float wage;
    int groupid;
    /* explanation: 1 for ID and NAME_MAX_LEN + 1 bytes for name... */
    unsigned long private__[1 + ((NAME_MAX_LEN + 1 + (sizeof(long) - 1)) / sizeof(long))];
} person_s;

// in .c file (private)
#define PERSON_ID(p) ((p)->private__[0])
#define PERSON_NAME(p) ((char*)((p)->private__ + 1))

Ceci est un indicateur très fort que l'accès aux données dans le private__ membre doit être évité. Les développeurs qui n'ont pas accès au fichier d'implémentation ne sauront même pas ce qu'il contient.

Cela dit, la meilleure approche est un type opaque, comme vous l'avez peut-être rencontré lors de l'utilisation de pthread_t API (POSIX).

typedef struct person_s person_s;
person_s * person_new(const char * name, size_t len);
const char * person_name(const person_s * person);
float person_wage_get(const person_s * person);
void person_wage_set(person_s * person, float wage);
// ...
void person_free(person_s * person);

Notes:

  1. évitez typedef avec un pointeur. Cela ne fait que confondre les développeurs.

    Il est préférable de garder les pointeurs explicites, afin que tous les développeurs puissent savoir que le type qu'ils utilisent est alloué dynamiquement.

    EDIT: De plus, en évitant de "dactylographier" un type de pointeur, l'API promet que les implémentations futures/alternatives utiliseront également un pointeur dans son API, permettant aux développeurs de faire confiance et de s'appuyer sur ce comportement (voir commentaires).

  2. Lorsque vous utilisez un type opaque, le NAME_MAX_LEN pourrait être évité, autorisant des noms de longueur arbitraire (en supposant que le changement de nom nécessite un nouvel objet). C'est une incitation supplémentaire à préférer l'approche du pointeur opaque.

  3. évitez de placer le _ au début d'un identifiant si possible (c'est-à-dire _name). Noms commençant par _ sont supposés avoir une signification spéciale et certains sont réservés. Il en va de même pour les types se terminant par _t (réservé par POSIX).

    Remarquez comment j'utilise le _s pour marquer le type comme une structure, je n'utilise pas _t (qui est réservé).

  4. C est plus souvent snake_case (au moins historiquement). Les API les plus connues et la plupart du standard C sont snake_case (sauf là où les choses ont été importées de C++).

    En outre, il est préférable d'être cohérent. L'utilisation de CamelCase (ou smallCamelCase) dans certains cas tout en utilisant snake_case pour d'autres choses peut être source de confusion lorsque les développeurs tentent de mémoriser votre API.

2
Myst

Ce que John Bollinger a écrit est une bonne façon d'utiliser le fonctionnement des structures et de la mémoire, mais c'est aussi un moyen facile d'obtenir une erreur de segmentation (imaginez allouer un tableau de Person puis passer plus tard le dernier élément d'une 'méthode' qui accède à l'id ou à son nom), ou corrompre vos données (dans un tableau de Person la suivante Person écrase les variables 'privées' de la précédente Person). Vous devez vous rappeler que vous devez créer un tableau de pointeurs vers Person au lieu d'un tableau de Person (cela semble assez évident jusqu'à ce que vous décidiez d'optimiser quelque chose et pensez que vous pouvez allouer et initialiser le plus efficace que la fonction d'initialisation).

Ne vous méprenez pas, c'est un excellent moyen de résoudre le problème, mais vous devez être prudent lorsque vous l'utilisez. Ce que je suggérerais (en utilisant 4/8 octets de mémoire supplémentaire par Person) est de créer une structure Person qui a un pointeur vers une autre structure qui n'est définie que dans le fichier .c et détient les données privées. De cette façon, il serait plus difficile de faire une erreur quelque part (et si c'est un projet plus important, faites-moi confiance - vous le ferez tôt ou tard).

Fichier .h:

#pragma once

#define NAME_MAX_LEN 20

typedef struct _person {
    float wage;
    int groupid;

    _personPriv *const priv;
} Person;

void personInit(Person *p, const char *name);
Person* personNew(const char *name);

const char const *getName (Person *p);
int getId (Person *p);

Fichier .c:

typedef struct {
    int id;
    char name[NAME_MAX_LEN];
} _personPriv;

const char const *getName (Person *p) {
    return p->priv->name;
}

int getId (Person *p) {
    return p->priv->id;
}

_personPriv* _personPrivNew(const char *name) {
    _personPriv *ret = memcpy(
        malloc(sizeof(*ret->priv)),
        &(_personPriv) {
            .id = generateId();
        },
        sizeof(*ret->priv)
    );

    // if(strlen(name) >= NAME_MAX_LEN) {
    //     raise an error or something?
    //     return NULL;
    // }

    strncpy(ret->name, name, strlen(name));

    return ret;
}

void personInit(Person *p, const char *name) {
    if(p == NULL)
        return;

    p->priv = memcpy(
        malloc(sizeof(*p->priv)),
        &(_personPriv) {
            .id = generateId();
        },
        sizeof(*p->priv)
    );

    ret->priv = _personPrivNew(name);
    if(ret->priv == NULL) {
        // raise an error or something
    }
}

Person* personNew(const char *name) {
    Person *ret = malloc(sizeof(*ret));

    ret->priv = _personPrivNew(name);
    if(ret->priv == NULL) {
        free(ret);
        return NULL;
    }
    return ret;
}

Remarque: cette version peut être implémentée de sorte que le bloc privé soit alloué juste après/avant la partie "publique" de la structure pour améliorer la localité. Allouez simplement sizeof(Person) + sizeof(_personPriv) et initialisez une partie comme Person et la seconde comme _personPriv.

1
Grabusz