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

Leopoldo Ghielmetti leopoldo.ghielmetti at a3.epfl.ch
Wed Apr 2 14:53:49 CEST 2008


On Wed, 2008-04-02 at 13:35 +0200, Daniel Cordey wrote:
> On Wednesday 02 April 2008, Leopoldo Ghielmetti wrote:
> 
> > > p = (int*)malloc(sizeof *p);  /* style peu courant */
> > > p = malloc(sizeof(int));  /* style très courant */
> >
> > Moi j'utilise ce que tu appelles le style peu courant et très couramment
> > d'ailleurs. :-)
> 
> Bravo !

Merci, merci!

> Le "soit-disant" style peu courant devrait etre le standard de tout 
> developpeur. La place "reservee" par malloc(sizeof(* o)) ne sera pas la meme 
> avec un systeme 32 ou 64 bits. Alors que malloc(sizeof(int)) n'engendredra 
> pas de differences. Le pire etant que cela ne se voit pas immediatement. 
> Ecrire :
> 
> 	 p = malloc(sizeof(int));
> 
> est tout simplement non-portable. Punkt ! 

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.

> Il est sans doute vrais que le "style tres courant" est le plus repandu, mais 
> cela est inquietant... quand on veut reserver une espace de memoire pour un 
> pointeur, on se doit de lui passer la taille necessaire a l'obtention de la 
> bonne valeur. Il est donc incorrecte d'utiliser la reservation pour le 
> stockage d'un 'int', alors qu'il n'y a AUNCUNE garantie qu'un 'int' puisse 
> contenir un pointeur. C'est une "chance" que cela soitr vrais sur la plupart 
> des systemes ayant un processeur 32 bits. 
> 
> Il s'avere que l'on effectue souvent des reservation pour des tableaux de 
> valeurs. Or, les deux ecriture mentionnees ont des effets differents suivant 
> les architectures ! si l'on veut que son code compile aussi bien en 32 bits 
> qu'en 64 bits, la discussion n'a pas lieu d'etre. Sur une machine 64 bits, 
> un 'int' est stocke dans 4 bytes, alors qu'un pointeur est stocke en 8 
> bytes...

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. Et ce selon les spécifications du C (sauf si les spécifications
récentes ont changé cela). C'est d'ailleurs ça qui à causé la plupart
des problèmes de portage entre les machines 16b et les machines 32b,
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.

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.

> > > Ah oui, les relations entre les pointeurs et les tables sont une
> > > inépuisable source de subtilités en C et j'écrirai surement des
> > > messages à ce propos à l'occasion. Je ne connais en revanche
> > > pas cette histoire avec SUN.
> >
> > Je ne connais pas l'histoire de Sun qui cite Daniel, mais j'ai travaillé
> > sur des Sun pendant un certain temps et il est vrai que le compilateur
> > de Sun permettait tout et n'importe quoi sans aucun Warning. En plus sur
> > les machines 32 bits sizeof(int)==sizeof(long)==sizeof(void*) (ou
> > n'importe quel pointeur) et les tableaux sont tous parfaitement
> > interchangeables.
> > Le résultat c'est que j'ai vu de grosses cochonneries sur les programmes
> > écrits pour les systèmes Sun.
> 
> La consequence en est que de tres nombreux developeurs continuent a penser que 
> **argv est equivalent a *argv[]. La permessivite du compilateur de SUN (dans 
> le seul but de generer du code plus rapide, au detriment du bon-sens) a 
> engendre de fausses verites et propager une culture erronee de la generation 
> du code.

Que dit le manuel du C en propos?

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.

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

ciao, Leo

> Le mercredi 1 Avril 2008, Mar Mongenet a ecrit :
> 
> > > >  Alors pi != pc !!!
> > >
> > > Ce n'est pas une expression valide en C (pointeurs
> > > incompatibles)
> 
> Il ne s'agissait evidememnt pas d'une expresion C, simplement d'une mode 
> d'ecriture (puisque je l'ai precede du mot 'Alors').
> 
> > > mais au niveau machine, les pointeurs 
> > > sont effectivement différents du moment que nous
> > > avons sizeof(char)!=sizeof(int).
> > > Mais je ne vois pas bien le rapport mes exemples.
> 
> Je trouve que la reduction qui consiste a dire que l'on peut considerer les 
> pointeurs et les 'int' comme dangereux, en plus de ne pas toujours etre 
> vrais. Si des developeurs experimentes sont capables de faire la difference, 
> il est dangereux et coupable de presenter cela a des gens qui ne sont pas 
> tres familier du C (ou C++). Ils auront alors tendance a croire que... ah 
> oui, c'est la meme chose... et nous aurons des gens qui continueront a ecrire 
> du code pourri et justifierons les arguments de ceux qui pretendent que le C 
> est un langage trop permissif, dangereux et ne permettant pas d'ecrire des 
> codes sures. Il y a des tas de moyens d'ecrire du code tres sure en C mais il 
> faut faire preuve de rigueur. Par exemple, ecrire des macros qui permettent 
> de calculer les alignements etc. Tel que :
> 
> #define RoundN(_v_,_b_) (((unsigned int)(_v_) + _b_) & ~(_b_))
> #define Round16(_s_)    RoundN(_s_,0xf)
> #define Round8(_s_)     RoundN(_s_,0x7)
> ...
> #define Addr16(_a_)     (void *)Round16(_a_)
> ...
> #define CRSIZEOF(_v_)   (Round16(sizeof(_v_)))
> 
> Ainsi, je peux m'affranchir de l'aproxinmation de sizeof() sur les structure. 
> Et non... sizeof ne calcul pas le padding a la fin de la structure... !
> 
> > > Je suppose que tu veux dire qu'on ne peut pas déréférencer
> > > un void* ni faire de l'arithmétique dessus (sans utiliser des
> > > extensions non standards, par exemple de GCC) ? Les
> > > autres opérations restent possibles (assignations, tests,
> > > adresse de, taille de...) :
> > > void *p, **pp;
> > > p = 0;
> > > pp = &p;
> > > if (p || pp) {}
> 
> Inutile d'utiliser des "extension non-standards". Le standard me permet de 
> manipuler des pointeurs de tous types et d'effectuer des operations entre 
> eux. Seulement, effectuer une incremention (++) sur un pointeur de void n'est 
> pas la meme chose que sur un pointeur de double. Les calculs sur des 
> pointeurs doivent etre ramenes au 'type' du pointeur de l'architecture 
> (unsigned !).
> 
> 
> > > L'opérateur sizeof retourne le nombre de bytes de son opérande ;
> > > c'est sa raison d'être. Appliqué à une structure, il retourne la taille
> > > de la structure, incluant les éventuels padding à l'intérieur et à la
> > > fin de la structure.
> >
> > Ce n'est pas ce que j'ai constaté, j'ai même remarqué que:
> > struct toto *t;
> > int i = sizeof(*t);
> > int j = sizeof(struct toto);
> >
> > on a i >= j
> 
> Exact.
> 
> >
> > D'ou j'en ai déduit que sizeof(struct toto) retourne la taille de la
> > structure sans le padding tandis que sizeof(*t) retourne la taille
> > exacte utilisée par la structure pointée par t, donc la taille avec le
> > pad.
> 
> Absolument. La taille peut d'ailleur varie si l'on deplace certains elements a 
> l'interieur de la structure. Ceci est toujours dependant des architectures 
> (processeurs & OS). 
> 
> struct {
> 	char c;
> 	int i;
> 	} a;
> 
> En permutant les elemenst c et i un sizeof(a) ne donnera pas forcement le meme 
> resultat suivant les architectures. En effet, certains processeurs alignent 
> les 'int' sur 4 bytes, alors que d'autres pas. Si c'est le cas, il y aura 3 
> bytes de padding entre c et a, alors qu'il n'y en aura plus apres 
> permutation. C'est la disponibilite des modes d'adressage du processeur qui 
> determine ou non ces necessites d'alignement. 

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.

> > Un logiciel que j'avais écrit il y a quelques années plantait avec 
> > un beau core dump après une corruption de mémoire causée justement pas
> > cette différence, j'avais fait un malloc(sizeof(toto)) et ensuite je
> > remplissait la structure. J'ai résolu le problème en changeant en
> > malloc(sizeof(*t)).
> 
> Voir la macro ci-dessus :-)

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.

> > C'est la raison pour laquelle j'utilise la notation peu courante, le
> > cast m'assure que je ne peux pas remplacer le type du pointeur tandis
> > que le sizeof sur la variable m'assure de sa bonne taille.
> 
> Tres bien ! Le standard ANSI existe depuis pres de 20 ans mais bon nombre de 
> developpeurs ne semble pas avoir assimile les "raisons" qui ont pousse a la 
> definition de ce standard. Aussi, un petit coup d'oeil dans stdlib.h et les 
> standards POSIX & ISO apporte un cerian eclairage concernant les points 
> discutes.
> 
> 
> > > Le standard garantit que la fonction malloc retourne un pointeur
> > > correctement aligné, ou un pointeur nul en cas d'erreur.
> 
> Oui, correctement aligne... pour l'architecture concernee uniquement. D'ou ma 
> macro ci-dessus.
> 
> > > Le Committe Draft du standard C99 que j'ai sous les yeux
> > > donne les exemples suivants :
> > >
> > > A principal use of the sizeof operator is in communications with
> > > routines such as storage allocators and I/O systems. A storage-
> > > allocation function might accept a size (in bytes) of an object to
> > > allocate and return a pointer to void. For example:
> > >
> > > extern void *alloc(size_t);
> > > double *dp = alloc(sizeof *dp);
> > >
> > > The implementation of the alloc function should ensure that its
> > > return value is aligned suitably for conversion to a pointer to
> > > double.
> 
> Ce qui represent 2 * sizeof(int)... d'ou mes remarques.
> 
> > > >  > Si int et int* ont par chance la même taille, alors
> > > >  > on ne se rendra probablement compte de rien.
> 
> Tout simplement genial... j'adore utiliser ce genre de code... EN general, 
> apres le troisieme bus-error (ou memory-fault, segmentation-violation), cela 
> engendre la reaction suivante de ma part :
> 
> 	cd code-pourri
> 	rm -r *
> 	(ou apt-get remove...)
> 
> > Et en plus moi j'active tous les warnings avec -Wall et tous les -W qui
> > sont pertinents. Ceci m'assure d'avoir tous les warnings possibles et
> > donc de détecter tout ce pourrait causer des problèmes.
> > Le seul défaut de cette méthode c'est qu'on ne peut jamais avoir un code
> > qui compile sans warnings car il y a des warnings qui sont
> > complémentaires, c'est à dire que si on résout le premier on a le
> > deuxième et vice-versa. En plus il y a certains warnings qu'on ne peut
> > pas éviter à cause de bizarreries dans la librairie standard (p.e.
> > l'utilisation d'un void* à la place d'un const void* qui fait que dès
> > qu'on passe le paramètre à la stdlib on à un warning car elle ne le
> > considère pas comme const comme il devrait l'être, en fait l'utilisation
> > des const est très délicate).
> 
> J'ai toujours eu pour regle de ne developer que du code qui compile sans 
> warnings. Cela a tres bienonctionne tant que j'etais sur une machine 
> completement  ANSI/POSIX. Les choses se sont gatees avec les LL ou certaines 
> librairies ont des declaration farfelues, voir fausses. Bien sur, on peut 
> patcher les *.h, mais il faut alors le faire a chaque release. Je trouve 
> qu'il y a un grand manque de rigueur dans certains codes sur le net... et 
> cela pose de gros problemes. C'est souvent de la simple paresse ou de la 
> meconnaissance. 

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.
Donc les programmeurs sont rarement poussés à faire mieux.

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.

D'un point de vu économique le raisonnement est sans faille. D'un point
de vue technique c'est une aberration. Le LL à au moins l'avantage que
le code est continuellement revu et amélioré pour marcher sur le plus de
systèmes possible et donc le code est généralement meilleur. Et si ça ne
te plait pas tu peux toujours essayer de soumettre un patch.

> > Mais au moins ça permet d'avoir un bon contrôle sur ce que l'on fait.
> 
> :-)
> 
> dc

ciao, Leo





More information about the gull mailing list