[gull] Subtilité de C n° 2 : type de retour de malloc

Leopoldo Ghielmetti leopoldo.ghielmetti at a3.epfl.ch
Wed Apr 2 19:52:42 CEST 2008


On Wed, 2008-04-02 at 18:16 +0200, Daniel Cordey wrote:
> On Wednesday 02 April 2008, Leopoldo Ghielmetti wrote:
> 
> > Pas totalement, le malloc(sizeof(int)) alloue un int et malloc(sizeof
> > *p) aussi (si p est un int*) la difference vient du moment ou tu alloue
> > un pointeur (comme tu le fais remarquer plus loin) ou une structure.
> 
> Non, ll est tres important d'arreter a considerer que c'est equivalent. 
> Ca "peut" etre equivalent sur certaines architectures, avec un certain OS et 
> certians compilateurs. Mais ce n'est ABSOLUMENT PAS GARANTI ! Un pointeur est 
> un pointeur et un int est un int ! Que je puisse mettre la valeur d'un 
> pointeur dans un int est possible mais pas une bonne pratique. EN reservant 
> une "espace" memoire pour y stocker une adresse (pointeur), il est plus 
> logique de dire je reserves une taille correspondant au stockage d'un 
> pointeur de type X. plutot que "je reserve un espace pour y stocker un 
> entier". Cela introduit une confusion sur laquelle se rue les pareseux et 
> ceux qui ne maitrisent pas bien la notion de pointeurs. C'est le jours ou 
> l'on manipule des pointeurs a 4 ou 5 niveaux que l'on commence a bien 
> comprendre les dangers de l'amalgame. 

Oui, et je dis exactement la même chose. La dessus on est d'accord à
100%, mais il faut que tu relise les bouts de code en question.
Il s'agit de la différence entre:
int *p;
p = (int*)malloc(sizeof(int));

et
int *p;
p = (int*)malloc(sizeof(*p));

Ce qui est EXACTEMENT equivalent car *p est un int. Il n'a jamais été
question d'une conversion entre un pointeur et un int.

Par contre je faisait référence à l'allocation d'un pointeur ou d'une
structure, c'est à dire:

struct toto *t;
t = (struct toto*)malloc(sizeof(struct toto)); // dangereux

et
struct toto *t;
t= (struct toto*)malloc(sizeof(*t));

ou
int **p;
p = (int**)malloc(sizeof(int)); // faux

et
int **p;
p = (int**)malloc(sizeof(*p));

Pour la structure c'est dangereux car on risque des problèmes de
padding. Pour le pointeur c'est faux car on alloue un int et on le caste
en pointeur. Note qu'il serait par contre correct d'écrire:
p = (int**)malloc(sizeof(int*));
car dans ce cas le sizeof serait calculé correctement.

Le seul vrai risque vient du padding des structures ou de l'utilisation
du mauvais type ou du mauvais cast.
Pour éviter tout cela je préfère caster le malloc et utiliser la
variable pour le sizeof.

> > Pas vrai car si j'ai bien lu la documentation du C le type int est le
> > type natif de la machine (ou à défaut celui qui s'exécute le plus
> > rapidement) c'est à dire que sur une machine à 16b le int est à 16b, sur
> > une machine à 32b le int est à 32b et sur une machine à 64b il est à
> > 64b.
> 
> Ah, ca c'est nouveau ! EN fait, ANSI ne decrit absolument pas le nombre de 
> bytes utilises pour stocker les short, int, lon et long long... Cela est 
> explicitement laisse au compilateur, en liaison avec le CPU et l'OS.

Je ne me rappelles plus ou je l'ai appris peut être même au poly à
l'époque de mes études, en tout cas sur google j'ai trouvé ceci
http://home.att.net/~jackklein/c/inttypes.html#int

qui indique qu'a l'origine le type int était le type "naturel" du
processeur (c'est la première phrase), ce qui rejoint ce que j'ai
affirmé. Peut être que cela n'a jamais été spécifié, mais c'est comme ça
qu'il a toujours été considéré par les compilateurs.

> La seule chose qui soit precisee est que :
> 
> sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long 
> long)
> 
> Il me semble qu'il existe une sorte de convention qui dit que sur une machine 
> 16 bits, short, int et long ont la meme taille (2 bytes). Sur une machine 32 
> bits short -> 2 bytes, int et long 4 bytes. Sur une machine 64 bits : 
> short -> 2 bytes, int -> 4 bytes et long -> 8 bytes. 

