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

Daniel Cordey dc at mjt.ch
Wed Apr 2 13:35:21 CEST 2008


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 !

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 ! 

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

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


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. 


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

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

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

:-)

dc



More information about the gull mailing list