Wprowadzenie do pętli
A teraz zaczyna się prawdziwa zabawa – w następnym zestawie lekcji zajmiemy się pętlami. Pętle to konstrukcje przepływu sterowania, które umożliwiają wielokrotne wykonanie fragmentu kodu, aż do spełnienia określonego warunku. Pętle dodają znaczną elastyczność do zestawu narzędzi programistycznych, umożliwiając wykonywanie wielu rzeczy, które w innym przypadku byłyby trudne.
Załóżmy na przykład, że chcesz wydrukować wszystkie liczby od 1 do 10. Bez pętli możesz spróbować czegoś takiego:
#include <iostream>
int main()
{
std::cout << "1 2 3 4 5 6 7 8 9 10";
std::cout << " done!\n";
return 0;
}Chociaż jest to wykonalne, staje się coraz mniejsze, gdy chcesz wydrukować więcej liczb: co jeśli chcesz wydrukować wszystkie liczby od 1 do 1000? To byłoby sporo pisania! Ale taki program da się w ten sposób zapisać, ponieważ w czasie kompilacji wiemy, ile liczb chcemy wydrukować.
Teraz zmieńmy trochę parametry. Co by było, gdybyśmy chcieli poprosić użytkownika o wprowadzenie liczby, a następnie wydrukować wszystkie liczby z zakresu od 1 do liczby wprowadzonej przez użytkownika? Liczba, którą wprowadzi użytkownik, nie jest znana w czasie kompilacji. Jak więc możemy rozwiązać ten problem?
Instrukcje while
Klasa inhile instrukcja (zwana także while pętla) jest najprostszym z trzech typów pętli dostępnych w C++ i ma definicję bardzo podobną do instrukcji if:
while (condition)
statement;
A while statement jest deklarowana przy użyciu słowo kluczowe podczas . Kiedy wykonywana jest instrukcja while, oceniane jest wyrażenie warunek . Jeżeli warunek ma wartość true, powiązany stwierdzenie wykonuje się.
Jednak w przeciwieństwie do instrukcji if, po zakończeniu wykonywania instrukcji sterowanie powraca na początek instrukcji while i proces jest powtarzany. Oznacza to, że instrukcja while będzie działać w pętli tak długo, jak długo warunek będzie oceniany true.
Przyjrzyjmy się prostej pętli while, która wypisuje wszystkie liczby od 1 do 10:
#include <iostream>
int main()
{
int count{ 1 };
while (count <= 10)
{
std::cout << count << ' ';
++count;
}
std::cout << "done!\n";
return 0;
}To daje:
1 2 3 4 5 6 7 8 9 10 done!
Przyjrzyjmy się bliżej, co robi ten program.
Najpierw definiujemy zmienną o nazwie count i ustawiamy ją do 1. Warunek count <= 10 Jest true, więc instrukcja zostanie wykonana. W tym przypadku nasza instrukcja jest blokiem, więc wszystkie instrukcje w bloku zostaną wykonane. Pierwsza instrukcja w bloku wypisuje 1 i spację, a druga zwiększa count do 2. Sterowanie wraca teraz na początek instrukcji while, a warunek jest ponownie oceniany. 2 <= 10 wyniesie true, więc blok kodu jest wykonywany ponownie. Pętla będzie wykonywana wielokrotnie aż do count Jest 11, w którym to momencie 11 <= 10otrzyma false, a instrukcja powiązana z pętlą zostanie pominięta. W tym momencie pętla jest zakończona.
Chociaż ten program wymaga nieco więcej kodu niż wpisanie wszystkich liczb od 1 do 10, zastanów się, jak łatwo byłoby zmodyfikować program tak, aby wypisywał wszystkie liczby z zakresu od 1 do 1000: wszystko, co musisz zrobić, to zmienić count <= 10 Do count <= 1000.
Instrukcje while, które początkowo mają wartość fałszywą
Zauważ, że jeśli warunek początkowo zostanie spełniony do false, powiązana instrukcja w ogóle nie zostanie wykonana. Rozważmy następujący program:
#include <iostream>
int main()
{
int count{ 15 };
while (count <= 10)
{
std::cout << count << ' ';
++count;
}
std::cout << "done!\n";
return 0;
}Warunek 15 <= 10 wyniesie false, w związku z czym powiązana instrukcja zostanie pominięta. Program kontynuuje działanie i jedyne, co zostaje wypisane, to done!.
Nieskończone pętle
Z drugiej strony, jeśli wyrażenie zawsze ma wartość true, pętla while będzie wykonywana w nieskończoność. Nazywa się to nieskończona pętla. Oto przykład nieskończonej pętli:
#include <iostream>
int main()
{
int count{ 1 };
while (count <= 10) // this condition will never be false
{
std::cout << count << ' '; // so this line will repeatedly execute
}
std::cout << '\n'; // this line will never execute
return 0; // this line will never execute
}Ponieważ count nie jest inkrementowana w tym programie, count <= 10 zawsze będzie prawdziwa. W rezultacie pętla nigdy się nie zakończy, a program będzie drukował 1 1 1 1 1... w nieskończoność.
Zamierzone pętle nieskończone
Możemy zadeklarować celową nieskończoną pętlę w następujący sposób:
while (true)
{
// this loop will execute forever
}Jedynym sposobem na wyjście z nieskończonej pętli jest instrukcja return, instrukcja break, instrukcja wyjścia, instrukcja goto, zgłoszenie wyjątku lub zabicie przez użytkownika programu.
Oto głupi przykład ilustrujący to:
#include <iostream>
int main()
{
while (true) // infinite loop
{
std::cout << "Loop again (y/n)? ";
char c{};
std::cin >> c;
if (c == 'n')
return 0;
}
return 0;
}Ten program będzie się powtarzał, dopóki użytkownik nie wejdzie n jako dane wejściowe, w którym to momencie instrukcja if osiągnie wartość true i powiązane return 0; spowoduje funkcję main() aby wyjść, kończąc program.
Tego rodzaju pętle często można spotkać w aplikacjach serwerów WWW, które działają w sposób ciągły i obsługują żądania sieciowe.
Najlepsza praktyka
Zamawiaj while(true) dla celowych nieskończonych pętli.
Niezamierzone nieskończone pętle
Niezamierzone umieszczenie średnika po warunku pętli while jest dobrym sposobem na zawieszenie programu.
Oto poprzedni przykład pokazujący, co się stanie, jeśli popełniony zostanie ten prosty błąd:
#include <iostream>
int main()
{
int count{ 1 };
while (count <= 10); // note the semicolon here
{
std::cout << count << ' ';
++count;
}
std::cout << "done!\n";
return 0;
}Program wykonuje się tak, jakbyśmy napisali to:
#include <iostream>
int main()
{
int count{ 1 };
while (count <= 10) // this is an infinite loop
; // whose body is a null statement
{ // this is no longer associated with the while loop
std::cout << count << ' ';
++count;
}
std::cout << "done!\n";
return 0;
}Ponieważ warunek pętli ma wartość true, wykonywane jest ciało pętli. Ale ciało pętli jest instrukcją zerową, która nic nie robi. Następnie warunek pętli jest ponownie oceniany. Od count nigdy nie jest zwiększana, warunek nigdy nie może osiągnąć wartości false, więc pętla będzie działać w nieskończoność, nie robiąc nic. Nasz program będzie wyglądał jakby był zawieszony.
W przeciwieństwie do instrukcji if, gdzie średnik po warunku jest zawsze błędem, czasami można spotkać instrukcje while, które robią to celowo. Na przykład, jeśli chcemy w sposób ciągły wywoływać funkcję, aż do jej powrotu false, możemy to zwięźle zapisać w następujący sposób:
while (keepRunning()); // will keep calling this function until it returns falseOczywiście, jeśli funkcja nigdy nie powróci false, otrzymasz nieskończoną pętlę.
Ostrzeżenie
Uważaj, umieszczając średnik po warunku instrukcji while, ponieważ spowoduje to nieskończoną pętlę, chyba że warunek zostanie w jakiś sposób obliczony false.
Zmienne pętli i nazewnictwo
A zmienna pętli to zmienna używana do kontrolowania liczby wykonań pętli. Na przykład dane while (count <= 10), count jest zmienną pętli. Chociaż większość zmiennych pętli ma typ int, czasami zobaczysz inne typy (np. char).
Zmienne pętli często otrzymują proste nazwy, np i, j, I k jest najczęstszy.
Na marginesie…
Użycie i, j, I k Nazwy zmiennych pętli for powstały, ponieważ są to pierwsze trzy najkrótsze nazwy zmiennych całkowitych w języku programowania Fortran. Od tego czasu konwencja trwa.
Jeśli jednak chcesz wiedzieć gdzie w Twoim programie jest używana zmienna pętli i korzystasz z funkcji wyszukiwania on i, j lub k, funkcja wyszukiwania zwróci połowę linii w twoim programie! Z tego powodu niektórzy programiści preferują nazwy zmiennych pętli, takie jak iii, jjj lub kkk. Ponieważ te nazwy są bardziej unikalne, znacznie ułatwia to wyszukiwanie zmiennych pętli i pomaga im wyróżnić się jako zmienne pętli. Jeszcze lepszym pomysłem jest użycie „prawdziwych” nazw zmiennych, takich jak count, indexlub nazwę zawierającą więcej szczegółów na temat tego, co liczysz (np. userCount).
Najpopularniejszym typem zmiennej pętli jest a licznik, która jest zmienną pętli, która zlicza, ile razy pętla została wykonana. W powyższych przykładach zmienna count jest licznik.
Zmienne pętli całkowej powinny być podpisane
Zmienne pętli całkowej powinny prawie zawsze być podpisane, ponieważ liczby całkowite bez znaku mogą prowadzić do nieoczekiwanych problemów. Rozważ następujący kod:
#include <iostream>
int main()
{
unsigned int count{ 10 }; // note: unsigned
// count from 10 down to 0
while (count >= 0)
{
if (count == 0)
{
std::cout << "blastoff!";
}
else
{
std::cout << count << ' ';
}
--count;
}
std::cout << '\n';
return 0;
}Spójrz na powyższy przykład i sprawdź, czy potrafisz dostrzec błąd. Nie jest to zbyt oczywiste, jeśli nie widziałeś tego wcześniej.
Okazuje się, że ten program jest nieskończoną pętlą. Zaczyna się od druku 10 9 8 7 6 5 4 3 2 1 blastoff! zgodnie z potrzebami, ale następnie zmienna pętli count przepełnia się i zaczyna odliczanie 4294967295 (przy założeniu 32-bitowych liczb całkowitych). Dlaczego? Ponieważ warunek pętli count >= 0 nigdy nie będzie fałszywe! Kiedy jest liczenie 0, 0 >= 0 jest prawdą. Następnie --count jest wykonywany, a liczba jest zawijana z powrotem do 4294967295. I od 4294967295 >= 0 Jest true, program jest kontynuowany. Ponieważ count jest bez znaku, nigdy nie może być ujemna, a ponieważ nigdy nie może być ujemna, pętla się nie zakończy.
Najlepsza praktyka
Zmienne pętli całkowej powinny ogólnie być typu całkowego ze znakiem.
Wykonywanie czegoś co N iteracji
Za każdym razem, gdy wykonywana jest pętla, nazywa się to iteracją.
Często chcemy zrobić coś co drugą, trzecią lub czwartą iterację, na przykład wydrukować znak nowej linii. Można to łatwo zrobić za pomocą operatora reszty na naszym liczniku:
#include <iostream>
// Iterate through every number between 1 and 50
int main()
{
int count{ 1 };
while (count <= 50)
{
// print the number (pad numbers under 10 with a leading 0 for formatting purposes)
if (count < 10)
{
std::cout << '0';
}
std::cout << count << ' ';
// if the loop variable is divisible by 10, print a newline
if (count % 10 == 0)
{
std::cout << '\n';
}
// increment the loop counter
++count;
}
return 0;
}Ten program generuje wynik:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
Zagnieżdżone pętle
Możliwe jest również zagnieżdżanie pętli wewnątrz innych pętli. Zagnieżdżone pętle wydają się być nieco mylące dla nowych programistów, więc zacznijmy od nieco łatwiejszego przykładu:
#include <iostream>
void printUpto(int outer)
{
// loop between 1 and outer
// note: inner will be created and destroyed at the end of the block
int inner{ 1 };
while (inner <= outer)
{
std::cout << inner << ' ';
++inner;
}
} // inner destroyed here
int main()
{
// outer loops between 1 and 5
int outer{ 1 };
while (outer <= 5)
{
// For each iteration of the outer loop, the code in the body of the loop executes once
// This function prints numbers between 1 and outer
printUpto(outer);
// print a newline at the end of each row
std::cout << '\n';
++outer;
}
return 0;
}W tym przykładzie mamy zewnętrzną pętlę z licznikiem o nazwie outer , który liczy od 1 do 5. Przy każdej iteracji pętli treść pętli wywołuje printUpto() używając zmiennej pętli zewnętrznej jako argumentu, wypisuje znak nowej linii i zwiększa również funkcję outer. Słowo kluczowe printUpto() posiada pętlę, która wypisuje wszystkie liczby pomiędzy 1 a przekazaną wartością.
Dlatego, gdy outer wynosi 1, treść pętli wywołuje printUpto(1), która wypisuje liczbę 1. Następnie treść pętli wypisuje znak nowej linii i zwiększa outer. Teraz outer wynosi 2. Treść pętli wykonuje się ponownie, wywołując printUpto(2), która drukuje 1 2. Treść pętli wypisuje kolejny znak nowej linii i zwiększa outer. Kolejne iteracje wywołują printUpto(3), printUpto(4), I printUpto(5).
Dlatego ten program wypisuje:
1 1 2 1 2 3 1 2 3 4 1 2 3 4 5
Zauważ, że jest to forma zagnieżdżonej pętli — w treści zewnętrznej pętli wywołujemy funkcję, która sama ma pętlę. Innymi słowy, dla każdej iteracji pętli zewnętrznej wykonywana jest pętla funkcji.
Przejdźmy teraz do bardziej zagmatwanego przykładu:
#include <iostream>
int main()
{
// outer loops between 1 and 5
int outer{ 1 };
while (outer <= 5)
{
// For each iteration of the outer loop, the code in the body of the loop executes once
// inner loops between 1 and outer
// note: inner will be created and destroyed at the end of the block
int inner{ 1 };
while (inner <= outer)
{
std::cout << inner << ' ';
++inner;
}
// print a newline at the end of each row
std::cout << '\n';
++outer;
} // inner destroyed here
return 0;
}Ten program ma dokładnie takie same dane wyjściowe:
1 1 2 1 2 3 1 2 3 4 1 2 3 4 5
Ponieważ mamy pętlę while zagnieżdżoną bezpośrednio w innej pętli while, wygląda to nieco bardziej zagmatwanie. Jednak jedyne, co zrobiliśmy, to umieściliśmy kod, który znajdował się wewnątrz funkcji printUpto() bezpośrednio w ciele zewnętrznej pętli.
Przyjrzyjmy się, jak to działa bardziej szczegółowo.
Najpierw mamy zewnętrzną pętlę (ze zmienną pętli outer), która wykona 5 pętli (z outer posiadaniem wartości 1, 2, 3, 4, I 5 sumiennie).
W pierwszej iteracji pętla zewnętrzna, outer ma wartość 1, a następnie wykonywany jest korpus pętli zewnętrznej. Wewnątrz korpusu pętli zewnętrznej mamy kolejną pętlę ze zmienną pętli inner. Wewnętrzna pętla wykonuje iterację od 1 Do outer (która ma wartość 1), zatem ta wewnętrzna pętla wykona się raz, wypisując wartość 1. Następnie drukujemy znak nowej linii i inkrementujemy outer Do 2.
W drugiej iteracji zewnętrznej pętli, outer ma wartość 2, po czym wykonywany jest korpus pętli zewnętrznej. Wewnątrz ciała zewnętrznej pętli inner ponownie iteruje od 1 Do outer (która ma teraz wartość 2), więc ta wewnętrzna pętla wykona się dwukrotnie, wypisując wartości 1 i 2. Następnie drukujemy znak nowej linii i inkrementujemy outer Do 3.
Proces ten jest kontynuowany, a wewnętrzna pętla drukuje 1 2 3, 1 2 3 4, I 1 2 3 4 5 w kolejnych przejściach. Ostatecznie outer zwiększy się do 6 i ponieważ warunek pętli zewnętrznej (outer <= 5) jest wtedy fałszywy, pętla zewnętrzna jest zakończona. Następnie program się kończy.
Jeśli nadal jest to dla Ciebie zagmatwane, dobrym sposobem na lepsze zrozumienie tego, co się dzieje, jest przechodzenie przez program linia po linii w debugerze i obserwowanie wartości inner i outer , aby lepiej zrozumieć, co się dzieje.
Czas quizu
Pytanie nr 1
Dlaczego w powyższym programie zmienna inner jest deklarowana w bloku while, a nie bezpośrednio po deklaracji of outer?
Pytanie nr 2
Napisz program, który wypisze litery od a do z wraz z ich kodami ASCII. Użyj zmiennej pętli typu char.
Pytanie nr 3
Odwróć przykład zagnieżdżonych pętli, aby wydrukował następujący obraz:
5 4 3 2 1 4 3 2 1 3 2 1 2 1 1
Pytanie nr 4
Teraz spraw, aby liczby były drukowane w ten sposób:
1
2 1
3 2 1
4 3 2 1
5 4 3 2 1
Wskazówka: najpierw wymyśl, jak to wydrukować w ten sposób:
X X X X 1 X X X 2 1 X X 3 2 1 X 4 3 2 1 5 4 3 2 1

