Pointeurs C: pointant vers un tableau de taille fixe

120

Cette question s'adresse aux gourous C là-bas:

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 compilation si vous essayez d'attribuer un pointeur d'un tableau de taille différente à p. Cela vous donnera également une erreur de compilation si vous essayez d'attribuer 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 vers une fonction. Habituellement, les gens écriraient le prototype d'une telle fonction comme ceci:

void foo(char * p, int plen);

Si vous attendiez un tampon d'une taille spécifique, vous testeriez simplement la valeur de plen. Cependant, vous ne pouvez pas être assuré que la personne qui vous transmet 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]);

.. forcerait 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 aucun code que j'ai jamais rencontré.

Ma question est la suivante: y a-t-il une raison pour laquelle les gens ne déclarent pas de pointeurs comme celui-ci? Ne vois-je pas un écueil évident?

figurassa
la source
3
Remarque: Depuis C99, le tableau n'a pas besoin d'être de taille fixe comme suggéré par le titre, 10peut être remplacé par n'importe quelle variable dans la portée
MM

Réponses:

174

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 (s'il) atteint un certain niveau de maîtrise du langage C.

Lorsque les spécificités de votre domaine d'application appellent un tableau de taille fixe spécifique (la taille du tableau est une constante au moment de la 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, le Vector3dtype étant un tableau ou un struct. Vous pouvez basculer la définition de Vector3dà tout moment d'un tableau à a structet inversement, et vous n'aurez pas à modifier la déclaration de la 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, c'est vrai).

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

Mais en réalité, utiliser le pointeur vers le premier élément pour le passage d'un tableau est une technique très niche, une astuce, qui sert un objectif très spécifique: son seul et unique objectif est de faciliter le passage de tableaux de taille différente (c'est-à-dire de 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 vers 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 jamais (ou ne reconnaissent jamais) la nécessité de traiter un tableau de taille fixe, restant ainsi inconscients de la technique de taille fixe appropriée.

Néanmoins, si la taille du tableau est fixe, le passer en tant que pointeur vers un élément

void foo(char p[])

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

Une autre raison qui pourrait entraver l'adoption de la technique de passage de tableaux de taille fixe est la pré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 utilisera mallocdes tableaux tels que

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

Ce tableau ne peut pas être passé à 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' mallocapproche naïve . Le mallocformat indiqué 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, une meilleure façon de mallocprocéder serait la suivante

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

Ceci, bien sûr, peut être facilement transmis à la déclaration ci-dessus foo

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".

Fourmi
la source
2
La réponse fournit une description très concise et informative de la façon dont sizeof () réussit, comment il échoue souvent et comment il échoue toujours. vos observations de la plupart des ingénieurs C / C ++ ne comprenant pas, et donc faire quelque chose qu'ils pensent comprendre à la place est l'une des choses les plus prophétiques que j'ai vues depuis un certain temps, et le voile n'est rien comparé à la précision qu'il décrit. sérieusement, monsieur. très bonne réponse.
WhozCraig
Je viens de refactoriser un code basé sur cette réponse, bravo et merci pour le Q et le A.
Perry
1
Je suis curieux de savoir comment vous gérez la constpropriété avec cette technique. Un const char (*p)[N]argument ne semble pas compatible avec un pointeur vers char table[N];Par contre, un simple char*ptr reste compatible avec un const char*argument.
Cyan
4
Il peut être utile de noter que pour accéder à un élément de votre tableau, vous devez le faire (*p)[i]et non *p[i]. Ce dernier sautera de la taille du tableau, ce qui n'est certainement pas ce que vous voulez. Au moins pour moi, apprendre cette syntaxe a causé, plutôt que prévenu, une erreur; J'aurais obtenu le code correct plus rapidement en passant simplement un float *.
Andrew Wagner le
1
Oui @mickey, ce que vous avez suggéré est un constpointeur vers un tableau d'éléments mutables. Et oui, c'est complètement différent d'un pointeur vers un tableau d'éléments immuables.
cyan
11

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):

Au fur et à mesure que je commence à jouer plus avec ces déclarations, je me rends compte qu'un handicap majeur y est associé en C (apparemment pas en C ++). Il est assez courant d'avoir une situation où vous voudriez 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, le standard 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 (à moins que le tampon lui-même n'ait é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 a souligné.

figurassa
la source
2
Cela semble être un problème spécifique au compilateur. J'ai pu dupliquer en utilisant gcc 4.9.1, mais clang 3.4.2 a pu passer d'une version non-const à const sans problème. J'ai lu la spécification C11 (p 9 dans ma version ... la partie parlant de deux types qualifiés étant compatibles) et suis d'accord qu'il semble dire que ces conversions sont illégales. Cependant, nous savons en pratique que vous pouvez toujours convertir automatiquement de char * en char const * sans avertissement. IMO, clang est plus cohérent en autorisant cela que gcc, bien que je convienne avec vous que la spécification semble interdire l'une de ces conversions automatiques.
Doug Richardson le
4

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 vers un pointeur non qualifié.

Voir également cette question , l'utilisation foo(&p)devrait fonctionner.

Keith Randall
la source
3
Bien sûr, foo (p) ne fonctionnera pas, foo demande un pointeur vers un tableau de 10 éléments, vous devez donc passer l'adresse de votre tableau ...
Brian R. Bondy
9
Comment est-ce la «raison évidente»? Il est évidemment entendu que la bonne façon d'appeler la fonction est foo(&p).
Du
3
Je suppose que «évident» est le mauvais mot. Je voulais dire "le plus simple". La distinction entre p et & p dans ce cas est assez obscure pour le programmeur C moyen. Quelqu'un essayant de faire ce que l'affiche a suggéré écrira ce que j'ai écrit, obtiendra une erreur de compilation et abandonnera.
Keith Randall
2

Je souhaite également utiliser cette syntaxe pour activer davantage de vérification de type.

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

Voici quelques autres obstacles 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 en partie à l'encontre de l'objectif d'utiliser le bon type.

  • Il faut se rappeler d'utiliser l'opérateur address-of lors de l'affectation de l'adresse d'un tableau à un pointeur vers 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 auquel l'affecter p. Sans l'opérateur, aest automatiquement convertie en l'adresse du premier élément du tableau, comme dans &a[0], qui a un type différent.

    Comme cette conversion automatique est déjà en cours, je suis toujours perplexe quant à la &nécessité. Cela est cohérent 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 d'adresse est la même.

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

  • Quand typedefn'est pas utilisé pour créer un type pour le pointeur-vers-tableau (dans un fichier d'en-tête commun), alors un pointeur-vers-tableau global a besoin d'une externdéclaration plus compliquée pour le partager entre les fichiers:

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

Eh bien, simplement, C ne fait pas les choses de cette façon. Un tableau de type Test passé en tant que pointeur vers le premier Tdu tableau, et c'est tout ce que vous obtenez.

Cela permet des algorithmes sympas et élégants, tels que la boucle dans 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 conduit à 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 est de faire 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 en accord sur la taille des données traitées.

Votre structure peut contenir toutes les données souhaitées; il peut 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 lui faisant traiter votre structure comme une structure de taille différente. La capacité presque sans entrave de faire ce genre de chose fait partie de la conception de C.

Carl Smotricz
la source
1

Vous pouvez déclarer un tableau de caractères de différentes manières:

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*
s1n
la source
2
N'oubliez pas qu'un tableau de taille fixe peut également être alloué dynamiquement en tant que char (*p)[10] = malloc(sizeof *p).
Du
Voir ici pour une discussion plus détaillée entre les différences de char array [] et char * ptr ici. stackoverflow.com/questions/1807530/…
t0mm13b
1

Je ne recommanderais pas cette solution

typedef int Vector3d[3];

car cela 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

Par Knytt
la source
Il ne proposait pas cela comme solution. Il utilisait simplement ceci comme exemple.
figurassa
Hm. Pourquoi n'est-ce sizeof(a)pas la même chose que sizeof(b)?
sherrellbc
0

Peut-être que je manque quelque chose, mais ... puisque les tableaux sont des pointeurs constants, cela signifie essentiellement qu'il est inutile de leur passer des pointeurs.

Ne pourriez-vous pas simplement utiliser void foo(char p[10], int plen);?

fortran
la source
4
Les tableaux ne sont PAS des pointeurs constants. Lisez une FAQ sur les tableaux, s'il vous plaît.
Du
2
Pour ce qui compte ici (tableaux unidimensionnels en tant que paramètres), le fait est qu'ils se désintègrent en pointeurs constants. Lisez une FAQ sur comment être moins pédant, s'il vous plaît.
fortran
-2

Sur mon compilateur (vs2008), il est traité char (*p)[10]comme un tableau de pointeurs de caractères, comme s'il n'y avait pas de parenthèses, même si je compile en tant que fichier C. Le compilateur prend-il en charge cette "variable"? Si tel est le cas, c'est une raison majeure de ne pas l'utiliser.

Tyson Jacobs
la source
1
-1 Faux. Cela fonctionne bien sur vs2008, vs2010, gcc. En particulier, cet exemple fonctionne très bien: stackoverflow.com/a/19208364/2333290
kotlomoy