13.11 — Różne struktury

Struktury zawierające elementy zdefiniowane w programie

W C++ struktury (i klasy) mogą mieć elementy będące innymi typami zdefiniowanymi w programie. Można to zrobić na dwa sposoby.

Najpierw możemy zdefiniować jeden typ zdefiniowany przez program (w zakresie globalnym), a następnie użyć go jako członka innego typu zdefiniowanego przez program:

#include <iostream>

struct Employee
{
    int id {};
    int age {};
    double wage {};
};

struct Company
{
    int numberOfEmployees {};
    Employee CEO {}; // Employee is a struct within the Company struct
};

int main()
{
    Company myCompany{ 7, { 1, 32, 55000.0 } }; // Nested initialization list to initialize Employee
    std::cout << myCompany.CEO.wage << '\n'; // print the CEO's wage

    return 0;
}

W powyższym przypadku zdefiniowaliśmy Employee struct, a następnie użyłem go jako członka w a Company struktura. Kiedy inicjujemy nasz Company, możemy również zainicjować nasz Employee przy użyciu zagnieżdżonej listy inicjującej. A jeśli chcemy wiedzieć, jaka była pensja dyrektora generalnego, po prostu dwukrotnie używamy operatora wyboru członka: myCompany.CEO.wage;

Po drugie, typy można także zagnieżdżać w innych typach, więc jeśli pracownik istniałby tylko jako część firmy, typ pracownika mógłby być zagnieżdżony w strukturze firmy:

#include <iostream>

struct Company
{
    struct Employee // accessed via Company::Employee
    {
        int id{};
        int age{};
        double wage{};
    };

    int numberOfEmployees{};
    Employee CEO{}; // Employee is a struct within the Company struct
};

int main()
{
    Company myCompany{ 7, { 1, 32, 55000.0 } }; // Nested initialization list to initialize Employee
    std::cout << myCompany.CEO.wage << '\n'; // print the CEO's wage

    return 0;
}

Częściej robi się to podczas zajęć, więc porozmawiamy o tym więcej w przyszłej lekcji (15.3 -- Typy zagnieżdżone (typy elementów)).

Struktury będące właścicielami powinny mieć elementy członkowskie danych będące właścicielami

W lekcji 5.9 — std::string_view (część 2)wprowadziliśmy dualne pojęcia właścicieli i widzów. Właściciele zarządzają własnymi danymi i kontrolują, kiedy zostaną one zniszczone. Przeglądający przeglądają dane innej osoby i nie mają wpływu na to, kiedy zostaną one zmienione lub zniszczone.

W większości przypadków chcemy, aby nasze struktury (i klasy) były właścicielami danych, które zawierają. Zapewnia to kilka przydatnych korzyści:

  • Elementy danych będą ważne tak długo, jak długo będzie trwała struktura (lub klasa).
  • Wartość tych elementów danych nie zmieni się nieoczekiwanie.

Najłatwiejszym sposobem uczynienia struktury (lub klasy) właścicielem jest nadanie każdemu elementowi danych typu, który jest właścicielem (np. nie jest przeglądarką, wskaźnikiem ani referencją). Jeśli struktura lub klasa ma elementy danych, które są właścicielami, wówczas sama struktura lub klasa automatycznie staje się właścicielem.

Jeśli struktura (lub klasa) zawiera element danych będący przeglądarką, możliwe jest, że obiekt oglądany przez tego członka zostanie zniszczony przed członkiem danych, który go przegląda. Jeśli tak się stanie, struktura pozostanie z wiszącym elementem, a dostęp do tego elementu doprowadzi do niezdefiniowanego zachowania.

Najlepsza praktyka

W większości przypadków chcemy, aby nasze struktury (i klasy) były właścicielami. Najłatwiejszym sposobem włączenia tego jest upewnienie się, że każdy element członkowski danych ma typ będący właścicielem (np. nie przeglądarkę, wskaźnik ani odwołanie).

Nota autora

Ćwicz bezpieczne struktury. Nie pozwól, aby Twój członek zwisał.

Z tego powodu elementy danych ciągu są prawie zawsze typu std::string (który jest właścicielem), a nie typu std::string_view (który jest widzem). Poniższy przykład ilustruje przypadek, w którym ma to znaczenie:

#include <iostream>
#include <string>
#include <string_view>

struct Owner
{
    std::string name{}; // std::string is an owner
};

struct Viewer
{
    std::string_view name {}; // std::string_view is a viewer
};

// getName() returns the user-entered string as a temporary std::string
// This temporary std::string will be destroyed at the end of the full expression
// containing the function call.
std::string getName()
{
    std::cout << "Enter a name: ";
    std::string name{};
    std::cin >> name;
    return name;
}

int main()
{
    Owner o { getName() };  // The return value of getName() is destroyed just after initialization
    std::cout << "The owners name is " << o.name << '\n';  // ok

    Viewer v { getName() }; // The return value of getName() is destroyed just after initialization
    std::cout << "The viewers name is " << v.name << '\n'; // undefined behavior

    return 0;
}

Klasa getName() funkcja zwraca nazwę wprowadzoną przez użytkownika jako tymczasową std::string. Ta tymczasowa wartość zwracana jest niszczona na końcu pełnego wyrażenia, w którym wywoływana jest funkcja.

W przypadku o, to tymczasowe std::string jest używany do inicjalizacji o.name. Ponieważ o.name jest std::string, o.name tworzy kopię tymczasową std::string. Tymczasowe std::string potem umiera, ale o.name nie ma to wpływu, ponieważ jest to kopia. Kiedy drukujemy o.name w kolejnym zestawieniu działa tak, jak tego oczekujemy.

W przypadku v, to tymczasowe std::string jest używany do inicjalizacji v.name. Ponieważ v.name jest std::string_view, v.name to tylko pogląd tymczasowy std::string, a nie kopia. Tymczasowe std::string potem umiera, odchodząc v.name zwisające. Kiedy drukujemy v.name w kolejnym stwierdzeniu otrzymamy niezdefiniowane zachowanie.

Rozmiar struktury i wyrównanie struktury danych

Zazwyczaj rozmiar struktury jest sumą rozmiaru wszystkich jej elementów, ale nie zawsze!

Rozważ następujący program:

#include <iostream>

struct Foo
{
    short a {};
    int b {};
    double c {};
};

int main()
{
    std::cout << "The size of short is " << sizeof(short) << " bytes\n";
    std::cout << "The size of int is " << sizeof(int) << " bytes\n";
    std::cout << "The size of double is " << sizeof(double) << " bytes\n";

    std::cout << "The size of Foo is " << sizeof(Foo) << " bytes\n";

    return 0;
}

Na maszynie autora wydrukowało to:

The size of short is 2 bytes
The size of int is 4 bytes
The size of double is 8 bytes
The size of Foo is 16 bytes

Należy pamiętać, że rozmiar short + int + double wynosi 14 bajtów, ale rozmiar Foo ma 16 bajtów!

Okazuje się, że możemy tylko powiedzieć, że rozmiar struktury będzie co najmniej tak duży, jak rozmiar wszystkich zmiennych, które zawiera. Ale mógłby być większy! Ze względu na wydajność kompilator czasami dodaje luki do struktur (jest to tzw dopełnianiem).

W Foo struct powyżej, kompilator w niewidoczny sposób dodaje 2 bajty dopełnienia po elemencie a, dzięki czemu rozmiar struktury wynosi 16 bajtów zamiast 14.

Dla zaawansowanych czytelników

Powód, dla którego kompilatory mogą dodawać dopełnienie wykracza poza zakres tego samouczka, ale czytelnicy, którzy chcą dowiedzieć się więcej, mogą przeczytać o wyrównaniu struktury danych w Wikipedii. Jest to lektura opcjonalna i nie wymagana do zrozumienia struktur ani C++!

Może to faktycznie mieć dość znaczący wpływ na rozmiar struktury, jak pokazuje poniższy program:

#include <iostream>

struct Foo1
{
    short a{}; // will have 2 bytes of padding after a
    int b{};
    short c{}; // will have 2 bytes of padding after c
};

struct Foo2
{
    int b{};
    short a{};
    short c{};
};

int main()
{
    std::cout << sizeof(Foo1) << '\n'; // prints 12
    std::cout << sizeof(Foo2) << '\n'; // prints 8

    return 0;
}

Ten program wypisuje:

12
8

Zauważ to Foo1 i Foo2 mają te same elementy, jedyną różnicą jest kolejność deklaracji. Jednak Foo1 jest o 50% większy ze względu na dodane dopełnienie.

Wskazówka

Możesz zminimalizować dopełnienie, definiując elementy członkowskie w kolejności malejącej wielkości.

Kompilator C++ nie może zmieniać kolejności elementów, więc należy to zrobić ręcznie.

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:  
40 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze