Wyzwanie związane ze skalowalnością zmiennych
Rozważmy scenariusz, w którym chcemy zarejestrować wyniki testów dla 30 uczniów i obliczyć średni wynik dla klasy. Aby to zrobić, będziemy potrzebować 30 zmiennych. Moglibyśmy zdefiniować je w ten sposób:
// allocate 30 integer variables (each with a different name)
int testScore1 {};
int testScore2 {};
int testScore3 {};
// ...
int testScore30 {};To wiele zmiennych do zdefiniowania! Aby obliczyć średni wynik dla klasy, musielibyśmy zrobić coś takiego:
int average { (testScore1 + testScore2 + testScore3 + testScore4 + testScore5
+ testScore6 + testScore7 + testScore8 + testScore9 + testScore10
+ testScore11 + testScore12 + testScore13 + testScore14 + testScore15
+ testScore16 + testScore17 + testScore18 + testScore19 + testScore20
+ testScore21 + testScore22 + testScore23 + testScore24 + testScore25
+ testScore26 + testScore27 + testScore28 + testScore29 + testScore30)
/ 30; };To nie tylko dużo pisania, ale także bardzo powtarzalne (i nie byłoby tak trudno wpisać jedną z liczb i tego nie zauważyć). A jeśli chcielibyśmy cokolwiek zrobić z każdą z tych wartości (np. wydrukować je na ekranie), musielibyśmy wprowadzić nazwy każdej z tych zmiennych od nowa.
Załóżmy teraz, że musimy zmodyfikować nasz program, aby uwzględnić innego ucznia, który właśnie został dodany do klasy. Będziemy musieli przeskanować całą bazę kodu i ręcznie dodać testScore31 tam, gdzie to konieczne. Za każdym razem, gdy modyfikujemy istniejący kod, ryzykujemy wprowadzeniem nowych błędów. Na przykład nie byłoby tak trudno zapomnieć o aktualizacji dzielnika przy obliczaniu średniej z 30 Do 31!
A to dotyczy tylko 30 zmiennych. Pomyśl o przypadku, gdy mamy setki lub tysiące obiektów. Kiedy potrzebujemy więcej niż kilku obiektów tego samego typu, zdefiniowanie poszczególnych zmiennych po prostu nie podlega skalowaniu.
Moglibyśmy umieścić nasze dane w strukturze:
struct testScores
{
// allocate 30 integer variables (each with a different name)
int score1 {};
int score2 {};
int score3 {};
// ...
int score30 {};
}Chociaż zapewnia to dodatkową organizację naszych wyników (i pozwala nam łatwiej przekazywać je do funkcji), nie rozwiązuje podstawowego problemu: nadal musimy definiować każdy obiekt wyniku testu i uzyskiwać do niego dostęp indywidualnie.
Jak można się domyślić, C++ ma rozwiązania na powyższe wyzwania. W tym rozdziale przedstawimy jedno z takich rozwiązań. W kolejnych rozdziałach omówimy inne warianty tego rozwiązania.
Pojemniki
Kiedy idziesz do sklepu spożywczego, aby kupić tuzin jaj, (prawdopodobnie) nie wybierasz pojedynczo 12 jaj i nie wkładasz ich do koszyka (nie jesteś, prawda?). Zamiast tego prawdopodobnie wybierzesz pojedynczy karton jajek. Karton to rodzaj pojemnika, w którym mieści się określona liczba jaj (prawdopodobnie 6, 12 lub 24). Teraz rozważ płatki śniadaniowe zawierające wiele małych kawałków płatków. Na pewno nie chciałbyś przechowywać wszystkich tych elementów w swojej spiżarni osobno! Płatki często dostarczane są w pudełku, które jest kolejnym pojemnikiem. W życiu codziennym używamy kontenerów, ponieważ ułatwiają one zarządzanie zbiorami obiektów.
Kontenery istnieją również w programowaniu, aby ułatwić tworzenie i zarządzanie (potencjalnie dużymi) zbiorami obiektów. W ogólnym programowaniu a kontener to typ danych, który zapewnia przechowywanie kolekcji nienazwanych obiektów (zwanych elementami).
Kluczowa informacja
Zazwyczaj używamy kontenerów, gdy musimy pracować z zestawem powiązanych wartości.
Jak się okazuje, używałeś już jednego typu kontenera: ciągów znaków! Kontener ciągów znaków umożliwia przechowywanie kolekcji znaków, które można następnie wyprowadzić w postaci tekstu:
#include <iostream>
#include <string>
int main()
{
std::string name{ "Alex" }; // strings are a container for characters
std::cout << name; // output our string as a sequence of characters
return 0;
}Elementy kontenera są bez nazwy
Podczas gdy sam obiekt kontenera zazwyczaj ma nazwę (w przeciwnym razie jak byśmy jej użyli?), elementy kontenera są bez nazwy. Dzieje się tak, abyśmy mogli umieścić w naszym pojemniku dowolną liczbę elementów, bez konieczności nadawania każdemu elementowi unikalnej nazwy! Ten brak nazwanych elementów jest ważny i to właśnie odróżnia kontenery od innych typów struktur danych. Właśnie dlatego zwykłe struktury (te, które są tylko zbiorem elementów danych, jak nasza testScores struktura powyżej) zazwyczaj nie są uważane za kontenery — ich elementy danych wymagają unikalnych nazw.
W powyższym przykładzie nasz kontener ciągów ma nazwę (name), ale znaki wewnątrz kontenera ('A', 'l', 'e', 'x') nie.
Ale jeśli same elementy są nienazwane, jak możemy uzyskać do nich dostęp? Każdy kontener udostępnia jedną lub więcej metod dostępu do swoich elementów — ale dokładny sposób zależy od typu kontenera. Pierwszy przykład tego zobaczymy na następnej lekcji.
Kluczowa informacja
Elementy kontenera nie mają własnych nazw, więc kontener może mieć tyle elementów, ile chcemy, bez konieczności nadawania każdemu elementowi unikalnej nazwy.
Każdy kontener zapewnia jakąś metodę dostępu do tych elementów, ale sposób zależy od konkretnego typu kontenera.
Długość kontenera
W programowaniu liczba elementów w kontenerze jest często nazywana jest długość (lub czasami liczbą).
W lekcji 5.7 — Wprowadzenie do std::string, pokazaliśmy, jak możemy użyć operatora length funkcją składową std::string aby uzyskać liczbę elementów znakowych w kontenerze ciągu znaków:
#include <iostream>
#include <string>
int main()
{
std::string name{ "Alex" };
std::cout << name << " has " << name.length() << " characters\n";
return 0;
}Wypisuje:
Alex has 4 charactersW C++ termin rozmiar jest również powszechnie używany do określenia liczby elementów w kontenerze. Jest to niefortunny wybór nomenklatury, ponieważ termin „rozmiar” może również odnosić się do liczby bajtów pamięci wykorzystywanych przez obiekt (zwracanej przez operator sizeof ).
Wolimy termin „długość” w odniesieniu do liczby elementów w kontenerze, a terminu „rozmiar” używamy w odniesieniu do ilości pamięci wymaganej przez obiekt.
Kontener operacje
Wróćmy na chwilę do naszego kartonu jaj. Co można zrobić z takiego kartonu? Cóż, najpierw możesz zdobyć karton jajek. Możesz otworzyć karton z jajkami i wybrać jedno z nich, a następnie zrobić z nim, co chcesz. Możesz usunąć istniejące jajko z kartonu lub dodać nowe jajko w puste miejsce. Możesz także policzyć ilość jaj w kartonie.
Podobnie, kontenery zazwyczaj realizują znaczący podzbiór następujących operacji:
- Utwórz pojemnik (np. pusty, z miejscem na pewną początkową liczbę elementów, z listy wartości).
- Dostęp do elementów (np. pobierz pierwszy element, pobierz ostatni element, pobierz dowolny element).
- Wstaw i usuń elementy.
- Uzyskaj liczbę elementów w kontenerze.
Kontenery mogą również udostępniać inne operacje (lub odmiany powyższych), które pomagają w zarządzaniu kolekcją elementów.
Współczesne języki programowania zazwyczaj udostępniają wiele różnych typów kontenerów. Te typy kontenerów różnią się pod względem tego, jakie operacje faktycznie obsługują i jak wydajne są te operacje. Na przykład jeden typ kontenera może zapewniać szybki dostęp do dowolnego elementu w kontenerze, ale nie umożliwia wstawiania ani usuwania elementów. Inny typ kontenera może zapewniać szybkie wstawianie i wyjmowanie elementów, ale umożliwiać dostęp do elementów jedynie w kolejności sekwencyjnej.
Każdy kontener ma zestaw mocnych stron i ograniczeń. Wybór odpowiedniego typu kontenera dla zadania, które próbujesz rozwiązać, może mieć ogromny wpływ zarówno na łatwość konserwacji kodu, jak i ogólną wydajność. Temat ten omówimy szerzej na przyszłej lekcji.
Typy elementów
W większości języków programowania (w tym C++) kontenery jednorodne, co oznacza, że elementy kontenera muszą być tego samego typu.
Niektóre kontenery wykorzystują wstępnie ustawiony typ elementu (np. ciąg znaków zazwyczaj zawiera char elementy), ale częściej typ elementu może ustawić użytkownik kontenera. W C++ kontenery są zwykle implementowane jako szablony klas, dzięki czemu użytkownik może podać żądany typ elementu jako argument typu szablonu. Zobaczymy przykład następnej lekcji.
Dzięki temu kontenery są elastyczne, ponieważ nie musimy tworzyć nowego typu kontenera dla każdego innego typu elementu. Zamiast tego po prostu tworzymy instancję szablonu klasy z żądanym typem elementu i gotowe.
Na marginesie…
Przeciwieństwem jednorodnego kontenera jest heterogeniczny kontener, który pozwala na to, aby elementy były różnych typów. Kontenery heterogeniczne są zazwyczaj obsługiwane przez języki skryptowe (takie jak Python).
Kontenery w C++
Klasa Biblioteka kontenerów jest częścią standardowej biblioteki C++, która zawiera różne typy klas, które implementują niektóre popularne typy kontenerów. Typ klasy implementujący kontener jest czasami nazywany klasą kontenera. Pełna lista kontenerów w bibliotece Containers jest udokumentowana tutaj.
W C++ definicja „kontenera” jest węższa niż ogólna definicja programowania. Tylko typy klas w bibliotece Containers są uważane za kontenery w C++. Będziemy używać terminu „kontener”, gdy będziemy mówić o kontenerach w ogóle, a „klasa kontenera”, gdy będziemy mówić konkretnie o typach klas kontenerów, które są częścią biblioteki Containers.
Dla zaawansowanych czytelników
Poniższe typy są kontenerami w ramach ogólnej definicji programowania, ale nie są uważane za kontenery w standardzie C++:
- tablicami w stylu C
std::stringstd::vector<bool>
Aby kontener był kontenerem w C++, musi spełniać wszystkie wymagania wymieniony tutaj. Należy pamiętać, że te wymagania obejmują implementację pewnych funkcji składowych — oznacza to, że kontenery C++ muszą być typami klasowymi! Wymienione powyżej typy nie realizują wszystkich tych wymagań.
Jednakże std::string i std::vector<bool> realizują większość wymagań, w większości przypadków zachowują się jak kontenery. W rezultacie są one czasami nazywane „pseudokontenerami”.
Spośród dostarczonych klas kontenerów std::vector i std::array są one zdecydowanie najczęściej używane i to na nich skupimy większość naszej uwagi. Pozostałe klasy kontenerów są zwykle używane tylko w bardziej wyspecjalizowanych sytuacjach.
Wprowadzenie do tablic
An array to typ danych kontenera, który przechowuje sekwencję wartości w sposób ciągły (co oznacza, że każdy element jest umieszczany w sąsiedniej lokalizacji pamięci, bez przerw). Tablice umożliwiają szybki, bezpośredni dostęp do dowolnego elementu. Są koncepcyjnie proste i łatwe w użyciu, co czyni je pierwszym wyborem, gdy musimy utworzyć zestaw powiązanych wartości i pracować z nimi. Tablice
C++ zawiera trzy podstawowe typy tablic: tablice (w stylu C), std::vector klasę kontenera i std::array klasę kontenera.
(w stylu C) zostały odziedziczone z języka C. Aby zapewnić kompatybilność wsteczną, tablice te są zdefiniowane jako część podstawowego języka C++ (podobnie jak podstawowe typy danych). Standard C++ nazywa te tablice „tablicami”, ale we współczesnym C++ są one często nazywane tablicami C lub tablicami w stylu C w celu odróżnienia ich od podobnie nazwanych std::array. Tablice w stylu C są czasami nazywane także „tablicami nagimi”, „tablicami o stałym rozmiarze”, „tablicami stałymi” lub „tablicami wbudowanymi”. Będziemy preferować termin „tablica w stylu C” i będziemy używać słowa „tablica” podczas ogólnego omawiania typów tablic. Według współczesnych standardów tablice typu C zachowują się dziwnie i są niebezpieczne. Wyjaśnimy dlaczego w przyszłym rozdziale.
Aby uczynić tablice bezpieczniejszymi i łatwiejszymi w użyciu w C++, w C++03 wprowadzono std::vector klasę kontenerową. std::vector jest najbardziej elastycznym z trzech typów tablic i ma wiele przydatnych możliwości, których nie mają inne typy tablic.
Na koniec klasa kontenerowa std::array został wprowadzony w C++ 11 jako bezpośredni zamiennik tablic w stylu C. Jest bardziej ograniczony niż std::vector, ale może być również bardziej wydajny, szczególnie w przypadku mniejszych tablic.
Wszystkie te typy tablic są nadal używane we współczesnym C++ w różnych pojemnościach, więc omówimy wszystkie trzy w różnym stopniu.
Idziemy dalej
W następnej lekcji przedstawimy naszą pierwszą klasę kontenera, std::vector i rozpoczniemy naszą podróż, aby pokazać, jak może skutecznie rozwiązać wyzwanie, które przedstawiliśmy na początku tej lekcji. Spędzimy dużo czasu z std::vector, ponieważ będziemy musieli wprowadzić sporo nowych koncepcji i po drodze stawić czoła dodatkowym wyzwaniom.
Jedną miłą rzeczą jest to, że wszystkie klasy kontenerów mają podobne interfejsy. Zatem, gdy już nauczysz się obsługi jednego pojemnika (np. std::vector), nauka pozostałych (np. std::array) będzie znacznie prostsza. W przypadku przyszłych kontenerów (np. std::array) omówimy zauważalne różnice (i powtórzymy najważniejsze punkty).
Nota autora
Krótka uwaga na temat terminologii:
- Użyjemy
container classeskiedy mówimy o czymś, co ma zastosowanie do większości lub wszystkich klas standardowych kontenerów bibliotek. - Użyjemy
arraykiedy mówimy o czymś, co ogólnie odnosi się do wszystkich typów tablic, nawet tych zaimplementowanych w innych językach programowania.
std::vector należy do obu tych kategorii, więc nawet jeśli używamy różnych terminów, nadal ma to zastosowanie do std::vector.
OK, gotowy?
Chodźmyooooooooooooooooooooooooooooooooooooooooooooo.

