Pourquoi la syntaxe C des tableaux, des pointeurs et des fonctions a-t-elle été conçue de cette façon?


16

Après avoir vu (et posé!) Tant de questions similaires à

Que int (*f)(int (*a)[5])signifie en C?

et même en voyant qu'ils avaient fait un programme pour aider les gens à comprendre la syntaxe C, je ne peux m'empêcher de me demander:

Pourquoi la syntaxe de C a-t-elle été conçue de cette façon?

Par exemple, si je concevais des pointeurs, je traduirais "un pointeur vers un tableau de 10 éléments de pointeurs" en

int*[10]* p;

et non

int* (*p)[10];

ce que je pense que la plupart des gens seraient d'accord est beaucoup moins simple.

Je me demande donc pourquoi la, euh, syntaxe non intuitive? Y a-t-il un problème spécifique résolu par la syntaxe (peut-être une ambiguïté?) Que je ne connais pas?


2
Vous savez qu'il n'y a pas de vraie réponse à cela, et à de telles questions. Droite? Ce que vous obtiendrez ne sont que des suppositions.
BЈовић

7
@VJo - il peut bien y avoir une réponse "réelle" (c'est-à-dire objective) - les auteurs de la langue et les comités de normalisation ont explicitement justifié (ou du moins expliqué) bon nombre de ces décisions.
detly

Je ne pense pas que la syntaxe que vous proposez soit nécessairement plus ou moins "intuitive" que la syntaxe C. C est ce que c'est; une fois que vous l'avez appris, vous n'aurez plus jamais ces questions. Si vous ne l'avez pas appris ... eh bien, c'est peut-être le vrai problème.
Caleb

1
@Caleb: C'est drôle comme vous avez conclu cela si facilement, parce que je l'ai appris et que j'avais encore cette question ...
user541686

1
La cdeclcommande est très pratique pour décoder des déclarations C complexes. Il y a aussi une interface Web sur cdecl.org .
Keith Thompson

Réponses:


16

Ma compréhension de l'histoire de celui-ci est basée sur deux points principaux ...

Premièrement, les auteurs du langage ont préféré rendre la syntaxe centrée sur les variables plutôt que centrée sur le type. Autrement dit, ils voulaient qu'un programmeur regarde la déclaration et pense "si j'écris l'expression *func(arg), cela se traduira par un int; si j'écris, *arg[N]j'aurai un flotteur" plutôt que " funcdoit être un pointeur vers une fonction prenant ceci et le retour que ».

L' entrée C sur Wikipedia affirme que:

L'idée de Ritchie était de déclarer les identifiants dans des contextes ressemblant à leur utilisation: "la déclaration reflète l'utilisation".

... citant la p122 de K & R2 que, hélas, je n'ai pas besoin de trouver pour vous trouver le devis étendu.

Deuxièmement, il est vraiment très difficile de trouver une syntaxe de déclaration cohérente lorsque vous avez affaire à des niveaux d'indirection arbitraires. Votre exemple pourrait bien fonctionner pour exprimer le type que vous avez pensé au départ, mais est-il adapté à une fonction prenant un pointeur vers un tableau de ces types et renvoyant un autre désordre hideux? (Peut-être que oui, mais avez-vous vérifié? Pouvez-vous le prouver? ).

Rappelez-vous, une partie du succès de C est due au fait que les compilateurs ont été écrits pour de nombreuses plates-formes différentes, et il aurait donc été préférable d'ignorer un certain degré de lisibilité pour faciliter l'écriture des compilateurs.

Cela dit, je ne suis pas un expert en grammaire linguistique ou en écriture de compilateur. Mais j'en sais assez pour savoir qu'il y a beaucoup à savoir;)


2
"rendre les compilateurs plus faciles à écrire" ... sauf que C est connu pour être difficile à analyser (uniquement surmonté par C ++).
Jan Hudec

1
@JanHudec - Eh bien ... oui. Ce n'est pas une déclaration étanche. Mais alors que C est impossible à analyser en tant que grammaire sans contexte, une fois qu'une personne a trouvé un moyen de l'analyser, cela cesse d'être l'étape difficile. Et le fait est que cela a été prolifique à ses débuts, car les gens étaient capables de frapper facilement les compilateurs, donc K&R a dû trouver un certain équilibre. (Dans l'infâme The Rise of "Worse is Better" de Richard Gabriel , il tient pour acquis - et déplore - le fait qu'il est facile d'écrire un compilateur C pour une nouvelle plate-forme.)
Detly

Je suis heureux d'être corrigé à ce sujet, d'ailleurs - je ne sais pas grand-chose sur l'analyse syntaxique et la grammaire. Je vais plus sur l'inférence de faits historiques.
detly

12

Beaucoup de bizarreries du langage C s'expliquent par le fonctionnement des ordinateurs lors de sa conception. Il y avait des quantités très limitées de mémoire de stockage, il était donc très important de minimiser la taille des fichiers de code source eux-mêmes. La pratique de la programmation dans les années 70 et 80 consistait à s'assurer que le code source contenait le moins de caractères possible, et de préférence aucun commentaire excessif sur le code source.

C'est bien sûr ridicule aujourd'hui, avec un espace de stockage pratiquement illimité sur les disques durs. Mais cela fait partie de la raison pour laquelle C a une syntaxe si étrange en général.


En ce qui concerne spécifiquement les pointeurs de tableau, votre deuxième exemple devrait être int (*p)[10];(ouais la syntaxe est très déroutante). Je pourrais peut-être lire cela comme "un pointeur int sur un tableau de dix" ... ce qui est quelque peu logique. Sinon pour la parenthèse, le compilateur l'interpréterait comme un tableau de dix pointeurs à la place, ce qui donnerait à la déclaration une signification entièrement différente.

Étant donné que les pointeurs de tableau et les pointeurs de fonction ont tous deux une syntaxe assez obscure en C, la chose raisonnable à faire est de taper la bizarrerie. Peut-être comme ça:

Exemple obscur:

int func (int (*arr_ptr)[10])
{
  return 0;
}

int main()
{
  int array[10];
  int (*arr_ptr)[10]  = &array;
  int (*func_ptr)(int(*)[10]) = &func;

  func_ptr(arr_ptr);
}

Exemple équivalent non obscur:

typedef int array_t[10];
typedef int (*funcptr_t)(array_t*);


int func (array_t* arr_ptr)
{
  return 0;
}

int main()
{
  int        array[10];
  array_t*   arr_ptr  = &array; /* non-obscure array pointer */
  funcptr_t  func_ptr = &func;  /* non-obscure function pointer */

  func_ptr(arr_ptr);
}

Les choses peuvent devenir encore plus obscures si vous avez affaire à des tableaux de pointeurs de fonction. Ou le plus obscur d'entre eux: des fonctions renvoyant des pointeurs de fonction (légèrement utiles). Si vous n'utilisez pas de typedefs pour de telles choses, vous deviendrez rapidement fou.


Ah, enfin une réponse raisonnable. :-) Je suis curieux de savoir comment la syntaxe particulière réduirait réellement la taille du code source, mais de toute façon c'est une idée plausible et logique. Merci. +1
user541686

Je dirais qu'il s'agissait moins de la taille du code source que de l'écriture du compilateur, mais certainement de +1 pour "taper loin de la bizarrerie". Ma santé mentale s'est considérablement améliorée le jour où j'ai réalisé que je pouvais le faire.
detly

2
[Citation nécessaire] sur la taille du code source. Je n'ai jamais entendu parler d'une telle limitation (même si c'est peut-être quelque chose que "tout le monde sait").
Sean McMillan

1
Eh bien, j'ai codé des programmes dans les années 70 en COBOL, Assembler, CORAL et PL / 1 sur IBM, DEC et XEROX et je n'ai jamais rencontré de limitation de taille de code source. Limitations sur la taille du tableau, la taille de l'exécutable, la taille du nom du programme - mais jamais la taille du code source.
James Anderson

1
@Sean McMillan: Je ne pense pas que la taille du code source était une limitation (considérez qu'à cette époque, les langages verbeux comme Pascal étaient très populaires). Et même si cela avait été le cas, je pense qu'il aurait été très facile de pré-analyser le code source et de remplacer les mots clés longs par des codes courts à un octet (comme par exemple certains interprètes de base). Je trouve donc l'argument "C est laconique car il a été inventé à une époque où moins de mémoire était disponible" un peu faible.
Giorgio

7

C'est assez simple: int *psignifie que *pc'est un int; int a[5]signifie que a[i]c'est un int.

int (*f)(int (*a)[5])

Signifie que *fc'est une fonction, *aest un tableau de cinq entiers, tout fcomme une fonction prenant un pointeur sur un tableau de cinq entiers, et retournant int. Cependant, en C, il n'est pas utile de passer un pointeur sur un tableau.

Les déclarations C se compliquent très rarement.

En outre, vous pouvez clarifier l'utilisation de typedefs:

typedef int vec5[5];
int (*f)(vec5 *a);

4
Toutes mes excuses si cela semble grossier (je ne veux pas que ce soit le cas), mais je pense que vous avez raté tout le point de la question ...: \
user541686

2
@ Mehrdad: Je ne peux pas vous dire ce qui était dans l'esprit de Kernighan et Ritchie; Je vous ai dit la logique derrière la syntaxe. Je ne connais pas la plupart des gens, mais je ne pense pas que votre syntaxe suggérée soit plus claire.
Kevin Cline

Je suis d'accord - il est inhabituel de voir une déclaration aussi compliquée.
Caleb

La conception de C Déclarations est antérieure typedef, const, volatileet la possibilité d'initialiser les choses dans les déclarations. De nombreuses ambiguïtés gênantes de la syntaxe de déclaration (par exemple, si elles int const *p, *q;doivent être liées constau type ou à la déclaration ) ne pouvaient pas apparaître dans le langage tel que conçu à l'origine. Je souhaite que la langue ait ajouté deux points entre le type et le declarand, mais a permis son omission lors de l'utilisation de types de "mots réservés" intégrés sans qualificatifs. Le sens int: const *p,*q;et int const *: p,*q;aurait été clair.
supercat

3

Je pense que vous devez considérer * [] comme des opérateurs attachés à une variable. * est écrit avant une variable, [] après.

Lisons l'expression de type

int* (*p)[10];

L'élément le plus intérieur est p, une variable, donc

p

signifie: p est une variable.

Avant la variable, il y a un *, l'opérateur * est toujours placé avant l'expression à laquelle il fait référence, par conséquent,

(*p)

signifie: la variable p est un pointeur. Sans (), l'opérateur [] à droite aurait une priorité plus élevée, c'est-à-dire

**p[]

serait analysé comme

*(*(p[]))

L'étape suivante est []: puisqu'il n'y a pas d'autre (), [] a une priorité plus élevée que l'extérieur *, donc

(*p)[]

signifie: (la variable p est un pointeur) vers un tableau. Ensuite, nous avons le deuxième *:

* (*p)[]

signifie: ((la variable p est un pointeur) vers un tableau) de pointeurs

Enfin, vous avez l'opérateur int (un nom de type), qui a la priorité la plus faible:

int* (*p)[]

signifie: (((la variable p est un pointeur) vers un tableau) de pointeurs) en entier.

Ainsi, l'ensemble du système est basé sur des expressions de type avec des opérateurs, et chaque opérateur a ses propres règles de priorité. Cela permet de définir des types très complexes.


0

Ce n'est pas si difficile quand vous commencez à penser et C n'a jamais été un langage très facile. Et int*[10]* pce n'est vraiment pas plus facile que int* (*p)[10] Et quel type de k seraitint*[10]* p, k;


2
k serait un examen de code échoué, je peux déterminer ce que le compilateur fera, je peux même être dérangé, mais je ne peux pas déterminer ce que le programmeur voulait - échouer ............
mattnz

et pourquoi k échouerait la révision du code?
Dainius

1
car le code est illisible et impossible à maintenir. Le code n'est pas correct à corriger, évidemment correct et susceptible de rester correct malgré la maintenance. Le fait que vous devez vous demander quel sera le type k est un signe que le code ne satisfait pas à ces exigences de base.
mattnz

1
Essentiellement, il y a 3 (dans ce cas) déclarations de variables de différents types sur la même ligne, par exemple int * p, int i [10] et int k. C’est inacceptable. Des déclarations multiples du même type sont acceptables, à condition que les variables aient une certaine forme de relation, par exemple int largeur, hauteur, profondeur; Gardez à l'esprit que beaucoup de gens programment en utilisant int * p, donc ce que je trouve dans 'int * p, i;'.
mattnz

1
Ce que @mattnz essaie de dire, c'est que vous pouvez être aussi intelligent que vous le souhaitez, mais tout cela n'a aucun sens lorsque votre intention n'est pas évidente et / ou que votre code est mal écrit / illisible. Ce genre de choses se traduit souvent par un code cassé et une perte de temps. De plus, pointer to intet intne sont même pas du même type, ils doivent donc être déclarés séparément. Période. Écoutez l'homme. Il a 18 000 représentants pour une raison.
Braden Best
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.