Pytanie nr 1: Czy zadeklarowanie zmiennej wewnątrz pętli jest dobrą lub złą praktyką?
Przeczytałem inne wątki na temat tego, czy występuje problem z wydajnością (większość mówiła „nie”) i że zawsze powinieneś deklarować zmienne tak blisko miejsca, w którym będą używane. Zastanawiam się, czy należy tego unikać, czy też rzeczywiście jest to preferowane.
Przykład:
for(int counter = 0; counter <= 10; counter++)
{
string someString = "testing";
cout << someString;
}
Pytanie nr 2: Czy większość kompilatorów zdaje sobie sprawę, że zmienna została już zadeklarowana i po prostu pomija tę część, czy też faktycznie tworzy dla niej miejsce w pamięci za każdym razem?
c++
loops
variable-declaration
JeramyRR
źródło
źródło
Odpowiedzi:
To doskonała praktyka.
Tworząc zmienne wewnątrz pętli, upewniasz się, że ich zakres jest ograniczony do wewnątrz pętli. Nie można się do niego odwoływać ani wywoływać poza pętlą.
Tą drogą:
Jeśli nazwa zmiennej jest nieco „ogólna” (jak „i”), nie ma ryzyka pomieszania jej z inną zmienną o tej samej nazwie gdzieś później w kodzie (można ją również złagodzić za pomocą
-Wshadow
instrukcji ostrzegawczej na GCC)Kompilator wie, że zakres zmiennej jest ograniczony do wewnątrz pętli i dlatego wyda odpowiedni komunikat o błędzie, jeśli zmienna zostanie omyłkowo przywołana w innym miejscu.
Na koniec, niektóre dedykowane optymalizacje mogą być przeprowadzane bardziej efektywnie przez kompilator (co najważniejsze alokacja rejestru), ponieważ wie, że zmiennej nie można używać poza pętlą. Na przykład nie trzeba przechowywać wyniku do późniejszego ponownego wykorzystania.
Krótko mówiąc, masz rację.
Zauważ jednak, że zmienna nie powinna zachowywać swojej wartości między każdą pętlą. W takim przypadku konieczne może być zainicjowanie go za każdym razem. Możesz również utworzyć większy blok obejmujący pętlę, której jedynym celem jest deklarowanie zmiennych, które muszą zachować swoją wartość z jednej pętli do drugiej. Zwykle obejmuje to sam licznik pętli.
W przypadku pytania nr 2: Zmienna jest przydzielana raz, gdy wywoływana jest funkcja. W rzeczywistości z punktu widzenia alokacji jest (prawie) tym samym, co deklaracja zmiennej na początku funkcji. Jedyną różnicą jest zakres: zmiennej nie można używać poza pętlą. Możliwe jest nawet, że zmienna nie jest przydzielona, wystarczy ponownie użyć wolnego miejsca (z innej zmiennej, której zakres się zakończył).
Z ograniczonym i bardziej precyzyjnym zakresem pochodzą dokładniejsze optymalizacje. Ale co ważniejsze, sprawia, że kod jest bezpieczniejszy, ponieważ mniej stanów (tj. Zmiennych) martwi się podczas czytania innych części kodu.
Jest to prawdą nawet poza
if(){...}
blokiem. Zazwyczaj zamiast:bezpieczniej jest pisać:
Różnica może wydawać się niewielka, szczególnie na tak małym przykładzie. Ale na większej podstawie kodu pomoże: teraz nie ma ryzyka przeniesienia pewnej
result
wartości zf1()
dof2()
bloku. Każdy z nichresult
jest ściśle ograniczony do własnego zakresu, dzięki czemu jego rola jest dokładniejsza. Z punktu widzenia recenzenta jest o wiele ładniejszy, ponieważ ma on mniej zmiennych stanu o dalekim zasięgu , którymi należy się martwić i śledzić.Nawet kompilator pomoże lepiej: zakładając, że w przyszłości, po pewnej błędnej zmianie kodu,
result
nie zostanie poprawnie zainicjowanyf2()
. Druga wersja po prostu odmówi działania, podając wyraźny komunikat o błędzie w czasie kompilacji (znacznie lepiej niż w czasie wykonywania). Pierwsza wersja niczego nie wykryje, wynikf1()
zostanie po prostu przetestowany po raz drugi, mylony z wynikiemf2()
.Informacje uzupełniające
Narzędzie open source CppCheck (narzędzie do analizy statycznej kodu C / C ++) zapewnia doskonałe wskazówki dotyczące optymalnego zakresu zmiennych.
W odpowiedzi na komentarz dotyczący alokacji: powyższa reguła jest prawdą w C, ale może nie dotyczyć niektórych klas C ++.
W przypadku standardowych typów i struktur rozmiar zmiennej jest znany w czasie kompilacji. W C nie ma czegoś takiego jak „konstrukcja”, więc miejsce na zmienną zostanie po prostu przydzielone do stosu (bez jakiejkolwiek inicjalizacji), gdy funkcja zostanie wywołana. Dlatego deklarowanie zmiennej w pętli wiąże się z „zerowym” kosztem.
Jednak w przypadku klas C ++ jest coś o konstruktorze, o którym wiem znacznie mniej. Myślę, że alokacja prawdopodobnie nie będzie problemem, ponieważ kompilator będzie wystarczająco sprytny, aby ponownie wykorzystać tę samą przestrzeń, ale inicjalizacja prawdopodobnie nastąpi przy każdej iteracji pętli.
źródło
string
avector
konkretnie operatorowi przypisanie może ponownie przydzielony bufor każdej z pętli, które (w zależności od pętli) może być ogromne oszczędności czasu.Ogólnie rzecz biorąc, bardzo dobrą praktyką jest trzymanie go bardzo blisko.
W niektórych przypadkach będą brane pod uwagę takie czynniki, jak wydajność, która uzasadnia wyciągnięcie zmiennej z pętli.
W twoim przykładzie program tworzy i niszczy ciąg za każdym razem. Niektóre biblioteki używają optymalizacji małych ciągów (SSO), więc w niektórych przypadkach można uniknąć dynamicznej alokacji.
Załóżmy, że chcesz uniknąć zbędnych kreacji / alokacji, zapisałbyś to jako:
lub możesz wyciągnąć stałą:
Może ponownie wykorzystać przestrzeń zajmowaną przez zmienną i wyciągnąć niezmienniki z pętli. W przypadku tablicy const char (powyżej) tablicę tę można wyciągnąć. Jednak konstruktor i destruktor muszą być wykonywane przy każdej iteracji w przypadku obiektu (takiego jak
std::string
). W przypadkustd::string
tej „spacja” zawiera wskaźnik zawierający dynamiczny przydział reprezentujący znaki. Więc to:wymagałoby nadmiarowego kopiowania w każdym przypadku oraz dynamicznej alokacji i bezpłatnej, jeśli zmienna znajduje się powyżej progu liczby znaków SSO (a SSO jest implementowane przez bibliotekę std).
Robiąc to:
nadal wymagałby fizycznej kopii znaków przy każdej iteracji, ale formularz może skutkować jedną dynamiczną alokacją, ponieważ przypisujesz ciąg, a implementacja powinna zauważyć, że nie ma potrzeby zmiany rozmiaru alokacji kopii zapasowej ciągu. Oczywiście nie zrobiłbyś tego w tym przykładzie (ponieważ pokazano już wiele lepszych alternatyw), ale możesz wziąć to pod uwagę, gdy zawartość łańcucha lub wektora jest różna.
Co robisz ze wszystkimi tymi opcjami (i nie tylko)? Domyślnie trzymaj go bardzo blisko - dopóki nie zrozumiesz dobrze kosztów i nie wiesz, kiedy powinieneś odstąpić.
źródło
W przypadku C ++ zależy to od tego, co robisz. OK, to jest głupi kod, ale wyobraź sobie
Poczekaj 55 sekund, aż otrzymasz wynik myFunc. Tylko dlatego, że każdy konstruktor pętli i destruktor razem potrzebują 5 sekund na zakończenie.
Będziesz potrzebował 5 sekund, aż otrzymasz wynik myOtherFunc.
Oczywiście to szalony przykład.
Ale ilustruje to, że może to stać się problemem wydajnościowym, gdy każda pętla wykonuje tę samą konstrukcję, gdy konstruktor i / lub destruktor potrzebują trochę czasu.
źródło
Nie opublikowałem, aby odpowiedzieć na pytania JeremyRR (ponieważ już na nie odpowiedzieli); zamiast tego napisałem jedynie, aby dać sugestię.
Do JeremyRR możesz to zrobić:
Nie wiem, czy zdajesz sobie sprawę (nie wiedziałem, kiedy zaczynałem programować), że nawiasy (o ile są w parach) można umieścić w dowolnym miejscu kodu, nie tylko po słowach „if”, „for”, „ podczas ”itp.
Mój kod skompilowany w Microsoft Visual C ++ 2010 Express, więc wiem, że działa; również próbowałem użyć zmiennej poza nawiasami, w których została zdefiniowana, i otrzymałem błąd, więc wiem, że zmienna została „zniszczona”.
Nie wiem, czy stosowanie tej metody jest złą praktyką, ponieważ wiele nieoznaczonych nawiasów może szybko sprawić, że kod będzie nieczytelny, ale może niektóre komentarze mogą wyjaśnić sytuację.
źródło
Jest to bardzo dobra praktyka, ponieważ wszystkie powyższe odpowiedzi zapewniają bardzo dobry teoretyczny aspekt pytania, pozwalam rzucić okiem na kod, próbowałem rozwiązać DFS ponad GEEKSFORGEEKS, napotkałem problem optymalizacji ...... Jeśli spróbujesz rozwiązać kod deklarujący liczbę całkowitą poza pętlą da błąd optymalizacji.
Teraz umieść liczby całkowite w pętli, to da ci poprawną odpowiedź ...
całkowicie odzwierciedla to, co powiedział pan @ justin w drugim komentarzu .... spróbuj tutaj https://practice.geeksforgeeks.org/problems/depth-first-traversal-for-a-graph/1 . po prostu spróbuj ... dostaniesz. Mam nadzieję, że ta pomoc.
źródło
flag
należy ponownie zainicjować przy 0 każdejwhile
iteracji. To problem logiczny, a nie problem definicji.Rozdział 4.8 Struktura bloku w języku programowania K&R The C Programming Language 2.Ed. :
Mogłem nie zobaczyć odpowiedniego opisu w książce, takiego jak:
Ale prosty test może udowodnić przyjęte założenie:
źródło