[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