Qu'arrive-t-il à une variable déclarée non initialisée en C? Cela a-t-il une valeur?


139

Si dans CI, écrivez:

int num;

Avant d'assigner quoi que ce soit à num, la valeur est-elle numindéterminée?


4
Euh, n'est-ce pas une variable définie , pas une variable déclarée ? (Je suis désolé si c'est mon C ++ qui brille à travers ...)
sbi

6
Non. Je peux déclarer une variable sans la définir: extern int x;cependant définir implique toujours de déclarer. Ce n'est pas vrai en C ++, avec des variables de membre de classe statiques que l'on peut définir sans déclarer, car la déclaration doit être dans la définition de classe (pas déclaration!) Et la définition doit être en dehors de la définition de classe.
bdonlan

ee.hawaii.edu/~tep/EE160/Book/chap14/subsection2.1.1.4.html On dirait que défini signifie que vous devez également l'initialiser.
atp

Réponses:


188

Les variables statiques (portée du fichier et fonction statique) sont initialisées à zéro:

int x; // zero
int y = 0; // also zero

void foo() {
    static int x; // also zero
}

Les variables non statiques (variables locales) sont indéterminées . Les lire avant d'attribuer une valeur entraîne un comportement indéfini.

void foo() {
    int x;
    printf("%d", x); // the compiler is free to crash here
}

En pratique, ils ont tendance à avoir juste une valeur absurde au départ - certains compilateurs peuvent même mettre des valeurs spécifiques et fixes pour le rendre évident lors de la recherche dans un débogueur - mais à proprement parler, le compilateur est libre de faire quoi que ce soit, du plantage à l'invocation. démons à travers vos voies nasales .

Quant à savoir pourquoi il s'agit d'un comportement non défini au lieu de simplement "valeur non définie / arbitraire", il existe un certain nombre d'architectures de processeur qui ont des bits d'indicateur supplémentaires dans leur représentation pour différents types. Un exemple moderne serait l'Itanium, qui a un bit "Not a Thing" dans ses registres ; bien sûr, les rédacteurs de la norme C envisageaient certaines architectures plus anciennes.

Tenter de travailler avec une valeur avec ces bits d'indicateur définis peut entraîner une exception CPU dans une opération qui ne devrait vraiment pas échouer (par exemple, ajout d'entiers ou affectation à une autre variable). Et si vous laissez une variable non initialisée, le compilateur peut ramasser des déchets aléatoires avec ces bits d'indicateur définis - ce qui signifie que toucher cette variable non initialisée peut être mortel.


2
oh non ils ne le sont pas. Ils pourraient être, en mode débogage, lorsque vous n'êtes pas devant un client, les mois avec un R, si vous avez de la chance
Martin Beckett

8
que ne sont pas? l'initialisation statique est requise par la norme; voir ISO / CEI 9899: 1999 6.7.8 # 10
bdonlan

2
le premier exemple est bien pour autant que je sache. Je ne comprends pas pourquoi le compilateur pourrait planter dans le second :)

6
@Stuart: il y a une chose appelée "représentation d'interruption", qui est essentiellement un modèle de bits qui ne dénote pas une valeur valide, et peut provoquer par exemple des exceptions matérielles lors de l'exécution. Le seul type C pour lequel il existe une garantie que tout motif de bits est une valeur valide est char; tous les autres peuvent avoir des représentations de piège. Alternativement - puisque l'accès à une variable non initialisée est de toute façon UB - un compilateur conforme pourrait simplement faire quelques vérifications et décider de signaler le problème.
Pavel Minaev

5
bdonian est correct. C a toujours été spécifié assez précisément. Avant C89 et C99, un article de dmr spécifiait toutes ces choses au début des années 1970. Même dans le système embarqué le plus grossier, il suffit d'un seul memset () pour faire les choses correctement, il n'y a donc aucune excuse pour un environnement non conforme. J'ai cité la norme dans ma réponse.
DigitalRoss

57

