W lekcji 16.1 — Wprowadzenie do kontenerów i tablic, wprowadziliśmy kontenery i tablice. Podsumowując:
- Kontenery zapewniają przechowywanie kolekcji nienazwanych obiektów (zwanych elementami).
- Tablice przydzielają swoje elementy w sposób ciągły w pamięci i umożliwiają szybki, bezpośredni dostęp do dowolnego elementu poprzez indeks dolny.
- C++ ma trzy różne typy tablic, które są powszechnie używane:
std::vector,std::arrayi tablice w stylu C.
W lekcji 16.10 -- std::vector zmiany rozmiaru i pojemności, wspomnieliśmy, że tablice dzielą się na dwie kategorie:
- Tablice o stałym rozmiarze (zwane także tablicami o stałej długości) wymagają, aby długość tablicy była znana w momencie tworzenia instancji i tej długości nie można później zmienić. Tablice typu C i
std::arraysą tablicami o stałym rozmiarze. - Rozmiar tablic dynamicznych można zmieniać w czasie wykonywania.
std::vectorjest tablicą dynamiczną.
W poprzednim rozdziale skupiliśmy się na std::vector, ponieważ jest ona szybka, stosunkowo łatwa w użyciu i wszechstronna. To sprawia, że jest to nasz ulubiony typ, gdy potrzebujemy kontenera tablicy.
Dlaczego więc nie używać tablic dynamicznych do wszystkiego?
Tablice dynamiczne są wydajne i wygodne, ale jak wszystko w życiu wymagają pewnych kompromisów w zamian za oferowane korzyści.
std::vectorjest nieco mniej wydajne niż tablice o stałym rozmiarze. W większości przypadków prawdopodobnie nie zauważysz różnicy (chyba że napiszesz niechlujny kod, który powoduje wiele niezamierzonych realokacji).std::vectorobsługujeconstexprtylko w bardzo ograniczonych kontekstach.
We współczesnym C++ to właśnie ten ostatni punkt jest tak naprawdę znaczący. Tablice Constexpr oferują możliwość pisania kodu, który jest bardziej niezawodny, a także może być lepiej zoptymalizowany przez kompilator. Ilekroć możemy użyć tablicy constexpr, powinniśmy -- a jeśli potrzebujemy tablicy constexpr, std::array jest klasą kontenera, której powinniśmy używać.
Najlepsza praktyka
Użyj std::array dla tablic constexpr i std::vector tablice inne niż constexpr.
Defining a std::array
std::array jest zdefiniowany w nagłówku <array>. Została zaprojektowana tak, aby działać podobnie do std::vector i jak zobaczysz, jest między nimi więcej podobieństw niż różnic.
Jedna różnica polega na tym, jak deklarujemy std::array:
#include <array> // for std::array
#include <vector> // for std::vector
int main()
{
std::array<int, 5> a {}; // a std::array of 5 ints
std::vector<int> b(5); // a std::vector of 5 ints (for comparison)
return 0;
}Nasz std::array deklarację mającą dwa argumenty szablonu. Pierwszy (int) to argument szablonu typu definiujący typ elementu tablicy. Drugi (5) jest całkowitym argumentem szablonu innego niż typ, definiującym długość tablicy.
Powiązana treść
Parametry szablonu innego typu omówimy w lekcji 11.9 — Parametry szablonu inne niż typ.
Długość std::array muszą być wyrażeniem stałym
W przeciwieństwie do a std::vector, którego rozmiar można zmieniać w czasie wykonywania, długość a std::array musi być wyrażeniem stałym. Najczęściej podaną wartością długości będzie literał całkowity, zmienna constexpr lub moduł wyliczający bez zakresu.
#include <array>
int main()
{
std::array<int, 7> a {}; // Using a literal constant
constexpr int len { 8 };
std::array<int, len> b {}; // Using a constexpr variable
enum Colors
{
red,
green,
blue,
max_colors
};
std::array<int, max_colors> c {}; // Using an enumerator
#define DAYS_PER_WEEK 7
std::array<int, DAYS_PER_WEEK> d {}; // Using a macro (don't do this, use a constexpr variable instead)
return 0;
}Zauważ, że zmienne inne niż stałe i stałe czasu wykonania nie można użyć do określenia długości:
#include <array>
#include <iostream>
void foo(const int length) // length is a runtime constant
{
std::array<int, length> e {}; // error: length is not a constant expression
}
int main()
{
// using a non-const variable
int numStudents{};
std::cin >> numStudents; // numStudents is non-constant
std::array<int, numStudents> {}; // error: numStudents is not a constant expression
foo(7);
return 0;
}Ostrzeżenie
Być może zaskakujące, a std::array można zdefiniować o długości 0:
#include <array>
#include <iostream>
int main()
{
std::array<int, 0> arr {}; // creates a zero-length std::array
std::cout << arr.empty(); // true if arr is zero-length
return 0;
}Długość zerowa std::array jest klasą specjalnego przypadku, która nie zawiera danych. W związku z tym wywołanie dowolnej funkcji członkowskiej, która uzyskuje dostęp do danych o zerowej długości std::array (w tym operator[]) spowoduje niezdefiniowane zachowanie.
Możesz sprawdzić, czy a std::array ma długość zerową, używając empty() funkcji członkowskiej, która zwraca true jeśli tablica ma zerową długość i false w przeciwnym razie.
Agregowana inicjalizacja a std::array
Być może, co zaskakujące, std::array jest agregatem. Oznacza to, że nie ma konstruktorów i zamiast tego jest inicjowany przy użyciu inicjalizacji agregowanej. Krótko mówiąc, inicjalizacja agregatu pozwala nam bezpośrednio inicjować elementy agregatów. Aby to zrobić, udostępniamy listę inicjalizatorów, która jest ujętą w nawiasy klamrowe listą wartości inicjalizacyjnych oddzielonych przecinkami.
Powiązana treść
Omówiliśmy inicjalizację zbiorczą struktur w lekcji 13.8 -- Inicjowanie agregatu Struct.
#include <array>
int main()
{
std::array<int, 6> fibonnaci = { 0, 1, 1, 2, 3, 5 }; // copy-list initialization using braced list
std::array<int, 5> prime { 2, 3, 5, 7, 11 }; // list initialization using braced list (preferred)
return 0;
}Każda z tych form inicjalizacji inicjuje elementy tablicy w kolejności, zaczynając od elementu 0.
Jeśli a std::array zdefiniowano bez inicjatora, elementy zostaną zainicjowane domyślnie. W większości przypadków spowoduje to, że elementy pozostaną niezainicjowane.
Ponieważ generalnie chcemy, aby nasze elementy były inicjowane, std::array powinna być wartością inicjowaną (używając pustych nawiasów klamrowych), gdy jest zdefiniowana bez inicjatorów.
#include <array>
#include <vector>
int main()
{
std::array<int, 5> a; // Members default initialized (int elements are left uninitialized)
std::array<int, 5> b{}; // Members value initialized (int elements are zero initialized) (preferred)
std::vector<int> v(5); // Members value initialized (int elements are zero initialized) (for comparison)
return 0;
}Jeśli na liście inicjatorów podano więcej inicjatorów niż zdefiniowana długość tablicy, kompilator wystąpi błąd. Jeśli na liście inicjatorów znajduje się mniej inicjatorów niż zdefiniowana długość tablicy, pozostałe elementy bez inicjatorów są inicjowane wartościami:
#include <array>
int main()
{
std::array<int, 4> a { 1, 2, 3, 4, 5 }; // compile error: too many initializers
std::array<int, 4> b { 1, 2 }; // b[2] and b[3] are value initialized
return 0;
}Const i constexpr std::array
A std::array mogą być const:
#include <array>
int main()
{
const std::array<int, 5> prime { 2, 3, 5, 7, 11 };
return 0;
}Nawet jeśli elementy a const std::array nie są jawnie oznaczone jako const, nadal są traktowane jako const (ponieważ cała tablica jest const).
std::array ma także pełne wsparcie dla constexpr:
#include <array>
int main()
{
constexpr std::array<int, 5> prime { 2, 3, 5, 7, 11 };
return 0;
}Ta obsługa constexpr jest kluczowym powodem użycia std::array.
Najlepsza praktyka
Zdefiniuj swój std::array jako constexpr, jeśli to możliwe. Jeśli std::array nie jest constexpr, rozważ użycie std::vector .
dedukcji argumentów szablonu klasy (CTAD) dla std::array C++17
Używania CTAD (dedukcja argumentów szablonu klasy) w C++ 17 możemy pozwolić kompilatorowi wydedukować zarówno typ elementu, jak i długość tablicy a std::array z listy inicjatorów:
#include <array>
#include <iostream>
int main()
{
constexpr std::array a1 { 9, 7, 5, 3, 1 }; // The type is deduced to std::array<int, 5>
constexpr std::array a2 { 9.7, 7.31 }; // The type is deduced to std::array<double, 2>
return 0;
}Preferujemy tę składnię, gdy jest to praktyczne. Jeśli Twój kompilator nie obsługuje C++17, musisz jawnie podać argumenty szablonu typu i długości.
Najlepsza praktyka
Użyj dedukcji argumentów szablonu klasy (CTAD), aby kompilator wydedukował typ i długość a std::array na podstawie swoich inicjatorów.
CTAD nie obsługuje częściowego pomijania argumentów szablonu (od C++ 23), więc nie ma możliwości użycia funkcji języka podstawowego aby pominąć tylko długość lub tylko typ a std::array:
#include <iostream>
int main()
{
constexpr std::array<int> a2 { 9, 7, 5, 3, 1 }; // error: too few template arguments (length missing)
constexpr std::array<5> a2 { 9, 7, 5, 3, 1 }; // error: too few template arguments (type missing)
return 0;
}Pomijanie tylko długości tablicy za pomocą std::to_array C++20
Jednak TAD (dedukcja argumentów szablonu, używana do rozpoznawania szablonów funkcji) obsługuje częściowe pominięcie argumentów szablonu. Od C++20 możliwe jest pominięcie długości tablicy a std::array używając std::to_array funkcji pomocniczej:
#include <array>
#include <iostream>
int main()
{
constexpr auto myArray1 { std::to_array<int, 5>({ 9, 7, 5, 3, 1 }) }; // Specify type and size
constexpr auto myArray2 { std::to_array<int>({ 9, 7, 5, 3, 1 }) }; // Specify type only, deduce size
constexpr auto myArray3 { std::to_array({ 9, 7, 5, 3, 1 }) }; // Deduce type and size
return 0;
}Niestety użycie std::to_array jest droższe niż bezpośrednie utworzenie std::array ponieważ wiąże się z utworzeniem tymczasowej std::array która jest następnie używana do kopiowania inicjalizacji naszego pożądane std::array. Z tego powodu std::to_array powinno być używane tylko w przypadkach, gdy nie można skutecznie określić typu na podstawie inicjatorów i należy go unikać, gdy tablica jest tworzona wiele razy (np. wewnątrz pętli).
Na przykład, ponieważ nie ma możliwości określenia literału typu short, możesz użyć poniższego, aby utworzyć wartości std::array z short (bez konieczności jawnego określania długość std::array):
#include <array>
#include <iostream>
int main()
{
constexpr auto shortArray { std::to_array<short>({ 9, 7, 5, 3, 1 }) };
std::cout << sizeof(shortArray[0]) << '\n';
return 0;
}Dostęp do elementów tablicy za pomocą operator[]
Podobnie jak w std::vector, najczęstszym sposobem uzyskiwania dostępu do elementów tablicy std::array jest użycie operatora indeksu dolnego (operator[]):
#include <array> // for std::array
#include <iostream>
int main()
{
constexpr std::array<int, 5> prime{ 2, 3, 5, 7, 11 };
std::cout << prime[3]; // print the value of element with index 3 (7)
std::cout << prime[9]; // invalid index (undefined behavior)
return 0;
}Dla przypomnienia, operator[] nie sprawdza granic. Jeśli podany zostanie nieprawidłowy indeks, niezdefiniowane zachowanie zostanie wynik.
W następnej lekcji omówimy kilka innych sposobów indeksowania a std::array .
Czas quizu
Pytanie nr 1
Jakiego rodzaju inicjalizacji używa std::array ?
Dlaczego powinieneś jawnie inicjować wartość-inicjalizację std::array jeśli nie zapewniasz inicjalizacji wartości?
Pytanie nr 2
Zdefiniuj std::array które będą utrzymywać wysoką temperaturę dla każdego dnia w roku (z dokładnością do dziesiątej części stopnia).
Pytanie nr 3
Zainicjuj a std::array za pomocą następujących wartości: „h”, „e”, „l”, „l”, „o”. elementu o indeksie 1.

