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 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> // dla std::array
#include <vector> // dla std::vector
int main()
{
std::array<int, 5> a {}; // a std::array of 5 ints
std::vector<int> b(5); // std::vector o długości 5 int (dla porównania)
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 std::vector, którego rozmiar można zmieniać w czasie wykonywania, długość 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 {}; // Użycie stałej literału
constexpr int len { 8 };
std::array<int, len> b {}; // Używanie constexpr zmienna
enum Colors
{
red,
green,
blue,
max_colors
};
std::array<int, max_colors> c {}; // Użycie modułu wyliczającego
#define DAYS_PER_WEEK 7
std::array<int, DAYS_PER_WEEK> d {}; // Użycie makra (nie rób tego, zamiast tego użyj zmiennej constexpr)
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) // długość to stała czasu wykonania
{
std::array<int, length> e {}; // błąd: długość nie jest wyrażeniem stałym
}
int main()
{
// używając wartości innej niż stała zmienna
int numStudents{};
std::cin >> numStudents; // numStudents jest niestałe
std::array<int, numStudents> {}; // błąd: numStudents nie jest wyrażeniem stałym
foo(7);
return 0;
}Ostrzeżenie
Być może zaskakujące, 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 jeśli arr ma długość zerową
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 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 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 }; // inicjalizacja listy kopiowania za pomocą nawiasów klamrowych lista
std::array<int, 5> prime { 2, 3, 5, 7, 11 }; // inicjalizacja listy przy użyciu listy w nawiasach klamrowych (preferowana)
return 0;
}Każda z tych form inicjalizacji inicjuje elementy tablicy w kolejności, zaczynając od elementu 0.
Jeśli 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; // Domyślnie inicjowane elementy składowe (elementy int pozostają niezainicjowane)
std::array<int, 5> b{}; // Wartość elementów inicjowanych jest inicjowana (elementy int są inicjowane zerem) (preferowane)
std::vector<int> v(5); // Wartość elementów inicjowanych (elementy int są inicjowane zerem) (dla porównania)
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 }; // błąd kompilacji: zbyt wiele inicjatorów
std::array<int, 4> b { 1, 2 }; // b[2] i b[3] są wartościami zainicjowanymi
return 0;
}Const i constexpr std::array
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 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 std::array z listy inicjatorów:
#include <array>
#include <iostream>
int main()
{
constexpr std::array a1 { 9, 7, 5, 3, 1 }; // Typ jest ustalany na std::array<int, 5>
constexpr std::array a2 { 9.7, 7.31 }; // Typ jest ustalany na 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ść 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 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 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 }) }; // Określ typ i rozmiar
constexpr auto myArray2 { std::to_array<int>({ 9, 7, 5, 3, 1 }) }; // Określ tylko typ, wydedukuj rozmiar
constexpr auto myArray3 { std::to_array({ 9, 7, 5, 3, 1 }) }; // Wydedukuj typ i rozmiar
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> // dla std::array
#include <iostream>
int main()
{
constexpr std::array<int, 5> prime{ 2, 3, 5, 7, 11 };
std::cout << prime[3]; // wydrukuj wartość elementu o indeksie 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 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 std::array za pomocą następujących wartości: „h”, „e”, „l”, „l”, „o”. elementu o indeksie 1.

