Szukaj…
Wprowadzenie
Instrukcja pętli wykonuje grupę instrukcji wielokrotnie, aż do spełnienia warunku. Istnieją 3 typy prymitywnych pętli w C ++: for, while i do ... while.
Składnia
- instrukcja while ( warunek );
- wykonaj instrukcję while ( wyrażenie );
- dla (for-Init-oświadczenie; warunek; wyrażenie) instrukcja;
- instrukcja for ( for-range-deklaration : for-range-initializer );
- przerwa ;
- kontyntynuj ;
Uwagi
wywołania algorithm
są na ogół lepsze niż ręcznie pisane pętle.
Jeśli chcesz czegoś, co algorytm już robi (lub czegoś bardzo podobnego), wywołanie algorytmu jest wyraźniejsze, często bardziej wydajne i mniej podatne na błędy.
Jeśli potrzebujesz pętli, która robi coś dość prostego (ale wymagałaby mylącego plątania segregatorów i adapterów, jeśli korzystasz z algorytmu), po prostu napisz pętlę.
Na podstawie zakresu dla
for
pętli można używać do iteracji elementów zakresu opartego na iteratorze, bez użycia indeksu numerycznego lub bezpośredniego dostępu do iteratorów:
vector<float> v = {0.4f, 12.5f, 16.234f};
for(auto val: v)
{
std::cout << val << " ";
}
std::cout << std::endl;
Spowoduje to iterację każdego elementu w v
, a val
otrzyma wartość bieżącego elementu. Następujące oświadczenie:
for (for-range-declaration : for-range-initializer ) statement
jest równa:
{
auto&& __range = for-range-initializer;
auto __begin = begin-expr, __end = end-expr;
for (; __begin != __end; ++__begin) {
for-range-declaration = *__begin;
statement
}
}
{
auto&& __range = for-range-initializer;
auto __begin = begin-expr;
auto __end = end-expr; // end is allowed to be a different type than begin in C++17
for (; __begin != __end; ++__begin) {
for-range-declaration = *__begin;
statement
}
}
Ta zmiana została wprowadzona w związku z planowanym wsparciem dla Ranges TS w C ++ 20.
W takim przypadku nasza pętla odpowiada:
{
auto&& __range = v;
auto __begin = v.begin(), __end = v.end();
for (; __begin != __end; ++__begin) {
auto val = *__begin;
std::cout << val << " ";
}
}
Zauważ, że auto val
deklaruje typ wartości, który będzie kopią wartości przechowywanej w zakresie (inicjalizujemy kopiowanie z iteratora w trakcie pracy). Jeśli wartości przechowywane w zakresie są kosztowne do skopiowania, możesz użyć const auto &val
. Nie musisz także używać auto
; możesz użyć odpowiedniej nazwy typu, o ile jest ona domyślnie konwertowana z typu wartości zakresu.
Jeśli potrzebujesz dostępu do iteratora, funkcja oparta na zakresie nie może ci pomóc (przynajmniej bez pewnego wysiłku).
Jeśli chcesz się do niego odwołać, możesz to zrobić:
vector<float> v = {0.4f, 12.5f, 16.234f};
for(float &val: v)
{
std::cout << val << " ";
}
Można iteracyjne na const
odniesienia, jeśli masz const
pojemnik:
const vector<float> v = {0.4f, 12.5f, 16.234f};
for(const float &val: v)
{
std::cout << val << " ";
}
Można by użyć referencji przekazujących, gdy iterator sekwencji zwraca obiekt proxy i trzeba operować na tym obiekcie w sposób const
. Uwaga: najprawdopodobniej dezorientuje czytelników twojego kodu.
vector<bool> v(10);
for(auto&& val: v)
{
val = true;
}
Typ „zasięgu” dla parametru opartego for
może być jednym z następujących:
Tablice językowe:
float arr[] = {0.4f, 12.5f, 16.234f}; for(auto val: arr) { std::cout << val << " "; }
Pamiętaj, że przydzielenie tablicy dynamicznej nie ma znaczenia:
float *arr = new float[3]{0.4f, 12.5f, 16.234f}; for(auto val: arr) //Compile error. { std::cout << val << " "; }
Każdy typ, który ma funkcje
begin()
iend()
, które zwracają iteratory do elementów tego typu. Standardowe kontenery biblioteczne kwalifikują się, ale można również użyć typów zdefiniowanych przez użytkownika:struct Rng { float arr[3]; // pointers are iterators const float* begin() const {return &arr[0];} const float* end() const {return &arr[3];} float* begin() {return &arr[0];} float* end() {return &arr[3];} }; int main() { Rng rng = {{0.4f, 12.5f, 16.234f}}; for(auto val: rng) { std::cout << val << " "; } }
Dowolny typ, który ma funkcje
begin(type)
iend(type)
które nie są członkami, które można znaleźć poprzez wyszukiwanie zależne od argumentów, w zależności odtype
. Jest to przydatne do tworzenia typu zakresu bez konieczności modyfikacji samego typu klasy:namespace Mine { struct Rng {float arr[3];}; // pointers are iterators const float* begin(const Rng &rng) {return &rng.arr[0];} const float* end(const Rng &rng) {return &rng.arr[3];} float* begin(Rng &rng) {return &rng.arr[0];} float* end(Rng &rng) {return &rng.arr[3];} } int main() { Mine::Rng rng = {{0.4f, 12.5f, 16.234f}}; for(auto val: rng) { std::cout << val << " "; } }
Dla pętli
Pętla for
wykonuje instrukcje w loop body
, podczas gdy condition
pętli jest prawdziwy. Zanim instrukcja initialization statement
pętli zostanie wykonana dokładnie raz. Po każdym cyklu iteration execution
jest część iteration execution
.
Pętla for
jest zdefiniowana następująco:
for (/*initialization statement*/; /*condition*/; /*iteration execution*/)
{
// body of the loop
}
Objaśnienie instrukcji zastępczych:
-
initialization statement
: Ta instrukcja jest wykonywana tylko raz, na początku pętlifor
. Możesz wprowadzić deklarację wielu zmiennych jednego typu, takich jakint i = 0, a = 2, b = 3
. Te zmienne są poprawne tylko w zakresie pętli. Zmienne zdefiniowane przed pętlą o tej samej nazwie są ukryte podczas wykonywania pętli. -
condition
: instrukcja ta jest oceniana przed wykonaniem każdego elementu pętli i przerywa pętlę, jeśli ma wartośćfalse
. -
iteration execution
: Ta instrukcja jest wykonywana po treści pętli, przed następną oceną warunku , chyba że pętlafor
zostanie przerwana w ciele (przezbreak
,goto
,return
lub zgłoszony wyjątek). Możesz wprowadzić wiele instrukcji w częściiteration execution
, takich jaka++, b+=10, c=b+a
.
Surowa odpowiednik for
pętli przepisany jako while
pętla jest:
/*initialization*/
while (/*condition*/)
{
// body of the loop; using 'continue' will skip to increment part below
/*iteration execution*/
}
Najczęstszym przypadkiem użycia pętli for
jest wykonywanie instrukcji określoną liczbę razy. Rozważ na przykład:
for(int i = 0; i < 10; i++) {
std::cout << i << std::endl;
}
Prawidłowa pętla to również:
for(int a = 0, b = 10, c = 20; (a+b+c < 100); c--, b++, a+=c) {
std::cout << a << " " << b << " " << c << std::endl;
}
Przykładem ukrywania zadeklarowanych zmiennych przed pętlą jest:
int i = 99; //i = 99
for(int i = 0; i < 10; i++) { //we declare a new variable i
//some operations, the value of i ranges from 0 to 9 during loop execution
}
//after the loop is executed, we can access i with value of 99
Ale jeśli chcesz użyć już zadeklarowanej zmiennej i nie ukrywać jej, to pomiń część deklaracyjną:
int i = 99; //i = 99
for(i = 0; i < 10; i++) { //we are using already declared variable i
//some operations, the value of i ranges from 0 to 9 during loop execution
}
//after the loop is executed, we can access i with value of 10
Uwagi:
- Instrukcje inicjalizacji i inkrementacji mogą wykonywać operacje niezwiązane z instrukcją warunku lub w ogóle nic - jeśli chcesz to zrobić. Jednak ze względu na czytelność najlepszą praktyką jest wykonywanie operacji bezpośrednio związanych z pętlą.
- Zmienna zadeklarowana w instrukcji inicjalizacji jest widoczna tylko w zakresie pętli
for
i jest zwalniana po zakończeniu pętli. - Nie zapominaj, że zmienna zadeklarowana w instrukcji
initialization statement
może być modyfikowana podczas pętli, a także zmienna sprawdzana wcondition
.
Przykład pętli liczącej od 0 do 10:
for (int counter = 0; counter <= 10; ++counter)
{
std::cout << counter << '\n';
}
// counter is not accessible here (had value 11 at the end)
Objaśnienie fragmentów kodu:
-
int counter = 0
inicjujecounter
zmiennej na 0. (Tej zmiennej można używać tylko wewnątrz pętlifor
.) -
counter <= 10
to warunek logiczny, który sprawdza, czycounter
jest mniejszy lub równy 10. Jeśli totrue
, pętla jest wykonywana. Jeśli jest tofalse
, pętla się kończy. -
++counter
to operacja inkrementacji, która zwiększa wartośćcounter
o 1 przed następnym sprawdzaniem warunków.
Pozostawiając wszystkie instrukcje puste, możesz utworzyć nieskończoną pętlę:
// infinite loop
for (;;)
std::cout << "Never ending!\n";
while
odpowiednik pętli powyższego jest następujący:
// infinite loop
while (true)
std::cout << "Never ending!\n";
Jednak nieskończoną pętlę można nadal opuścić, używając instrukcji break
, goto
lub return
lub zgłaszając wyjątek.
Następnym częstym przykładem iteracji po wszystkich elementach z kolekcji STL (np. vector
) bez użycia nagłówka <algorithm>
jest:
std::vector<std::string> names = {"Albert Einstein", "Stephen Hawking", "Michael Ellis"};
for(std::vector<std::string>::iterator it = names.begin(); it != names.end(); ++it) {
std::cout << *it << std::endl;
}
Podczas pętli
while
wykonuje pętli wielokrotnie oświadczenia aż podanych analizuje schemat Warunkiem false
. Ta instrukcja sterująca jest używana, gdy nie wiadomo z góry, ile razy blok kodu ma zostać wykonany.
Na przykład, aby wydrukować wszystkie liczby od 0 do 9, można użyć następującego kodu:
int i = 0;
while (i < 10)
{
std::cout << i << " ";
++i; // Increment counter
}
std::cout << std::endl; // End of line; "0 1 2 3 4 5 6 7 8 9" is printed to the console
Zauważ, że od C ++ 17 pierwsze 2 instrukcje można łączyć
while (int i = 0; i < 10)
//... The rest is the same
Aby utworzyć nieskończoną pętlę, można użyć następującej konstrukcji:
while (true)
{
// Do something forever (however, you can exit the loop by calling 'break'
}
Istnieje inny wariant pętli while
, a mianowicie konstrukcja do...while
. Zobacz przykład pętli „do-while”, aby uzyskać więcej informacji.
Deklaracja zmiennych w warunkach
W warunkach pętli for
i while
dozwolone jest także deklarowanie obiektu. Ten obiekt będzie uważany za objęty zakresem do końca pętli i będzie trwał przez każdą iterację pętli:
for (int i = 0; i < 5; ++i) {
do_something(i);
}
// i is no longer in scope.
for (auto& a : some_container) {
a.do_something();
}
// a is no longer in scope.
while(std::shared_ptr<Object> p = get_object()) {
p->do_something();
}
// p is no longer in scope.
Jednak nie można robić tego samego z pętlą do...while
while; zamiast tego zadeklaruj zmienną przed pętlą i (opcjonalnie) dołącz zarówno zmienną, jak i pętlę do zakresu lokalnego, jeśli chcesz, aby zmienna wychodziła poza zakres po zakończeniu pętli:
//This doesn't compile
do {
s = do_something();
} while (short s > 0);
// Good
short s;
do {
s = do_something();
} while (s > 0);
Wynika to z tego, że część instrukcji pętli do...while
while (ciało pętli) jest oceniana przed osiągnięciem części wyrażenia ( while
), a zatem żadna deklaracja w wyrażeniu nie będzie widoczna podczas pierwszej iteracji pętla.
Pętla „do-while”
Do-while jest bardzo podobna do pętli while, z wyjątkiem, że warunek jest sprawdzany na końcu każdego cyklu, a nie na początku. Pętla gwarantuje zatem wykonanie co najmniej raz.
Poniższy kod wyświetli 0
, ponieważ warunek będzie miał wartość false
na końcu pierwszej iteracji:
int i =0;
do
{
std::cout << i;
++i; // Increment counter
}
while (i < 0);
std::cout << std::endl; // End of line; 0 is printed to the console
Uwaga: Nie zapomnij o średniku na końcu while(condition);
, co jest potrzebne w konstrukcji do-while .
W przeciwieństwie do pętli „ do- while”, poniższe polecenie nic nie wydrukuje, ponieważ warunek ma wartość false
na początku pierwszej iteracji:
int i =0;
while (i < 0)
{
std::cout << i;
++i; // Increment counter
}
std::cout << std::endl; // End of line; nothing is printed to the console
Uwaga: podczas gdy pętla może zostać zakończony bez warunek staje się fałszywy, stosując break
, goto
, lub return
oświadczenie.
int i = 0;
do
{
std::cout << i;
++i; // Increment counter
if (i > 5)
{
break;
}
}
while (true);
std::cout << std::endl; // End of line; 0 1 2 3 4 5 is printed to the console
Trywialna pętla „ do- while” jest również czasami używana do pisania makr, które wymagają własnego zakresu (w takim przypadku średnik końcowy jest pomijany w definicji makra i musi być dostarczony przez użytkownika):
#define BAD_MACRO(x) f1(x); f2(x); f3(x);
// Only the call to f1 is protected by the condition here
if (cond) BAD_MACRO(var);
#define GOOD_MACRO(x) do { f1(x); f2(x); f3(x); } while(0)
// All calls are protected here
if (cond) GOOD_MACRO(var);
Instrukcje kontroli pętli: Przerwij i kontynuuj
Instrukcje sterujące pętlą służą do zmiany przepływu wykonania z jego normalnej sekwencji. Gdy wykonanie opuszcza zakres, wszystkie obiekty automatyczne utworzone w tym zakresie są niszczone. break
i continue
są instrukcjami sterującymi pętli.
Instrukcja break
kończy pętlę bez dalszego rozpatrywania.
for (int i = 0; i < 10; i++)
{
if (i == 4)
break; // this will immediately exit our loop
std::cout << i << '\n';
}
Powyższy kod zostanie wydrukowany:
1
2
3
Instrukcja continue
nie kończy natychmiast pętli, lecz pomija resztę treści pętli i przechodzi na jej szczyt (łącznie ze sprawdzeniem warunku).
for (int i = 0; i < 6; i++)
{
if (i % 2 == 0) // evaluates to true if i is even
continue; // this will immediately go back to the start of the loop
/* the next line will only be reached if the above "continue" statement
does not execute */
std::cout << i << " is an odd number\n";
}
Powyższy kod zostanie wydrukowany:
1 is an odd number
3 is an odd number
5 is an odd number
Ponieważ takie zmiany w przepływie kontroli są czasem trudne do zrozumienia dla ludzi, break
i continue
są stosowane oszczędnie. Prostsza implementacja jest zwykle łatwiejsza do odczytania i zrozumienia. Na przykład pierwsza pętla for
z powyższym break
może zostać przepisana jako:
for (int i = 0; i < 4; i++)
{
std::cout << i << '\n';
}
Drugi przykład z continue
można przepisać jako:
for (int i = 0; i < 6; i++)
{
if (i % 2 != 0) {
std::cout << i << " is an odd number\n";
}
}
Zasięg dla ponad zakresu
Korzystając z pętli bazowych zakresu, można zapętlać pod-część danego kontenera lub innego zakresu, generując obiekt proxy, który kwalifikuje się do pętli opartych na zakresie.
template<class Iterator, class Sentinel=Iterator>
struct range_t {
Iterator b;
Sentinel e;
Iterator begin() const { return b; }
Sentinel end() const { return e; }
bool empty() const { return begin()==end(); }
range_t without_front( std::size_t count=1 ) const {
if (std::is_same< std::random_access_iterator_tag, typename std::iterator_traits<Iterator>::iterator_category >{} ) {
count = (std::min)(std::size_t(std::distance(b,e)), count);
}
return {std::next(b, count), e};
}
range_t without_back( std::size_t count=1 ) const {
if (std::is_same< std::random_access_iterator_tag, typename std::iterator_traits<Iterator>::iterator_category >{} ) {
count = (std::min)(std::size_t(std::distance(b,e)), count);
}
return {b, std::prev(e, count)};
}
};
template<class Iterator, class Sentinel>
range_t<Iterator, Sentinel> range( Iterator b, Sentinal e ) {
return {b,e};
}
template<class Iterable>
auto range( Iterable& r ) {
using std::begin; using std::end;
return range(begin(r),end(r));
}
template<class C>
auto except_first( C& c ) {
auto r = range(c);
if (r.empty()) return r;
return r.without_front();
}
teraz możemy zrobić:
std::vector<int> v = {1,2,3,4};
for (auto i : except_first(v))
std::cout << i << '\n';
i wydrukuj
2
3
4
Należy pamiętać, że obiekty pośrednie wygenerowane w części for(:range_expression)
pętli for
wygasną do momentu uruchomienia pętli for
.