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.
{
int i, retainValue;
for (i=0; i<N; i++)
{
int tmpValue;
/* tmpValue is uninitialized */
/* retainValue still has its previous value from previous loop */
/* Do some stuff here */
}
/* Here, retainValue is still valid; tmpValue no longer */
}
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:
int result;
(...)
result = f1();
if (result) then { (...) }
(...)
result = f2();
if (result) then { (...) }
bezpieczniej jest pisać:
(...)
{
int const result = f1();
if (result) then { (...) }
}
(...)
{
int const result = f2();
if (result) then { (...) }
}
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 z f1()
do f2()
bloku. Każdy z nich result
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 zainicjowany f2()
. 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, wynik f1()
zostanie po prostu przetestowany po raz drugi, mylony z wynikiem f2()
.
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.