11.2 -- Tablice (Część II)

Ostrzeżenie

Ta lekcja została wycofana i zastąpiona zaktualizowanymi lekcjami z drugiej połowy rozdziału 17 (zaczynając od lekcji 17.7 -- Wprowadzenie do tablic w stylu C).

Ta lekcja stanowi kontynuację dyskusji na temat tablic rozpoczętej na lekcji 17.7 -- Wprowadzenie do tablic w stylu C.

Inicjowanie stałych tablic

Elementy tablicy są traktowane jak zwykłe zmienne i jako takie nie są inicjowane podczas tworzenia.

Jednym ze sposobów „inicjalizacji” tablicy jest wykonanie tego element po elemencie:

int prime[5]; // hold the first 5 prime numbers
prime[0] = 2;
prime[1] = 3;
prime[2] = 5;
prime[3] = 7;
prime[4] = 11;

Jest to jednak uciążliwe, zwłaszcza gdy tablica staje się większa. Co więcej, nie jest to inicjalizacja, ale przypisanie. Przypisania nie działają, jeśli tablica jest const.

Na szczęście C++ zapewnia wygodniejszy sposób inicjowania całych tablic za pomocą metody lista inicjatorów. Poniższy przykład inicjuje tablicę tymi samymi wartościami, co powyższy:

int prime[5]{ 2, 3, 5, 7, 11 }; // use initializer list to initialize the fixed array

Jeśli na liście znajduje się więcej inicjatorów niż tablica może pomieścić, kompilator wygeneruje błąd.

Jeśli jednak na liście jest mniej inicjatorów, niż może pomieścić tablica, pozostałe elementy są inicjalizowane na 0 (lub inną wartość, na którą 0 jest konwertowane dla nieintegralnego typu podstawowego - np. 0,0 dla typu double). To się nazywa inicjalizacja zerowa.

Poniższy przykład pokazuje to w akcji:

#include <iostream>

int main()
{
    int array[5]{ 7, 4, 5 }; // only initialize first 3 elements

    std::cout << array[0] << '\n';
    std::cout << array[1] << '\n';
    std::cout << array[2] << '\n';
    std::cout << array[3] << '\n';
    std::cout << array[4] << '\n';

    return 0;
}

Wypisuje:

7
4
5
0
0

W związku z tym, aby zainicjować wszystkie elementy tablicy na 0, możesz to zrobić:

int array[5]{};          // Initialize all elements to 0
double array[5] {};      // Initialize all elements to 0.0
std::string array[5] {}; // Initialize all elements to an empty string

Jeśli lista inicjatorów zostanie pominięta, elementy nie zostaną zainicjowane, chyba że są typem klasy, który samoinicjuje się.

int array[5];         // uninitialized (since int doesn't self-initialize)
double array[5];      // uninitialized (since double doesn't self-initialize)
std::string array[5]; // Initialize all elements to an empty string

Najlepsza praktyka

Jawnie zainicjuj swoje tablice (nawet jeśli typ elementu jest samoinicjujący).

Pominięta długość

Jeśli inicjujesz stałą tablicę elementów przy użyciu listy inicjatorów, kompilator może obliczyć długość tablicy za Ciebie i możesz pominąć jawne deklarowanie długości tablicy.

Poniższe dwie linie są równoważne:

int array[5]{ 0, 1, 2, 3, 4 }; // explicitly define the length of the array
int array[]{ 0, 1, 2, 3, 4 }; // let the initializer list set length of the array

To nie tylko oszczędza pisanie, ale także oznacza, że ​​nie musisz aktualizować długości tablicy, jeśli później dodasz lub usuniesz elementy.

Tablice i wyliczenia

Jednym z dużych problemów z dokumentacją tablic jest to, że indeksy całkowite nie dostarczają programiście żadnych informacji na temat znaczenia indeksu. Rozważmy klasę składającą się z 5 uczniów:

constexpr int numberOfStudents{5};
int testScores[numberOfStudents]{};
testScores[2] = 76;

Kogo reprezentują testScores[2]? To nie jest jasne.

Można to rozwiązać, konfigurując wyliczenie, w którym jeden moduł wyliczający odwzorowuje każdy z możliwych indeksów tablicy:

enum StudentNames
{
    kenny, // 0
    kyle, // 1
    stan, // 2
    butters, // 3
    cartman, // 4
    max_students // 5
};