0 si statique ou global, indéterminé si la classe de stockage est automatique

C a toujours été très précis sur les valeurs initiales des objets. Si global ou static, ils seront mis à zéro. Si auto, la valeur est indéterminée .

C'était le cas dans les compilateurs pré-C89 et cela a été spécifié par K&R et dans le rapport C original de DMR.

C'était le cas en C89, voir section 6.5.7 Initialisation .

Si un objet qui a une durée de stockage automatique n'est pas initialisé explicitement, sa valeur est indéterminée. Si un objet qui a une durée de stockage statique n'est pas initialisé explicitement, il est initialisé implicitement comme si chaque membre qui a un type arithmétique était assigné 0 et chaque membre qui a un type pointeur a reçu une constante de pointeur null.

C'était le cas en C99, voir section 6.7.8 Initialisation .

Si un objet qui a une durée de stockage automatique n'est pas initialisé explicitement, sa valeur est indéterminée. Si un objet qui a une durée de stockage statique n'est pas initialisé explicitement, alors:
- s'il a un type pointeur, il est initialisé à un pointeur nul;
- s'il est de type arithmétique, il est initialisé à zéro (positif ou non signé);
- 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) selon ces règles.

Quant à ce que signifie exactement indéterminé , je ne suis pas sûr pour C89, C99 dit:

3.17.2
valeur indéterminée

soit une valeur non spécifiée, soit une représentation d' interruption

Mais indépendamment de ce que disent les normes, dans la vraie vie, chaque page de pile commence en fait à zéro, mais lorsque votre programme regarde autoles valeurs de classe de stockage, il voit ce qui a été laissé par votre propre programme lors de sa dernière utilisation de ces adresses de pile. Si vous allouez beaucoup de autotableaux, vous les verrez finalement commencer proprement par des zéros.

Vous vous demandez peut-être pourquoi est-ce ainsi? Une autre réponse SO traite cette question, voir: https://stackoverflow.com/a/2091505/140740


3
indéterminé généralement (utilisé?) signifie qu'il peut tout faire. Cela peut être zéro, cela peut être la valeur qui s'y trouvait, cela peut faire planter le programme, cela peut faire en sorte que l'ordinateur produise des crêpes aux myrtilles à partir de la fente de CD. vous n'avez absolument aucune garantie. Cela pourrait causer la destruction de la planète. Au moins en ce qui concerne les spécifications ... quiconque a créé un compilateur qui fait quelque chose comme ça serait très mal vu B-)
Brian Postow

Dans le projet C11 N1570, la définition de indeterminate valuepeut être trouvée à 3.19.2.
user3528438

Est-ce que cela dépend toujours du compilateur ou du système d'exploitation de la valeur qu'il définit pour la variable statique? Par exemple, si quelqu'un écrit un système d'exploitation ou un compilateur de mon cru, et s'il définit également la valeur initiale par défaut pour la statique comme indéterminée, est-ce possible?
Aditya Singh

1
@AdityaSingh, le système d'exploitation peut faciliter la tâche du compilateur, mais en fin de compte, c'est la responsabilité principale du compilateur d'exécuter le catalogue de code C existant dans le monde, et une responsabilité secondaire de respecter les normes. Il serait certainement possible de le faire différemment, mais pourquoi? De plus, il est difficile de rendre les données statiques indéterminées, car le système d'exploitation voudra vraiment mettre à zéro les pages en premier pour des raisons de sécurité. (Les variables automatiques ne sont que superficiellement imprévisibles car votre propre programme a généralement utilisé ces adresses de pile à un moment antérieur.)
DigitalRoss

@BrianPostow Non, ce n'est pas correct. Voir stackoverflow.com/a/40674888/584518 . L'utilisation d'une valeur indéterminée entraîne un comportement non spécifié , et non un comportement indéfini, sauf pour le cas des représentations d'interruption.
Lundin le

12

Cela dépend de la durée de stockage de la variable. Une variable avec une durée de stockage statique est toujours initialisée implicitement avec zéro.

