8.10 — Dla wypowiedzi

Zdecydowanie najczęściej używaną instrukcją pętli w C++ jest instrukcja for. Instrukcja for (zwana także pętlę for) jest preferowana, gdy mamy oczywistą zmienną pętli, ponieważ pozwala nam łatwo i zwięźle definiować, inicjować, testować i zmieniać wartość zmiennych pętli.

Począwszy od C++ 11, istnieją dwa różne rodzaje pętli for. Klasyczną instrukcję for omówimy w tej lekcji, a nowszą instrukcję for opartą na zakresach w przyszłej lekcji (16.8 -- Pętle for oparte na zakresach (dla każdego)), gdy omówimy inne wymagane tematy.

Instrukcja for wygląda dość prosto w abstrakcyjnym wydaniu:

for (init-statement; condition; end-expression)
   statement;

Najłatwiejszym sposobem, aby początkowo zrozumieć, jak działa instrukcja for, jest przekształcenie jej w równoważną instrukcję while:

{ // note the block here
    init-statement; // used to define variables used in the loop
    while (condition)
    {
        statement; 
        end-expression; // used to modify the loop variable prior to reassessment of the condition
    }
} // variables defined inside the loop go out of scope here

Ocena instrukcje for

Instrukcja for jest oceniana w 3 częściach:

Najpierw wykonywana jest instrukcja init. Dzieje się tak tylko raz, gdy pętla jest inicjowana. Instrukcja init jest zwykle używana do definiowania i inicjowania zmiennych. Zmienne te mają „zakres pętli”, który w rzeczywistości jest formą zakresu blokowego, w którym zmienne te istnieją od punktu definicji do końca instrukcji pętli. W naszym odpowiedniku pętli while widać, że instrukcja init znajduje się wewnątrz bloku zawierającego pętlę, więc zmienne zdefiniowane w instrukcji init wychodzą poza zakres, gdy blok zawierający pętlę się kończy.

Po drugie, przy każdej iteracji pętli oceniany jest warunek. Jeśli wynikiem będzie true, instrukcja zostanie wykonana. Jeżeli wynikiem jest false, pętla kończy się i wykonywanie jest kontynuowane od następnej instrukcji poza pętlą.

Na koniec, po wykonaniu instrukcji, oceniane jest wyrażenie końcowe. Zwykle to wyrażenie służy do zwiększania lub zmniejszania zmiennych pętli zdefiniowanych w instrukcji init. Po ocenie wyrażenia końcowego wykonanie powraca do drugiego kroku (i warunek jest ponownie oceniany).

Kluczowa informacja

Kolejność wykonywania poszczególnych części instrukcji for jest następująca:

  • Instrukcja init
  • Warunek (jeśli jest fałszywy, pętla kończy się w tym miejscu).
  • Treść pętli
  • Wyrażenie końcowe (wtedy wraca do warunku)

Zauważ, że wyrażenie końcowe jest wykonywane po instrukcji pętli, a następnie warunek jest ponownie oceniany.

Przyjrzyjmy się przykładowej pętli for i omówmy, jak ona działa:

#include <iostream>

int main()
{
    for (int i{ 1 }; i <= 10; ++i)
        std::cout << i << ' ';

    std::cout << '\n';

    return 0;
}

Najpierw deklarujemy zmienną pętli o nazwie i, oraz zainicjuj go wartością 1.

Po drugie, i <= 10 jest oceniana, a ponieważ i Jest 1, daje to true. W rezultacie wykonywana jest instrukcja, która wypisuje 1 i spację.

Na koniec przeprowadzana jest ocena ++i , która zwiększa się i Do 2. Następnie pętla wraca do drugiego kroku.

Teraz i<= 10 jest ponownie oceniana. Ponieważ i ma wartość 2, daje to true, więc pętla wykonuje kolejną iterację. Instrukcja wypisuje 2 i spację oraz i zwiększy się do 3. Pętla kontynuuje iterację aż do i zwiększy się do 11, w którym to momencie i <= 10 wyniesie false i pętla się kończy.

