Podstawowe wytyczne C ++ mają zasadę ES.20: Zawsze inicjuj obiekt .
Unikaj błędów wcześniej ustawionych i związanych z nimi niezdefiniowanych zachowań. Unikaj problemów ze zrozumieniem złożonej inicjalizacji. Uprość refaktoryzację.
Ale ta zasada nie pomaga znaleźć błędów, tylko je ukrywa.
Załóżmy, że program ma ścieżkę wykonania, w której wykorzystuje niezainicjowaną zmienną. To jest błąd. Nieokreślone zachowanie na bok oznacza również, że coś poszło nie tak, a program prawdopodobnie nie spełnia wymagań dotyczących produktu. Kiedy zostanie wdrożony do produkcji, może wystąpić utrata pieniędzy, a nawet gorzej.
Jak sprawdzamy błędy? Piszemy testy. Ale testy nie obejmują 100% ścieżek wykonania, a testy nigdy nie obejmują 100% danych wejściowych programu. Co więcej, nawet test obejmuje wadliwą ścieżkę wykonania - wciąż może przejść. W końcu jest to niezdefiniowane zachowanie, niezainicjowana zmienna może mieć nieco poprawną wartość.
Ale oprócz naszych testów mamy kompilatory, które mogą zapisywać coś takiego jak 0xCDCDCDCD do niezainicjowanych zmiennych. To nieznacznie poprawia wskaźnik wykrywalności testów.
Jeszcze lepiej - istnieją narzędzia takie jak Address Sanitizer, który przechwytuje wszystkie odczyty niezainicjowanych bajtów pamięci.
I w końcu są analizatory statyczne, które mogą spojrzeć na program i stwierdzić, że na tej ścieżce wykonania znajduje się ustawiony przed odczytem.
Mamy więc wiele potężnych narzędzi, ale jeśli zainicjalizujemy zmienną - środki dezynfekujące nie znajdą nic .
int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.
// Another bug: use empty buffer after read error.
use(buffer);
Jest jeszcze jedna zasada - jeśli wykonanie programu napotka błąd, program powinien umrzeć jak najszybciej. Nie musisz utrzymywać go przy życiu, po prostu rozbij się, napisz zrzut awaryjny, daj inżynierom do zbadania.
Inicjowanie zmiennych niepotrzebnie robi coś przeciwnego - program jest utrzymywany przy życiu, gdy w przeciwnym razie dostałby błąd segmentacji.
bytes_read
nie zostanie on zmieniony (więc zero), dlaczego to ma być błąd? Program może nadal być kontynuowany w rozsądny sposób, o ile nie spodziewa siębytes_read!=0
później. Więc dobrze, że środki odkażające nie narzekają. Z drugiej strony, gdybytes_read
nie jest zainicjowany wcześniej, program nie będzie w stanie kontynuować w sposób rozsądny, więc nie inicjalizacjibytes_read
faktycznie wprowadza błąd, który nie był tam wcześniej.\0
niego, jest błędna. Jeśli udokumentowano, że nie można sobie z tym poradzić, kod wywołujący jest wadliwy. Jeśli naprawisz swój kod telefoniczny, aby sprawdzićbytes_read==0
przed użyciem, powrócisz do miejsca, w którym zacząłeś: kod jest błędny, jeśli nie zainicjujeszbytes_read
, bezpieczny, jeśli to zrobisz. ( Zwykle funkcje mają wypełniać swoje parametry wyjściowe nawet w przypadku błędu : niezupełnie. Dość często dane wyjściowe są pozostawione same sobie lub niezdefiniowane.)err_t
zwracane przezmy_read()
? Jeśli w tym przykładzie jest błąd, to wszystko.Odpowiedzi:
Twoje rozumowanie jest błędne na kilku kontach:
bytes_read
ma wartość10
, ponieważ ma wartość0xcdcdcdcd
.Ideą przewodnią dotyczącą zawsze inicjowania zmiennych jest umożliwienie tych dwóch sytuacji
Zmienna zawiera użyteczną wartość od samego początku jej istnienia. Jeśli połączysz to ze wskazówkami, aby zadeklarować zmienną tylko wtedy, gdy jej potrzebujesz, możesz uniknąć, że przyszli programiści zajmujący się konserwacją wpadną w pułapkę rozpoczęcia używania zmiennej między jej deklaracją a pierwszym przypisaniem, w którym zmienna istniałaby, ale nie byłaby inicjowana.
Zmienna zawiera zdefiniowaną wartość, którą można przetestować później, aby stwierdzić, czy funkcja podobna
my_read
zaktualizowała wartość. Bez inicjalizacji nie można stwierdzić, czybytes_read
rzeczywiście ma prawidłową wartość, ponieważ nie wiadomo, od jakiej wartości się ona rozpoczęła.źródło
= 0;
. Celem porady jest zadeklarowanie zmiennej w punkcie, w którym będziesz miał dla niej użyteczną wartość i natychmiast przypisać tę wartość. Jest to wyraźnie określone w bezpośrednio następujących regułach ES21 i ES22. Tych trzech należy rozumieć jako współpracę; nie jako indywidualne niepowiązane reguły.Napisałeś „ta reguła nie pomaga znaleźć błędów, tylko je ukrywa” - cóż, celem tej reguły nie jest pomoc w znajdowaniu błędów, ale ich unikanie . A kiedy unika się błędu, nic nie jest ukryte.
Omówmy kwestię w odniesieniu do twojego przykładu: załóżmy, że
my_read
funkcja ma pisemną umowę do zainicjowaniabytes_read
w każdych okolicznościach, ale nie dzieje się tak w przypadku błędu, więc jest co najmniej wadliwa w tym przypadku. Twoim celem jest użycie środowiska wykonawczego, aby pokazać ten błąd, nie inicjującbytes_read
najpierw parametru. Tak długo, jak wiesz na pewno, że istnieje środek dezynfekujący adresy, jest to rzeczywiście możliwy sposób na wykrycie takiego błędu. Aby naprawić błąd, należy zmienićmy_read
funkcję wewnętrznie.Ale jest inny punkt widzenia, który jest co najmniej równie ważny: wadliwe zachowanie wynika jedynie z kombinacji
bytes_read
wcześniejszej inicjalizacji imy_read
późniejszego wywołania (z oczekiwaniembytes_read
inicjowanym później). Jest to sytuacja, która często zdarza się w komponentach świata rzeczywistego, gdy zapisana specyfikacja takiej funkcjimy_read
nie jest w 100% jasna, a nawet błędna co do zachowania w przypadku błędu. Jednak dopókibytes_read
inicjowane jest do zera przed wywołaniem, program zachowuje się tak samo, jakby inicjalizacja została wykonana wewnątrzmy_read
, więc zachowuje się poprawnie, w tej kombinacji nie ma błędu w programie.Tak więc moje zalecenie, które z tego wynika, jest następujące: użyj podejścia nieinicjalizującego tylko wtedy, gdy
Są to warunki, które zazwyczaj można ustawić w kodzie testowym dla określonego środowiska narzędziowego.
Jednak w kodzie produkcyjnym lepiej zawsze inicjować taką zmienną wcześniej, jest to bardziej defensywne podejście, które zapobiega błędom w przypadku, gdy umowa jest niekompletna lub błędna, lub w przypadku, gdy dezynfekcja adresu lub podobne środki bezpieczeństwa nie są aktywowane. A jeśli poprawnie napisałeś, obowiązuje zasada „wczesnego awarii”, jeśli wykonanie programu napotka błąd. Ale jeśli wcześniej zainicjowano zmienną, oznacza to, że nie ma nic złego, nie ma potrzeby przerywania dalszego wykonywania.
źródło
Zawsze inicjuj swoje zmienne
Różnica między sytuacjami, które rozważasz, polega na tym, że przypadek bez inicjalizacji powoduje niezdefiniowane zachowanie , podczas gdy przypadek, w którym zainicjowałeś czas, tworzy dobrze zdefiniowany i deterministyczny błąd. Nie mogę podkreślić, jak bardzo różne są te dwa przypadki.
Rozważ hipotetyczny przykład, który mógł się przytrafić hipotetycznemu pracownikowi w programie hipotetycznych symulacji. Ten hipotetyczny zespół próbował hipotetycznie przeprowadzić deterministyczną symulację, aby wykazać, że produkt, który sprzedawali hipotetycznie, spełniał potrzeby.
Okej, przestanę od słowa zastrzyki. Myślę, że rozumiesz o co chodzi ;-)
W tej symulacji były setki niezainicjowanych zmiennych. Jeden z programistów przeprowadził symulację valgrind i zauważył, że wystąpiło kilka błędów „rozgałęzienia przy niezainicjowanej wartości”. „Hmm, to wygląda na to, że może powodować niedeterminizm, utrudniając powtarzanie testów, gdy najbardziej tego potrzebujemy”. Deweloper poszedł do zarządzania, ale zarządzanie było bardzo napięte i nie mógł oszczędzić zasobów, aby wyśledzić ten problem. „W końcu inicjalizujemy wszystkie nasze zmienne przed ich użyciem. Mamy dobre praktyki kodowania”.
Kilka miesięcy przed ostateczną dostawą, gdy symulacja jest w trybie pełnego odejścia, a cały zespół biegnie, aby dokończyć wszystko, co obiecało zarządzanie przy budżecie, który - jak każdy finansowany projekt - był zbyt mały. Ktoś zauważył, że nie mogli przetestować istotnej funkcji, ponieważ z jakiegoś powodu deterministyczna karta SIM nie zachowywała się deterministycznie podczas debugowania.
Cały zespół mógł zostać zatrzymany i spędził większą część 2 miesięcy na przeczesywaniu całej bazy kodu symulacji naprawiając niezainicjowane błędy wartości zamiast implementacji i testowania funkcji. Nie trzeba dodawać, że pracownik pominął „Powiedziałem ci tak” i od razu pomógł innym programistom zrozumieć, jakie są niezainicjowane wartości. O dziwo, standardy kodowania zostały zmienione wkrótce po tym incydencie, zachęcając programistów do zawsze inicjowania swoich zmiennych.
I to jest strzał ostrzegawczy. Jest to kula, która przecięła ci nos. Rzeczywisty problem jest o wiele daleko bardziej podstępny, niż można sobie wyobrazić.
Używanie niezainicjowanej wartości jest „niezdefiniowanym zachowaniem” (z wyjątkiem kilku przypadków narożnych, takich jak
char
). Nieokreślone zachowanie (w skrócie UB) jest dla ciebie tak obłędnie i całkowicie złe, że nigdy nie powinieneś nigdy wierzyć, że jest lepsze niż alternatywa. Czasami możesz stwierdzić, że Twój konkretny kompilator definiuje UB, a następnie jest bezpieczny w użyciu, ale w przeciwnym razie niezdefiniowane zachowanie to „dowolne zachowanie, jakie odczuwa kompilator”. Może zrobić coś, co nazwałbyś „zdrowym”, na przykład o nieokreślonej wartości. Może emitować nieprawidłowe kody, potencjalnie powodując uszkodzenie twojego programu. Może wywołać ostrzeżenie w czasie kompilacji lub kompilator może nawet uznać to za błąd.Lub może nic nie robić
Mój kanarek w kopalni węgla dla UB pochodzi z silnika SQL, o którym czytałem. Wybacz, że nie powiązałem go, nie udało mi się ponownie znaleźć tego artykułu. Wystąpił problem z przepełnieniem bufora w silniku SQL, gdy przekazałeś większy rozmiar bufora do funkcji, ale tylko w określonej wersji Debiana. Błąd został należycie zarejestrowany i zbadany. Zabawna część była: przekroczenie bufor był sprawdzany . Wprowadzono kod do obsługi przekroczenia bufora. Wyglądało to mniej więcej tak:
Dodałem więcej komentarzy w moim wykonaniu, ale pomysł jest taki sam. Jeśli zostanie
put + dataLength
zawinięty, będzie mniejszy niżput
wskaźnik (dla ciekawskich musieli sprawdzić czas kompilacji, aby upewnić się, że unsigned int ma rozmiar wskaźnika). Jeśli tak się stanie, wiemy, że standardowe algorytmy bufora pierścieniowego mogą się pomylić z powodu tego przepełnienia, więc zwracamy 0. Czy my?Jak się okazuje, przepełnienie wskaźników jest niezdefiniowane w C ++. Ponieważ większość kompilatorów traktuje wskaźniki jako liczby całkowite, otrzymujemy typowe zachowania polegające na przepełnieniu liczb całkowitych, które akurat są zachowaniem, którego chcemy. Jednak to jest niezdefiniowane zachowanie, co oznacza, że kompilator może zrobić cokolwiek chce.
W przypadku tego błędu, Debian się wybrać do korzystania z nowej wersji gcc, że żaden z pozostałych głównych smaków Linux był zaktualizowany w ich wersjach produkcyjnych. Ta nowa wersja gcc miała bardziej agresywny optymalizator dead-code. Kompilator dostrzegł niezdefiniowane zachowanie i zdecydował, że wynikiem
if
instrukcji będzie „cokolwiek, co optymalizuje kod najlepiej”, co było absolutnie legalnym tłumaczeniem UB. W związku z tym przyjęto założenie, że ponieważptr+dataLength
nigdy nie może być poniżejptr
bez przepełnienia wskaźnika UB,if
instrukcja nigdy się nie uruchomi, i zoptymalizowała kontrolę przekroczenia bufora.Użycie „rozsądnego” UB faktycznie spowodowało, że główny produkt SQL wykorzystał lukę przepełnienia bufora , której napisał kod, aby tego uniknąć!
Nigdy nie polegaj na nieokreślonym zachowaniu. Zawsze.
źródło
bool
jest doskonałym przykładem, w którym występują oczywiste problemy, ale pojawiają się one gdzie indziej, chyba że zakładasz, że pracujesz na bardzo pomocnej platformie, takiej jak x86, ARM lub MIPS, gdzie wszystkie te problemy zostały rozwiązane w czasie kodowania.switch
jest mniejsza niż 8, ze względu na rozmiary arytmetyki liczb całkowitych, aby mogli skorzystać z szybkich instrukcji, które zakładały, że nie ma ryzyka pojawienia się „dużej” wartości. Nagle pojawia się nieokreślona wartość (której nigdy nie można skonstruować przy użyciu reguł kompilatora), robiąc coś nieoczekiwanego, i nagle masz ogromny skok z końca tabeli skoków. Zezwolenie na nieokreślone wyniki tutaj oznacza, że każda instrukcja switch w programie musi mieć dodatkowe pułapki, aby obsłużyć te przypadki, które „nigdy nie mogą wystąpić”.Pracuję głównie w funkcjonalnym języku programowania, w którym nie wolno zmieniać przypisań zmiennych. Zawsze. To całkowicie eliminuje tę klasę błędów. Z początku wydawało się to ogromnym ograniczeniem, ale zmusza cię do strukturyzacji kodu w sposób zgodny z kolejnością uczenia się nowych danych, co upraszcza kod i ułatwia jego utrzymanie.
Te nawyki można również przenieść na języki imperatywne. Prawie zawsze jest możliwe refaktoryzacja kodu, aby uniknąć zainicjowania zmiennej wartością zastępczą. Właśnie to nakazują ci te wytyczne. Chcą, abyś umieścił tam coś znaczącego, a nie coś, co tylko uszczęśliwi automatyzowane narzędzia.
Twój przykład z interfejsem API w stylu C jest nieco trudniejszy. W tych przypadkach, gdy użyję funkcji, zainicjuję do zera, aby kompilator nie narzekał, ale jeden raz w
my_read
testach jednostkowych zainicjuję coś innego, aby upewnić się, że warunek błędu działa poprawnie. Nie musisz testować każdego możliwego warunku błędu przy każdym użyciu.źródło
Nie, nie ukrywa błędów. Zamiast tego determinuje zachowanie w taki sposób, że jeśli użytkownik napotka błąd, programista może go odtworzyć.
źródło
TL; DR: Istnieją dwa sposoby na poprawienie tego programu, zainicjowanie zmiennych i modlitwa. Tylko jeden zapewnia spójne wyniki.
Zanim będę mógł odpowiedzieć na twoje pytanie, muszę najpierw wyjaśnić, co oznacza Niezdefiniowane zachowanie . Właściwie pozwolę autorowi kompilatora wykonać większość pracy:
Jeśli nie chcesz czytać tych artykułów, TL; DR to:
Archetyp „Demonów lecących z nosa” niestety, niestety, nie przekazał implikacji tego faktu. Choć miało to udowodnić, że wszystko może się zdarzyć, było to tak niewiarygodne, że w większości zostało zlekceważone.
Prawda jest jednak taka, że Undefined Behavior wpływa na samą kompilację na długo przed próbą użycia programu (instrumentalnego lub nie, w debuggerze lub nie) i może całkowicie zmienić swoje zachowanie.
Przykład w części 2 powyżej jest dla mnie uderzający:
przekształca się w:
ponieważ jest oczywiste, że tak
P
nie jest,0
ponieważ jest sprawdzany przed odwołaniem.Jak to się odnosi do twojego przykładu?
Popełniłeś powszechny błąd, zakładając, że Niezdefiniowane zachowanie spowoduje błąd w czasie wykonywania. Może nie.
Wyobraźmy sobie, że definicja
my_read
to:i postępuj zgodnie z oczekiwaniami dobrego kompilatora z wbudowanym:
Następnie, zgodnie z oczekiwaniami dobrego kompilatora, optymalizujemy bezużyteczne gałęzie:
bytes_read
byłby użyty niezainicjowany, gdybyresult
nie był0
result
nigdy nie będzie0
!Więc
result
nigdy nie jest0
:Och,
result
nigdy nie jest używane:Och, możemy odroczyć deklarację
bytes_read
:I oto jesteśmy, ściśle potwierdzająca transformacja oryginału, i żaden debugger nie złapie niezainicjowanej zmiennej, ponieważ nie ma żadnej.
Byłem na tej drodze, zrozumienie problemu, gdy oczekiwane zachowanie i montaż nie pasują do siebie, naprawdę nie jest zabawne.
źródło
Przyjrzyjmy się przykładowemu kodowi:
To dobry przykład. Jeśli spodziewamy się takiego błędu, możemy wstawić linię
assert(bytes_read > 0);
i złapać ten błąd w czasie wykonywania, co nie jest możliwe w przypadku niezainicjowanej zmiennej.Ale załóżmy, że nie, i znajdziemy błąd w funkcji
use(buffer)
. Ładujemy program do debugera, sprawdzamy ślad i dowiadujemy się, że został on wywołany z tego kodu. Dlatego umieszczamy punkt przerwania na początku tego fragmentu, uruchamiamy ponownie i odtwarzamy błąd. Próbujemy to złapać.Jeśli nie zainicjowaliśmy
bytes_read
, zawiera śmieci. Nie zawsze zawiera te same śmieci za każdym razem. Przechodzimy obok liniimy_read(buffer, &bytes_read);
. Teraz, jeśli jest to inna wartość niż wcześniej, być może w ogóle nie będziemy w stanie odtworzyć naszego błędu! Może zadziałać następnym razem, na tym samym wejściu, przez całkowity wypadek. Jeśli jest konsekwentnie zerowy, uzyskujemy spójne zachowanie.Sprawdzamy wartość, być może nawet na śladzie wstecznym w tym samym przebiegu. Jeśli wynosi zero, możemy zobaczyć, że coś jest nie tak;
bytes_read
nie powinno wynosić zero w przypadku sukcesu. (Lub jeśli tak, możemy chcieć zainicjować go na -1.) Prawdopodobnie możemy tutaj złapać błąd. Jeśli jednakbytes_read
jest prawdopodobna wartość, która akurat jest błędna, czy dostrzeglibyśmy ją na pierwszy rzut oka?Jest to szczególnie prawdziwe w przypadku wskaźników: wskaźnik NULL zawsze będzie oczywisty w debuggerze, można go bardzo łatwo przetestować i powinien ulec awarii na nowoczesnym sprzęcie, jeśli spróbujemy go wyrejestrować. Wskaźnik śmieci może później spowodować nieodwracalne błędy uszkodzenia pamięci, a ich debugowanie jest prawie niemożliwe.
źródło
OP nie opiera się na nieokreślonym zachowaniu, a przynajmniej nie do końca. Rzeczywiście, poleganie na niezdefiniowanym zachowaniu jest złe. Jednocześnie zachowanie programu w nieoczekiwanym przypadku jest również niezdefiniowane, ale innego rodzaju niezdefiniowane. Jeśli ustawisz zmienną do zera, ale nie zamierzają mieć ścieżki wykonania korzystające z tego początkowego zera, to program zachowuje się zdrowo, kiedy masz problem i zrobić mieć taką drogę? Jesteś teraz w chwastach; nie planowałeś użyć tej wartości, ale i tak jej używasz. Być może będzie to nieszkodliwe, a może spowoduje awarię programu, a może spowoduje dyskretne uszkodzenie danych. Nie wiesz
OP mówi, że istnieją narzędzia, które pomogą ci znaleźć ten błąd, jeśli im na to pozwolisz. Jeśli nie zainicjujesz wartości, ale i tak ją wykorzystasz, istnieją statyczne i dynamiczne analizatory, które poinformują cię, że masz błąd. Analizator statyczny powie ci zanim jeszcze zaczniesz testować program. Z drugiej strony, jeśli ślepo zainicjujesz wartość, analizatory nie mogą powiedzieć, że nie planujesz użyć tej wartości początkowej, więc twój błąd pozostaje niewykryty. Jeśli masz szczęście, jest to nieszkodliwe lub powoduje jedynie awarię programu; jeśli masz pecha, dyskretnie psuje dane.
Jedyne miejsce, w którym nie zgadzam się z OP, znajduje się na samym końcu, gdzie mówi „kiedy w przeciwnym razie dostałby się błąd segmentacji”. Rzeczywiście, niezainicjowana zmienna nie spowoduje niezawodnego błędu segmentacji. Zamiast tego powiedziałbym, że powinieneś używać narzędzi do analizy statycznej, które nie pozwolą ci przejść nawet do próby uruchomienia programu.
źródło
Odpowiedź na twoje pytanie musi być podzielona na różne typy zmiennych, które pojawiają się w programie:
Zmienne lokalne
Zwykle deklaracja powinna znajdować się dokładnie w miejscu, w którym zmienna najpierw otrzymuje swoją wartość. Nie usuwaj wcześniej zmiennych, jak w starym stylu C:
Eliminuje to 99% potrzeby inicjalizacji, zmienne mają swoją ostateczną wartość od samego początku. Kilka wyjątków dotyczy inicjalizacji zależnej od pewnych warunków:
Uważam, że dobrym pomysłem jest napisanie takich przypadków w ten sposób:
I. e. wyraźnie stwierdzaj, że przeprowadzana jest rozsądna inicjalizacja twojej zmiennej.
Zmienne składowe
Zgadzam się z tym, co powiedzieli inni odpowiedzieli: Powinny one być zawsze inicjowane przez listy konstruktorów / inicjatorów. W przeciwnym razie trudno jest zapewnić spójność między członkami. A jeśli masz zestaw elementów, które nie wymagają inicjalizacji we wszystkich przypadkach, refaktoryzuj swoją klasę, dodając tych członków do klasy pochodnej, w której są zawsze potrzebni.
Bufory
Tutaj nie zgadzam się z innymi odpowiedziami. Kiedy ludzie podchodzą do tematu inicjowania zmiennych, często inicjują bufory w następujący sposób:
Uważam, że jest to prawie zawsze szkodliwe: Jedynym efektem tych inicjalizacji jest to, że powodują, że narzędzia stają się
valgrind
bezsilne. Każdy kod, który czyta więcej z zainicjowanych buforów, niż powinien, jest bardzo prawdopodobne, że jest to błąd. Ale przy inicjalizacji tego błędu nie można wykryćvalgrind
. Więc nie używaj ich, chyba że naprawdę polegasz na wypełnieniu pamięci zerami (w takim przypadku zostaw komentarz mówiący, do czego potrzebujesz zer).Zdecydowanie poleciłbym również dodanie celu do systemu kompilacji, który uruchamia całe testsuite pod
valgrind
lub podobne narzędzie, aby ujawnić błędy przed użyciem i wycieki pamięci. Jest to cenniejsze niż wszystkie wstępne inicjalizacje zmiennych.valgrind
Cel ten powinien być wykonywany regularnie, co najważniejsze, zanim jakikolwiek kod zostanie opublikowany.Zmienne globalne
Nie możesz mieć globalnych zmiennych, które nie zostały zainicjowane (przynajmniej w C / C ++ itp.), Więc upewnij się, że ta inicjalizacja jest tym, czego chcesz.
źródło
Base& b = foo() ? new Derived1 : new Derived2;
Base &b = base_factory(which);
. Jest to najbardziej przydatne, jeśli trzeba wywołać kod więcej niż jeden raz lub jeśli pozwala to na uzyskanie stałego wyniku.?:
PITA, a funkcja fabryczna jest nadal nadmierna. Te przypadki są nieliczne, ale istnieją.Przyzwoity kompilator C, C ++ lub Objective-C z odpowiednim zestawem opcji kompilatora powie ci w czasie kompilacji, czy zmienna jest używana przed ustawieniem jej wartości. Ponieważ w tych językach używanie wartości niezainicjowanej zmiennej jest niezdefiniowanym zachowaniem, „ustaw wartość przed użyciem” nie jest wskazówką, wytyczną ani dobrą praktyką, jest to wymóg 100%; w przeciwnym razie twój program jest całkowicie zepsuty. W innych językach, takich jak Java i Swift, kompilator nigdy nie zezwoli na użycie zmiennej przed jej zainicjowaniem.
Istnieje logiczna różnica między „inicjowaniem” a „ustawieniem wartości”. Jeśli chcę znaleźć kurs wymiany między dolarami a euro i napisać „podwójny kurs = 0,0;” wtedy zmienna ma ustawioną wartość, ale nie jest inicjowana. Zapisane tutaj 0.0 nie ma nic wspólnego z poprawnym wynikiem. W tej sytuacji, jeśli z powodu błędu nigdy nie zapisujesz poprawnego współczynnika konwersji, kompilator nie ma szansy ci powiedzieć. Jeśli właśnie napisałeś „podwójna stawka”; i nigdy nie zapisywał znaczącego współczynnika konwersji, kompilator powiedziałby ci.
Więc: Nie inicjuj zmiennej tylko dlatego, że kompilator mówi ci, że jest używana bez inicjalizacji. To ukrywa błąd. Prawdziwy problem polega na tym, że używasz zmiennej, której nie powinieneś używać, lub że na jednej ścieżce kodu nie ustawiłeś wartości. Napraw problem, nie ukrywaj go.
Nie inicjuj zmiennej tylko dlatego, że kompilator może powiedzieć, że jest używana bez inicjalizacji. Znów ukrywasz problemy.
Zadeklaruj zmienne blisko do użycia. Zwiększa to szanse na zainicjowanie go znaczącą wartością w punkcie deklaracji.
Unikaj ponownego wykorzystywania zmiennych. Gdy ponownie użyjesz zmiennej, najprawdopodobniej zostanie ona zainicjowana na bezużyteczną wartość, gdy użyjesz jej do drugiego celu.
Skomentowano, że niektóre kompilatory mają fałszywe negatywy, a sprawdzanie inicjalizacji jest równoważne z problemem zatrzymania. Oba są w praktyce nieistotne. Jeśli cytowany kompilator nie może znaleźć użycia niezainicjowanej zmiennej dziesięć lat po zgłoszeniu błędu, nadszedł czas, aby poszukać alternatywnego kompilatora. Java implementuje to dwukrotnie; raz w kompilatorze, raz w weryfikatorze, bez żadnych problemów. Prostym sposobem na obejście problemu zatrzymania nie jest wymaganie, aby zmienna była inicjowana przed użyciem, ale inicjowana przed użyciem w sposób, który można sprawdzić za pomocą prostego i szybkiego algorytmu.
źródło