16.6 -- Tablice i pętle

W lekcji wprowadzającej do tego rozdziału (16.1 — Wprowadzenie do kontenerów i tablic) przedstawiliśmy wyzwania związane ze skalowalnością, które pojawiają się, gdy mamy wiele powiązanych zmiennych indywidualnych. Podczas tej lekcji ponownie przedstawimy problem, a następnie omówimy, w jaki sposób tablice mogą pomóc nam w eleganckim rozwiązaniu tego typu problemów.

Ponowne spojrzenie na wyzwanie związane ze skalowalnością zmiennych

Rozważmy przypadek, w którym chcemy znaleźć średni wynik testu dla klasy uczniów. Aby zachować zwięzłość tych przykładów, załóżmy, że klasa liczy tylko 5 uczniów.

Oto jak możemy rozwiązać ten problem za pomocą poszczególnych zmiennych:

#include <iostream>

int main()
{
    // allocate 5 integer variables (each with a different name)
    int testScore1{ 84 };
    int testScore2{ 92 };
    int testScore3{ 76 };
    int testScore4{ 81 };
    int testScore5{ 56 };

    int average { (testScore1 + testScore2 + testScore3 + testScore4 + testScore5) / 5 };

    std::cout << "The class average is: " << average << '\n';

    return 0;
}

To dużo zmiennych i dużo pisania. Wyobraź sobie, ile pracy musielibyśmy wykonać dla 30 uczniów, czyli 600. Co więcej, jeśli zostanie dodany nowy wynik testu, należy zadeklarować, zainicjować i dodać nową zmienną do obliczenia średniej. Czy pamiętałeś o aktualizacji dzielnika? Jeśli zapomniałeś, masz teraz błąd semantyczny. Za każdym razem, gdy musisz zmodyfikować istniejący kod, ryzykujesz wprowadzeniem błędów.

Wiesz już, że powinniśmy używać tablicy, gdy mamy kilka powiązanych zmiennych. Zastąpmy więc nasze indywidualne zmienne a std::vector:

#include <iostream>
#include <vector>

int main()
{
    std::vector testScore { 84, 92, 76, 81, 56 };
    std::size_t length { testScore.size() };
    
    int average { (testScore[0] + testScore[1] + testScore[2] + testScore[3] + testScore[4])
        / static_cast<int>(length) };

    std::cout << "The class average is: " << average << '\n';

    return 0;
}

Tak jest lepiej — znacząco zmniejszyliśmy liczbę zdefiniowanych zmiennych, a dzielnik do obliczenia średniej jest teraz określany bezpośrednio na podstawie długości tablicy.

Ale obliczenie średniej nadal stanowi problem, ponieważ musimy ręcznie wyszczególniać każdy element z osobna. A ponieważ musimy jawnie wymienić każdy element, nasze obliczenia średniej działają tylko w przypadku tablic zawierających dokładnie tyle elementów, ile wymieniliśmy. Jeśli chcemy mieć także możliwość uśredniania tablic o innych długościach, będziemy musieli napisać nowe obliczenia średniej dla każdej innej długości tablicy.

Naprawdę potrzebujemy sposobu na dostęp do każdego elementu tablicy bez konieczności jawnego wymieniania każdego z nich.

Tablice i pętle

W poprzednich lekcjach zauważyliśmy, że indeksy dolne tablicy nie muszą być wyrażeniami stałymi — mogą to być wyrażenia w czasie wykonywania. Oznacza to, że możemy użyć wartości zmiennej jako indeksu.

Zauważ także, że indeksy tablicy użyte w obliczeniach średniej w poprzednim przykładzie to sekwencja rosnąca: 0, 1, 2, 3, 4. Zatem gdybyśmy mieli jakiś sposób na ustawienie zmiennej na wartości 0, 1, 2, 3 i 4, moglibyśmy po prostu użyć tej zmiennej jako indeksu naszej tablicy, a nie literałów. I już wiemy, jak to zrobić -- za pomocą pętli for.

Powiązana treść

Pętle omówiliśmy w lekcji 8.10 -- Dla instrukcji.

Przepiszmy powyższy przykład, używając pętli for, gdzie zmienna pętli jest używana jako indeks tablicy:

#include <iostream>
#include <vector>

int main()
{
    std::vector testScore { 84, 92, 76, 81, 56 };
    std::size_t length { testScore.size() };

    int average { 0 };
    for (std::size_t index{ 0 }; index < length; ++index) // index from 0 to length-1
        average += testScore[index];                      // add the value of element with index `index`
    average /= static_cast<int>(length);                  // calculate the average

    std::cout << "The class average is: " << average << '\n';

    return 0;
}

To powinno być całkiem proste. index zaczyna się w 0, testScore[0] jest dodawane do average, I index jest zwiększane do 1. testScore[1] jest dodawane do average, I index zwiększy się do 2. Ostatecznie, gdy index zwiększy się do 5, index < length jest fałszywe i pętla się zakończy.

W tym momencie pętla dodała wartości testScore[0], testScore[1], testScore[2], testScore[3], I testScore[4] Do average.

Na koniec obliczamy naszą średnią dzieląc skumulowane wartości przez długość tablicy.

To rozwiązanie jest idealne pod względem łatwości konserwacji. Liczba iteracji pętli jest określana na podstawie długości tablicy, a zmienna pętli służy do wykonania całego indeksowania tablicy. Nie musimy już ręcznie wypisywać każdego elementu tablicy.

Jeśli chcemy dodać lub usunąć wynik testu, możemy po prostu zmodyfikować liczbę inicjatorów tablicy, a reszta kodu będzie nadal działać bez dalszych zmian!

Dostęp do każdego elementu kontenera w określonej kolejności nazywany jest przechodzeniem lub przechodzeniem kontenerem. Przechodzenie jest często nazywane iteracją lub iteracją po lub iteracją po kontenerze.

Nota autora

Ponieważ klasy kontenerów używają typu size_t dla długości i indeksów, w tej lekcji zrobimy to samo. Używanie długości i indeksów ze znakiem omówimy na nadchodzącej lekcji 16.7 — Tablice, pętle i rozwiązania w zakresie wyzwań związanych ze znakami.

Szablony, tablice i pętle odblokowują skalowalność

Tablice umożliwiają przechowywanie wielu obiektów bez konieczności nazywania każdego elementu.

Pętle umożliwiają przeglądanie tablicy bez konieczności jawnego wymieniania każdego elementu.

Szablony umożliwiają parametryzację typu elementu.

W sumie szablony, tablice i pętle pozwalają nam napisać kod, który może operować na kontenerze elementów, niezależnie od typu elementu lub liczby elementów w kontenerze!

Aby to lepiej zilustrować, przepiszmy powyższy przykład, refaktoryzując średnie obliczenia do funkcji szablon:

#include <iostream>
#include <vector>

// Function template to calculate the average of the values in a std::vector
template <typename T>
T calculateAverage(const std::vector<T>& arr)
{
    std::size_t length { arr.size() };
    
    T average { 0 };                                      // if our array has elements of type T, our average should have type T too
    for (std::size_t index{ 0 }; index < length; ++index) // iterate through all the elements
        average += arr[index];                            // sum up all the elements
    average /= static_cast<int>(length);                  // divide by count of items (integral in nature)
    
    return average;
}

int main()
{
    std::vector class1 { 84, 92, 76, 81, 56 };
    std::cout << "The class 1 average is: " << calculateAverage(class1) << '\n'; // calc average of 5 ints

    std::vector class2 { 93.2, 88.6, 64.2, 81.0 };
    std::cout << "The class 2 average is: " << calculateAverage(class2) << '\n'; // calc average of 4 doubles
    
    return 0;
}

Wypisuje:

The class 1 average is: 77
The class 2 average is: 81.75

W powyższym przykładzie utworzyliśmy funkcję szablon calculateAverage(), która pobiera std::vector dowolnego typu elementu i dowolnej długości i zwraca średnią. W main() pokazujemy, że funkcja ta działa równie dobrze, gdy jest wywoływana z tablicą 5 int elementów lub tablicą 4 double elementów!

calculateAverage() będzie działać dla dowolnego typu T obsługującego operatory użyte wewnątrz funkcji (operator+=(T), operator/=(int)). Jeśli spróbujesz użyć T , który nie obsługuje tych operatorów, kompilator wystąpi błąd podczas próby kompilacji szablonu funkcji utworzonej.

Możesz się zastanawiać, dlaczego rzutujemy length na int a nie a T. Obliczając średnią, dzielimy sumę przez liczbę elementów. Liczba elementów jest wartością całkowitą. Dlatego z semantycznego punktu widzenia bardziej sensowne jest dzielenie przez int.

Co możemy zrobić za pomocą tablic i pętli

Teraz, gdy wiemy, jak poruszać się po kontenerze elementów za pomocą pętli, przyjrzyjmy się najczęstszym rzeczom, do których możemy wykorzystać przechodzenie przez kontener. Zwykle przemierzamy kontener, aby wykonać jedną z czterech rzeczy:

  1. Obliczyć nową wartość na podstawie wartości istniejących elementów (np. wartości średniej, sumy wartości).
  2. Wyszukać istniejący element (np. ma dokładne dopasowanie, policzyć liczbę dopasowań, znaleźć najwyższą wartość).
  3. Operuj na każdym elemencie (np. wyprowadź każdy element, pomnóż wszystkie elementy przez 2).
  4. Zmień kolejność elementów (np. posortuj elementy w kolejności rosnącej).

Pierwsze trzy z nich są dość proste. Za pomocą pojedynczej pętli możemy przeglądać tablicę, sprawdzając lub modyfikując odpowiednio każdy element.

Zmiana kolejności elementów kontenera jest nieco trudniejsza, ponieważ zwykle wiąże się to z użyciem pętli wewnątrz innej pętli. Chociaż możemy to zrobić ręcznie, zwykle lepiej jest użyć do tego istniejącego algorytmu ze standardowej biblioteki. Omówimy to bardziej szczegółowo w następnym rozdziale, kiedy będziemy omawiać algorytmy.

Tablice i błędy pojedyncze

Podczas przechodzenia przez kontener za pomocą indeksu musisz zadbać o to, aby pętla wykonała się odpowiednią liczbę razy. Błędy co do jednego (gdzie ciało pętli wykonuje się o jeden raz za dużo lub o jeden za mało) są łatwe do popełnienia.

Zazwyczaj podczas przechodzenia przez kontener za pomocą indeksu rozpoczynamy indeks od 0 i wykonujemy pętlę aż do index < length.

Nowi programiści czasami przypadkowo używają index <= length jako warunku pętli. Spowoduje to wykonanie pętli gdy index == length, co spowoduje przekroczenie zakresu indeksu dolnego i niezdefiniowane zachowanie.

Czas quizu

Pytanie nr 1

Napisz krótki program, który za pomocą pętli wypisuje na ekran elementy następującego wektora:

#include <iostream>
#include <vector>

int main()
{
    std::vector arr{ 4, 6, 7, 3, 8, 2, 1, 9 };

    // Add your code here

    return 0;
}

Wyniki powinny wyglądać następująco:

4 6 7 3 8 2 1 9

Pokaż rozwiązanie

Pytanie nr 2

Zaktualizuj kod poprzedniego rozwiązania quizu, tak aby następujący program się skompilował i dał takie same wyniki:

#include <iostream>
#include <vector>

// Implement printArray() here

int main()
{
    std::vector arr{ 4, 6, 7, 3, 8, 2, 1, 9 };

    printArray(arr); // use function template to print array

    return 0;
}

Pokaż rozwiązanie

Pytanie nr 3

Biorąc pod uwagę rozwiązanie quizu 2, wykonaj następujące czynności:

  • Poproś użytkownika o podanie wartości od 1 do 9. Jeśli użytkownik nie wprowadzi wartości od 1 do 9, wielokrotnie pytaj o wartość całkowitą, aż to zrobi. Jeśli użytkownik wprowadzi liczbę, po której następują dodatkowe dane, zignoruj ​​je.
  • Wydrukuj tablicę.
  • Napisz szablon funkcji, aby przeszukać tablicę pod kątem wartości wprowadzonej przez użytkownika. Jeśli wartość znajduje się w tablicy, zwróć indeks tego elementu. Jeśli wartości nie ma w tablicy, zwróć odpowiednią wartość.
  • Jeśli wartość została znaleziona, wydrukuj wartość i indeks. Jeśli wartość nie została znaleziona, wydrukuj ją i informację, że nie została znaleziona.

Na lekcji omawiamy sposób postępowania z nieprawidłowymi danymi wejściowymi 9.5 — std::cin i obsługa nieprawidłowych danych wejściowych.

Oto dwa przykładowe uruchomienia tego programu:

Enter a number between 1 and 9: d
Enter a number between 1 and 9: 6
4 6 7 3 8 2 1 9
The number 6 has index 1
Enter a number between 1 and 9: 5
4 6 7 3 8 2 1 9
The number 5 was not found

Pokaż rozwiązanie

Pytanie nr 4

Dodatkowe punkty: Zmodyfikuj poprzedni program tak, aby mógł obsłużyć std::vector zawierające wartości liczbowe inne niż int, takie jak as std::vector arr{ 4.4, 6.6, 7.7, 3.3, 8.8, 2.2, 1.1, 9.9 };.

Pokaż rozwiązanie

Pytanie #5

Napisz szablon funkcji, aby znaleźć największą wartość w std::vector. Jeśli wektor jest pusty, zwróć wartość domyślną dla typu elementu.

Należy wykonać następujący kod:

int main()
{
    std::vector data1 { 84, 92, 76, 81, 56 };
    std::cout << findMax(data1) << '\n';

    std::vector data2 { -13.0, -26.7, -105.5, -14.8 };
    std::cout << findMax(data2) << '\n';

    std::vector<int> data3 { };
    std::cout << findMax(data3) << '\n';
    
    return 0;
}

I wydrukować następujący wynik:

92
-13
0

Pokaż wskazówkę

Pokaż rozwiązanie

Pytanie nr 6

W quizie na lekcję 8.10 -- Dla instrukcji zaimplementowaliśmy grę o nazwie FizzBuzz dla liczb trzy, pięć i siedem.

W tym quizie zaimplementuj grę w następujący sposób:

  • Liczby podzielne tylko przez 3 powinny zostać wydrukowane jako „fizz”.
  • Liczby podzielne tylko przez 5 powinny zostać wydrukowane jako „buzz”.
  • Liczby podzielne tylko przez 7 powinny zostać wydrukowane jako „pop”.
  • Liczby podzielne tylko przez 11 powinny zostać wydrukowane „bang”.
  • Liczby podzielne tylko przez 13 powinny zostać wydrukowane „jazz”.
  • Liczby podzielne tylko przez 17 powinny zostać wydrukowane „pow”.
  • Liczby podzielne tylko przez 19 powinny zostać wydrukowane „boom”.
  • Liczby podzielne przez więcej niż jedno z powyższych powinny wydrukować każde ze słów powiązanych z jej dzielniki.
  • Liczby, których nie można podzielić przez żaden z powyższych, powinny po prostu wydrukować liczbę.

Użyj std::vector do przechowywania dzielników, a kolejne std::vector do przechowywania słów (jak wpisz std::string_view). Jeśli tablice nie mają tej samej długości, program powinien potwierdzić. Utwórz wynik dla 150 liczb.

Pokaż wskazówkę

Pokaż wskazówkę

Oto oczekiwany wynik z pierwszych 21 iteracji:

1
2
fizz
4
buzz
fizz
pop
8
fizz
buzz
bang
fizz
jazz
pop
fizzbuzz
16
pow
fizz
boom
buzz
fizzpop

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:  
837 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze