C: Pourquoi les pointeurs non affectés pointent vers une mémoire imprévisible et ne pointent pas sur NULL?

Il y a longtemps, je programmais en C pour l’école. Je me souviens de quelque chose que je détestais vraiment à propos de C: les pointeurs non assignés ne désignent pas NULL.

J’ai demandé à beaucoup de gens, y compris aux enseignants, pourquoi, dans le monde, ils feraient en sorte que le comportement par défaut d’un pointeur non assigné ne pointe pas vers NULL, car il semble beaucoup plus dangereux d’être imprévisible.

La réponse était censément la performance mais je n’ai jamais acheté ça. Je pense que beaucoup de bogues dans l’histoire de la programmation auraient pu être évités si C avait été défini par défaut sur NULL.

Voici un code C pour indiquer (jeu de mots) ce dont je parle:

#include  void main() { int * randomA; int * randomB; int * nullA = NULL; int * nullB = NULL; printf("randomA: %p, randomB: %p, nullA: %p, nullB: %p\n\n", randomA, randomB, nullA, nullB); } 

Qui comstack avec des avertissements (c’est bien de voir que les compilateurs C sont beaucoup plus agréables que quand j’étais à l’école) et les sorties:

randomA: 0xb779eff4, randomB: 0x804844b, nullA: (nul), nullB: (nul)

En fait, cela dépend du stockage du pointeur. Les pointeurs avec stockage statique sont initialisés avec des pointeurs nuls. Les pointeurs avec une durée de stockage automatique ne sont pas initialisés. Voir ISO C 99 6.7.8.10:

Si un object qui a une durée de stockage automatique n’est pas initialisé explicitement, sa valeur est indéterminée. Si un object qui a une durée de stockage statique n’est pas initialisé explicitement, alors:

  • s’il a un type de pointeur, il est initialisé à un pointeur nul;
  • s’il a un type arithmétique, il est initialisé à (positif ou non signé) zéro;
  • s’il s’agit d’un agrégat, chaque membre est initialisé (récursivement) selon ces règles;
  • s’il s’agit d’une union, le premier membre nommé est initialisé (récursivement) conformément à ces règles.

Et oui, les objects avec une durée de stockage automatique ne sont pas initialisés pour des raisons de performances. Imaginez juste d’initialiser un tableau 4K à chaque appel à une fonction de journalisation (quelque chose que j’ai vu sur un projet sur lequel j’ai travaillé, heureusement que C m’a permis d’éviter l’initialisation, ce qui donne un bon coup de pouce).

Parce qu’en C, la déclaration et l’initialisation sont délibérément différentes . Ils sont délibérément différents, car c’est ainsi que C est conçu.

Lorsque vous dites cela dans une fonction:

 void demo(void) { int *param; ... } 

Vous dites, “mon cher compilateur C, quand vous créez le frame de stack pour cette fonction, rappelez-vous s’il vous plaît de réserver sizeof(int*) octets pour stocker un pointeur.” Le compilateur ne demande pas ce qui se passe – cela suppose que vous allez le dire rapidement. Si vous ne le faites pas, il y a peut-être une meilleure langue pour vous;)

Peut-être qu’il ne serait pas difficile de générer du code de nettoyage de stack sécurisé. Mais il faudrait appeler sur chaque invocation de fonction, et je doute que beaucoup de développeurs de C apprécieraient le coup quand ils vont simplement le remplir eux-mêmes. Incidemment, il y a beaucoup à faire en matière de performance si vous êtes autorisé à faire preuve de souplesse avec la stack. Par exemple, le compilateur peut faire l’optimisation où …

Si votre function1 appelle une autre function2 et stocke sa valeur de retour, ou peut-être que certains parameters sont passés à function2 qui ne sont pas modifiés dans function2 … nous n’avons pas besoin de créer d’espace supplémentaire, n’est-ce pas? Il suffit d’utiliser la même partie de la stack pour les deux! Notez que ceci est en conflit direct avec le concept d’initialisation de la stack avant chaque utilisation.

Mais dans un sens plus large (et à mon avis, plus important encore), cela correspond à la philosophie de C de ne pas faire beaucoup plus que ce qui est absolument nécessaire. Et cela s’applique que vous travailliez sur un PDP11, un PIC32MX (ce que je l’utilise) ou un Cray XT3. C’est exactement pourquoi les gens peuvent choisir d’utiliser C plutôt que d’autres langues.

  • Si je veux écrire un programme sans trace de malloc et free , je n’y suis pas obligé! Aucune gestion de la mémoire n’est imposée à moi!
  • Si je veux empaqueter et taper une union de données, je le peux! (Tant que je lis les notes de mon implémentation sur l’adhésion standard, bien sûr.)
  • Si je sais exactement ce que je fais avec mon frame de stack, le compilateur n’a rien d’autre à faire pour moi!

En bref, lorsque vous demandez au compilateur C de sauter, il ne vous demande pas quelle est la hauteur. Le code résultant ne reviendra probablement même plus.

Étant donné que la plupart des gens qui choisissent de se développer en langage C de cette manière, cela a suffisamment d’inertie pour ne pas changer. Votre façon de faire n’est peut-être pas une mauvaise idée en soi, elle n’est tout simplement pas vraiment demandée par de nombreux développeurs C.

C’est pour la performance.

C a été développé pour la première fois à l’époque du PDP 11, pour lequel 60 k était une quantité maximale de mémoire commune, beaucoup en auraient eu beaucoup moins. Les affectations inutiles seraient particulièrement coûteuses dans ce type d’environnement

De nos jours, il existe de nombreux appareils embarqués utilisant C pour lesquels 60k de mémoire sembleraient infinis, le PIC 12F675 a 1k de mémoire.

En effet, lorsque vous déclarez un pointeur, votre compilateur C réserve l’espace nécessaire pour le placer. Ainsi, lorsque vous exécutez votre programme, cet espace même peut déjà avoir une valeur, résultant probablement d’une précédente donnée allouée sur cette partie de la mémoire.

Le compilateur C pourrait atsortingbuer une valeur à ce pointeur, mais dans la plupart des cas, cela serait une perte de temps puisque vous êtes autorisé à atsortingbuer vous-même une valeur personnalisée à une partie du code.

C’est pourquoi les bons compilateurs vous avertissent lorsque vous n’initialisez pas vos variables. donc je ne pense pas qu’il y ait tant de bugs à cause de ce comportement. Il suffit de lire les avertissements.

Les pointeurs ne sont pas spéciaux à cet égard; les autres types de variables ont exactement le même problème si vous les utilisez non initialisées:

 int a; double b; printf("%d, %f\n", a, b); 

La raison en est simple: demander à l’environnement d’exécution de définir des valeurs non initialisées sur une valeur connue ajoute un surcoût à chaque appel de fonction. La surcharge peut ne pas être beaucoup avec une seule valeur, mais considérez si vous avez un grand nombre de pointeurs:

 int *a[20000]; 

Lorsque vous déclarez une variable (de pointeur) au début de la fonction, le compilateur effectuera l’une des deux opérations suivantes: définissez un registre à utiliser comme variable ou affectez-lui de l’espace sur la stack. Pour la plupart des processeurs, l’allocation de la mémoire pour toutes les variables locales de la stack se fait avec une seule instruction. il calcule combien de mémoire tous les vars locaux auront besoin, et arrête (ou pousse, sur certains processeurs) le pointeur de stack de ce sharepoint vue. Tout ce qui existe déjà dans cette mémoire n’est pas modifié à moins que vous ne le changiez explicitement.

Le pointeur n’est pas “défini” sur une valeur “aléatoire”. Avant l’allocation, la mémoire de la stack située sous le pointeur de la stack (SP) contient tout ce qu’il ya d’usage antérieur:

  . . SP ---> 45 ff 04 f9 44 23 01 40 . . . 

Après avoir alloué de la mémoire pour un pointeur local, la seule chose qui a changé est le pointeur de stack:

  . . 45 ff | 04 | allocated memory for pointer. f9 | SP ---> 44 | 23 01 40 . . . 

Cela permet au compilateur d’allouer tous les vars locaux dans une instruction qui déplace le pointeur de la stack le long de la stack (et les libère tous en une seule instruction, en déplaçant le pointeur de stack), mais vous oblige à les initialiser vous-même fais ça.

En C99, vous pouvez combiner du code et des déclarations afin de pouvoir reporter votre déclaration dans le code jusqu’à ce que vous puissiez l’initialiser. Cela vous permettra d’éviter d’avoir à définir NULL.

Tout d’abord, l’initialisation forcée ne corrige pas les bogues. Il les masque. Utiliser une variable qui n’a pas de valeur valide (et ce qui varie selon l’application) est un bogue.

Deuxièmement, vous pouvez souvent faire votre propre initialisation. Au lieu de int *p; , écrivez int *p = NULL; ou int *p = 0; . Utilisez calloc() (qui initialise la mémoire à zéro) plutôt que malloc() (qui ne le fait pas). (Non, tous les bits nuls ne signifient pas nécessairement des pointeurs NULL ou des valeurs à virgule flottante égales à zéro. Oui, c’est le cas sur la plupart des implémentations modernes.)

Troisièmement, la philosophie C (et C ++) consiste à vous donner les moyens de faire quelque chose rapidement. Supposons que vous ayez le choix d’implémenter, dans le langage, un moyen sûr de faire quelque chose et un moyen rapide de faire quelque chose. Vous ne pouvez pas gagner en rapidité en ajoutant plus de code autour de cela, mais vous pouvez le faire plus rapidement. De plus, vous pouvez parfois rendre les opérations rapides et sûres, en vous assurant que l’opération sera sécurisée sans vérification supplémentaire, en supposant bien sûr que vous ayez l’option rapide pour commencer.

C a été conçu à l’origine pour écrire un système d’exploitation et le code associé, et certaines parties des systèmes d’exploitation doivent être aussi rapides que possible. Ceci est possible en C, mais moins dans des langues plus sûres. De plus, C a été développé lorsque les plus gros ordinateurs étaient moins puissants que le téléphone dans ma poche (que je mets à jour rapidement car il se sent vieux et lent). La sauvegarde de quelques cycles de machine dans un code fréquemment utilisé peut avoir des résultats visibles.

Donc, pour résumer ce que ninjalj a expliqué, si vous modifiez légèrement votre exemple de programme, les pointeurs s’initialiseront en NULL:

 #include  // Change the "storage" of the pointer-variables from "stack" to "bss" int * randomA; int * randomB; void main() { int * nullA = NULL; int * nullB = NULL; printf("randomA: %p, randomB: %p, nullA: %p, nullB: %p\n\n", randomA, randomB, nullA, nullB); } 

Sur ma machine cela imprime

randomA: 00000000, randomB: 00000000, nullA: 00000000, nullB: 00000000

Je pense que cela vient de ce qui suit: il n’y a aucune raison pour que les mémoires contiennent (lorsqu’elles sont sous tension) des valeurs spécifiques (0, NULL ou autre). Donc, si ce n’est pas écrit précédemment, un emplacement de mémoire peut contenir n’importe quelle valeur, de votre sharepoint vue, de toute façon aléatoire (mais cet emplacement aurait pu être utilisé auparavant par un autre logiciel, et contiendrait donc une valeur significative pour cette application, par exemple un compteur, mais de “votre” sharepoint vue, n’est qu’un nombre aléatoire. Pour l’initialiser à une valeur spécifique, vous avez besoin d’au moins une instruction de plus; mais il y a des situations où vous n’avez pas besoin de cette initialisation a priori , par exemple v = malloc(x) affectera une adresse valide ou NULL, quel que soit le contenu initial de v. Ainsi, l’initialisation pourrait être considérée comme une perte de temps , et une langue (comme C) peut choisir de ne pas le faire a priori . Bien sûr, cela est aujourd’hui essentiellement insignifiant et il existe des langages dans lesquels les variables non initialisées ont des valeurs par défaut (null pour les pointeurs, quand supporté; 0 / 0.0 pour le numérique, etc. un tableau de 1 million d’éléments, par exemple, car ils ne sont initialisés pour le réel que s’ils sont accessibles avant une affectation).

L’idée que cela a quelque chose à voir avec le contenu de la mémoire aléatoire quand une machine est mise sous tension est fausse, sauf sur les systèmes embarqués. Toute machine avec mémoire virtuelle et système d’exploitation multiprocessus / multi-utilisateurs initialisera la mémoire (généralement à 0) avant de la donner à un processus. Ne pas le faire constituerait une violation majeure de la sécurité. Les valeurs «aléatoires» dans les variables de stockage automatique proviennent de l’utilisation antérieure de la stack par le même processus. De même, les valeurs «aléatoires» en mémoire sont retournées par malloc / new / etc. proviennent des allocations précédentes (qui ont été libérées par la suite) dans le même processus.

Pour qu’il pointe vers NULL, il faudrait lui assigner NULL (même si cela a été fait automatiquement et de manière transparente).

Donc, pour répondre à votre question, la raison pour laquelle un pointeur ne peut pas être à la fois non atsortingbué et NULL est qu’un pointeur ne peut pas être affecté ni affecté simultanément.