Comme pour les variables automatiques (locales), une variable non initialisée a une valeur indéterminée . Une valeur indéterminée, entre autres, signifie que quelle que soit la «valeur» que vous pourriez «voir» dans cette variable, elle n'est pas seulement imprévisible, elle n'est même pas garantie d'être stable . Par exemple, en pratique (c'est-à-dire en ignorant l'UB pendant une seconde) ce code

int num;
int a = num;
int b = num;

ne garantit pas que les variables aet brecevront des valeurs identiques. Fait intéressant, ce n'est pas un concept théorique pédant, cela se produit facilement dans la pratique en raison de l'optimisation.

Donc, en général, la réponse populaire selon laquelle "il est initialisé avec tout ce qui était en mémoire" n'est même pas correcte à distance. Uninitialized le comportement variable est différente de celle d'une variable initialisée avec les ordures.


Je ne comprends pas (bien que je très bien peux ) pourquoi cela a beaucoup moins upvotes que celui de DigitalRoss une minute après: D
Antti Haapala

7

Exemple Ubuntu 15.10, Kernel 4.2.0, x86-64, GCC 5.2.1

Assez de normes, regardons une implémentation :-)

Variable locale

Normes: comportement indéfini.

Implémentation: le programme alloue de l'espace de pile, et ne déplace jamais rien vers cette adresse, donc tout ce qui s'y trouvait auparavant est utilisé.

#include <stdio.h>
int main() {
    int i;
    printf("%d\n", i);
}

compiler avec:

gcc -O0 -std=c99 a.c

les sorties:

0

et décompile avec:

objdump -dr a.out

à:

0000000000400536 <main>:
  400536:       55                      push   %rbp
  400537:       48 89 e5                mov    %rsp,%rbp
  40053a:       48 83 ec 10             sub    $0x10,%rsp
  40053e:       8b 45 fc                mov    -0x4(%rbp),%eax
  400541:       89 c6                   mov    %eax,%esi
  400543:       bf e4 05 40 00          mov    $0x4005e4,%edi
  400548:       b8 00 00 00 00          mov    $0x0,%eax
  40054d:       e8 be fe ff ff          callq  400410 <printf@plt>
  400552:       b8 00 00 00 00          mov    $0x0,%eax
  400557:       c9                      leaveq
  400558:       c3                      retq

D'après notre connaissance des conventions d'appel x86-64:

  • %rdiest le premier argument printf, donc la chaîne "%d\n"à l'adresse0x4005e4

  • %rsiest donc le deuxième argument printf i.

    Il provient de -0x4(%rbp), qui est la première variable locale de 4 octets.

    À ce stade, rbpest dans la première page de la pile a été allouée par le noyau, donc pour comprendre cette valeur, nous devrions examiner le code du noyau et découvrir ce qu'il définit.

    TODO est-ce que le noyau définit cette mémoire sur quelque chose avant de la réutiliser pour d'autres processus lorsqu'un processus meurt? Sinon, le nouveau processus serait capable de lire la mémoire d'autres programmes terminés, ce qui fuirait des données. Voir: Les valeurs non initialisées représentent-elles un risque de sécurité?

Nous pouvons alors également jouer avec nos propres modifications de pile et écrire des choses amusantes comme:

#include <assert.h>

int f() {
    int i = 13;
    return i;
}

int g() {
    int i;
    return i;
}

int main() {
    f();
    assert(g() == 13);
}

Variable locale dans -O3

Analyse d'implémentation à: Que signifie <value optimized out> dans gdb?

Variables globales

Normes: 0

Mise en œuvre: .bsssection.

#include <stdio.h>
int i;
int main() {
    printf("%d\n", i);
}

gcc -00 -std=c99 a.c

compile en:

0000000000400536 <main>:
  400536:       55                      push   %rbp
  400537:       48 89 e5                mov    %rsp,%rbp
  40053a:       8b 05 04 0b 20 00       mov    0x200b04(%rip),%eax        # 601044 <i>
  400540:       89 c6                   mov    %eax,%esi
  400542:       bf e4 05 40 00          mov    $0x4005e4,%edi
  400547:       b8 00 00 00 00          mov    $0x0,%eax
  40054c:       e8 bf fe ff ff          callq  400410 <printf@plt>
  400551:       b8 00 00 00 00          mov    $0x0,%eax
  400556:       5d                      pop    %rbp
  400557:       c3                      retq
  400558:       0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
  40055f:       00

# 601044 <i>dit que ic'est à l'adresse 0x601044et:

readelf -SW a.out

contient:

[25] .bss              NOBITS          0000000000601040 001040 000008 00  WA  0   0  4

qui dit 0x601044est en plein milieu de la .bsssection, qui commence à 0x601040et fait 8 octets de long.

La norme ELF garantit alors que la section nommée .bssest complètement remplie de zéros:

.bssCette section contient des données non initialisées qui contribuent à l'image mémoire du programme. Par définition, le système initialise les données avec des zéros lorsque le programme commence à s'exécuter. La section des tartes oc- pas d' espace de fichier, comme indiqué par le type de section, SHT_NOBITS.

De plus, le type SHT_NOBITSest efficace et n'occupe aucun espace sur le fichier exécutable:

sh_sizeCe membre donne la taille de la section en octets. Sauf si le type de SHT_NOBITSsection est , la section occupe des sh_size octets dans le fichier. Une section de type SHT_NOBITSpeut avoir une taille différente de zéro, mais elle n'occupe aucun espace dans le fichier.

Ensuite, c'est au noyau Linux de remettre à zéro cette région mémoire lors du chargement du programme en mémoire au démarrage.


4

Ça dépend. Si cette définition est globale (en dehors de toute fonction), elle numsera initialisée à zéro. S'il est local (à l'intérieur d'une fonction), sa valeur est indéterminée. En théorie, même essayer de lire la valeur a un comportement indéfini - C permet la possibilité de bits qui ne contribuent pas à la valeur, mais doivent être définis de manière spécifique pour que vous puissiez même obtenir des résultats définis en lisant la variable.


1

Étant donné que les ordinateurs ont une capacité de stockage limitée, les variables automatiques seront généralement conservées dans des éléments de stockage (qu'il s'agisse de registres ou de RAM) qui ont été précédemment utilisés à d'autres fins arbitraires. Si une telle variable est utilisée avant qu'une valeur ne lui ait été assignée, ce stockage peut contenir tout ce qu'il détenait auparavant, et ainsi le contenu de la variable sera imprévisible.

De plus, de nombreux compilateurs peuvent conserver des variables dans des registres qui sont plus grands que les types associés. Bien qu'un compilateur soit nécessaire pour s'assurer que toute valeur qui est écrite dans une variable et relue sera tronquée et / ou étendue par signe à sa taille appropriée, de nombreux compilateurs effectueront une telle troncature lorsque les variables sont écrites et s'attendent à ce qu'elle ait a été effectuée avant la lecture de la variable. Sur de tels compilateurs, quelque chose comme:

uint16_t hey(uint32_t x, uint32_t mode)
{ uint16_t q; 
  if (mode==1) q=2; 
  if (mode==3) q=4; 
  return q; }

 uint32_t wow(uint32_t mode) {
   return hey(1234567, mode);
 }

pourrait très bien avoir pour résultat de wow()stocker les valeurs 1234567 dans les registres 0 et 1, respectivement, et d'appeler foo(). Comme il xn'est pas nécessaire dans "foo", et comme les fonctions sont censées mettre leur valeur de retour dans le registre 0, le compilateur peut allouer le registre 0 à q. Si modeest 1 ou 3, le registre 0 sera chargé avec 2 ou 4, respectivement, mais s'il s'agit d'une autre valeur, la fonction peut renvoyer ce qui était dans le registre 0 (c'est-à-dire la valeur 1234567) même si cette valeur n'est pas dans la plage de uint16_t.

Pour éviter d'exiger des compilateurs qu'ils fassent un travail supplémentaire pour s'assurer que les variables non initialisées ne semblent jamais contenir de valeurs en dehors de leur domaine, et pour éviter d'avoir à spécifier des comportements indéterminés de manière trop détaillée, la norme indique que l'utilisation de variables automatiques non initialisées est un comportement non défini. Dans certains cas, les conséquences peuvent être encore plus surprenantes qu'une valeur se situant en dehors de la plage de son type. Par exemple, étant donné:

void moo(int mode)
{
  if (mode < 5)
    launch_nukes();
  hey(0, mode);      
}

un compilateur pourrait en déduire que parce que l'invocation moo()avec un mode supérieur à 3 conduira inévitablement le programme à invoquer un comportement indéfini, le compilateur peut omettre tout code qui ne serait pertinent que s'il modeest supérieur ou égal à 4, tel que le code qui empêcherait normalement le lancement d'armes nucléaires dans de tels cas. Notez que ni le standard, ni la philosophie du compilateur moderne ne se soucieraient du fait que la valeur de retour de "hey" soit ignorée - le fait d'essayer de le renvoyer donne au compilateur une licence illimitée pour générer du code arbitraire.


0

La réponse de base est, oui, elle n'est pas définie.

Si vous constatez un comportement étrange à cause de cela, cela peut dépendre de l'endroit où il est déclaré. Si dans une fonction de la pile, le contenu sera probablement différent à chaque fois que la fonction est appelée. S'il s'agit d'une portée statique ou de module, elle n'est pas définie mais ne changera pas.


0

Si la classe de stockage est statique ou globale, pendant le chargement, le BSS initialise la variable ou l'emplacement de mémoire (ML) à 0 à moins que la variable ne reçoive initialement une valeur. Dans le cas de variables locales non initialisées, la représentation d'interruption est affectée à l'emplacement mémoire. Donc, si l'un de vos registres contenant des informations importantes est écrasé par le compilateur, le programme peut planter.

mais certains compilateurs peuvent avoir un mécanisme pour éviter un tel problème.

Je travaillais avec la série nec v850 quand j'ai réalisé qu'il y avait une représentation de trap qui a des modèles de bits qui représentent des valeurs non définies pour les types de données à l'exception de char. Lorsque j'ai pris un caractère non initialisé, j'ai obtenu une valeur par défaut nulle en raison de la représentation des interruptions. Cela peut être utile pour tout utilisateur utilisant necv850es


Votre système n'est pas conforme si vous obtenez des représentations d'interruption lors de l'utilisation de caractères non signés. Ils ne sont explicitement pas autorisés à contenir des représentations d'interruption, C17 6.2.6.1/5.
Lundin le

-2

La valeur de num sera une valeur de garbage de la mémoire principale (RAM). c'est mieux si vous initialisez la variable juste après la création.


-4

Dans la mesure où je suis allé, cela dépend principalement du compilateur, mais dans la plupart des cas, la valeur est présumée à 0 par les complicateurs.
J'ai eu la valeur des déchets dans le cas de VC ++ alors que TC a donné la valeur 0. Je l'imprime comme ci-dessous

int i;
printf('%d',i);

Si vous obtenez une valeur déterministe comme par exemple, 0votre compilateur va probablement faire des étapes supplémentaires pour s'assurer qu'il obtient cette valeur (en ajoutant du code pour initialiser les variables de toute façon). Certains compilateurs font cela lors de la compilation "debug", mais choisir la valeur 0pour ceux-ci est une mauvaise idée car cela cachera les défauts dans votre code (une chose plus appropriée serait de garantir un nombre vraiment improbable comme 0xBAADF00Dou quelque chose de similaire). Je pense que la plupart des compilateurs ne laisseront que les déchets qui occupent la mémoire comme valeur de la variable (c'est-à-dire qu'ils ne sont généralement pas assimilés comme 0).
skyking
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.