W rezultacie program wypisuje wynik:

1 2 3 4 5 6 7 8 9 10

Dla przykładu przekonwertujmy powyższą pętlę for na równoważną pętlę while:

#include <iostream>

int main()
{
    { // the block here ensures block scope for i
        int i{ 1 }; // our init-statement
        while (i <= 10) // our condition
        {
            std::cout << i << ' '; // our statement
            ++i; // our end-expression
        }
    }

    std::cout << '\n';
}

Nie wygląda to tak źle, prawda? Należy zauważyć, że zewnętrzne nawiasy klamrowe są tutaj konieczne, ponieważ i wychodzą poza zakres, gdy pętla się kończy.

Pętle for mogą być trudne do odczytania dla nowych programistów — jednak doświadczeni programiści je uwielbiają, ponieważ stanowią bardzo kompaktowy sposób wykonywania pętli z licznikiem, ze wszystkimi niezbędnymi informacjami o zmiennych pętli, warunkach pętli i modyfikatorach zmiennych pętli przedstawionymi na początku. Pomaga to ograniczyć błędy.

Więcej przykładów pętli for

Oto przykład funkcji, która wykorzystuje pętlę for do obliczania wykładników całkowitych:

#include <cstdint> // for fixed-width integers

// returns the value base ^ exponent -- watch out for overflow!
std::int64_t pow(int base, int exponent)
{
    std::int64_t total{ 1 };

    for (int i{ 0 }; i < exponent; ++i)
        total *= base;

    return total;
}

Ta funkcja zwraca wartość wykładnik bazowy^ (podstawa potęgi wykładnika).

Jest to prosta inkrementacyjna pętla for, z i pętlą od 0 do (ale z wyłączeniem) exponent.

We wszystkich przypadkach total jest inicjowany na 1.
Jeśli exponent wynosząc 0, pętla for wykona się 0 razy. total (z wartością 1) zostanie zwrócona, co jest równoważne base^0.
Jeśli exponent wynoszącemu 1, pętla for wykona się 1 raz. total (z wartością 1) jest mnożona przez base, więc jest ma teraz wartość base, co jest równoznaczne z base^1, która jest następnie zwracana.
Jeśli exponent wynosi 2, pętla for zostanie wykonana 2 razy. total (z wartością 1) jest mnożona przez base dwa razy, więc teraz ma wartość base * base, co jest równoznaczne z base^2, która jest następnie zwracana.

Chociaż większość pętli for zwiększ zmienną pętli o 1, możemy ją również zmniejszyć:

#include <iostream>

int main()
{
    for (int i{ 9 }; i >= 0; --i)
        std::cout << i << ' ';

    std::cout << '\n';

    return 0;
}

To wypisuje wynik:

9 8 7 6 5 4 3 2 1 0

Alternatywnie możemy zmienić wartość naszej zmiennej pętli o więcej niż 1 przy każdej iteracji:

#include <iostream>

int main()
{
    for (int i{ 0 }; i <= 10; i += 2) // increment by 2 each iteration
        std::cout << i << ' ';

    std::cout << '\n';

    return 0;
}

To wypisuje wynik:

0 2 4 6 8 10

Niebezpieczeństwa operator!= w pętli for warunki

Pisząc warunek pętli zawierający wartość, często możemy zapisać warunek na wiele różnych sposobów. Poniższe dwie pętle wykonują się identycznie:

#include <iostream>

int main()
{
    for (int i { 0 }; i < 10; ++i) // uses <
         std::cout << i;

    for (int i { 0 }; i != 10; ++i) // uses !=
         std::cout << i;

     return 0;
}

Którą zatem powinniśmy preferować? Ten pierwszy jest lepszym wyborem, ponieważ zakończy się nawet jeśli i przeskoczy wartość 10, podczas gdy drugi nie. Poniższy przykład ilustruje to:

#include <iostream>

int main()
{
    for (int i { 0 }; i < 10; ++i) // uses <, still terminates
    {
         std::cout << i;
         if (i == 9) ++i; // jump over value 10
    }

    for (int i { 0 }; i != 10; ++i) // uses !=, infinite loop
    {
         std::cout << i;
         if (i == 9) ++i; // jump over value 10
    }

     return 0;
}

Najlepsza praktyka

Unikaj operator!= podczas wykonywania porównań numerycznych w warunku pętli for. Preferuj operator< lub operator<= tam, gdzie to możliwe.

Błędy o jeden

Jednym z największych problemów, jakie nowi programiści mają z pętlami for (i innymi pętlami wykorzystującymi liczniki), są błędy o jeden. Błędy o jeden występują, gdy pętla wykonuje iteracje o jeden za dużo lub o jeden za mało, aby uzyskać pożądany wynik.

Oto przykład:

#include <iostream>

int main()
{
    // oops, we used operator< instead of operator<=
    for (int i{ 1 }; i < 5; ++i)
    {
        std::cout << i << ' ';
    }

    std::cout << '\n';

    return 0;
}

Ten program powinien wydrukować 1 2 3 4 5, ale wypisuje tylko 1 2 3 4 , ponieważ użyliśmy nieprawidłowy operator relacyjny.

Chociaż najczęstszą przyczyną tych błędów jest użycie niewłaściwego operatora relacyjnego, czasami mogą one wystąpić w przypadku użycia wstępnego zwiększania lub wstępnego zmniejszania zamiast późniejszego zwiększania lub zmniejszania lub odwrotnie.

Pominięte wyrażenia

Możliwe jest zapisanie dla pętli które pomijają dowolne lub wszystkie instrukcje lub wyrażenia. Na przykład w poniższym przykładzie pominiemy instrukcję init i wyrażenie end, pozostawiając jedynie warunek:

#include <iostream>

int main()
{
    int i{ 0 };
    for ( ; i < 10; ) // no init-statement or end-expression
    {
        std::cout << i << ' ';
        ++i;
    }

    std::cout << '\n';

    return 0;
}

This pętlę for da wynik:

0 1 2 3 4 5 6 7 8 9

Zamiast wykonywać pętlę for inicjalizację i inkrementację, zrobiliśmy to ręcznie. W tym przykładzie zrobiliśmy to wyłącznie w celach akademickich, ale są przypadki, w których pożądane jest niedefiniowanie zmiennej pętli (ponieważ już ją masz) lub nie zwiększanie jej w wyrażeniu końcowym (ponieważ zwiększasz ją w inny sposób).

Chociaż nie widzisz tego zbyt często, warto zauważyć, że poniższy przykład tworzy nieskończoną pętlę:

for (;;)
    statement;

Powyższy przykład jest równoważny do:

while (true)
    statement;

Może to być trochę nieoczekiwane, ponieważ prawdopodobnie można by się spodziewać, że pominięte wyrażenie-warunek zostanie potraktowane jako false. Jednak standard C++ wyraźnie (i niekonsekwentnie) definiuje, że pominięte wyrażenie warunku w pętli for powinno być traktowane jako true.

Zalecamy całkowite unikanie tej formy pętli for i używanie while (true) .

pętli For z wieloma licznikami

Chociaż pętle for zazwyczaj iterują tylko po jednej zmiennej, czasami pętle for muszą pracować z wieloma zmiennymi. Aby to ułatwić, programista może zdefiniować wiele zmiennych w instrukcji init i może użyć operatora przecinka do zmiany wartości wielu zmiennych w wyrażeniu końcowym:

#include <iostream>

int main()
{
    for (int x{ 0 }, y{ 9 }; x < 10; ++x, --y)
        std::cout << x << ' ' << y << '\n';

    return 0;
}

Ta pętla definiuje i inicjuje dwie nowe zmienne: x i y. Wykonuje iterację x w zakresie 0 Do 9 i po każdej iteracji x jest zwiększany i y jest zmniejszany.

Ten program generuje wynik:

0 9
1 8
2 7
3 6
4 5
5 4
6 3
7 2
8 1
9 0

To jedyne miejsce w C++, gdzie definiowanie wielu zmiennych w tej samej instrukcji, a użycie operatora przecinka jest akceptowaną praktyką.

Powiązana treść

Operator przecinka omówimy w lekcji 6.5 -- Operator przecinka.

Najlepsza praktyka

Definiowanie wielu zmiennych (w init-instrukcja) i użycie operatora przecinka (w wyrażeniu końcowym) jest dopuszczalne wewnątrz instrukcji for.

Zagnieżdżone pętle for

Podobnie jak inne typy pętli, pętle for mogą być zagnieżdżane w innych pętlach. W poniższym przykładzie zagnieżdżamy pętlę for w innej pętli for:

#include <iostream>

int main()
{
	for (char c{ 'a' }; c <= 'e'; ++c) // outer loop on letters
	{
		std::cout << c; // print our letter first
		
		for (int i{ 0 }; i < 3; ++i) // inner loop on all numbers
			std::cout << i;

		std::cout << '\n';
	}

	return 0;
}

Przy każdej iteracji pętli zewnętrznej pętla wewnętrzna działa w całości. W rezultacie wynik jest następujący:

a012
b012
c012
d012
e012

Oto więcej szczegółów na temat tego, co się tutaj dzieje. Pętla zewnętrzna jest uruchamiana jako pierwsza, a char c jest inicjowany do 'a'. Następnie c <= 'e' jest oceniany, czyli true, zatem zostaje wykonane ciało pętli. Ponieważ c ustawiono na 'a', to najpierw drukuje a. Następnie pętla wewnętrzna wykonuje się w całości (co powoduje wypisanie 0, 1, I 2). Następnie drukowany jest znak nowej linii. Teraz korpus pętli zewnętrznej jest gotowy, więc pętla zewnętrzna powraca na górę, c zwiększy się do 'b', a warunek pętli jest ponownie oceniany. Ponieważ warunek pętli jest nadal true rozpoczyna się następna iteracja pętli zewnętrznej. Spowoduje to wydrukowanie b012\n. I tak dalej.

Zmienne używane tylko wewnątrz pętli należy zdefiniować wewnątrz pętli

Nowi programiści często uważają, że tworzenie zmiennych jest kosztowne, dlatego lepiej stworzyć zmienną raz (a potem przypisać do niej wartości), niż tworzyć ją wielokrotnie (i stosować inicjalizację). Prowadzi to do pętli, które wyglądają jak odmiany poniższego:

#include <iostream>

int main()
{
    int i {}; // i defined outside loop
    for (i = 0; i < 10; ++i) // i assigned value
    {
        std::cout << i << ' ';        
    }

    // i can still be accessed here

    std::cout << '\n';

    return 0;
}

Jednak utworzenie zmiennej nie wiąże się z żadnymi kosztami — kosztem jest inicjalizacja i zazwyczaj nie ma różnicy w kosztach pomiędzy inicjalizacją a przypisaniem. Powyższy przykład umożliwia i dostępność poza pętlą. O ile nie jest wymagane użycie zmiennej poza pętlą, zdefiniowanie zmiennej poza pętlą może mieć dwie konsekwencje:

  1. To sprawia, że nasz program jest bardziej złożony, ponieważ musimy przeczytać więcej kodu, aby zobaczyć, gdzie zmienna jest używana.
  2. W rzeczywistości może to być wolniejsze, ponieważ kompilator może nie być w stanie skutecznie zoptymalizować zmiennej o większym zakresie.

Zgodnie z naszą najlepszą praktyką dotyczącą definiowania zmiennych w możliwie najmniejszym rozsądnym zakresie, a zmienna używana tylko w pętli powinna być definiowana wewnątrz pętli, a nie na zewnątrz.

Najlepsza praktyka

Zmienne używane tylko wewnątrz pętli powinny być definiowane wewnątrz zakresu pętli.

Wnioski

Instrukcje for są najczęściej używanymi pętlami w języku C++, ponieważ umieszczają wszystkie niezbędne informacje o zmiennych pętli, stanie pętli i modyfikacjach zmiennej pętli na górze pętli, co pomaga zmniejszyć błędy. Mimo że jej składnia jest zazwyczaj nieco myląca dla nowych programistów, pętle for będziesz spotykać tak często, że zrozumiesz je w mgnieniu oka!

Instrukcje For doskonale sprawdzają się, gdy masz zmienną licznikową. Jeśli nie masz licznika, prawdopodobnie lepszym wyborem będzie instrukcja while.

Najlepsza praktyka

Preferuj pętle for zamiast pętli while, gdy istnieje oczywista zmienna pętli.
Preferuj pętle while zamiast pętli for, gdy nie ma oczywistej zmiennej pętli.

Czas quizu

Pytanie nr 1

Napisz pętlę for, która wypisuje każdą liczbę parzystą od 0 do 20.

Pokaż rozwiązanie

Pytanie nr 2

Napisz funkcję o nazwie sumTo() który przyjmuje parametr całkowity o nazwie wartość i zwraca sumę wszystkich liczb od 1 do wartości.

Dla przykład sumTo(5) powinien zwrócić 15, czyli 1 + 2 + 3 + 4 + 5.

Wskazówka: Użyj zmiennej niebędącej pętlą, aby akumulować sumę podczas iteracji od 1 do wartości wejściowej, podobnie jak w pow() powyższym przykładzie zastosowano zmienną total do akumulowania wartości zwracanej w każdej iteracji.

Pokaż rozwiązanie

Pytanie nr 3

Co jest nie tak z następującą pętlą for?

// Print all numbers from 9 to 0
for (unsigned int i{ 9 }; i >= 0; --i)
    std::cout << i<< ' ';

Pokaż rozwiązanie

Pytanie nr 4

Fizz Buzz to prosta gra matematyczna służąca do nauczania dzieci o podzielności. Czasami jest również używane jako pytanie podczas rozmowy kwalifikacyjnej w celu oceny podstawowych umiejętności programowania.

Zasady gry są proste: zaczynając od 1 i licząc w górę, dowolną liczbę podzielną tylko przez trzy zamień słowem „fizz”, każdą liczbę podzielną tylko przez pięć słowem „buzz”, a każdą liczbę podzielną przez 3 i 5 słowem „fizzbuzz”.

Zaimplementuj tę grę wewnątrz funkcji o nazwie fizzbuzz() , który przyjmuje parametr określający, do jakiej liczby należy zliczać. Użyj pętli for i pojedynczego łańcucha if-else (co oznacza, że ​​możesz użyć dowolnej liczby else-if, ile chcesz).

Wyjście fizzbuzz(15) powinno być zgodne z poniższym:

1
2
fizz
4
buzz
fizz
7
8
fizz
buzz
11
fizz
13
14
fizzbuzz

Pokaż rozwiązanie

Pytanie #5

Zmodyfikuj program FizzBuzz, który napisałeś w poprzednim quizie, aby dodać zasadę, że liczby podzielne przez siedem należy zastępować przez „pop”. Uruchom program na 150 iteracji.

W tej wersji użycie łańcucha if/else do jawnego uwzględnienia każdej możliwej kombinacji słów spowoduje, że funkcja będzie za długa. Zoptymalizuj swoją funkcję tak, aby używane były tylko 4 instrukcje if: jedna dla każdego ze słów niezłożonych („fizz”, „buzz”, „pop”) i jedna dla przypadku, gdy drukowana jest liczba.

Pokaż wskazówkę

Oto kilka fragmentów oczekiwanych wyjście:

4
buzz
fizz
pop
8
19
buzz
fizzpop
22
104
fizzbuzzpop
106

Pokaż rozwiązanie

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
696 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze