Dlaczego wskaźniki nie są domyślnie inicjowane z wartością NULL?

118

Czy ktoś może wyjaśnić, dlaczego wskaźniki nie są inicjalizowane NULL?
Przykład:

  void test(){
     char *buf;
     if (!buf)
        // whatever
  }

Program nie wkroczyłby do if, ponieważ bufnie jest zerowe.

Chciałbym wiedzieć, dlaczego, w jakim przypadku potrzebujemy zmiennej z włączonym koszem, a zwłaszcza wskaźników adresujących śmieci w pamięci?

Jonathan
źródło
13
Cóż, ponieważ podstawowe typy są niezainicjowane. Zakładam więc twoje „prawdziwe” pytanie: dlaczego nie zainicjowano typów podstawowych?
GManNickG,
11
"program nie wkroczyłby do if, ponieważ buf nie jest zerowy". To nie jest poprawne. Ponieważ nie wiesz, co to jest buf , nie możesz wiedzieć, co to nie jest .
Drew Dormann
W przeciwieństwie do czegoś takiego jak Java, C ++ daje znacznie większą odpowiedzialność programiście.
Rishi
liczby całkowite, wskaźniki, domyślnie 0, jeśli używasz konstruktora ().
Erik Aronesty
Z powodu założenia, że ​​ktoś używający C ++ wie, co robi, ponadto ktoś, kto używa surowych wskaźników na inteligentnym wskaźniku, wie (nawet bardziej), co robi!
Lofty Lion

Odpowiedzi:

161

Wszyscy zdajemy sobie sprawę, że wskaźnik (i inne typy POD) powinny zostać zainicjalizowane.
Pojawia się wtedy pytanie „kto powinien je zainicjować”.

Cóż, są zasadniczo dwie metody:

  • Kompilator inicjuje je.
  • Deweloper inicjuje je.

Załóżmy, że kompilator zainicjował dowolną zmienną, która nie została jawnie zainicjowana przez programistę. Następnie napotykamy sytuacje, w których inicjalizacja zmiennej była nietrywialna, a programista nie zrobił tego w punkcie deklaracji, że musiał wykonać jakąś operację, a następnie przypisać.

Mamy więc teraz sytuację, w której kompilator dodał do kodu dodatkową instrukcję, która inicjalizuje zmienną do wartości NULL, a następnie kod programisty jest dodawany w celu wykonania poprawnej inicjalizacji. Lub w innych warunkach zmienna potencjalnie nigdy nie jest używana. Wielu programistów C ++ w obu przypadkach wrzeszczałoby w obu przypadkach kosztem tej dodatkowej instrukcji.

Nie chodzi tylko o czas. Ale także przestrzeń. Istnieje wiele środowisk, w których oba zasoby są na wagę złota, a programiści też nie chcą się poddawać.

ALE : Możesz zasymulować efekt wymuszenia inicjalizacji. Większość kompilatorów ostrzeże Cię o niezainicjowanych zmiennych. Dlatego zawsze ustawiam poziom ostrzegawczy na najwyższym możliwym poziomie. Następnie powiedz kompilatorowi, aby traktował wszystkie ostrzeżenia jako błędy. W tych warunkach większość kompilatorów wygeneruje błąd dla zmiennych, które nie są zainicjowane, ale są używane, co uniemożliwi wygenerowanie kodu.

Martin York
źródło
5
Bob Tabor powiedział: „Zbyt wiele osób nie poświęciło wystarczająco dużo uwagi inicjalizacji!” Jest to „przyjazne” automatyczne inicjowanie wszystkich zmiennych, ale zajmuje to trochę czasu, a wolne programy są „nieprzyjazne”. Arkusz kalkulacyjny lub edytory, które pokazywałyby znalezione przypadkowe śmieci, byłyby niedopuszczalne. C, ostre narzędzie dla przeszkolonych użytkowników (niebezpieczne w przypadku niewłaściwego użycia) nie powinno zająć czasu na inicjalizację zmiennych automatycznych. Może to być makro z koła treningowego do inicjowania zmiennych, ale wielu uważa, że ​​lepiej jest wstać, być uważnym i trochę krwawić. W mgnieniu oka pracujesz tak, jak ćwiczysz. Więc ćwicz tak, jak chcesz.
Bill IV
2
Zdziwiłbyś się, jak wielu błędów można by uniknąć, gdyby ktoś naprawił całą ich inicjalizację. Byłaby to żmudna praca, gdyby nie ostrzeżenia kompilatora.
Jonathan Henson
4
@Loki, ciężko jest mi zrozumieć twój punkt widzenia. Chciałem tylko pochwalić twoją odpowiedź jako pomocną, mam nadzieję, że ją zebrałeś. Jeśli nie, to przepraszam.
Jonathan Henson
3
Jeśli wskaźnik jest najpierw ustawiony na NULL, a następnie na dowolną wartość, kompilator powinien być w stanie to wykryć i zoptymalizować pierwszą inicjalizację NULL, prawda?
Korchkidu
1
@Korchkidu: Czasami. Jednym z głównych problemów jest jednak to, że nie ma sposobu, aby ostrzec Cię, że zapomniałeś wykonać inicjalizację, ponieważ nie może wiedzieć, że ustawienie domyślne nie jest idealne do Twojego użytku.
Deduplicator
41

Cytując Bjarne Stroustrup w TC ++ PL (wydanie specjalne str. 22):

Implementacja funkcji nie powinna nakładać znacznych kosztów na programy, które tego nie wymagają.

Jan
źródło
i nie dawaj też opcji. Wygląda na to
Jonathan
8
@ Jonathan nic nie stoi na przeszkodzie, aby zainicjować wskaźnik do null - lub do 0, co jest standardem w C ++.
stefanB
8
Tak, ale Stroustrup mógł uczynić domyślną składnię faworyzującą poprawność programu, a nie wydajność, inicjując wskaźnik zerowy, i sprawić, że programista będzie musiał jawnie zażądać niezainicjowania wskaźnika. W końcu większość ludzi woli poprawne, ale powolne niż szybkie, ale błędne, ponieważ generalnie łatwiej jest zoptymalizować niewielką ilość kodu niż naprawić błędy w całym programie. Zwłaszcza, gdy wiele z tego może zrobić porządny kompilator.
Robert Tuck
1
Nie łamie kompatybilności. Pomysł był rozważany w połączeniu z „int * x = __uninitialized” - domyślnym bezpieczeństwem, zamierzoną szybkością.
MSalters
4
Lubię to, co Drobi. Jeśli nie chcesz inicjalizacji, użyj tej składni float f = void;lub int* ptr = void;. Teraz jest inicjalizowany domyślnie, ale jeśli naprawdę potrzebujesz, możesz powstrzymać kompilator.
deft_code,
23

Ponieważ inicjalizacja wymaga czasu. W C ++ pierwszą rzeczą, którą powinieneś zrobić z dowolną zmienną, jest jawne zainicjowanie jej:

int * p = & some_int;

lub:

int * p = 0;

lub:

class A {
   public:
     A() : p( 0 ) {}  // initialise via constructor
   private:
     int * p;
};

źródło
1
k, jeśli inicjalizacja wymaga czasu, a nadal tego chcę, czy mimo to, aby moje wskaźniki były zerowe bez ręcznego ustawiania? widzisz, nie dlatego, że nie chcę tego naprawić, ponieważ wydaje mi się, że nigdy nie użyję jednolitych wskaźników ze śmieciami na ich adresie
Jonathan
1
Inicjalizujesz członków klasy w konstruktorze klasy - tak działa C ++.
3
@Jonathan: ale null to też śmieci. Nie możesz zrobić nic użytecznego ze wskaźnikiem zerowym. Dereferencja jednego jest równie dużym błędem. Twórz wskaźniki z odpowiednimi wartościami, a nie wartościami null.
DrPizza
2
Zainicjowanie apointera do wartości Nnull może być rozsądną rzeczą, a na wskaźnikach zerowych można wykonać kilka operacji - można je przetestować i wywołać na nich usuwanie.
4
Jeśli nigdy nie zamierzasz używać wskaźnika bez jego jawnej inicjalizacji, nie ma znaczenia, co zawierał, zanim nadałeś mu wartość, a zgodnie z zasadą C i C ++ płacenia tylko za to, czego używasz, nie jest to zrobione automatycznie. Jeśli istnieje akceptowalna wartość domyślna (zwykle wskaźnik null), należy ją zainicjować. Możesz go zainicjować lub pozostawić niezainicjowany, Twój wybór.
David Thornley,
20

Ponieważ jednym z motto C ++ jest:


Nie płacisz za to, czego nie używasz


Z tego samego powodu, operator[]z vectorklasą nie sprawdza, czy indeks jest poza granicami, na przykład.

KeatsPeeks
źródło
12

Ze względów historycznych, głównie dlatego, że tak się to robi w C. Dlaczego tak się to robi w C, to inna kwestia, ale myślę, że zasada zerowego narzutu była w jakiś sposób zaangażowana w tę decyzję projektową.

AraK
źródło
Wydaje mi się, że ponieważ C jest uważany za język niższego poziomu z łatwym dostępem do pamięci (inaczej wskazówkami), więc daje ci swobodę robienia tego, co chcesz i nie narzuca narzutów poprzez inicjowanie wszystkiego. BTW, myślę, że to zależy od platformy, ponieważ pracowałem na platformie mobilnej opartej na Linuksie, która przed użyciem zainicjowała całą swoją pamięć na 0, więc wszystkie zmienne byłyby ustawione na 0.
stefanB
8

Poza tym, mamy ostrzeżenie, kiedy to wyrzucisz: "jest prawdopodobnie używane przed przypisaniem wartości" lub podobna informacja, w zależności od twojego kompilatora.

Kompilujesz z ostrzeżeniami, prawda?

Joshua
źródło
I jest to możliwe jako potwierdzenie, że śledzenie kompilatorów może być błędne.
Deduplicator
6

Niewiele jest sytuacji, w których niezainicjowanie zmiennej ma sens, a inicjalizacja domyślna ma niewielki koszt, więc po co to robić?

C ++ to nie C89. Do diabła, nawet C to nie C89. Możesz mieszać deklaracje i kod, więc powinieneś odłożyć deklarację do momentu, gdy będziesz mieć odpowiednią wartość do zainicjowania.

DrPizza
źródło
2
Wtedy tylko każda wartość będzie musiała zostać zapisana dwukrotnie - raz przez procedurę konfiguracyjną kompilatora i ponownie przez program użytkownika. Zwykle nie jest to duży problem, ale sumuje się (np. Jeśli tworzysz tablicę 1 miliona pozycji). Jeśli chcesz automatycznej inicjalizacji, zawsze możesz stworzyć własne typy, które to robią; ale w ten sposób nie musisz akceptować niepotrzebnych kosztów, jeśli nie chcesz.
Jeremy Friesner
3

Wskaźnik to po prostu inny typ. Jeśli utworzyć int, charlub dowolny inny typ POD nie jest inicjowany na zero, więc dlaczego wskaźnik? Może to zostać uznane za niepotrzebne obciążenie dla kogoś, kto pisze taki program.

char* pBuf;
if (condition)
{
    pBuf = new char[50];
}
else
{
    pBuf = m_myMember->buf();
}

Jeśli wiesz, że zamierzasz go zainicjalizować, dlaczego program miałby ponosić koszty, gdy po raz pierwszy tworzysz pBufw górnej części metody? To jest zasada zerowego narzutu.

LeopardSkinPillBoxHat
źródło
1
z drugiej strony możesz zrobić char * pBuf = condition? nowy znak [50]: m_myMember-> buf (); To bardziej kwestia składni niż wydajności, ale mimo wszystko zgodzę się z tobą.
the_drow
1
@the_drow: Cóż, można uczynić to bardziej złożonym, żeby takie przepisanie nie było możliwe.
Deduplicator
2

Jeśli potrzebujesz wskaźnika, który jest zawsze inicjalizowany na NULL, możesz użyć szablonu C ++ do emulacji tej funkcji:

template<typename T> class InitializedPointer
{
public:
    typedef T       TObj;
    typedef TObj    *PObj;
protected:
    PObj        m_pPointer;

public:
    // Constructors / Destructor
    inline InitializedPointer() { m_pPointer=0; }
    inline InitializedPointer(PObj InPointer) { m_pPointer = InPointer; }
    inline InitializedPointer(const InitializedPointer& oCopy)
    { m_pPointer = oCopy.m_pPointer; }
    inline ~InitializedPointer() { m_pPointer=0; }

    inline PObj GetPointer() const  { return (m_pPointer); }
    inline void SetPointer(PObj InPtr)  { m_pPointer = InPtr; }

    // Operator Overloads
    inline InitializedPointer& operator = (PObj InPtr)
    { SetPointer(InPtr); return(*this); }
    inline InitializedPointer& operator = (const InitializedPointer& InPtr)
    { SetPointer(InPtr.m_pPointer); return(*this); }
    inline PObj operator ->() const { return (m_pPointer); }
    inline TObj &operator *() const { return (*m_pPointer); }

    inline bool operator!=(PObj pOther) const
    { return(m_pPointer!=pOther); }
    inline bool operator==(PObj pOther) const
    { return(m_pPointer==pOther); }
    inline bool operator!=(const InitializedPointer& InPtr) const
    { return(m_pPointer!=InPtr.m_pPointer); }
    inline bool operator==(const InitializedPointer& InPtr) const
    { return(m_pPointer==InPtr.m_pPointer); }

    inline bool operator<=(PObj pOther) const
    { return(m_pPointer<=pOther); }
    inline bool operator>=(PObj pOther) const
    { return(m_pPointer>=pOther); }
    inline bool operator<=(const InitializedPointer& InPtr) const
    { return(m_pPointer<=InPtr.m_pPointer); }
    inline bool operator>=(const InitializedPointer& InPtr) const
    { return(m_pPointer>=InPtr.m_pPointer); }

    inline bool operator<(PObj pOther) const
    { return(m_pPointer<pOther); }
    inline bool operator>(PObj pOther) const
    { return(m_pPointer>pOther); }
    inline bool operator<(const InitializedPointer& InPtr) const
    { return(m_pPointer<InPtr.m_pPointer); }
    inline bool operator>(const InitializedPointer& InPtr) const
    { return(m_pPointer>InPtr.m_pPointer); }
};
Adisak
źródło
1
Gdybym to implementował, nie zawracałbym sobie głowy te copy ctor ani opcją przypisania - domyślne ustawienia są całkiem OK. A twój destruktor nie ma sensu. Oczywiście w pewnych okolicznościach możesz również przetestować wskaźniki przy użyciu narzędzia less than operater et all), więc powinieneś je podać.
OK, mniej niż trywialne do wdrożenia. Miałem destruktor, więc jeśli obiekt wyjdzie poza zakres (tj. Lokalnie zdefiniowany w podzakresie funkcji), ale nadal zajmuje miejsce na stosie, pamięć nie pozostaje jako wiszący wskaźnik do śmieci. Ale poważnie, napisałem to w mniej niż 5 minut. To nie ma być idealne.
Adisak
OK dodano wszystkie operatory porównania. Domyślne przesłonięcia mogą być zbędne, ale są tutaj jawnie, ponieważ jest to przykład.
Adisak
1
Nie mogłem zrozumieć, jak to spowodowałoby, że wszystkie wskaźniki byłyby zerowe bez ustawiania ich ręcznie. Czy mógłbyś wyjaśnić, co tutaj zrobiłeś?
Jonathan
1
@Jonathan: Jest to po prostu „inteligentny wskaźnik”, który nie robi nic poza ustawieniem wskaźnika na null. IE zamiast Foo *a, używasz InitializedPointer<Foo> a- Ćwiczenie czysto akademickie, podobnie jak Foo *a=0mniej pisania. Jednak powyższy kod jest bardzo przydatny z edukacyjnego punktu widzenia. Po niewielkiej modyfikacji (do „placeholding” ctor / dtor i assignment ops), można go łatwo rozszerzyć na różne typy inteligentnych wskaźników, w tym wskaźniki zakresów (które są wolne na destruktorze) i wskaźniki zliczane do odniesień, dodając inc / dec operacji, gdy m_pPointer jest ustawiony lub wyczyszczony.
Adisak
2

Zauważ, że dane statyczne są inicjowane na 0 (chyba że powiesz inaczej).

I tak, powinieneś zawsze deklarować swoje zmienne tak późno, jak to możliwe, z wartością początkową. Kod jak

int j;
char *foo;

powinien uruchomić dzwonek alarmowy, kiedy go czytasz. Nie wiem, czy da się przekonać jakieś kłaczki, żeby to oszukać, ponieważ jest to w 100% legalne.

pm100
źródło
jest to GWARANTOWANE, czy tylko powszechna praktyka stosowana przez dzisiejszych kompilatorów?
gha.st
1
zmienne statyczne są inicjowane na 0, co działa również dobrze w przypadku wskaźników (tj. ustawia je na NULL, a nie wszystkie bity 0). Takie zachowanie jest gwarantowane przez standard.
Alok Singhal,
1
inicjalizacja danych statycznych do zera jest gwarantowana przez standard C i C ++, to nie tylko powszechna praktyka
groovingandi
1
być może dlatego, że niektórzy ludzie chcą się upewnić, że ich stos jest ładnie wyrównany, wstępnie deklarują wszystkie zmienne na górze funkcji? Może piszą w dialekcie, który tego WYMAGA?
KitsuneYMG
1

Innym możliwym powodem jest to, że w momencie łącza wskaźniki otrzymują adres, ale za pośrednie adresowanie / usuwanie odniesień do wskaźnika odpowiada programista. Zwykle kompilator nie przejmuje się tym mniej, ale obciążenie jest przenoszone na programistę w celu zarządzania wskaźnikami i upewnienia się, że nie wystąpią wycieki pamięci.

W rzeczywistości, w skrócie, są one inicjowane w tym sensie, że w czasie łączenia zmiennej wskaźnikowej nadawany jest adres. W powyższym przykładowym kodzie gwarantuje to awarię lub wygenerowanie SIGSEGV.

Ze względu na rozsądek, zawsze inicjalizuj wskaźniki na NULL, w ten sposób, jeśli jakakolwiek próba wyodrębnienia go bez malloclub newwskaże programiście powód, dla którego program źle się zachował.

Mam nadzieję, że to pomoże i ma sens,

t0mm13b
źródło
0

Cóż, gdyby C ++ zainicjował wskaźniki, ludzie C narzekający, że „C ++ jest wolniejsze niż C”, mieliby coś prawdziwego, na czym mogliby się oprzeć;)

Fred
źródło
To nie jest mój powód. Powodem jest to, że jeśli sprzęt ma 512 bajtów pamięci ROM i 128 bajtów pamięci RAM i dodatkową instrukcję zerowania, wskaźnik jest nawet jednym bajtem, co stanowi dość duży procent całego programu. Potrzebuję tego bajtu!
Jerry Jeremiah
0

C ++ wywodzi się z C - i jest kilka powodów, dla których wraca z tego:

C, nawet bardziej niż C ++, jest zamiennikiem języka asemblera. Nie robi niczego, czego mu nie każesz. Dlatego: Jeśli chcesz to ZEROWAĆ - zrób to!

Ponadto, jeśli wyzerujesz rzeczy w czystym języku, takim jak C, automatycznie pojawią się pytania dotyczące spójności: Jeśli coś zrobisz - czy powinno to zostać automatycznie wyzerowane? A co ze strukturą utworzoną na stosie? czy wszystkie bajty powinny być wyzerowane? A co ze zmiennymi globalnymi? co z instrukcją typu „(* 0x18)”; czy to nie znaczy, że pozycja pamięci 0x18 powinna być wyzerowana?

gha.st
źródło
Właściwie w C, jeśli chcesz przydzielić zerową pamięć, możesz użyć calloc().
David Thornley
1
tylko moja uwaga - jeśli chcesz to zrobić, możesz, ale nie jest to zrobione za Ciebie automagicznie
gha.st
0

O jakich wskazówkach mówisz?

Dla bezpieczeństwa wyjątku, zawsze używaj auto_ptr, shared_ptr, weak_ptra ich inne warianty.
Cechą charakterystyczną dobrego kodu jest taki, który nie zawiera ani jednego wywołania delete.

shoosh
źródło
3
Od C ++ 11 shun auto_ptri substitute unique_ptr.
Deduplicator
-2

O chłopie. Prawdziwą odpowiedzią jest to, że łatwo jest wyzerować pamięć, co jest podstawową inicjalizacją, powiedzmy, wskaźnika. Co również nie ma nic wspólnego z inicjalizacją samego obiektu.

Biorąc pod uwagę ostrzeżenia, które większość kompilatorów podaje na najwyższych poziomach, nie wyobrażam sobie programowania na najwyższym poziomie i traktowania ich jako błędów. Ponieważ ich włączenie nigdy nie uratowało mi ani jednego błędu w ogromnej ilości wyprodukowanego kodu, nie mogę tego polecić.

Ser Charles Eli
źródło
Jeśli nie jest wskaźnik Oczekuje się NULL, że inicjowanie go jest tak samo błąd.
Deduplicator