Was passiert mit einer deklarierten, nicht initialisierten Variablen in C? Hat es einen Wert?

138

Wenn in CI schreiben:

int num;

Ist numder Wert von numunbestimmt, bevor ich etwas zuordne?

atp
quelle
4
Ist das nicht eine definierte Variable, keine deklarierte ? (Es tut mir leid, wenn das mein C ++ ist ...)
sbi
6
Nein. Ich kann eine Variable deklarieren, ohne sie zu definieren. extern int x;Das Definieren impliziert jedoch immer das Deklarieren. Dies ist in C ++ nicht der Fall, da statische Klassenmitgliedsvariablen ohne Deklaration definiert werden können, da die Deklaration in der Klassendefinition (nicht Deklaration!) Und die Definition außerhalb der Klassendefinition liegen muss.
Bdonlan
ee.hawaii.edu/~tep/EE160/Book/chap14/subsection2.1.1.4.html Sieht aus wie definiert bedeutet, dass Sie es auch initialisieren müssen.
Am

Antworten:

187

Statische Variablen (Dateibereich und statische Funktion) werden auf Null initialisiert:

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

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

Nicht statische Variablen (lokale Variablen) sind unbestimmt . Das Lesen vor dem Zuweisen eines Werts führt zu undefiniertem Verhalten.

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

In der Praxis haben sie anfangs nur einen unsinnigen Wert - einige Compiler geben möglicherweise sogar bestimmte feste Werte ein, um dies bei der Suche in einem Debugger deutlich zu machen -, aber genau genommen kann der Compiler alles tun, vom Absturz bis zur Beschwörung Dämonen durch deine Nasengänge .

Was den Grund betrifft, warum es sich um ein undefiniertes Verhalten handelt, anstatt nur um einen "undefinierten / willkürlichen Wert", gibt es eine Reihe von CPU-Architekturen, deren Darstellung für verschiedene Typen zusätzliche Flag-Bits enthält. Ein modernes Beispiel wäre das Itanium, dessen Register ein "Not a Thing" -Bit enthält . Natürlich haben die C-Standard-Zeichner einige ältere Architekturen in Betracht gezogen.

Der Versuch, mit einem Wert zu arbeiten, bei dem diese Flag-Bits gesetzt sind, kann zu einer CPU-Ausnahme in einer Operation führen, die wirklich nicht fehlschlagen sollte (z. B. Hinzufügen einer Ganzzahl oder Zuweisen zu einer anderen Variablen). Und wenn Sie eine Variable nicht initialisiert lassen, nimmt der Compiler möglicherweise zufälligen Müll mit diesen Flag-Bits auf - was bedeutet, dass das Berühren dieser nicht initialisierten Variablen tödlich sein kann.

bdonlan
quelle
2
Oh nein, sind sie nicht. Sie können im Debug-Modus sein, wenn Sie nicht vor einem Kunden sind, an Monaten mit einem R in, wenn Sie Glück haben
Martin Beckett
8
was ist nicht Die statische Initialisierung wird vom Standard verlangt. siehe ISO / IEC 9899: 1999 6.7.8 # 10
bdonlan
2
Das erste Beispiel ist soweit ich das beurteilen kann in Ordnung. Ich weiß weniger, warum der Compiler im zweiten abstürzen könnte :)
6
@Stuart: Es gibt eine Sache namens "Trap-Darstellung", bei der es sich im Grunde um ein Bitmuster handelt, das keinen gültigen Wert angibt und das zur Laufzeit z. B. Hardware-Ausnahmen verursachen kann. Der einzige C-Typ, für den garantiert wird, dass jedes Bitmuster ein gültiger Wert ist char, ist ; Alle anderen können Fallendarstellungen haben. Alternativ kann ein konformer Compiler - da der Zugriff auf eine nicht initialisierte Variable ohnehin UB ist - einfach eine Überprüfung durchführen und beschließen, das Problem zu signalisieren.
Pavel
5
bdonian ist richtig. C wurde immer ziemlich genau angegeben. Vor C89 und C99 spezifizierte ein Artikel von dmr all diese Dinge in den frühen 1970er Jahren. Selbst im gröbsten eingebetteten System ist nur ein memset () erforderlich, um die Dinge richtig zu machen. Es gibt also keine Entschuldigung für eine fehlerhafte Umgebung. Ich habe den Standard in meiner Antwort zitiert.
DigitalRoss
57

