J'ai du mal à me faire une idée de son fonctionnement, j'ai l'impression qu'il s'applique tantôt à ce qu'il y a à sa droite, tantôt à ce qu'il y a à sa gauche.
là, l'opérateur s'applique à ce qu'il y a à sa droite :
int * ptr_sur_entier,entier; // déclare un pointeur sur int, et un int, donc c'est équivalent à :
int (*ptr_sur_entier),entier;
pour définir un pointeur sur fonction :
int * f(void); // déclare une fonction qui retourne un pointeur sur entier, équivalent à:
int * (f(void));// les parenthèses sont implicites dans la déclaration précédente ?
int (*p_sur_f)(void); // déclare un pointeur sur fonction
typedef int (*pf)(); // déclare le type pf "pointeur sur fonction qui retourne un entier"
Dans ce qui précède, l'étoile s'applique en gros à ce qu'il y a à sa droite, mais, les opérateurs de cast :
pf * tableau_pointeurs_fonctions = (pf *) malloc( sizeof(pf) * 4)
// déclare un pointeur sur 4 pointeurs sur fonction qui retournent un int
pourquoi est-ce que l'opérateur de cast s'écrit (pf *) ?
Est-ce qu'il y a une définition formelle de l'opérateur * qui explique cette syntaxe,, pf ( * chose_à_caster) n'aurait pas été plus cohérent ?
D'autre part, je remarque que dans ce tableau, je peux mettre comme fonctions:
int f(); // sans avertissement
int g(int, int, int); // sans avertissement
int h(int,char); // là j'ai un "attention : assignment from incompatible pointer type"
int i(char); // là de même, il y a un lien entre le type de l'argument et le type de retour vu du pointeur vers la fonction ?
par contre, si je fais un tableau de pointeurs vers des fonctions de la forme int f(int), je ne peux pas mettre de fonctions sans arguments dedans. Ça déclenche carrément une erreur "too few arguments"
Je sais bien que toutes ces affectations n'ont pas de sens, c'est juste pour tenter de comprendre le compilateur (gcc 4.6 en l’occurrence), et à quoi ressemble un tableau de pointeur sur fonctions qui retournent un int en mémoire ? est-ce que les argument des fonctions de ce tableau changent sa représentation ?
J'imagine que par ailleurs c'est très dépendant de l’architecture.
J'ai essayé de formuler clairement mon interrogation, mais j'ai l'impression que tout m'échappe concernant les pointeurs sur fonctions en C, alors c'est décousu.
Sinon…
Est-ce qu'il est possible d'avoir plus d'avertissement qu'avec -Wall avec gcc ? Est-ce qu'il y a des compilos moins arrangeants sur les conversions de type, dont les avertissements m'aideraient à comprendre ? Où d'une manière générale un compilo qui donne de meilleures habitudes ?
Qu'est-ce que je peux lire ou faire pour mieux comprendre ?
# dans une déclaration de type le * s'applique à l'identifiant de gauche
Posté par delio . Évalué à 0.
i est de type "int"
ptr est de type "int *", soit pointeur sur int.
le resultat de malloc est casté en "int *" pour être stocké dans une variable de type "int *"
pf est de type "int (* )(void)", pointeur sur fonction "int ( ) (void)"
[^] # Re: dans une déclaration de type le * s'applique à l'identifiant de gauche
Posté par calandoa . Évalué à 3.
« ptr est de type "int *", soit pointeur sur int. »
Et bien, justement, non! C'est
*ptr
qui est du typeint
. Bon en fait, c'est un peu une question de goût et de couleur, mais cette façon de voir les choses les simplifie grandement quand on passe à des déclarations plus compliquées.Dans le cas de déclaration pour un cast ou un argument de fonction, comme
(int *)
, il faut s'imaginer qu'il existe un nom de variable qui a été omis dans l'expression, sachant qu'il ne pourrait de toutes façons n'apparaitre qu'à un seul endroit.Je conseille vivement la lecture de la section 5.12 du K&R qui donne moult détails ainsi même qu'un programme pour mieux comprendre les déclaration compliquées. En voici un extrait :
Un autre extrait de la section A.8.8 :
« For example, [example below] name respectively the types "integer", "pointer to integer", "array of 3 pointers to integers", "pointer to an unspecified number of integers", "function of unspecified parameters returning pointer to integer", and "array, of unspecified size, of pointers to functions with no parameters each returning an integer". »
[^] # Re: dans une déclaration de type le * s'applique à l'identifiant de gauche
Posté par delio . Évalué à 1.
C'est marrant de voir comment tu me contredis avant de donner l'extrait du K&R qui confirme ce que j'ai dit...
[^] # Re: dans une déclaration de type le * s'applique à l'identifiant de gauche
Posté par calandoa . Évalué à 2.
Je crois que tu as mal compris le sens du message. Il ne s'agit pas de dire que ce que tu as marqué est techniquement faux, mais plutôt qu'il existe plusieurs manières de penser une déclaration en C, et que certaines, moins intuitives, sont plus efficaces dans des cas complexes.
# Oula plein de chose dans le poste
Posté par TheBreton . Évalué à 2.
(j'ai jamais d'inspiration pour les titres des commentaires...)
Dans ce qui précède, l'étoile s'applique en gros à ce qu'il y a à sa droite, mais, les opérateurs de cast :
pf * tableau_pointeurs_fonctions = (pf *) malloc( sizeof(pf) * 4)
// déclare un pointeur sur 4 pointeurs sur fonction qui retournent un int
pourquoi est-ce que l'opérateur de cast s'écrit (pf *) ?
Non, c'est pareil,
malloc retourne un void, et donc le compilo gueulerait disant que tu associe un void à ton pointeur de tableau qui est de type pf. Le cast (pf)malloc dis au compilo que tu sait ce que tu fais et que le void* est comme un pf* (on appelle ça transtypage d'une variable).
La ligne complète cumule déclaration d'une variable de type pf* et la création d'un espace mémoire de taille 4 pf
Je sais bien que toutes ces affectations n'ont pas de sens, c'est juste pour tenter de comprendre le compilateur (gcc 4.6 en l’occurrence), et à quoi ressemble un tableau de pointeur sur fonctions qui retournent un int en mémoire ? est-ce que les argument des fonctions de ce tableau changent sa représentation ?
J'imagine que par ailleurs c'est très dépendant de l’architecture.
Alors la pour faire simple on va faire du pseudo assembleur, il faut comprendre comment marche un micro. Un pointeur sur fonction ce n'est jamais rien d'autre qu'une adresse mémoire ou aller exécuter du code.
En pseudo asm on doit donc lire un adresse dans un tableau et sauter à cette adresse (si ta fonction n'as pas d'argument).
Après le passage d'argument et la valeur de retour de la fonction sont dépendant du micro utilisé.
Sur certaines architecture RISC on va passer les arguments dans des registres micro (et si il y a trop d'argument par un pointeur un emplacement dans la pile), le code retour de la fonction est toujours dans un registre/pseudo registre défini à l'avance (pseudo registre car si le code retour est 64bits sur un micro 16bits soit c'est un pointeur sur la valeur 64bits qui est retourné, soit la valeur est séparée sur plusieurs registres).
Ce qui complique beaucoup les choses si on mixe du code venant de compilo différent et le rend presque impossible sans l'utilisation de thunk écris en assembleur (je vois pas le mots français pour ces routines à part glue-logique).
Sur les architecture CISC (comme le x86) on n'as souvent que très peut de registre donc les arguments sont mis d'office dans la pile et on passe un pointeur dans un registre à la fonction. Le code retour est dans EAX (de mémoire).
Qu'est-ce que je peux lire ou faire pour mieux comprendre ?
Le compilo produit de l'assembleur, donc apprendre à le lire est une bonne chose pour vérifier que le résultat produit est bien celui attendu. En programmation embarqué je regarde toujours toujours l'assembleur généré.
faire objdump -D mon_fichier.o |less
est riche d'enseignement.
[^] # Re: Oula plein de chose dans le poste
Posté par Zylabon . Évalué à 0.
Donc, le cast sert juste à supprimer un avertissement du compilo ? Je veux dire que sauf le cas où il faut forcer des conversions pour avoir un résultat juste, c'est comme mettre entre parenthèse une affectation en guise de test ? Plus précisément, à expliciter une conversion implicite, vu que toute les valeurs affectées sont converties dans le type de la variable qui la reçoit.
Sinon, je comprend mieux. Pour l'assembleur, je me doutais que je pourrais pas y échapper.
rien que le code généré par le programme « int main(void){ return 0;} » est effrayant… D'ailleurs, la commande cc -S n'est pas mieux pour ça ? objdump me donne un fichier de 700 lignes, cc -S seulement quelques unes. Avec un ligne « movl $0, %eax » que j'étais très fier de trouver :p
J'ai encore du boulot ^^
Please do not feed the trolls
[^] # Re: Oula plein de chose dans le poste
Posté par TheBreton . Évalué à 1.
Donc, le cast sert juste à supprimer un avertissement du compilo ?
Le cast sur la valeur du malloc oui, un avertissement est à prendre quand même au sérieux car t'indique un problème potentielle.
Exemple tu fais ton prog et tu le teste sur un x86 (donc 32 bits), puis ton PC change et tu le recompile sur 64 bits et la paf, plus rien ne marche à cause d'un cast inconsidéré/aux effetr mal évalué à l'époque, tu peut passer beaucoup de temps à trouver l'origine du pb...il faut absolument réfléchir avant de faire un cast sur les archi possible du programme.
c'est comme mettre entre parenthèse une affectation en guise de test ?
Ca c'est pas un test puisque c'est toujours vrai (de faire une affectation).
vu que toute les valeurs affectées sont converties dans le type de la variable qui la reçoit.
Normalement tu dois avoir un warning a faire des conversion implicites en cas de perte de bits significatif (exemple tu mets le résultat d'une opération 64bit dans un char qui fais 8 bits (sur beaucoup d'arch) tu perd au passage de l'information ), mais comme sur x86 presque tous les type de variable C sont codé vers du 32bits par les compilos...
La différence entre "cc -S" et objdump c'est que objdump te montre tout le code de ton appli (incluant ctr0 et crt1 les fichier de préambule des programme réalisant les initialisation des variables et des piles avant démarrage du main) ainsi que les libs static lié à ton programme, "cc -S" ne montre que ton fichier compilé avant linkage (donc les variables/fonctions externe ne sont pas référencées)
« movl $0, %eax »
return 0;
J'ai encore du boulot ^^
Surtout il ne faut pas croire que tout ce que fais le compilo est conforme à ce que tu veux et ne compter que sur lui pour indiquer les erreurs. C'est ton job de programmeur ca. ;-)
[^] # Re: Oula plein de chose dans le poste
Posté par GaMa (site web personnel) . Évalué à 0.
Pas sur de bien comprendre "c'est pas un test" mais :
Matthieu Gautier|irc:starmad
[^] # Re: Oula plein de chose dans le poste
Posté par calandoa . Évalué à 2.
Le cast dans ce cas ne sert pas à grand chose vu qu'il faut transformer un pointeur en pointeur, ce qui, sur un processeur habituel ne demande qu'un « move », mais il y a des cas où le cast force une opération arithmétique bien particulière :
123.456001 123 1123477881
-107 149
[^] # Re: Oula plein de chose dans le poste
Posté par Obsidian . Évalué à 3.
Dans ce cas précis, c'est inutile. Le compilateur ne se plaint jamais lorsqu'on affecte un pointeur void vers un pointeur d'un type quelconque et vice-versa. C'est vrai au moins en C99. Ça semble être aussi le cas en C89 mais je ne dispose pas du document correspondant :
# tableau
Posté par delio . Évalué à -2.
tableau de N pointeurs sur fonctions sans arguments qui renvoient un int
# Parsing en C
Posté par Obsidian . Évalué à 4.
En fait, ça s'applique bien à gauche mais… une seule fois. :-) C'est dû au fait que contrairement aux autres spécificateurs de type, tu peux enchaîner les pointeurs sur une longueur arbitraire. Ainsi :
… te donnera respectivement « char ***** a » et « char **** b ». L'opérateur « * » sert donc bien à définir un type, mais est un non-terminal.
Pour le reste, tout devient plus clair lorsque l'on admet qu'un type est défini de manière récursive, comme lorsque tu développes une expression mathématique en commençant par les parenthèses les plus imbriquées, celles-ci pouvant se trouver au centre de l'expression et pas forcément sur les côtés.
Un type en lui-même pourrait être défini de n'importe quelle manière mais que, dans les faits, il se trouve généralement à gauche de l'identifiant auquel il est ÉVENTUELLEMENT associé, sauf dans deux cas : les déclarations de fonctions et les déclarations de tableaux, qui se représentent respectivement avec () et [], lesquels prennent place à droite de l'identifiant. Donc, dans l'ordre :
— Rien (instance ordinaire)
— Une paire de parenthèses (fonction), à l'intérieur desquelles tu vas parser… des noms de types éventuellement nommés (les paramètres), exactement comme tu es actuellement en train de le faire. Récursivement, donc.
— Une paire de crochets (tableau) contenant une taille éventuelle.
Et comme le point numéro 2 est prioritaire par rapport au n° 3 et au n°4, tu peux utiliser des parenthèses dans la construction de ton expression pour forcer la priorité.
Ainsi, une fonction admettant un int en entrée et te renvoyant un pointeur de fonction acceptant deux float et renvoyant un double se construirait ainsi :
— « fnct » le nom de mon instance ;
— « fnct(int) » suivie des parenthèses avec des types à l'intérieur : fonction ;
— « * fnct(int) » précédée d'une étoile : l'expression entière est de type pointeur. Note que la priorité de 2. sur 3. fait que c'est ma « fonction qui est de pointeur » et non mon « pointeur qui pointe une fonction » ;
— « (* fnct(int)) » ici, je suis obligé d'ajouter des parenthèses, sinon ce que je rajouterais à droite serait prioritaire sur mon pointeur. Mon expression est donc une fonction de type pointeur sur quelque chose. Mais quoi ?
— « (* fnct(int))(float,float) » … sur une fonction, impliquée par mes parenthèses. Cette fonction attend donc deux _float ;
— « double (* fnct(int))(float,float) » cette fonction est une expression mathématique qui peut être évaluée. Comme elle renvoie un double, je peux placer mon terminal « double » à gauche. Mais si elle renvoyait quelque chose de plus évolué, je pourrais très bien repartir pour un tour.
À noter ainsi que je pourrais très bien faire une fonction qui renvoie non pas un pointeur de fonction mais… une autre fonction ! Syntaxiquement, ce serait possible. Exemple : une fonction « f » qui admettrait un int et renverrait une fonction de type « int g (void) » s'écrirait :
int f(int)(void);
En soi, ce serait un bon moyen d'implémenter les lambdas-fonctions. Bon, en fait, cela ne pourrait pas être écrit directement de cette façon car cela impliquerait que l'on puisse instancier plusieurs fois la même fonction. Mais le C++11 a quand même introduit une syntaxe pour le faire…
L'opérateur de transtypage, maintenant, admet tout simplement un type, sans identifiant, entre une paire de parenthèses. Il faut donc simplement résoudre ce qui se trouve à l'intérieur sans se soucier de ce à quoi on l'applique, aller au bout de la procédure, et seulement ensuite examiner ce qui suit.
Ça ne change pas la manière de les construire. Par contre, quand tu définis ce type, tu déclares dans la foulée les fonctions qu'il pointe. Il est donc nécessaire d'établir proprement leur signature, pour que le programme qui déréférence le pointeur puisse utiliser les fonctions qui sont pointées.
En outre, il y a une subtilité qui dit qu'en C ISO, fnct() et fnct(void) sont identiques mais qu'en K&R, le premier sert simplement à déclarer l'existence de la fonction sans la décrire plus. Je ne sais pas si cela s'applique sur les définitions de type.
En principe, non. Un code C canonique propre est censé être parfaitement portable.
[^] # Re: Parsing en C
Posté par Obsidian . Évalué à 2.
UPDATE: J'ai des crochets qui ont disparu dans la déclaration de « tableau ». Je la refais :
[^] # Re: Parsing en C
Posté par Zylabon . Évalué à 0.
J'ai relu une bonne dizaines de fois, et j'ai à peu près tout compris ! Merci beaucoup.
Cependant, il y a encore des choses qui m'échappent. Tu dis « Tu poses (éventuellement) un identifiant »
Ne pas mettre l'identifiant sert à donner le type de retours d'une fonction par exemple ?
dans l'exemple
on peut dire que le retour de f est de type int (void) ? Par exemple, imaginons une fonction qui retourne un pointeur vers une fonction int (g)(void) mais sous forme d'un pointeur (void *) comme le ferait un thread par exemple, il est possible de caster ce pointeur avec ( int ()(void) ) ?
Pour ma question, sur la représentation en mémoire du tableau. Si je compile ce bout de code
Je trouve que les pointeurs vers des fonctions font 4 octets (quelle qu'elles soient), et que les fonctions n'en font qu'un (d'ailleurs, on est limité à 256 types de fonctions ?) j'ai l'impression d'être complètement à coté de la plaque. Je crois qu'il faut vraiment que j'apprenne des bases d'assembleur.
Please do not feed the trolls
[^] # Re: Parsing en C
Posté par Zylabon . Évalué à 0.
ya un problème, avec des étoiles qui ont sauté, je recommence
Please do not feed the trolls
[^] # Re: Parsing en C
Posté par Zylabon . Évalué à 0.
Je suis fatigué… Bien sûr que les pointeurs font tous la même taille vu qu'ils sont des adresses de la même mémoire ! Cela dit, le reste m'échappe toujours.
Please do not feed the trolls
[^] # Re: Parsing en C
Posté par Obsidian . Évalué à 2.
Hello,
C'est un exemple qui ne fonctionnera pas. Le compilo ne te laissera pas faire. Par contre, il te dira quand même « erreur: ‘f’ declared as function returning a function », ce qui montre que notre syntaxe était correcte sur le plan formel.
N'oublie pas que tu retournes un pointeur ! Il faut donc le caster vers un « pointeur sur une fonction n'admettant aucun argument, mais retournant un int ». Donc « (int(*)(void)) ». Ça, oui, c'est possible. Tu peux même l'invoquer directement en ajoutant des paramètres entre parenthèses à la suite de ton appel à « f », ce qui donne une syntaxe toute particulière. :-)
Il est probable (c'est à vérifier) que dans ce cas précis, le compilateur utilise un mécanisme similaire aux énumérations pour cataloguer tes définitions de type de fonction. Ces énumérations sont réputées être codé sur un type entier de n'importe quelle taille (donc y compris char). Les membres eux-mêmes devant pouvoir être convertis en int.
C'est toujours une bonne chose de le faire, et ça t'aidera beaucoup dans la compréhension du C et des systèmes informatiques en général. Par contre, dans ce cas précis, ça ne te sera d'aucune utilité.
# Grammaire C
Posté par Obsidian . Évalué à 2.
Je rajoute un commentaire pour préciser que la norme C spécifie et détaille la grammaire formelle du langage (tant et si bien qu'en théorie, on pourrait presque la balancer directement à Lex & Yacc pour recompiler un compilateur, ce qui est précisément la définition de « Y.A.C.C. »).
Et en ce qui concerne les types en particulier, ceux-ci font l'objet d'une section dédiée : 6.7.6
Les « opt » en fin de noms de règles sont en fait écrits en indice dans le document PDF. Donc, ici, « pointer » et « pointeropt » sont la même règle.
On voit, comme c'est l'usage dans la définition de telles grammaires, que les différentes règles font massivement références à elles-mêmes, d'où la récursivité. C'est ce qui te permet de construire des types de plus en plus compliqués, en partant du centre comme expliqué dans nos commentaires précédents.
# Merci beaucoup à vous, j'ai tout compris !
Posté par Zylabon . Évalué à 0.
J'ai toujours un avertissement que je ne comprend pas, concernant la ligne return a(); :
attention : ‘return’ with a value, in function returning void [enabled by default]
a() ne retourne pas void puis-ce qu'il est de type int (*) (void)…
Bref ! merci beaucoup à tous !
Please do not feed the trolls
[^] # Re: Merci beaucoup à vous, j'ai tout compris !
Posté par Obsidian . Évalué à 2.
Oui, et si c'est le cas, ça fonctionne aussi dans l'autre sens. Ces deux expressions sont équivalentes s'il s'agit d'une fonction :
Non, mais ta fonction
main()
, elle, est déclarée «void main (void)
». Ce qui gène alors le compilo, c'est justement le fait que tu renvoies un int.[^] # Re: Merci beaucoup à vous, j'ai tout compris !
Posté par Zylabon . Évalué à 0.
Haha ^^ quel idiot ! j'étais tellement concentré sur mes pointeurs… Merci. Je m'étais expliqué l'avertissement par le fait que j'avais embrouillé le compilo avec mes bidouillages de pointeurs, il est malin quand même !
Du coup ça compile tout seul.
Please do not feed the trolls
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.