int main()
{
    int testScores[max_students]{}; // allocate 5 integers
    testScores[stan] = 76;

    return 0;
}

W ten sposób znacznie jaśniejsze jest, co reprezentuje każdy element tablicy. Należy pamiętać, że dodano dodatkowy moduł wyliczający o nazwie max_students. Ten moduł wyliczający jest używany podczas deklaracji tablicy, aby upewnić się, że tablica ma odpowiednią długość (ponieważ długość tablicy powinna być o jeden większa niż największy indeks). Jest to przydatne zarówno do celów dokumentacyjnych, jak i dlatego, że rozmiar tablicy zostanie automatycznie zmieniony, jeśli zostanie dodany kolejny moduł wyliczający:

enum StudentNames
{
    kenny, // 0
    kyle, // 1
    stan, // 2
    butters, // 3
    cartman, // 4
    wendy, // 5
    max_students // 6
};

int main()
{
    int testScores[max_students]{}; // allocate 6 integers
    testScores[stan] = 76; // still works

    return 0;
}

Pamiętaj, że ta „sztuczka” działa tylko wtedy, gdy nie zmienisz ręcznie wartości modułu wyliczającego!

Tablice i wyliczenie klasy

Klasy wyliczeniowe nie mają ukrytej konwersji na liczbę całkowitą, więc jeśli spróbujesz wykonać następujące czynności:

enum class StudentNames
{
    kenny, // 0
    kyle, // 1
    stan, // 2
    butters, // 3
    cartman, // 4
    wendy, // 5
    max_students // 6
};

int main()
{
    int testScores[StudentNames::max_students]{}; // allocate 6 integers
    testScores[StudentNames::stan] = 76;

    return 0;
}

Otrzymasz błąd kompilatora. Można temu zaradzić, używając static_cast do konwersji modułu wyliczającego na liczbę całkowitą:

int main()
{
    int testScores[static_cast<int>(StudentNames::max_students)]{}; // allocate 6 integers
    testScores[static_cast<int>(StudentNames::stan)] = 76;

    return 0;
}

Jednak wykonanie tej czynności jest dość kłopotliwe, więc lepszym rozwiązaniem może być użycie standardowego wyliczenia w przestrzeni nazw:

namespace StudentNames
{
    enum StudentNames
    {
        kenny, // 0
        kyle, // 1
        stan, // 2
        butters, // 3
        cartman, // 4
        wendy, // 5
        max_students // 6
    };
}

int main()
{
    int testScores[StudentNames::max_students]{}; // allocate 6 integers
    testScores[StudentNames::stan] = 76;

    return 0;
}

Przekazywanie tablic do funkcji

Chociaż przekazywanie tablicy do funkcji na pierwszy rzut oka wygląda jak przekazywanie normalnej zmiennej, pod maską C++ traktuje tablice inaczej.

Kiedy zwykła zmienna jest przekazywana przez wartość, C++ kopiuje wartość argumentu do parametru funkcji. Ponieważ parametr jest kopią, zmiana wartości parametru nie powoduje zmiany wartości oryginalnego argumentu.

Jednakże kopiowanie dużych tablic może być bardzo kosztowne, C++ nie kopiuje tablicę, gdy jest ona przekazywana do funkcji. Zamiast tego przekazywana jest tablica aktualna . Ma to efekt uboczny polegający na umożliwieniu funkcjom bezpośredniej zmiany wartości elementów tablicy!

Poniższy przykład ilustruje tę koncepcję:

#include <iostream>

void passValue(int value) // value is a copy of the argument
{
    value = 99; // so changing it here won't change the value of the argument
}

void passArray(int prime[5]) // prime is the actual array
{
    prime[0] = 11; // so changing it here will change the original argument!
    prime[1] = 7;
    prime[2] = 5;
    prime[3] = 3;
    prime[4] = 2;
}

int main()
{
    int value{ 1 };
    std::cout << "before passValue: " << value << '\n';
    passValue(value);
    std::cout << "after passValue: " << value << '\n';

    int prime[]{ 2, 3, 5, 7, 11 }; // type deduced as int prime[5]
    std::cout << "before passArray: " << prime[0] << " " << prime[1] << " " << prime[2] << " " << prime[3] << " " << prime[4] << '\n';
    passArray(prime);
    std::cout << "after passArray: " << prime[0] << " " << prime[1] << " " << prime[2] << " " << prime[3] << " " << prime[4] << '\n';

    return 0;
}
before passValue: 1
after passValue: 1
before passArray: 2 3 5 7 11
after passArray: 11 7 5 3 2

W powyższym przykładzie wartość nie jest zmieniana w funkcji main(), ponieważ wartość parametru w funkcji passValue() była kopią wartości zmiennej w funkcji main(), a nie rzeczywistą zmienną. Jednakże, ponieważ tablica parametrów w funkcji passArray() jest rzeczywistą tablicą, passArray() może bezpośrednio zmieniać wartości elementów!

Przyczyna takiego stanu rzeczy jest związana ze sposobem implementacji tablic w C++, do tematu, do którego wrócimy podczas lekcji 17.8 -- Zanikanie tablicy w stylu C. Na razie możesz to uznać za dziwactwo języka.

Na marginesie: jeśli chcesz mieć pewność, że funkcja nie modyfikuje przekazywanych do niej elementów tablicy, możesz ustawić tablicę const:

// even though prime is the actual array, within this function it should be treated as a constant
void passArray(const int prime[5])
{
    // so each of these lines will cause a compile error!
    prime[0] = 11;
    prime[1] = 7;
    prime[2] = 5;
    prime[3] = 3;
    prime[4] = 2;
}

Określanie długości tablicy

Funkcji std::size() z nagłówka <iterator> można użyć do określenia długości tablice.

Oto przykład:

#include <iostream>
#include <iterator> // for std::size

int main()
{
    int array[]{ 1, 1, 2, 3, 5, 8, 13, 21 };
    std::cout << "The array has: " << std::size(array) << " elements\n";

    return 0;
}

Wypisuje:

The array has: 8 elements

Zauważ, że ze względu na sposób, w jaki C++ przekazuje tablice do funkcji, będzie to nie działać w przypadku tablic, które zostały przekazane do funkcji!

#include <iostream>
#include <iterator>

void printSize(int array[])
{
    std::cout << std::size(array) << '\n'; // Error
}

int main()
{
    int array[]{ 1, 1, 2, 3, 5, 8, 13, 21 };
    std::cout << std::size(array) << '\n'; // will print the length of the array
    printSize(array);

    return 0;
}

std::size() będzie działać z innymi rodzajami obiektów (takimi jak std::array i std::vector), a próba jej użycia spowoduje błąd kompilatora na ustalonej tablicy, która została przekazana do funkcji! Zauważ, że std::size zwraca wartość bez znaku. Jeśli potrzebujesz wartości ze znakiem, możesz albo rzucić wynik, albo, ponieważ C++ 20, użyć std::ssize() (skrót od rozmiaru ze znakiem).

std::size() został dodany w C++17. Jeśli używasz C++11 lub C++14, możesz zamiast tego użyć tej funkcji:

#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;
}

W starszym kodzie zamiast tego możesz zobaczyć długość obliczoną za pomocą operatora sizeof. sizeof nie jest tak łatwy w użyciu jak std::size() i jest kilka rzeczy, na które należy zwrócić uwagę.

Operatora sizeof można używać na tablicach i zwróci on całkowity rozmiar tablicy (długość tablicy pomnożona przez rozmiar elementu).

#include <iostream>

int main()
{
    int array[]{ 1, 1, 2, 3, 5, 8, 13, 21 };
    std::cout << sizeof(array) << '\n'; // will print the size of the array multiplied by the size of an int
    std::cout << sizeof(int) << '\n';

    return 0;
}

Na maszynie z 4-bajtowymi liczbami całkowitymi i 8-bajtowymi wskaźnikami zostanie wydrukowane:

32
4

(Możesz otrzymać inny wynik, jeśli rozmiar twoich typów jest inny).

Jeden sprytny trik: możemy określić długość stałej tablicy, dzieląc rozmiar całej tablicy przez rozmiar elementu tablicy:

#include <iostream>

int main()
{
    int array[]{ 1, 1, 2, 3, 5, 8, 13, 21 };
    std::cout << "The array has: " << sizeof(array) / sizeof(array[0]) << " elements\n";

    return 0;
}

To jest drukowane

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: rozmiar tablicy = długość tablicy * rozmiar elementu.

Korzystając z algebry, możemy zmienić układ tego równania: długość tablicy = rozmiar tablicy / rozmiar elementu. sizeof(tablica) to rozmiar tablicy, a sizeof(tablica[0]) to rozmiar elementu, więc nasze równanie ma postać długość tablicy = rozmiar(tablica) / rozmiar(tablica[0]). Czasami *array jest używany zamiast array[0] (omawiamy, co *array jest w 17,9 -- Arytmetyka wskaźników i indeksy dolne).

Zauważ, że to zadziała tylko wtedy, gdy tablica jest tablicą o stałej długości i robisz tę sztuczkę w tej samej funkcji, w której zadeklarowana jest tablica (porozmawiamy więcej o tym, dlaczego to ograniczenie istnieje w lekcja w tym rozdziale).

Kiedy sizeof zostanie użyte w tablicy przekazanej do funkcji, nie spowoduje to błędu, jak std::size(). Zamiast tego zwraca rozmiar wskaźnika.

#include <iostream>

void printSize(int array[])
{
    std::cout << sizeof(array) / sizeof(array[0]) << '\n';
}

int main()
{
    int array[]{ 1, 1, 2, 3, 5, 8, 13, 21 };
    std::cout << sizeof(array) / sizeof(array[0]) << '\n';
    printSize(array);

    return 0;
}

Ponownie zakładając 8-bajtowe wskaźniki i 4-bajtowe liczby całkowite, wyświetli się

8
2

Nota autora

Prawidłowo skonfigurowany kompilator powinien wyświetlić ostrzeżenie, jeśli spróbujesz użyć sizeof() na tablicy przekazanej do funkcji.

Obliczenia w funkcji main() były poprawne, ale funkcja sizeof() w funkcji printSize() zwróciła 8 (rozmiar wskaźnika), a 8 podzielone przez 4 daje 2.

Z tego powodu uważaj, używając sizeof() na tablicach!

Uwaga: w powszechnym użyciu terminy „rozmiar tablicy” i „długość tablicy” są najczęściej używane w odniesieniu do długości tablicy (rozmiar tablicy w większości przypadków nie jest przydatny, poza sztuczką, którą pokazaliśmy powyżej).

Indeksowanie tablicy poza zakresem

Pamiętaj że tablica o długości N zawiera elementy tablicy od 0 do N-1. Co więc się stanie, jeśli spróbujesz uzyskać dostęp do tablicy z indeksem dolnym spoza tego zakresu?

Rozważ następujący program:

int main()
{
    int prime[5]{}; // hold the first 5 prime numbers
    prime[5] = 13;

    return 0;
}

W tym programie nasza tablica ma długość 5, ale staramy się zapisać liczbę pierwszą w szóstym elemencie (indeks 5).

C++ nie wykonuje wszelkie sprawdzenia, aby upewnić się, że Twoje indeksy są prawidłowe dla długości tablicy. Zatem w powyższym przykładzie wartość 13 zostanie wstawiona do pamięci w miejscu, w którym znajdowałby się szósty element, gdyby istniał. Kiedy tak się stanie, otrzymasz niezdefiniowane zachowanie — na przykład może to spowodować nadpisanie wartości innej zmiennej lub spowodować awarię programu.

Chociaż zdarza się to rzadziej, C++ pozwoli ci również użyć indeksu ujemnego, z podobnie niepożądanymi skutkami.

Reguła

Jeśli używasz tablic, upewnij się, że twoje indeksy są prawidłowe dla zakresu twojego tablica!

Quiz

  1. Zadeklaruj tablicę przechowującą wysoką temperaturę (z dokładnością do dziesiątej części stopnia) dla każdego dnia w roku (zakładając, że rok ma 365 dni). Zainicjuj tablicę wartością 0,0 dla każdego dnia.
  2. Utwórz wyliczenie z nazwami następujących zwierząt: kurczak, pies, kot, słoń, kaczka i wąż. Umieść wyliczenie w przestrzeni nazw. Zdefiniuj tablicę zawierającą element dla każdego z tych zwierząt i użyj listy inicjalizacyjnej, aby zainicjować każdy element w celu przechowywania liczby nóg zwierzęcia.

Napisz główną funkcję, która wypisze liczbę nóg słonia, używając modułu wyliczającego.

Odpowiedzi do quizu

  1. Pokaż rozwiązanie
  1. Pokaż rozwiązanie
%Brakujące wyszukiwanie identyfikatora lekcji 83%
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:  
490 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze