web-dev-qa-db-fra.com

Pointeurs C: pointant vers un tableau de taille fixe

Cette question s'adresse aux gourous du C:

En C, il est possible de déclarer un pointeur comme suit:

char (* p)[10];

.. qui indique essentiellement que ce pointeur pointe vers un tableau de 10 caractères. La chose intéressante à propos de la déclaration d'un pointeur comme celui-ci est que vous obtiendrez une erreur de temps de compilation si vous essayez d'affecter un pointeur d'un tableau de taille différente à p. Il vous donnera également une erreur de temps de compilation si vous essayez d'affecter la valeur d'un simple pointeur char à p. J'ai essayé cela avec gcc et cela semble fonctionner avec ANSI, C89 et C99.

Il me semble que déclarer un pointeur comme celui-ci serait très utile - en particulier, lors du passage d'un pointeur à une fonction. Habituellement, les gens écrivaient le prototype d'une telle fonction comme ceci:

void foo(char * p, int plen);

Si vous vous attendiez à un tampon d'une taille spécifique, vous testeriez simplement la valeur de plen. Cependant, vous ne pouvez pas être sûr que la personne qui vous passe p vous donnera vraiment plen emplacements de mémoire valides dans ce tampon. Vous devez avoir confiance que la personne qui a appelé cette fonction fait la bonne chose. D'autre part:

void foo(char (*p)[10]);

..obligerait l'appelant à vous donner un tampon de la taille spécifiée.

Cela semble très utile, mais je n'ai jamais vu un pointeur déclaré comme celui-ci dans un code que j'ai jamais rencontré.

Ma question est: y a-t-il une raison pour laquelle les gens ne déclarent pas des pointeurs comme ça? Ne vois-je pas un piège évident?

105
figurassa

Je voudrais ajouter à la réponse d'AndreyT (au cas où quelqu'un tomberait sur cette page à la recherche de plus d'informations sur ce sujet):

Alors que je commence à jouer davantage avec ces déclarations, je me rends compte qu'il y a un handicap majeur qui leur est associé en C (apparemment pas en C++). Il est assez courant d'avoir une situation où vous souhaitez donner à un appelant un pointeur const vers un tampon dans lequel vous avez écrit. Malheureusement, cela n'est pas possible lors de la déclaration d'un pointeur comme celui-ci en C. En d'autres termes, la norme C (6.7.3 - paragraphe 8) est en contradiction avec quelque chose comme ceci:


   int array[9];

   const int (* p2)[9] = &array;  /* Not legal unless array is const as well */

Cette contrainte ne semble pas être présente en C++, ce qui rend ce type de déclarations beaucoup plus utile. Mais dans le cas de C, il est nécessaire de revenir à une déclaration de pointeur régulière chaque fois que vous voulez un pointeur const vers le tampon de taille fixe (sauf si le tampon lui-même a été déclaré const pour commencer). Vous pouvez trouver plus d'informations dans ce fil de discussion: texte du lien

C'est une contrainte sévère à mon avis et cela pourrait être l'une des principales raisons pour lesquelles les gens ne déclarent généralement pas de pointeurs comme celui-ci en C. L'autre étant le fait que la plupart des gens ne savent même pas que vous pouvez déclarer un pointeur comme celui-ci comme AndreyT l'a souligné.

9
figurassa

Ce que vous dites dans votre message est absolument correct. Je dirais que chaque développeur C arrive exactement à la même découverte et exactement à la même conclusion quand (si) ils atteignent un certain niveau de compétence en langage C.

Lorsque les spécificités de votre zone d'application nécessitent un tableau de taille fixe spécifique (la taille du tableau est une constante de compilation), la seule façon appropriée de transmettre un tel tableau à une fonction consiste à utiliser un paramètre pointeur vers tableau

void foo(char (*p)[10]);

(en langage C++, cela se fait également avec des références

void foo(char (&p)[10]);

).

Cela activera la vérification du type au niveau de la langue, ce qui garantira que le tableau de taille exactement correcte est fourni comme argument. En fait, dans de nombreux cas, les gens utilisent cette technique implicitement, sans même s'en rendre compte, en cachant le type de tableau derrière un nom typedef

typedef int Vector3d[3];

void transform(Vector3d *vector);
/* equivalent to `void transform(int (*vector)[3])` */
...
Vector3d vec;
...
transform(&vec);

Notez en outre que le code ci-dessus est invariant par rapport à Vector3d type étant un tableau ou un struct. Vous pouvez changer la définition de Vector3d à tout moment d'un tableau vers un struct et retour, et vous n'aurez pas à modifier la déclaration de fonction. Dans les deux cas, les fonctions recevront un objet agrégé "par référence" (il y a des exceptions à cela, mais dans le contexte de cette discussion, cela est vrai).

Cependant, vous ne verrez pas cette méthode de passage de tableau utilisée de manière explicite trop souvent, simplement parce que trop de gens sont confus par une syntaxe plutôt compliquée et ne sont tout simplement pas assez à l'aise avec de telles fonctionnalités du langage C pour les utiliser correctement. Pour cette raison, dans la vie réelle moyenne, le passage d'un tableau comme pointeur vers son premier élément est une approche plus populaire. Cela semble juste "plus simple".

Mais en réalité, l'utilisation du pointeur sur le premier élément pour le passage de tableau est une technique très niche, une astuce, qui sert un but très spécifique: son seul et unique but est de faciliter le passage de tableaux de taille différente (c.-à-d. taille d'exécution). Si vous avez vraiment besoin de pouvoir traiter des tableaux de taille d'exécution, alors la bonne façon de passer un tel tableau est par un pointeur sur son premier élément avec la taille concrète fournie par un paramètre supplémentaire

void foo(char p[], unsigned plen);

En fait, dans de nombreux cas, il est très utile de pouvoir traiter des tableaux de taille d'exécution, ce qui contribue également à la popularité de la méthode. De nombreux développeurs C ne rencontrent tout simplement (ou ne reconnaissent jamais) la nécessité de traiter un tableau de taille fixe, restant ainsi inconscients de la bonne technique de taille fixe.

Néanmoins, si la taille du tableau est fixe, passez-la comme un pointeur vers un élément

void foo(char p[])

est une erreur technique majeure, qui est malheureusement assez répandue de nos jours. Une technique de pointeur sur tableau est une bien meilleure approche dans de tels cas.

Une autre raison qui pourrait entraver l'adoption de la technique de passage de tableau de taille fixe est la dominance de l'approche naïve du typage des tableaux alloués dynamiquement. Par exemple, si le programme appelle des tableaux fixes de type char[10] (comme dans votre exemple), un développeur moyen malloc des tableaux tels que

char *p = malloc(10 * sizeof *p);

Ce tableau ne peut pas être transmis à une fonction déclarée comme

void foo(char (*p)[10]);

ce qui déroute le développeur moyen et lui fait abandonner la déclaration de paramètre de taille fixe sans y réfléchir davantage. En réalité cependant, la racine du problème réside dans l'approche naïve malloc. Le format malloc illustré ci-dessus doit être réservé aux tableaux de taille d'exécution. Si le type de tableau a une taille au moment de la compilation, un meilleur moyen de malloc il ressemblerait à ceci:

char (*p)[10] = malloc(sizeof *p);

Ceci, bien sûr, peut être facilement transmis au foo déclaré ci-dessus

foo(p);

et le compilateur effectuera la vérification de type appropriée. Mais encore une fois, cela est trop déroutant pour un développeur C non préparé, c'est pourquoi vous ne le verrez pas trop souvent dans le code quotidien moyen "typique".

158
AnT

La raison évidente est que ce code ne compile pas:

extern void foo(char (*p)[10]);
void bar() {
  char p[10];
  foo(p);
}

La promotion par défaut d'un tableau est un pointeur non qualifié.

Voir aussi cette question , l'utilisation de foo(&p) devrait fonctionner.

4
Keith Randall

Je ne recommanderais pas cette solution

typedef int Vector3d[3];

car il masque le fait que Vector3D a un type que vous devez connaître. Les programmeurs ne s'attendent généralement pas à ce que les variables du même type aient des tailles différentes. Considérer :

void foo(Vector3d a) {
   Vector3D b;
}

où sizeof a! = sizeof b

1
Per Knytt

Je veux également utiliser cette syntaxe pour permettre plus de vérification de type.

Mais je conviens également que la syntaxe et le modèle mental de l'utilisation des pointeurs sont plus simples et plus faciles à retenir.

Voici quelques obstacles supplémentaires que j'ai rencontrés.

  • L'accès à la baie nécessite l'utilisation de (*p)[]:

    void foo(char (*p)[10])
    {
        char c = (*p)[3];
        (*p)[0] = 1;
    }
    

    Il est tentant d'utiliser à la place un pointeur vers un caractère local:

    void foo(char (*p)[10])
    {
        char *cp = (char *)p;
        char c = cp[3];
        cp[0] = 1;
    }
    

    Mais cela irait à l'encontre du but d'utiliser le bon type.

  • Il ne faut pas oublier d'utiliser l'opérateur address-of lors de l'attribution de l'adresse d'un tableau à un pointeur vers un tableau:

    char a[10];
    char (*p)[10] = &a;
    

    L'opérateur address-of obtient l'adresse de l'ensemble du tableau dans &a, avec le type correct pour l'attribuer à p. Sans l'opérateur, a est automatiquement converti en l'adresse du premier élément du tableau, comme dans &a[0], qui a un type différent.

    Étant donné que cette conversion automatique a déjà lieu, je suis toujours perplexe que le & est nécessaire. Il est compatible avec l'utilisation de & sur des variables d'autres types, mais je dois me rappeler qu'un tableau est spécial et que j'ai besoin du & pour obtenir le type d'adresse correct, même si la valeur de l'adresse est la même.

    Une des raisons de mon problème est peut-être que j'ai appris le K&R C dans les années 80, ce qui ne permettait pas d'utiliser le & opérateur sur des tableaux entiers encore (bien que certains compilateurs l'ignorent ou tolèrent la syntaxe). Ce qui, en passant, peut être une autre raison pour laquelle les pointeurs vers les tableaux ont du mal à être adoptés: ils ne fonctionnent correctement que depuis ANSI C et le & la limitation de l'opérateur peut avoir été une autre raison de les juger trop gênantes.

  • Lorsque typedef est pas utilisé pour créer un type pour le pointeur vers un tableau (dans un fichier d'en-tête commun), un pointeur vers un tableau global a besoin d'un extern pour le partager entre les fichiers:

    fileA:
    char (*p)[10];
    
    fileB:
    extern char (*p)[10];
    
1
Orafu

Eh bien, tout simplement, C ne fait pas les choses de cette façon. Un tableau de type T est transmis comme pointeur vers le premier T du tableau, et c'est tout ce que vous obtenez.

Cela permet des algorithmes sympas et élégants, tels que parcourir le tableau avec des expressions comme

*dst++ = *src++

L'inconvénient est que la gestion de la taille dépend de vous. Malheureusement, le fait de ne pas le faire consciencieusement a également entraîné des millions de bogues dans le codage C et/ou des opportunités d'exploitation malveillante.

Ce qui se rapproche de ce que vous demandez en C, c'est de passer un struct (par valeur) ou un pointeur vers un (par référence). Tant que le même type de structure est utilisé des deux côtés de cette opération, le code qui distribue la référence et le code qui l'utilise sont d'accord sur la taille des données traitées.

Votre structure peut contenir toutes les données que vous souhaitez; il pourrait contenir votre tableau d'une taille bien définie.

Pourtant, rien ne vous empêche, vous ou un codeur incompétent ou malveillant, d'utiliser des transtypages pour tromper le compilateur en traitant votre structure comme une structure de taille différente. La capacité presque sans entraves de faire ce genre de chose fait partie de la conception de C.

1
Carl Smotricz

Vous pouvez déclarer un tableau de caractères de plusieurs façons:

char p[10];
char* p = (char*)malloc(10 * sizeof(char));

Le prototype d'une fonction qui prend un tableau par valeur est:

void foo(char* p); //cannot modify p

ou par référence:

void foo(char** p); //can modify p, derefernce by *p[0] = 'f';

ou par syntaxe de tableau:

void foo(char p[]); //same as char*
1
s1n