0 wenn statisch oder global, unbestimmt, wenn die Speicherklasse automatisch ist

C war immer sehr spezifisch in Bezug auf die Anfangswerte von Objekten. Wenn global oder static, werden sie auf Null gesetzt. Wenn auto, ist der Wert unbestimmt .

Dies war bei Compilern vor C89 der Fall und wurde von K & R und im ursprünglichen C-Bericht von DMR spezifiziert.

Dies war in C89 der Fall, siehe Abschnitt 6.5.7 Initialisierung .

Wenn ein Objekt mit automatischer Speicherdauer nicht explizit initialisiert wird, ist sein Wert unbestimmt. Wenn ein Objekt mit statischer Speicherdauer nicht explizit initialisiert wird, wird es implizit initialisiert, als ob jedem Element mit arithmetischem Typ 0 zugewiesen wurde und jedem Element mit Zeigertyp eine Nullzeigerkonstante zugewiesen wurde.

Dies war in C99 der Fall, siehe Abschnitt 6.7.8 Initialisierung .

Wenn ein Objekt mit automatischer Speicherdauer nicht explizit initialisiert wird, ist sein Wert unbestimmt. Wenn ein Objekt mit statischer Speicherdauer nicht explizit initialisiert wird, gilt Folgendes:
- Wenn es einen Zeigertyp hat, wird es mit einem Nullzeiger initialisiert.
- Wenn es einen arithmetischen Typ hat, wird es auf (positiv oder ohne Vorzeichen) Null initialisiert.
- Wenn es sich um ein Aggregat handelt, wird jedes Mitglied (rekursiv) gemäß diesen Regeln initialisiert.
- Wenn es sich um eine Gewerkschaft handelt, wird das zuerst genannte Mitglied (rekursiv) gemäß diesen Regeln initialisiert.

Was genau Unbestimmtes bedeutet, weiß ich für C89 nicht genau, sagt C99:

3.17.2
Unbestimmter Wert

entweder ein nicht spezifizierter Wert oder eine Trap-Darstellung

Unabhängig davon, was Standards sagen, beginnt im wirklichen Leben jede Stapelseite tatsächlich mit Null. Wenn Ihr Programm jedoch autoSpeicherklassenwerte betrachtet, sieht es, was von Ihrem eigenen Programm zurückgelassen wurde, als es diese Stapeladressen zuletzt verwendet hat. Wenn Sie viele autoArrays zuweisen, werden Sie sehen, dass sie schließlich ordentlich mit Nullen beginnen.

Sie fragen sich vielleicht, warum das so ist? Eine andere SO-Antwort befasst sich mit dieser Frage, siehe: https://stackoverflow.com/a/2091505/140740

