Co dzieje się z zadeklarowaną, niezainicjowaną zmienną w C? Czy to ma wartość?


139

Jeśli w CI napisz:

int num;

Zanim cokolwiek przypiszę num, czy wartość jest numnieokreślona?


4
Czy to nie jest zdefiniowana zmienna, a nie zadeklarowana ? (Przepraszam, jeśli to mój C ++ jest świetny ...)
sbi

6
Nie. Mogę zadeklarować zmienną bez jej definiowania: extern int x;Jednak definiowanie zawsze oznacza deklarację. Nie jest to prawdą w C ++, w przypadku statycznych zmiennych składowych klasy można zdefiniować bez deklarowania, ponieważ deklaracja musi znajdować się w definicji klasy (nie deklaracji!), A definicja musi znajdować się poza definicją klasy.
bdonlan

ee.hawaii.edu/~tep/EE160/Book/chap14/subsection2.1.1.4.html Wygląda na zdefiniowany oznacza, że ​​musisz go również zainicjalizować.
ATP

Odpowiedzi:


188

Zmienne statyczne (zakres pliku i statyczna funkcja) są inicjowane na zero:

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

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

Zmienne niestatyczne (zmienne lokalne) są nieokreślone . Przeczytanie ich przed przypisaniem wartości skutkuje niezdefiniowanym zachowaniem.

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

W praktyce początkowo mają one po prostu jakąś bezsensowną wartość - niektóre kompilatory mogą nawet wprowadzić określone, ustalone wartości, aby było to oczywiste podczas wyszukiwania w debugerze - ale ściśle mówiąc, kompilator może robić wszystko, od awarii do przywołania demony przez twoje przewody nosowe .

Jeśli chodzi o to, dlaczego jest to niezdefiniowane zachowanie, a nie po prostu „niezdefiniowana / arbitralna wartość”, istnieje wiele architektur procesora, które mają dodatkowe bity flagi w reprezentacji dla różnych typów. Współczesnym przykładem może być Itanium, który ma w rejestrach bit „Not a Thing” ; oczywiście twórcy standardu C brali pod uwagę niektóre starsze architektury.

Próba pracy z wartością z ustawionymi bitami flagi może spowodować wyjątek procesora w operacji, która naprawdę nie powinna zakończyć się niepowodzeniem (np. Dodanie liczb całkowitych lub przypisanie do innej zmiennej). A jeśli zostawisz zmienną niezainicjowaną, kompilator może zebrać jakieś losowe śmieci z ustawionymi bitami flagi - co oznacza, że ​​dotknięcie tej niezainicjowanej zmiennej może być śmiertelne.


2
o nie, nie są. Mogą być, w trybie debugowania, kiedy nie jesteś przed klientem, w miesiącach z R in, jeśli masz szczęście
Martin Beckett

8
co nie jest inicjalizacja statyczna jest wymagana przez normę; patrz ISO / IEC 9899: 1999 6.7.8 # 10
bdonlan

2
O ile wiem, pierwszy przykład jest w porządku. Nie wiem, dlaczego kompilator może się zawiesić w drugim :)

6
@Stuart: istnieje coś, co nazywa się „reprezentacją pułapki”, która jest w zasadzie wzorem bitowym, który nie oznacza prawidłowej wartości i może powodować np. Wyjątki sprzętowe w czasie wykonywania. Jedynym typem C, dla którego istnieje gwarancja, że ​​dowolny wzorzec bitowy jest prawidłową wartością, jest char; wszystkie inne mogą mieć reprezentacje pułapek. Alternatywnie - ponieważ dostęp do niezainicjowanej zmiennej i tak jest UB - zgodny kompilator może po prostu sprawdzić i zdecydować o zasygnalizowaniu problemu.
Pavel Minaev

5
bdonian ma rację. C zawsze było określane dość precyzyjnie. Przed C89 i C99 artykuł DMR wyszczególnił wszystkie te rzeczy na początku lat siedemdziesiątych. Nawet w najbardziej prymitywnym systemie wbudowanym wystarczy jedna funkcja memset (), aby zrobić wszystko poprawnie, więc nie ma usprawiedliwienia dla niezgodnego środowiska. W mojej odpowiedzi przytoczyłem wzorzec.
DigitalRoss

57

0, jeśli jest statyczny lub globalny, nieokreślony, jeśli klasa pamięci to auto

C zawsze był bardzo konkretny, jeśli chodzi o początkowe wartości obiektów. Jeśli globalne lub static, zostaną wyzerowane. Jeśli autowartość jest nieokreślona .

Tak było w kompilatorach sprzed wersji C89 i zostało to tak określone przez K&R oraz w oryginalnym raporcie C DMR.

Tak było w przypadku C89, patrz rozdział 6.5.7 Inicjalizacja .

Jeśli obiekt, który ma automatyczny czas trwania przechowywania, nie jest jawnie zainicjowany, jego wartość jest nieokreślona. Jeśli obiekt, który ma statyczny czas trwania magazynu, nie jest inicjowany jawnie, jest inicjowany niejawnie, tak jakby każdy element członkowski, który ma typ arytmetyczny, został przypisany 0, a każdemu elementowi członkowskiemu, który ma typ wskaźnika, przypisano stałą wskaźnika o wartości null.

Tak było w przypadku C99, patrz rozdział 6.7.8 Inicjalizacja .

Jeśli obiekt, który ma automatyczny czas trwania przechowywania, nie jest jawnie zainicjowany, jego wartość jest nieokreślona. Jeśli obiekt, który ma statyczny czas trwania nie jest inicjowany jawnie, to:
- jeśli ma typ wskaźnikowy, jest inicjowany jako wskaźnik pusty;
- jeśli ma typ arytmetyczny, jest inicjalizowany na zero (dodatnie lub bez znaku);
- jeśli jest agregatem, każdy element członkowski jest inicjalizowany (rekurencyjnie) zgodnie z tymi regułami;
- jeśli jest to unia, pierwszy nazwany element członkowski jest inicjowany (rekurencyjnie) zgodnie z tymi regułami.

Co dokładnie oznacza nieokreślony , nie jestem pewien dla C89, C99 mówi:

3.17.2
nieokreślona wartość

- nieokreślona wartość lub reprezentacja pułapki

Ale niezależnie od tego, co mówią standardy, w prawdziwym życiu każda strona stosu faktycznie zaczyna się od zera, ale kiedy program patrzy na autowartości dowolnej klasy pamięci, widzi wszystko, co pozostawił twój własny program, kiedy ostatnio używał tych adresów stosu. Jeśli przydzielisz wiele autotablic, zobaczysz, że w końcu zaczynają się równo od zer.

Możesz się zastanawiać, dlaczego tak jest? Inna odpowiedź SO dotyczy tego pytania, patrz: https://stackoverflow.com/a/2091505/140740


3
nieokreślony zwykle (kiedyś?) oznacza, że ​​może wszystko. Może to być zero, może to być wartość, która tam była, może spowodować awarię programu, może spowodować, że komputer będzie wytwarzał naleśniki z jagodami ze szczeliny na płytę CD. nie masz absolutnie żadnych gwarancji. Może to spowodować zniszczenie planety. Przynajmniej jeśli chodzi o specyfikację ... każdy, kto stworzył kompilator, który faktycznie zrobił coś takiego, byłby
niezadowolony z

W szkicu C11 N1570, definicję indeterminate valuemożna znaleźć w 3.19.2.
user3528438

Czy jest tak, że zawsze zależy od kompilatora lub systemu operacyjnego, jaką wartość ustawia dla zmiennej statycznej? Na przykład, jeśli ktoś pisze mój własny system operacyjny lub kompilator i jeśli domyślnie ustawia również wartość początkową dla statystyk jako nieokreślona, ​​czy jest to możliwe?
Aditya Singh

1
@AdityaSingh, system operacyjny może ułatwić pracę kompilatora, ale ostatecznie głównym obowiązkiem kompilatora jest uruchomienie istniejącego na świecie katalogu kodu w języku C, a dodatkową odpowiedzialnością jest spełnienie standardów. Z pewnością dałoby się to zrobić inaczej, ale dlaczego? Ponadto trudno jest uczynić dane statyczne nieokreślonymi, ponieważ system operacyjny naprawdę będzie chciał najpierw wyzerować strony ze względów bezpieczeństwa. (Zmienne automatyczne są tylko pozornie nieprzewidywalne, ponieważ Twój własny program zwykle używał tych adresów stosu we wcześniejszym momencie.)
DigitalRoss

@BrianPostow Nie, to nieprawda. Zobacz stackoverflow.com/a/40674888/584518 . Użycie nieokreślonej wartości powoduje nieokreślone zachowanie, a nie niezdefiniowane zachowanie, z wyjątkiem przypadku reprezentacji pułapek.
Lundin

12

Zależy to od czasu przechowywania zmiennej. Zmienna o statycznym czasie trwania jest zawsze niejawnie inicjowana wartością zero.

Jeśli chodzi o zmienne automatyczne (lokalne), niezainicjowana zmienna ma nieokreśloną wartość . Wartość nieokreślona oznacza między innymi, że jakakolwiek „wartość”, którą można „zobaczyć” w tej zmiennej, jest nie tylko nieprzewidywalna, ale nawet nie gwarantuje się jej stabilności . Na przykład w praktyce (tj. Ignorując na sekundę UB) ten kod

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

nie gwarantuje, że zmienne ai botrzymają identyczne wartości. Co ciekawe, nie jest to jakaś pedantyczna koncepcja teoretyczna, to w praktyce łatwo się dzieje w wyniku optymalizacji.

Więc ogólnie rzecz biorąc, popularna odpowiedź, że „jest inicjowana przez jakiekolwiek śmieci, które były w pamięci”, nie jest nawet poprawna. Zachowanie niezainicjowanej zmiennej różni się od zachowania zmiennej zainicjowanej za pomocą śmieci.


Nie mogę zrozumieć (cóż, bardzo dobrze potrafię ), dlaczego to ma znacznie mniej głosów pozytywnych niż ta z DigitalRoss zaledwie minutę po: D
Antti Haapala,

7

Przykład Ubuntu 15.10, Kernel 4.2.0, x86-64, GCC 5.2.1

Dość standardów, spójrzmy na realizację :-)

Zmienna lokalna

Normy: niezdefiniowane zachowanie.

Implementacja: program przydziela miejsce na stosie i nigdy nie przenosi niczego na ten adres, więc to, co było wcześniej, jest używane.

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

Połącz z:

gcc -O0 -std=c99 a.c

wyjścia:

0

i dekompiluje się z:

objdump -dr a.out

do:

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

Z naszej wiedzy o konwencjach wywoływania x86-64:

  • %rdijest pierwszym argumentem printf, a więc łańcuchem "%d\n"pod adresem0x4005e4

  • %rsijest więc drugim argumentem printf i.

    Pochodzi z -0x4(%rbp), która jest pierwszą 4-bajtową zmienną lokalną.

    W tym momencie rbpto pierwsza strona stosu została przydzielona przez jądro, więc aby zrozumieć tę wartość, powinniśmy zajrzeć do kodu jądra i dowiedzieć się, na co to ustawia.

    DO ZROBIENIA, czy jądro ustawia tę pamięć na coś przed ponownym użyciem jej dla innych procesów, gdy proces umiera? Jeśli nie, nowy proces byłby w stanie odczytać pamięć innych ukończonych programów, powodując wyciek danych. Zobacz: Czy niezainicjowane wartości stanowią kiedykolwiek zagrożenie bezpieczeństwa?

Możemy wtedy również bawić się naszymi własnymi modyfikacjami stosu i pisać zabawne rzeczy, takie jak:

#include <assert.h>

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

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

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

Zmienna lokalna w -O3

Analiza implementacji pod adresem: Co oznacza <wartość zoptymalizowana na zewnątrz> w gdb?

Zmienne globalne

Normy: 0

Realizacja: .bsssekcja.

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

gcc -00 -std=c99 a.c

kompiluje się do:

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>mówi, że ijest pod adresem 0x601044i:

readelf -SW a.out

zawiera:

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

który mówi, że 0x601044znajduje się dokładnie w środku .bsssekcji, która zaczyna się od 0x601040i ma długość 8 bajtów.

Standard ELF gwarantuje następnie, że wymieniona sekcja .bssjest całkowicie wypełniona zerami:

.bssTa sekcja zawiera niezainicjowane dane, które mają wpływ na obraz pamięci programu. Z definicji system inicjuje dane zerami, gdy program zaczyna działać. Sekcja plasuje ma miejsca pliku, jak wskazano rodzaju Sekcji SHT_NOBITS.

Ponadto typ SHT_NOBITSjest wydajny i nie zajmuje miejsca w pliku wykonywalnym:

sh_sizeTen element członkowski podaje rozmiar sekcji w bajtach. O ile typ sekcji to nie jest SHT_NOBITS, sekcja zajmuje sh_size bajty w pliku. Sekcja typu SHT_NOBITSmoże mieć niezerowy rozmiar, ale nie zajmuje miejsca w pliku.

Następnie do jądra Linuksa należy wyzerowanie tego regionu pamięci podczas ładowania programu do pamięci po uruchomieniu.


4

To zależy. Jeśli ta definicja jest globalna (poza jakąkolwiek funkcją), numzostanie zainicjowana na zero. Jeśli jest lokalna (wewnątrz funkcji), jej wartość jest nieokreślona. Teoretycznie nawet próba odczytania wartości ma nieokreślone zachowanie - C dopuszcza możliwość bitów, które nie mają wpływu na wartość, ale muszą być ustawione w określony sposób, abyś mógł uzyskać określone wyniki z odczytu zmiennej.


1

Ponieważ komputery mają ograniczoną pojemność, zmienne automatyczne będą zazwyczaj przechowywane w elementach pamięci (rejestrach lub pamięci RAM), które były wcześniej używane do innych dowolnych celów. Jeśli taka zmienna zostanie użyta przed przypisaniem jej wartości, pamięć ta może pomieścić wszystko, co posiadała poprzednio, a zatem zawartość zmiennej będzie nieprzewidywalna.

Dodatkowym problemem jest to, że wiele kompilatorów może przechowywać zmienne w rejestrach, które są większe niż powiązane typy. Chociaż kompilator byłby wymagany do zapewnienia, że ​​każda wartość, która jest zapisywana w zmiennej i odczytywana z powrotem, zostanie obcięta i / lub rozszerzona przez znak do odpowiedniego rozmiaru, wiele kompilatorów wykona takie obcięcie, gdy zmienne zostaną zapisane i oczekuje, że zostało wykonane przed odczytaniem zmiennej. Na takich kompilatorach coś takiego:

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

może bardzo dobrze skutkować wow()zapisaniem wartości 1234567 odpowiednio w rejestrach 0 i 1 i wywołaniem foo(). Ponieważ xnie jest potrzebne w "foo", a funkcje mają umieszczać zwracaną wartość w rejestrze 0, kompilator może przydzielić rejestr 0 do q. Jeśli modewynosi 1 lub 3, rejestr 0 zostanie załadowany odpowiednio 2 lub 4, ale jeśli jest to inna wartość, funkcja może zwrócić wszystko, co było w rejestrze 0 (tj. Wartość 1234567), nawet jeśli ta wartość nie mieści się w zakresie z uint16_t.

Aby uniknąć konieczności wykonywania przez kompilatory dodatkowej pracy, aby upewnić się, że niezainicjowane zmienne nigdy nie zdają się przechowywać wartości poza ich domeną, i uniknąć konieczności określania nieokreślonych zachowań z nadmierną szczegółowością, Standard mówi, że użycie niezainicjowanych zmiennych automatycznych jest niezdefiniowanym zachowaniem. W niektórych przypadkach konsekwencje tego mogą być nawet bardziej zaskakujące niż wykroczenie wartości poza jej typ. Na przykład, biorąc pod uwagę:

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

kompilator mógłby wywnioskować, że ponieważ wywołanie moo()w trybie większym niż 3 nieuchronnie doprowadzi do wywołania przez program niezdefiniowanego zachowania, kompilator może pominąć dowolny kod, który byłby istotny tylko wtedy, gdy modema wartość 4 lub więcej, taki jak kod, który normalnie uniemożliwiłby w takich przypadkach wystrzeliwanie broni nuklearnej. Zauważ, że ani Standard, ani nowoczesna filozofia kompilatora nie przejmowałyby się faktem, że wartość zwracana z "hey" jest ignorowana - czynność próby jej zwrócenia daje kompilatorowi nieograniczoną licencję na generowanie dowolnego kodu.


0

Podstawowa odpowiedź brzmi: tak, jest nieokreślona.

Jeśli z tego powodu widzisz dziwne zachowanie, może to zależeć od tego, gdzie zostało to zgłoszone. Jeśli wewnątrz funkcji na stosie, zawartość będzie najprawdopodobniej inna za każdym razem, gdy funkcja zostanie wywołana. Jeśli jest to zakres statyczny lub modułowy, jest on niezdefiniowany, ale nie ulegnie zmianie.


0

Jeśli klasa pamięci jest statyczna lub globalna, to podczas ładowania BSS inicjalizuje zmienną lub lokalizację pamięci (ML) na 0, chyba że zmiennej początkowo przypisano jakąś wartość. W przypadku niezainicjowanych zmiennych lokalnych reprezentacja pułapki jest przypisywana do komórki pamięci. Więc jeśli którykolwiek z twoich rejestrów zawierających ważne informacje zostanie nadpisany przez kompilator, program może się zawiesić.

ale niektóre kompilatory mogą mieć mechanizm pozwalający uniknąć takiego problemu.

Pracowałem z serią nec v850, kiedy zdałem sobie sprawę, że istnieje reprezentacja pułapki, która ma wzory bitowe reprezentujące niezdefiniowane wartości dla typów danych z wyjątkiem znaków. Kiedy wziąłem niezainicjowany znak, otrzymałem zerową wartość domyślną ze względu na reprezentację pułapki. Może to być przydatne dla każdego, kto używa necv850es


Twój system nie jest zgodny, jeśli otrzymujesz reprezentacje pułapek podczas używania znaku bez znaku. Wyraźnie nie mogą zawierać reprezentacji pułapek, C17 6.2.6.1/5.
Lundin

-2

Wartość num będzie wartością śmieci z pamięci głównej (RAM). lepiej, jeśli zainicjujesz zmienną zaraz po jej utworzeniu.


-4

O ile poszedłem, jest to głównie zależne od kompilatora, ale w większości przypadków wartość jest wstępnie przyjmowana jako 0 przez osoby zgodne.
Otrzymałem śmieciową wartość w przypadku VC ++, podczas gdy TC podało wartość 0. Wydrukuję to jak poniżej

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

Jeśli otrzymasz deterministyczną wartość, jak na przykład, 0Twój kompilator najprawdopodobniej podejmie dodatkowe kroki, aby upewnić się, że otrzyma tę wartość (przez dodanie kodu w celu zainicjowania zmiennych). Niektóre kompilatory robią to podczas kompilacji "debugowania", ale wybranie ich wartości 0jest złym pomysłem, ponieważ ukryje to błędy w kodzie (właściwsze byłoby zagwarantowanie naprawdę mało prawdopodobnej liczby, takiej jak 0xBAADF00Dlub coś podobnego). Myślę, że większość kompilatorów po prostu pozostawi wszelkie śmieci, które akurat zajmują pamięć, jako wartość zmiennej (tj. Generalnie nie jest ona przypisywana jako 0).
jazda na nartach
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.