Dans une récente révision du code , il a été affirmé que
Sur certains systèmes,
calloc()
peut allouer plus deSIZE_MAX
octets au total, alors quemalloc()
est limité.
Je prétends que c'est une erreur, car calloc()
crée de l'espace pour un tableau d'objets - qui, étant un tableau, est lui-même un objet. Et aucun objet ne peut avoir une taille plus grande que SIZE_MAX
.
Alors lequel de nous est correct? Sur un système (éventuellement hypothétique) avec un espace adresse supérieur à la plage de size_t
, calloc()
est-il autorisé à réussir lorsqu'il est appelé avec des arguments dont le produit est supérieur à SIZE_MAX
?
Pour le rendre plus concret: le programme suivant va-t-il jamais sortir avec un statut non nul?
#include <stdint.h>
#include <stdlib.h>
int main()
{
return calloc(SIZE_MAX, 2) != NULL;
}
SIZE_MAX
ne spécifie pas nécessairement la taille maximale d'un objet, mais plutôt la valeur maximale de size_t
, qui n'est pas nécessairement la même chose. Voir Pourquoi la taille maximale d'un tableau est-elle "trop grande"? ,
Mais évidemment, il n’est pas bien défini de transmettre une valeur supérieure à SIZE_MAX
à une fonction qui attend un paramètre size_t
. Donc, en théorie, SIZE_MAX
est la limite, et en théorie, calloc
autoriserait SIZE_MAX * SIZE_MAX
octets à allouer.
Le problème avec malloc
calloc
est qu’ils allouent des objets sans type. Les objets de type ont des restrictions, telles que ne jamais dépasser une certaine limite, telle que SIZE_MAX
. Mais les données pointées par le résultat de ces fonctions n'ont pas de type. Ce n'est pas (encore) un tableau.
Formellement, les données n'ont pas de type _/déclaré, mais lorsque vous stockez quelque chose dans les données allouées, il obtient le type effectif de l'accès aux données utilisé pour le stockage (C17 6.5 §6).
Ceci à son tour signifie qu'il serait possible pour calloc
d'allouer plus de mémoire que n'importe quel type en C peut en contenir, car ce qui est alloué n'a pas (encore) de type.
Par conséquent, en ce qui concerne le standard C, il est parfaitement correct que calloc(SIZE_MAX, 2)
renvoie une valeur différente de NULL. Comment utiliser réellement cette mémoire allouée de manière judicieuse, ou quels systèmes supportant même de tels blocs de mémoire sur le tas, est une autre histoire.
Calloc () peut-il allouer plus que SIZE_MAX au total?
L'affirmation "Sur certains systèmes, calloc()
peut allouer plus de SIZE_MAX
total d'octets alors que malloc()
est limité." provient d'un commentaire que j'ai posté, je vais expliquer mon raisonnement.
size_t
size_t
est du type non signé d'au moins 16 bits.
size_t
qui est le type entier non signé du résultat de l'opérateursizeof
; C11dr §7.19 2"Sa valeur définie pour l'implémentation doit être supérieure ou égale à la magnitude ... de la valeur correspondante indiquée ci-dessous" ... limite de
size_t
SIZE_MAX
... 65535 §7.20.3 2
taille de
L'opérateur
sizeof
donne la taille (en octets) de son opérande, qui peut être un expression ou le nom entre parenthèses d'un type. §6.5.3.4 2
calloc
void *calloc(size_t nmemb, size_t size);
La fonction
calloc
alloue de l'espace pour un tableau d'objetsnmemb
, chacun desize
étant de taille. §7.22.3.2 2
Prenons une situation où nmemb * size
dépasse bien SIZE_MAX
.
size_t alot = SIZE_MAX/2;
double *p = calloc(alot, sizeof *p); // assume `double` is 8 bytes.
Si calloc()
a réellement alloué nmemb * size
octets et si p != NULL
est vrai, quelle spécification cela at-il violé?
La taille de chaque élément (chaque objet) est représentable.
// Nicely reports the size of a pointer and an element.
printf("sizeof p:%zu, sizeof *p:%zu\n", sizeof p, sizeof *p);
Chaque élément est accessible.
// Nicely reports the value of an `element` and the address of the element
for (size_t i = 0; i<alot; i++) {
printf("value a[%zu]:%g, address:%p\n", i, p[i], (void*) &p[i]);
}
calloc()
details
"espace pour un tableau d'objets nmemb
": il s'agit certainement d'un point de discorde clé. Est-ce que l'option "alloue de l'espace pour le tableau" requiert <= SIZE_MAX
? Je n'ai rien trouvé dans la spécification C d'exiger cette limite et conclu donc:
calloc()
peut allouer plus deSIZE_MAX
au total.
Il est certainement inhabituel} pour calloc()
avec de grands arguments pour renvoyer non -NULL
- conforme ou non. Habituellement, ces allocations dépassent la mémoire disponible, le problème est donc sans objet. Le seul cas que j'ai rencontré concernait le modèle de mémoire Huge , où size_t
était de 16 bits et le pointeur de l'objet, de 32 bits.
Juste un ajout: avec un peu de math, vous pouvez montrer que SIZE_MAX * SIZE_MAX = 1 (lorsqu’il est évalué selon les règles C).
Toutefois, calloc (SIZE_MAX, SIZE_MAX) n'est autorisé qu'à effectuer l'une des deux opérations suivantes: Renvoyer un pointeur sur un tableau d'éléments SIZE_MAX d'octets SIZE_MAX, OR renvoie NULL. Il n'est PAS permis de calculer la taille totale simplement en multipliant les arguments, en obtenant un résultat de 1 et en allouant un octet, effacé à 0.
Si un programme dépasse les limites d'implémentation, le comportement n'est pas défini. Ceci découle de la définition d'une limite de mise en œuvre comme une restriction imposée aux programmes par la mise en œuvre (3.13 en C11). La norme indique également que les programmes strictement conformes doivent respecter les limites d’implémentation (4p5 en C11). Mais cela implique également les programmes en général, car la norme ne dit pas ce qui se passe lorsque la plupart des limites d’implémentation sont dépassées (c’est donc l’autre type de comportement indéfini, dans lequel la norme ne spécifie pas ce qui se passe).
La norme ne définit pas non plus les limites d’implémentation pouvant exister, donc ceci un peu de carte blanche, mais je pense qu’il est raisonnable que la taille maximale de l’objet soit réellement pertinente pour les allocations d’objets. (Soit dit en passant, la taille maximale de l'objet est inférieure à celle de SIZE_MAX
, car la différence entre les pointeurs et -char
au sein de l'objet doit pouvoir être représentée dans ptrdiff_t
.)
Cela nous amène à l'observation suivante: un appel à calloc (SIZE_MAX, 2)
dépasse la taille maximale de l'objet; par conséquent, une implémentation peut renvoyer une valeur arbitraire tout en restant conforme à la norme.
Certaines implémentations renverront en fait un pointeur non nul pour un appel tel que calloc (SIZE_MAX / 2 + 2, 2)
car l'implémentation ne vérifie pas que le résultat de la multiplication ne correspond pas à une valeur size_t
. La question de savoir s'il s'agit d'une bonne idée est une autre affaire, étant donné que la limite d'implémentation peut être vérifiée aussi facilement dans ce cas, et qu'il existe un moyen parfaitement simple de signaler les erreurs. Personnellement, je considère le manque de contrôle de débordement dans calloc
comme un bogue d’implémentation, et j’ai signalé les bogues aux développeurs quand je les ai vus, mais techniquement, c’est simplement un problème de qualité d’implémentation.
Pour les tableaux de longueur variable sur la pile, la règle concernant le dépassement des limites d'implémentation entraînant un comportement indéfini est plus évidente:
size_t length = SIZE_MAX / 2 + 2;
short object[length];
Il n'y a vraiment rien qu'une implémentation puisse faire ici, donc ça doit être indéfini.
Selon le texte de la norme, peut-être, parce que la norme est (certains diraient intentionnellement) vague sur ce genre de chose.
Selon 6.5.3.4 ¶2:
L'opérateur
sizeof
donne la taille (en octets) de son opérande
et selon 7.19 ¶2:
taille_t
qui est le type entier non signé du résultat de l'opérateur
sizeof
;
Le premier ne peut pas être satisfait en général si l'implémentation admet n'importe quel type (y compris les types de tableaux) dont la taille n'est pas représentable dans size_t
. Notez que, que vous interprétiez ou non le texte relatif au pointeur renvoyé par calloc
pointant vers "un tableau", il existe toujours un tableau impliqué dans tout objet: le tableau superposé de type unsigned char[sizeof object]
qui correspond à representation.
Au mieux, une implémentation qui permet la création de tout objet plus grand que SIZE_MAX
(ou PTRDIFF_MAX
, pour d'autres raisons) pose des problèmes de qualité de travail irréprochable. L'affirmation selon laquelle vous devez comptabiliser de telles implémentations sur la révision de code est fausse, à moins que vous ne cherchiez spécifiquement à assurer la compatibilité avec une implémentation C cassée particulière (parfois pertinente pour Embedded, etc.).
De
7.22.3.2 La fonction calloc
Synopsis
1#include <stdlib.h> void *calloc(size_t nmemb, size_t size);`
La description
2 La fonction calloc alloue de l'espace pour un tableau d'objets nmemb dont la taille correspond à la taille. L'espace est initialisé à tous les bits zéro.Résultats
3 La fonction calloc renvoie un pointeur null ou un pointeur vers l'espace alloué.
Je ne vois pas pourquoi l'espace alloué devrait être limité à SIZE_MAX
octets.
La norme ne dit rien sur la possibilité de créer un pointeur de telle sorte que ptr+number1+number2
puisse être un pointeur valide, mais number1+number2
dépasserait SIZE_MAX
. Cela permet certainement la possibilité de number1+number2
dépasser PTRDIFF_MAX
(bien que pour une raison quelconque, C11 ait décidé d’exiger que même les implémentations avec un espace d’adresse 16 bits utilisent un ptrdiff_t
32 bits).
La norme ne stipule pas que les implémentations fournissent un moyen de créer des pointeurs sur des objets aussi volumineux. Cependant, il définit une fonction, calloc()
, dont la description suggère qu'il pourrait être demandé de tenter de créer un tel objet, et suggérerait que calloc()
renvoie un pointeur nul s'il ne peut pas créer l'objet.
La possibilité d’attribuer utilement tout type d’objet est toutefois un problème de qualité d’implémentation. La norme n’imposerait jamais le succès d’une demande d’allocation particulière, ni interdirait à une implémentation de renvoyer un pointeur qui pourrait se révéler inutilisable (dans certains environnements Linux, un malloc () pourrait renvoyer un pointeur vers une région de espace adresse; une tentative d'utilisation du pointeur lorsque la mémoire physique disponible est insuffisante peut provoquer un piège fatal). Il serait certainement préférable pour une implémentation non capricieuse de calloc(x,y)
de renvoyer null si le produit numérique x
et y
dépasse SIZE_MAX plutôt que de générer un pointeur qui ne peut pas être utilisé pour accéder à ce nombre d'octets. Toutefois, la norme ne précise pas si le renvoi d'un pointeur pouvant être utilisé pour accéder à des objets y
de x
octets doit être considéré comme meilleur ou pire que le renvoi de null
. Chaque comportement serait avantageux dans certaines situations et défavorable dans d'autres.