DigitalRoss
quelle
3
Unbestimmt bedeutet normalerweise (früher?), dass es alles kann. Es kann Null sein, es kann der Wert sein, der dort war, es kann das Programm zum Absturz bringen, es kann den Computer dazu bringen, Blaubeerpfannkuchen aus dem CD-Steckplatz zu produzieren. Sie haben absolut keine Garantien. Es könnte die Zerstörung des Planeten verursachen. Zumindest was die Spezifikation angeht ... jeder, der einen Compiler erstellt hat, der tatsächlich so etwas gemacht hat, würde B-)
Brian Postow
Im Entwurf von C11 N1570 finden Sie die Definition von indeterminate valueunter 3.19.2.
Benutzer3528438
Ist es so, dass es immer vom Compiler oder dem Betriebssystem abhängt, welchen Wert es für statische Variablen setzt? Wenn zum Beispiel jemand ein Betriebssystem oder einen eigenen Compiler schreibt und standardmäßig auch den Anfangswert für die Statik als unbestimmt festlegt, ist das möglich?
Aditya Singh
1
@AdityaSingh, das Betriebssystem kann es dem Compiler leichter machen, aber letztendlich liegt es in der Hauptverantwortung des Compilers, den weltweit vorhandenen Katalog von C-Code auszuführen, und in der Nebenverantwortung, die Standards zu erfüllen. Es wäre sicherlich möglich , es anders zu machen, aber warum? Außerdem ist es schwierig, statische Daten unbestimmt zu machen, da das Betriebssystem die Seiten aus Sicherheitsgründen wirklich zuerst auf Null setzen möchte. (Automatische Variablen sind nur oberflächlich unvorhersehbar, da Ihr eigenes Programm diese
Stapeladressen
@BrianPostow Nein, das ist nicht korrekt. Siehe stackoverflow.com/a/40674888/584518 . Die Verwendung eines unbestimmten Werts führt zu nicht angegebenem Verhalten, nicht zu undefiniertem Verhalten, außer bei Trap-Darstellungen.
Lundin
11

Dies hängt von der Speicherdauer der Variablen ab. Eine Variable mit statischer Speicherdauer wird immer implizit mit Null initialisiert.

Bei automatischen (lokalen) Variablen hat eine nicht initialisierte Variable einen unbestimmten Wert . Unbestimmter Wert bedeutet unter anderem, dass jeder "Wert", den Sie in dieser Variablen "sehen", nicht nur unvorhersehbar ist, sondern auch nicht garantiert stabil ist . Zum Beispiel in der Praxis (dh das UB für eine Sekunde ignorieren) dieser Code

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

garantiert nicht, dass Variablen aund bidentische Werte erhalten. Interessanterweise ist dies kein pedantisches theoretisches Konzept, dies geschieht in der Praxis leicht als Folge der Optimierung.

Im Allgemeinen ist die populäre Antwort, dass "es mit dem Müll initialisiert wird, der sich im Speicher befindet", nicht einmal im entferntesten richtig. Nicht initialisierte Variable Verhalten unterscheidet sich von einer Variablen initialisiert mit Müll.

Ameise
quelle
Ich kann nicht verstehen (auch ich sehr gut kann ) , warum dies hat viel weniger upvotes als die von DigitalRoss nur eine Minute nach: D
Antti Haapala
7

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

Genug Standards, schauen wir uns eine Implementierung an :-)

Lokale Variable

Standards: undefiniertes Verhalten.

Implementierung: Das Programm weist Stapelspeicherplatz zu und verschiebt niemals etwas an diese Adresse. Daher wird alles verwendet, was zuvor vorhanden war.

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

kompilieren mit:

gcc -O0 -std=c99 a.c

Ausgänge:

0

und dekompiliert mit:

objdump -dr a.out

zu:

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

Aus unserem Wissen über x86-64-Aufrufkonventionen:

  • %rdiist das erste printf-Argument, also die Zeichenfolge "%d\n"an der Adresse0x4005e4

  • %rsiist also das zweite printf-Argument i.

    Es kommt von -0x4(%rbp), was die erste lokale 4-Byte-Variable ist.

    Zu diesem Zeitpunkt wurde rbpauf der ersten Seite der Stapel vom Kernel zugewiesen. Um diesen Wert zu verstehen, sollten wir uns den Kernel-Code ansehen und herausfinden, auf was er diesen Wert setzt.

    TODO Setzt der Kernel diesen Speicher auf etwas, bevor er ihn für andere Prozesse wiederverwendet, wenn ein Prozess stirbt? Wenn nicht, könnte der neue Prozess den Speicher anderer abgeschlossener Programme lesen und Daten verlieren. Siehe: Sind nicht initialisierte Werte jemals ein Sicherheitsrisiko?

Wir können dann auch mit unseren eigenen Stack-Modifikationen spielen und lustige Dinge schreiben wie:

#include <assert.h>

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

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

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

Lokale Variable in -O3

Implementierungsanalyse unter: Was bedeutet <Wert optimiert aus> in GDB?

Globale Variablen

Standards: 0

Implementierung: .bssAbschnitt.

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

gcc -00 -std=c99 a.c

kompiliert zu:

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>sagt das iist an adresse 0x601044und:

readelf -SW a.out

enthält:

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

Das heißt, es 0x601044befindet sich genau in der Mitte des .bssAbschnitts, der bei 0x6010408 Bytes beginnt und 8 Byte lang ist.

Der ELF-Standard garantiert dann, dass der genannte Abschnitt .bssvollständig mit Nullen gefüllt ist:

.bssDieser Abschnitt enthält nicht initialisierte Daten, die zum Speicherbild des Programms beitragen. Per Definition initialisiert das System die Daten mit Nullen, wenn das Programm gestartet wird. Der Abschnitt belegt keinen Dateibereich, wie durch den Abschnittstyp angegeben SHT_NOBITS.

Darüber hinaus ist der Typ SHT_NOBITSeffizient und belegt keinen Platz in der ausführbaren Datei:

sh_sizeDieses Mitglied gibt die Größe des Abschnitts in Bytes an. Sofern der SHT_NOBITSAbschnittstyp nicht lautet , belegt der Abschnitt sh_size Bytes in der Datei. Ein Abschnitt vom Typ SHT_NOBITSkann eine Größe ungleich Null haben, belegt jedoch keinen Platz in der Datei.

Dann ist es an dem Linux-Kernel, diesen Speicherbereich auf Null zu setzen, wenn das Programm beim Start in den Speicher geladen wird.

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
quelle
4

Kommt darauf an. Wenn diese Definition global ist (außerhalb einer Funktion), numwird sie auf Null initialisiert. Wenn es lokal ist (innerhalb einer Funktion), ist sein Wert unbestimmt. Theoretisch hat sogar der Versuch, den Wert zu lesen, ein undefiniertes Verhalten - C ermöglicht die Möglichkeit von Bits, die nicht zum Wert beitragen, sondern auf bestimmte Weise festgelegt werden müssen, damit Sie beim Lesen der Variablen überhaupt definierte Ergebnisse erhalten.

Jerry Sarg
quelle
1

Da Computer über eine begrenzte Speicherkapazität verfügen, werden automatische Variablen normalerweise in Speicherelementen (Register oder RAM) gespeichert, die zuvor für einen anderen beliebigen Zweck verwendet wurden. Wenn eine solche Variable verwendet wird, bevor ihr ein Wert zugewiesen wurde, kann dieser Speicher alles enthalten, was er zuvor gespeichert hat, sodass der Inhalt der Variablen nicht vorhersehbar ist.

Als zusätzliche Falte können viele Compiler Variablen in Registern speichern, die größer als die zugehörigen Typen sind. Obwohl ein Compiler sicherstellen müsste, dass jeder Wert, der in eine Variable geschrieben und zurückgelesen wird, abgeschnitten und / oder auf seine richtige Größe vorzeichenerweitert wird, führen viele Compiler eine solche Kürzung durch, wenn Variablen geschrieben werden, und erwarten, dass dies der Fall ist wurde durchgeführt, bevor die Variable gelesen wurde. Auf solchen Compilern so etwas wie:

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);
 }

Dies kann sehr gut dazu führen, dass wow()die Werte 1234567 in den Registern 0 bzw. 1 gespeichert und aufgerufen werden foo(). Da xdies in "foo" nicht benötigt wird und Funktionen ihren Rückgabewert in Register 0 einfügen sollen, kann der Compiler Register 0 zuweisen q. Wenn mode1 oder 3 ist, wird Register 0 mit 2 bzw. 4 geladen. Wenn es sich jedoch um einen anderen Wert handelt, kann die Funktion alles zurückgeben, was sich in Register 0 befand (dh den Wert 1234567), obwohl dieser Wert nicht innerhalb des Bereichs liegt von uint16_t.

Um zu vermeiden, dass Compiler zusätzliche Arbeit leisten müssen, um sicherzustellen, dass nicht initialisierte Variablen niemals Werte außerhalb ihrer Domäne zu enthalten scheinen, und um zu vermeiden, dass unbestimmte Verhaltensweisen übermäßig detailliert angegeben werden müssen, heißt es im Standard, dass die Verwendung nicht initialisierter automatischer Variablen undefiniertes Verhalten ist. In einigen Fällen können die Konsequenzen sogar noch überraschender sein als ein Wert, der außerhalb des Bereichs seines Typs liegt. Zum Beispiel gegeben:

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

Ein Compiler könnte daraus schließen, dass moo()der Compiler möglicherweise jeden Code weglässt, der nur relevant ist, wenn er mode4 oder höher ist, wie z. B. den Code, der normalerweise verhindert , da das Aufrufen mit einem Modus, der größer als 3 ist, zwangsläufig dazu führt, dass das Programm Undefiniertes Verhalten aufruft der Start von Atomwaffen in solchen Fällen. Beachten Sie, dass weder der Standard noch die moderne Compiler-Philosophie die Tatsache berücksichtigen würden, dass der Rückgabewert von "hey" ignoriert wird - der Versuch, ihn zurückzugeben, gibt einem Compiler eine unbegrenzte Lizenz zum Generieren von beliebigem Code.

Superkatze
quelle
0

Die grundlegende Antwort lautet: Ja, es ist undefiniert.

Wenn Sie aus diesem Grund ein merkwürdiges Verhalten feststellen, hängt dies möglicherweise davon ab, wo es deklariert ist. Wenn sich eine Funktion auf dem Stapel befindet, ist der Inhalt höchstwahrscheinlich jedes Mal anders, wenn die Funktion aufgerufen wird. Wenn es sich um einen statischen Bereich oder einen Modulbereich handelt, ist er undefiniert, ändert sich jedoch nicht.

Simon
quelle
0

Wenn die Speicherklasse statisch oder global ist, initialisiert das BSS während des Ladens die Variable oder den Speicherort (ML) auf 0, es sei denn, der Variablen wird anfänglich ein Wert zugewiesen. Bei lokalen nicht initialisierten Variablen wird die Trap-Darstellung dem Speicherort zugewiesen. Wenn also eines Ihrer Register mit wichtigen Informationen vom Compiler überschrieben wird, kann das Programm abstürzen.

Einige Compiler verfügen jedoch möglicherweise über einen Mechanismus, um ein solches Problem zu vermeiden.

Ich habe mit der NEC V850-Serie gearbeitet, als mir klar wurde, dass es eine Trap-Darstellung gibt, die Bitmuster enthält, die undefinierte Werte für Datentypen außer char darstellen. Als ich ein nicht initialisiertes Zeichen nahm, bekam ich aufgrund der Trap-Darstellung einen Standardwert von Null. Dies kann für any1 nützlich sein, das necv850es verwendet

Hanish
quelle
Ihr System ist nicht kompatibel, wenn Sie Trap-Darstellungen erhalten, wenn Sie nicht signiertes Zeichen verwenden. Sie dürfen ausdrücklich keine Trap-Darstellungen enthalten, C17 6.2.6.1/5.
Lundin
-2

Der Wert von num ist ein Müllwert aus dem Hauptspeicher (RAM). Es ist besser, wenn Sie die Variable direkt nach dem Erstellen initialisieren.

Shrikant Singh
quelle
-4

Soweit ich gegangen war, hängt es hauptsächlich vom Compiler ab, aber im Allgemeinen wird der Wert von den Compliern in den meisten Fällen als 0 angenommen.
Ich habe bei VC ++ einen Müllwert erhalten, während TC den Wert 0 angegeben hat. Ich drucke ihn wie unten

int i;
printf('%d',i);
Rajeev Kumar
quelle
Wenn Sie einen deterministischen Wert erhalten, wie zum Beispiel 0Ihr Compiler höchstwahrscheinlich zusätzliche Schritte unternimmt, um sicherzustellen, dass er diesen Wert erhält (indem Sie Code hinzufügen, um die Variablen trotzdem zu initialisieren). Einige Compiler tun dies, wenn sie eine "Debug" -Kompilierung durchführen, aber die Auswahl des Werts 0für diese ist eine schlechte Idee, da dadurch Fehler in Ihrem Code ausgeblendet werden (eine bessere Sache würde eine wirklich unwahrscheinliche Zahl wie 0xBAADF00Doder etwas Ähnliches garantieren ). Ich denke, die meisten Compiler lassen einfach den Müll, der den Speicher belegt, als Wert der Variablen (dh er wird im Allgemeinen nicht als zusammengesetzt 0).
Skyking