Probablement. En tout cas je trouve que c'est très mal défini. Ils
auraient du spécifier la taille de ces objets depuis le début. Ça aurait
évité un tas de confusions. Mais ils ne l'ont pas fait justement parce
qu'à l'origine le int était le type "naturel" du processeur, donc ça
dépendait de la machine.

> l'option  =m64 de gcc di ceci :
> 
>  Generate code for a 32-bit or 64-bit environment. The 32-bit environment sets 
> int, long and pointer to 32 bits and generates code that runs on any i386 
> system. The 64-bit environment sets int to 32 bits and long and pointer to 64 
> bits and generates code for AMD's x86-64 architecture. 
> 
> Le manuel de gcc dit aussi ceci a propos des options pour un processeur 
> Athlon :
> 
> The default size of ints, longs and pointers depends on the ABI. All the 
> supported ABIs use 32-bit ints. The n64 ABI uses 64-bit longs, as does the 
> 64-bit EABI; the others use 32-bit longs. Pointers are the same size as 
> longs, or the same size as integer registers, whichever is smaller.
> 
> Il faut donc bien faire attention aux options du compilateur ainsi que la 
> version de l'OS et le type de processeurs pour lequel on compile !

Eh oui. :-(

> > Et ce selon les spécifications du C (sauf si les spécifications 
> > récentes ont changé cela).
> 
> Il n'y a pas de specification du C a ce sujet.
> 
> > j'imagine que c'est de même pour tous les portages 32b->64b.
> > Cette définition de int fait en sorte que sur la plupart des machines
> > 32b le int est égal au long (car ils font les deux 32b) et c'est une des
> > raisons de la mauvaise programmation qui s'est instaurée cette dernière
> > décennie.
> 
> Oui, et surtout parceque cette ambiguite a ete petpetuee par   beaucoup de 
> gens. Il serait sans doute plus juste de dire que l'on peut considerer les 
> pointeurs comme des 'unsigned long', plustot que comme des 'int'. Le fait de 
> ne pas preciser 'unsigned' peut avoir des consequences catastrophiques car il 
> peut y avoir 'masquage' du bit de signe lors d'operation arithmetiques.

Oui, et ce n'est pas le seul cas ou il faudrait utiliser le unsigned.

> > Par contre ce sera vrai sur les machines qui ont les registres qui
> > stockent les pointeur différents en taille de ceux qui stockent un
> > entier.
> 
> Typyquement 64 bits...
> 
> > Que dit le manuel du C en propos?
> 
> Lequel ? ANSI C ou K&R ? Oublions K&R qui a ete justement suplante par ANSI 
> pour ces raisons d'ambiguite... il y a deja 20 ans.
> 
> > A ma connaissance tous les compilateurs considères les deux comme des
> > notations alternatives.
> > En effet:
> > int a[];
> > et
> > int *a;
> > sont compilé de la même façon partout (à ma connaissance). Et avec un
> > langage comme le C je vois mal faire autrement.
> 
> Le pobleme vient a partir du deuxieme niveau de pointeur, pas du premier comme 
> decrit ici. Dans ce cas, on ne peut pas considerer a[][] comme **a ou *a[]. 
> Le probleme reside lors de l'allocation et de l'acces. On peut bien sur tout 
> simuler en C avec N mallocs. Ce n'est pas parceque l'on est en mesure de 
> construire un tableau a deux dimensions a l'aide de **a et de malloc que l'on 
> peut considerer que c'est equivalent. Pour un type donne, il est important de 
> lire la syntaxe pour ce qu'elle decrit. A savoir :
> 
> int a[][]; /* tableau d'entier a deux dimensions */
> int *a[]; /* pointeur de tableau d'entier a une dimension */
> int **a; /* pointeur de pointeur d'entier */

Le compilateur gcc fait la différence entre les écritures a[x][y] et **a
par contre il ne fait pas de différence si le tableau est
unidimensionnel.

Effectivement les accès à un tableau multidimensionnels sont différents
des accès monodimensionnels, pour les premiers on utilise un pointeur
sur une zone de mémoire mappée de façon multidimensionnelle ou la
position du mot recherché est donné par la formule i*x+j (ou similaire
pour un bidimensionnel) tandis que pour un monodimensionnel c'est
simplement i. Et à ma connaissance tous les compilateurs font ainsi
(avec juste quelque variation).
Donc l'écriture "int *a[]" et "int **a" sont équivalentes du point de
vue de l'implémentation. Par contre l'écriture "int a[x][y]" (ou x et y
sont des constantes) ce n'est pas implémenté de la même façon.

En effet dans la déclaration d'un tableau multidimensionnel il faut
impérativement définir les tailles tandis que ce n'est pas demandé pour
un tableau monodimensionnel.

Donc finalement "char **argv" et "char *argv[]" sont deux écritures
équivalentes du point de vue de l'implémentation.

Sémantiquement les trois écritures représentent trois objets différents.
Mais ceci est laissé aux bonnes pratiques des programmeurs, le
compilateur ne s'intéresse qu'à l'implémentation.

> > Finalement a[3] est équivalent à *(a+3) bien que selon logique les deux
> > ne devraient pas être exactement la même chose quoique du côté du
> > langage machine l'implémentation est exactement la même (et c'est
> > d'ailleurs pour cela que ça ne me pose pas de problème de
> > compréhension).
> 
> Il importe aussi de ne pas ramener tous les pointeurs a des entiers pour une 
> autre raison. Ce qui permet d'ecrire *(a + 3) ne peut se faire que si 'a' a 
> ete defini comme un 'int *'. En effet, le compilateur reconnaitra que, comme 
> il s'agit d'un pointeur d'entier, celui-ci doit etre incremente pas pas de 4. 
> Si a est defini comme un simple 'int', non seulement nous aurons un warning 
> (peut-etre meme une erreur de compilation), mais l'incrementation ne se fera 
> pas correctement. On peut "bricoler" du casting pour eviter les warnings, 
> mais si on ne le fait pas correctement, on risque quand meme le bus-error. Si 
> par malchance, on ecrit *(a + 4), on n'aura pas de bus error mais on accedera 
> seulement au quart des valeurs du tableau :-). Remarque que cette o[eration 
> ne compilera pas non-plus si a est de type '* void'...

Exact.

> Tout ca pour dire qu'il est plus simple, logique et cense d'ecrire et de 
> definir le type de pointeur que l'on manipule, plutot que de dire que tous 
> les pointeurs sont des 'int' (ce qui est faux, je le repete).

La logique veut qu'un objet donné soit considéré via un type compatible
tout le long du programme. Ceci est bien vérifié avec des langages
objet, mais pas avec l'assembleur ou le C ou les autres langages qui ne
vérifient pas strictement le type des objets.
Un des devoirs du programmeur c'est de bien utiliser les types à
disposition sans créer des ambiguïtés inutiles (il y en aura quand même
toujours, surtout en C, d'où les cast).

> > Et j'ai d'ailleurs eu un problème similaire la fois ou j'ai du
> > transmettre des structures binaires entre deux machines avec des
> > architectures différentes. Il m'a fallu jouer avec la position des
> > variables pour que ça marche. C'est pourri mais c'était ce qui était
> > demandé pour des raison de vitesse de traitement.
> >
> > C'est un code ou il faudra faire attention à chaque opération de
> > maintenance.
> 
> On peut soit utiliser ce qui est fournit par xdr (issu de NFS)m, soit, comme 
> je l'ai fait, developper une librairie d'echange d'objets avec reconnaissance 
> des objets de chaque cote (chargement dynamique de la librairie), incluant 
> des fonction wrap/unwrap dans cahcune des versiond e lalibrairie. L'avantage 
> suplementaire est que j'ai aussi pu inclure une notion de 'version' des 
> objects ce qui me permettait de pouvoir m'affranchir de la necessite de 
> synchronisation des applications. Parfois, il ne faut pas attendre la 
> solution miracle mais la developper soi-meme.

Oui, on avait une telle librairie, mais elle ralentissait trop
l'exécution et donc j'ai implémenté une solution qui ne convertit pas.
(C'est pour ça que je parlais de "raisons de vitesse").

> > Oui, mais en utilisant la variable au lieu de la macro on obtient le
> > même résultat et je trouve que c'est quand même plus lisible si on ne
> > met pas de macro. Soit dit en passant, j'adore les macros du C, ça
> > permet de faire beaucoup de choses (entre autre augmenter la lisibilité
> > du code même si ça peut paraître étrange à quelqu'un qui n'est pas
> > habitué), mais si je peux obtenir le même résultat (en fonctionnel et en
> > lisibilité) sans les utiliser c'est mieux.
> 
> Une variable ou une macro... ce n'est pas non plus pareil. L'objectif de la 
> macro est de permettre au compilateur d'effectuer son travail 
> d'optimisation ! ALors que cela n'est plus possible si l'information est 
> encapsulee dans une variable. Il se trouve que les compilateurs sont capables 
> d'effectuer des optimisations impresionnantes lorsque l'on manipule des 
> pointeurs. Le compilateur le sait et agit en consequence. C'est 
> particulierement vrais sur les architectures de type EPIC et WLIW.

Désolé, je ne vois pas ou est l'encapsulation dans une variable.
Dans un cas tu calcules toi même la taille des objets en espérant que
ton calcul soit exactement ce que le compilateur fait quand il alloue
les objets, dans l'autre tu dis au compilateur de se baser sur la taille
de la variable telle que lui même la conçoit.
Disons que parmi les deux je préfère nettement le deuxième.

Je suis d'accord d'utiliser ta méthode des macros si le calcul doit se
faire dans une façon bien précise et maîtrisée par le programmeur mais
il s'agit d'un cas particulier. Moi je préfère bien instruire le
compilateur de façon qu'il puisse faire son travail au mieux et dans le
cas de la taille d'une structure ou une variable je me vois mal faire
mieux que lui en utilisant des macros.

> > Les deux, et ne t'en fais pas, ce n'est pas qu'un problème de LL,
> > beaucoup de ces problèmes je les ai vu aussi en milieu industriel ou
> > l'impératif est sortir un code fonctionnel le plus vite possible et au
> > moindre coût.
> 
> Certains constructuers *NIX ont collectes les mauvaises notes a la pelle dans 
> ce domaine. Pour d'autres, un gros travail avait ete fait pour rendre tous 
> les includes standard ANSI/POSIX/etc. et c'etait un vrais bonheur.
> 
> > Donc les programmeurs sont rarement poussés à faire mieux.
> 
> Jamais... mieux c'est plus vite :-)
> 
> > Tu écris un code pourri mais qui marche sur la machine XY et tu as mis
> > la moitié du temps? Très bien.
> > Est-ce que ce code marchera sur une autre architecture sans tout
> > retravailler? on s'en fout, le client ne l'a pas demandé et le jour ou
> > il le demandera il va payer la migration et alors on aura un autre
> > projet et un autre budget pour améliorer le code.
> 
> SI pas de contraintes trop importantes au niveau vitesse d'execution... 
> Python, Java... est une reponse. L'avantahe de Python etant que 
> l'interpreteur se comporte de la meme maniere sur toutes les machines, alors 
> que les differentes JVM tendent a agacer...

C'est une des raisons pour laquelle aujourd'hui on programme de plus en
plus en Java ou en .net, seulement parce que de programmeurs qui
comprennent ce que font en C il n'y en a pas des masses (lire: il faut
beaucoup d'expérience).
Malheureusement ils ne se rendent pas compte que les cochonneries on
peut les faire la aussi.

Python est un langage joli, mais malheureusement les tentatives que j'ai
faites pour l'utiliser ne sont pas toutes allées à bon port. Vu qu'il
vérifie les types à l'exécution et non à la "compilation" il m'est
souvent arrivé d'avoir un code qui tourne très bien dans le 99% des cas
et puis tout à coup voilà une belle exception car je me retrouve un
objet non attendu. Inutile de dire qu'il est impossible de savoir qui a
initialisé la variable en question car toutes les opérations précédentes
étaient parfaitement légales.
Donc la je préfère nettement le langage qui me plante tout de suite au
nez dès qu'il y a une affectation sur un type non correct.

> dc

ciao, Leo




More information about the gull mailing list