Teraz, gdy już to omówiliśmy std::vector i std::array, uzupełnimy nasze omówienie tablic o ostatni typ tablicy: tablice w stylu C.
Jak wspomniano na lekcji 16.1 — Wprowadzenie do kontenerów i tablic, tablice w stylu C zostały odziedziczone z języka C i są wbudowane w rdzeń języka C++ (w przeciwieństwie do pozostałych typów tablic, które są standardowymi klasami kontenerów bibliotek). Oznacza to, że nie musimy #include pliku nagłówkowego, aby z nich skorzystać.
Na marginesie…
Ponieważ są to jedyne typy tablic natywnie obsługiwane przez język, standardowe typy kontenerów tablic bibliotek (np. std::array i std::vector) są zwykle implementowane przy użyciu tablicy w stylu C.
Deklarowanie tablicy w stylu C
Ponieważ są częścią języka podstawowego, tablice w stylu C mają własną składnię deklaracji specjalnych. W deklaracji tablicy w stylu C używamy nawiasów kwadratowych ([]), aby poinformować kompilator, że zadeklarowany obiekt jest tablicą w stylu C. Wewnątrz nawiasów kwadratowych możemy opcjonalnie podać długość tablicy, która jest wartością całkowitą typu std::size_t która mówi kompilatorowi, ile elementów znajduje się w tablicy.
Poniższa definicja tworzy zmienną tablicową w stylu C o nazwie testScore który zawiera 30 elementów typu int:
int main()
{
int testScore[30] {}; // Defines a C-style array named testScore that contains 30 value-initialized int elements (no include required)
// std::array<int, 30> arr{}; // For comparison, here's a std::array of 30 value-initialized int elements (requires #including <array>)
return 0;
}Długość tablicy w stylu C musi wynosić co najmniej 1. Kompilator wyświetli błąd, jeśli długość tablicy będzie równa zero, będzie ujemna lub będzie niecałkowita.
Dla zaawansowanych czytelników
Tablice w stylu C dynamicznie przydzielane na stercie mogą mieć długość 0.
Długość tablicy w stylu c musi być wyrażeniem stałym
Po prostu podobnie jak std::array, podczas deklarowania tablicy w stylu C długość tablicy musi być wyrażeniem stałym (typu std::size_t, chociaż zazwyczaj nie ma to znaczenia).
Wskazówka
Niektóre kompilatory mogą umożliwiać tworzenie tablic o długościach innych niż constexpr, w celu zapewnienia zgodności z funkcją C99 zwaną tablicami o zmiennej długości (VLA).
Tablice o zmiennej długości nie są prawidłowymi językami C++ i nie powinny być używane w programach C++. Jeśli twój kompilator pozwala na te tablice, prawdopodobnie zapomniałeś wyłączyć rozszerzenia kompilatora (zobacz 0.10 -- Konfigurowanie kompilatora: Rozszerzenia kompilatora).
Subskrypcja tablicy w stylu C
Podobnie jak w przypadku A std::array, tablice w stylu C można indeksować za pomocą operatora indeksu dolnego (operator[]):
#include <iostream>
int main()
{
int arr[5]; // define an array of 5 int values
arr[1] = 7; // use subscript operator to index array element 1
std::cout << arr[1]; // prints 7
return 0;
}W przeciwieństwie do standardowych klas kontenerów bibliotek (które używają indeksów typu bez znaku std::size_t tylko), indeks tablicy w stylu C może być wartością dowolnego typu całkowitego (ze znakiem lub bez znaku) lub wyliczeniem bez zakresu. Oznacza to, że tablice w stylu C nie podlegają wszystkim problemom z indeksowaniem konwersji znaków, które występują w standardowych klasach kontenerów bibliotek!
#include <iostream>
int main()
{
const int arr[] { 9, 8, 7, 6, 5 };
int s { 2 };
std::cout << arr[s] << '\n'; // okay to use signed index
unsigned int u { 2 };
std::cout << arr[u] << '\n'; // okay to use unsigned index
return 0;
} Wskazówka
Tablice w stylu C akceptują indeksy ze znakiem lub bez znaku (lub wyliczenia bez zakresu).
operator[] nie sprawdza żadnych granic, a przekazanie indeksu spoza zakresu spowoduje niezdefiniowane zachowanie.
Na marginesie…
Deklarując tablicę (np. int arr[5]), użycie [] jest częścią składni deklaracji, a nie wywołaniem operatora indeksu dolnego operator[].
Zagregowana inicjalizacja tablic w stylu C
Po prostu podobnie jak std::array, tablice w stylu C są agregowane, co oznacza, że można je inicjować 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ę inicjatorów, która jest ujętą w nawiasy klamrowe listą wartości inicjalizacyjnych oddzielonych przecinkami.
int main()
{
int fibonnaci[6] = { 0, 1, 1, 2, 3, 5 }; // copy-list initialization using braced list
int prime[5] { 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 nie podasz inicjatora dla tablicy w stylu C, elementy zostaną domyślnie zainicjowane. W większości przypadków spowoduje to pozostawienie elementów niezainicjowanych. Ponieważ generalnie chcemy, aby nasze elementy były inicjowane, tablice w stylu C powinny być inicjowane wartościami (przy użyciu pustych nawiasów klamrowych), gdy są zdefiniowane bez inicjatorów.
int main()
{
int arr1[5]; // Members default initialized int elements are left uninitialized)
int arr2[5] {}; // Members value initialized (int elements are zero uninitialized) (preferred)
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:
int main()
{
int a[4] { 1, 2, 3, 4, 5 }; // compile error: too many initializers
int b[4] { 1, 2 }; // arr[2] and arr[3] are value initialized
return 0;
}Jedną wadą używania tablicy w stylu C jest to, że typ elementu musi być jawnie określony. CTAD nie działa, ponieważ tablice w stylu C nie są szablonami klas. A użycie auto do próby wydedukowania typu elementu tablicy z listy inicjatorów również nie działa:
int main()
{
auto squares[5] { 1, 4, 9, 16, 25 }; // compile error: can't use type deduction on C-style arrays
return 0;
}Pominięta długość
W poniższej definicji tablicy występuje subtelna redundancja. Widzisz to?
int main()
{
const int prime[5] { 2, 3, 5, 7, 11 }; // prime has length 5
return 0;
}Wyraźnie mówimy kompilatorowi, że tablica ma długość 5, a następnie inicjujemy ją również za pomocą 5 elementów. Kiedy inicjujemy tablicę w stylu C za pomocą listy inicjatorów, możemy pominąć długość (w definicji tablicy) i pozwolić kompilatorowi wydedukować długość tablicy na podstawie liczby inicjatorów.
Następujące definicje tablic zachowują się identycznie:
int main()
{
const int prime1[5] { 2, 3, 5, 7, 11 }; // prime1 explicitly defined to have length 5
const int prime2[] { 2, 3, 5, 7, 11 }; // prime2 deduced by compiler to have length 5
return 0;
}Działa to tylko wtedy, gdy inicjatory są jawnie podane dla wszystkich elementów tablicy.
int main()
{
int bad[] {}; // error: the compiler will deduce this to be a zero-length array, which is disallowed!
return 0;
}W przypadku używania listy inicjatorów do inicjowania wszystkich elementów tablicy w stylu C, lepiej jest pominąć długość i pozwolić kompilatorowi obliczyć długość tablicy. W ten sposób, jeśli inicjatory zostaną dodane lub usunięte, długość tablicy zostanie automatycznie dostosowana i nie będziemy narażeni na ryzyko niezgodności pomiędzy zdefiniowaną długością tablicy a liczbą dostarczonych inicjatorów.
Najlepsza praktyka
Wolę pomijać długość tablicy w stylu C podczas jawnego inicjowania każdego elementu tablicy wartością.
Tablice w stylu C const i constexpr
Po prostu podobnie jak std::array, tablice w stylu C mogą być const lub constexpr. Podobnie jak inne zmienne const, tablice const muszą zostać zainicjowane i wartości elementów nie można później zmienić.
#include <iostream>
namespace ProgramData
{
constexpr int squares[5] { 1, 4, 9, 16, 25 }; // an array of constexpr int
}
int main()
{
const int prime[5] { 2, 3, 5, 7, 11 }; // an array of const int
prime[0] = 17; // compile error: can't change const int
return 0;
}Rozmiar tablicy w stylu C
W poprzednich lekcjach używaliśmy operatora sizeof() , aby uzyskać rozmiar obiektu lub wpisać w bajtach. Zastosowany do tablicy w stylu C, sizeof() zwraca liczbę bajtów wykorzystanych przez całą tablicę:
#include <iostream>
int main()
{
const int prime[] { 2, 3, 5, 7, 11 }; // the compiler will deduce prime to have length 5
std::cout << sizeof(prime); // prints 20 (assuming 4 byte ints)
return 0;
}Zakładając 4-bajtowe wartości całkowite, powyższy program wypisuje 20. Słowo kluczowe prime tablicę zawierającą 5 int elementów po 4 bajty każdy, więc 5 * 4 = 20 bajtów.
Zauważ, że nie ma tu żadnych kosztów ogólnych. Obiekt tablicy w stylu C zawiera swoje elementy i nic więcej.
Uzyskiwanie długości tablicy w stylu C
W C++17 możemy użyć std::size() (zdefiniowanego w nagłówku <iterator>), który zwraca długość tablicy jako wartość całkowitą bez znaku (typu std::size_t). W C++20 możemy także użyć std::ssize(), który zwraca długość tablicy jako wartość całkowitą ze znakiem (dużego typu całkowitego ze znakiem, prawdopodobnie std::ptrdiff_t).
#include <iostream>
#include <iterator> // for std::size and std::ssize
int main()
{
const int prime[] { 2, 3, 5, 7, 11 }; // the compiler will deduce prime to have length 5
std::cout << std::size(prime) << '\n'; // C++17, returns unsigned integral value 5
std::cout << std::ssize(prime) << '\n'; // C++20, returns signed integral value 5
return 0;
}Wskazówka
Kanoniczny nagłówek definicji std::size() i std::ssize() to <iterator>. Ponieważ jednak te funkcje są tak przydatne, udostępnia je również wiele innych nagłówków, w tym <array> i <vector>. Podczas używania std::size() lub std::ssize() na Tablica w stylu C, być może nie dołączyliśmy jeszcze jednego z pozostałych nagłówków. W takim przypadku nagłówek <iterator> jest tym, który jest zwykle dołączany.
Pełną listę nagłówków definiujących te funkcje można zobaczyć w dokumentacji cppreference dla funkcji size.
Uzyskiwanie długości tablicy w stylu C (C++14 lub starsze)
Przed C++17 nie było standardowej funkcji bibliotecznej umożliwiającej uzyskanie długości tablicy w stylu C.
Jeśli używasz C++11 lub C++14, możesz zamiast tego użyć tej funkcji:
#include <cstddef> // for std::size_t
#include <iostream>
template <typename T, std::size_t N>
constexpr std::size_t length(const T(&)[N]) noexcept
{
return N;
}
int main() {
int array[]{ 1, 1, 2, 3, 5, 8, 13, 21 };
std::cout << "The array has: " << length(array) << " elements\n";
return 0;
}Wykorzystuje szablon funkcji, który przyjmuje tablicę w stylu C przez odniesienie, a następnie zwraca parametr szablonu niebędący typem reprezentujący długość tablicy.
W znacznie starszych bazach kodu możesz zobaczyć długość tablicy w stylu C określoną przez podzielenie rozmiaru całej tablicy przez rozmiar elementu tablicy:
#include <iostream>
int main()
{
int array[8] {};
std::cout << "The array has: " << sizeof(array) / sizeof(array[0]) << " elements\n";
return 0;
}Wypisuje:
The array has: 8 elements
Jak to działa? Po pierwsze, zauważ, że rozmiar całej tablicy jest równy długości tablicy pomnożonej przez rozmiar elementu. Mówiąc prościej: array size = length * element size.
Korzystając z algebry, możemy zmienić układ tego równania: length = array size / element size. Zwykle używamy sizeof(array[0]) dla rozmiaru elementu. Zatem długość = sizeof(array) / sizeof(array[0]). Czasami możesz zobaczyć tę formułę zapisaną jako sizeof(array) / sizeof(*array), która robi to samo.
Jednakże, jak pokażemy ci w następnej lekcji, ta formuła może dość łatwo zawieść (po przekazaniu zniszczonej tablicy), powodując nieoczekiwane uszkodzenie programu. Obydwa szablony funkcji C++17 std::size() i length() pokazane powyżej będą w tym przypadku powodować błędy kompilacji, więc można ich bezpiecznie używać.
Powiązana treść
Rozpad tablic omówimy w następnej lekcji 17.8 -- Zanikanie tablicy w stylu C.
Tablice w stylu C nie obsługują przypisania
Być może, co zaskakujące, tablice C++ nie obsługują przypisania:
int main()
{
int arr[] { 1, 2, 3 }; // okay: initialization is fine
arr[0] = 4; // assignment to individual elements is fine
arr = { 5, 6, 7 }; // compile error: array assignment not valid
return 0;
}Technicznie rzecz biorąc, to nie działa, ponieważ przypisanie wymaga lewy argument za modyfikowalną lwartość, a tablice w stylu C nie są uważane za modyfikowalne lwartości.
Jeśli chcesz przypisać nową listę wartości do tablicy w stylu C, najlepiej użyć zamiast tego a std::vector . Alternatywnie możesz przypisać nowe wartości do tablicy w stylu C element po elemencie lub użyć std::copy , aby skopiować istniejącą tablicę w stylu C:
#include <algorithm> // for std::copy
int main()
{
int arr[] { 1, 2, 3 };
int src[] { 5, 6, 7 };
// Copy src into arr
std::copy(std::begin(src), std::end(src), std::begin(arr));
return 0;
}Czas quizu
Pytanie nr 1
Przekonwertuj następującą std::array definicję na równoważną definicję tablicy w stylu C constexpr:
constexpr std::array<int, 3> a{}; // allocate 3 intsPytanie nr 2
Jakie trzy rzeczy są nie tak z następującym programem?
#include <iostream>
int main()
{
int length{ 5 };
const int arr[length] { 9, 7, 5, 3, 1 };
std::cout << arr[length];
arr[0] = 4;
return 0;
}Pytanie nr 3
„Doskonały kwadrat” to liczba naturalna, której pierwiastek kwadratowy jest liczbą całkowitą. Idealne kwadraty możemy uzyskać, mnożąc liczbę naturalną (w tym zero) przez samą siebie. Pierwsze 4 idealne kwadraty to: 0, 1, 4, 9.
Użyj globalnej tablicy constexpr w stylu C, aby przechowywać idealne kwadraty od 0 do 9 (włącznie). Wielokrotnie proś użytkownika o wprowadzenie jednocyfrowej liczby całkowitej lub -1, aby wyjść. Wydrukuj, czy cyfra wprowadzona przez użytkownika jest idealnym kwadratem.
Wyjście powinno być zgodne z poniższymi informacjami:
Enter a single digit integer, or -1 to quit: 4 4 is a perfect square Enter a single digit integer, or -1 to quit: 5 5 is not a perfect square Enter a single digit integer, or -1 to quit: -1 Bye
Wskazówki: Użyj pętli for opartej na zakresie, aby przeglądać tablicę w stylu C w celu znalezienia dopasowania.

