W poprzedniej lekcji (13.7 — Wprowadzenie do struktur, elementów i wyboru elementów), rozmawialiśmy o tym, jak definiować struktury, tworzyć instancje obiektów struktur i uzyskiwać dostęp do ich składowych. W tej lekcji omówimy sposób inicjowania struktur.
Elementy danych nie są domyślnie inicjowane
Podobnie jak zwykłe zmienne, elementy danych nie są inicjowane domyślnie. Rozważ następującą strukturę:
#include <iostream>
struct Employee
{
int id; // note: no initializer here
int age;
double wage;
};
int main()
{
Employee joe; // note: no initializer here either
std::cout << joe.id << '\n';
return 0;
}Ponieważ nie udostępniliśmy żadnych inicjatorów, po utworzeniu instancji joe wszystkie joe.id, joe.age, I joe.wage nie zostaną zainicjowane. Otrzymamy wtedy niezdefiniowane zachowanie, gdy spróbujemy wydrukować wartość joe.id.
Jednak zanim pokażemy, jak zainicjować strukturę, zróbmy krótką wycieczkę.
W ogólnym programowaniu zagregowany typ danych (zwany także agregatem) to dowolny typ, który może zawierać wiele elementów danych. Niektóre typy agregatów umożliwiają członkom posiadanie różnych typów (np. struktur), podczas gdy inne wymagają, aby wszyscy członkowie byli jednego typu (np. tablice).
W C++ definicja agregatu jest węższa i nieco bardziej skomplikowana.
Nota autora
W tej serii tutoriali, kiedy używamy terminu „agregat” (lub „nieagregat”), będziemy mieli na myśli definicję C++ agregat.
Dla zaawansowanych czytelników
Agregat w C++ jest albo tablicą w stylu C (17.7 -- Wprowadzenie do tablic w stylu C), albo typem klasy (struktura, klasa lub unia), który ma:
- Brak konstruktorów zadeklarowanych przez użytkownika (14.9 -- Wprowadzenie do konstruktorów)
- Brak prywatnych lub chronionych niestatycznych elementów danych (14.5 -- Członkowie publiczni i prywatni oraz specyfikatory dostępu)
- Brak funkcji wirtualnych (25.2 -- Funkcje wirtualne i polimorfizm)
Popularny typ std::array (17.1 — Wprowadzenie do std::array) jest również agregatem.
Można znaleźć dokładną definicję agregatu C++ tutaj.
Kluczową rzeczą do zrozumienia w tym miejscu jest to, że struktury zawierające tylko elementy danych są agregatami.
Agregowana inicjalizacja struktury
Ponieważ normalna zmienna może tylko przechowywać pojedynczą wartość, musimy zapewnić tylko jeden inicjator:
int x { 5 };Jednak struktura może mieć wiele elementów:
struct Employee
{
int id {};
int age {};
double wage {};
};Gdy definiujemy obiekt o typie struktury, potrzebujemy sposobu na zainicjowanie wielu elementów w czasie inicjalizacji:
Employee joe; // how do we initialize joe.id, joe.age, and joe.wage?Agregaty korzystają z formy inicjalizacji zwanej inicjowaniem agregatu, która pozwala nam bezpośrednio inicjować elementy agregatów. Aby to zrobić, jako inicjator podajemy lista inicjatorów jako inicjator, który jest po prostu listą wartości oddzielonych przecinkami w nawiasach klamrowych.
Istnieją 2 podstawowe formy agregacji inicjalizacja:
struct Employee
{
int id {};
int age {};
double wage {};
};
int main()
{
Employee frank = { 1, 32, 60000.0 }; // copy-list initialization using braced list
Employee joe { 2, 28, 45000.0 }; // list initialization using braced list (preferred)
return 0;
}Każda z tych form inicjalizacji wykonuje inicjalizację członową, co oznacza, że każdy element w strukturze jest inicjowany w kolejności deklaracji. Zatem Employee joe { 2, 28, 45000.0 }; najpierw inicjuje joe.id o wartości 2, wówczas joe.age o wartości 28, I joe.wage o wartości 45000.0 ostatni.
Najlepsza praktyka
Preferuj (niekopiowaną) formę listy z nawiasami klamrowymi podczas inicjowania agregatów.
In C++20, możemy także inicjować (niektóre) agregaty przy użyciu listy wartości w nawiasach:
Employee robert ( 3, 45, 62500.0 ); // direct initialization using parenthesized list (C++20)Zalecamy unikanie tej ostatniej formy w jak największym stopniu, ponieważ obecnie nie działa ona z agregatami korzystającymi z eliminacji nawiasów (w szczególności std::array).
Brakujące inicjatory na liście inicjatorów
Jeśli agregat jest inicjowany, ale liczba wartości inicjujących jest mniejsza niż liczba elementów członkowskich, wówczas każdy element bez jawny inicjator jest inicjowany w następujący sposób:
- Jeśli element ma domyślny inicjator elementu, który jest używany.
- W przeciwnym razie element jest inicjowany przez kopiowanie z pustej listy inicjatorów. W większości przypadków spowoduje to inicjalizację wartości na tych elementach (w typach klas spowoduje to wywołanie konstruktora domyślnego, nawet jeśli istnieje konstruktor listy).
struct Employee
{
int id {};
int age {};
double wage { 76000.0 };
double whatever;
};
int main()
{
Employee joe { 2, 28 }; // joe.whatever will be value-initialized to 0.0
return 0;
}W powyższym przykładzie joe.id zostanie zainicjowana wartością 2 i joe.age zostanie zainicjowana wartością 28. Ponieważ joe.wage nie ma jawnego inicjatora, ale ma domyślny inicjator elementu członkowskiego, joe.wage zostanie zainicjowana do 76000.0. I wreszcie, ponieważ joe.whatever nie otrzymał jawnego inicjatora, joe.whatever jest inicjowana wartością do 0.0.
Wskazówka
To oznacza ogólnie możemy użyć pustej listy inicjalizacyjnej do zainicjowania wartości wszystkich elementów struktury:
Employee joe {}; // value-initialize all membersPrzeciążenie operator<< aby wydrukować strukturę
W lekcji 13.5 — Wprowadzenie do przeciążania operatorów I/O, pokazaliśmy, jak przeciążyć operator<< aby wydrukować wyliczenie. Przydaje się także przeciążenie operator<< dla struktur.
Oto ten sam przykład, co w poprzedniej sekcji, teraz z przeciążoną operator<<:
#include <iostream>
struct Employee
{
int id {};
int age {};
double wage {};
};
std::ostream& operator<<(std::ostream& out, const Employee& e)
{
out << e.id << ' ' << e.age << ' ' << e.wage;
return out;
}
int main()
{
Employee joe { 2, 28 }; // joe.wage will be value-initialized to 0.0
std::cout << joe << '\n';
return 0;
}Wypisuje:
2 28 0
Widzimy, że joe.wage rzeczywiście została zainicjowana wartością do 0.0 (która jest drukowana jako 0).
W przeciwieństwie do wyliczenia, struktura może przechowywać wiele wartości. Sposób formatowania danych wyjściowych (np. w celu oddzielenia wartości) zależy wyłącznie od ty.
Trzy wartości wyprowadzane przez nasze przeciążone operator<< powyżej nie są intuicyjne, ponieważ nie ma wskazania, co te wartości oznaczają. Zróbmy ten sam przykład, ale zaktualizuj naszą funkcję wyjściową, aby była nieco bardziej opisowa:
#include <iostream>
struct Employee
{
int id {};
int age {};
double wage {};
};
std::ostream& operator<<(std::ostream& out, const Employee& e)
{
out << "id: " << e.id << " age: " << e.age << " wage: " << e.wage;
return out;
}
int main()
{
Employee joe { 2, 28 }; // joe.wage will be value-initialized to 0.0
std::cout << joe << '\n';
return 0;
}To teraz wypisuje:
id: 2 age: 28 wage: 0
To jest trochę łatwiejsze do zrozumienia.
Const structs
Zmienne typu struct mogą być const (lub constexpr) i tak jak wszystkie zmienne const, muszą zostać zainicjowane.
struct Rectangle
{
double length {};
double width {};
};
int main()
{
const Rectangle unit { 1.0, 1.0 };
const Rectangle zero { }; // value-initialize all members
return 0;
}Wyznaczone inicjatory C++20
Podczas inicjowania struktury z listy wartości, inicjatory są stosowane do elementów w kolejności deklaracji.
struct Foo
{
int a {};
int c {};
};
int main()
{
Foo f { 1, 3 }; // f.a = 1, f.c = 3
return 0;
}Zastanów się teraz, co by się stało, gdybyś zaktualizował definicję tej struktury w celu dodania nowego elementu, który nie jest ostatnim member:
struct Foo
{
int a {};
int b {}; // just added
int c {};
};
int main()
{
Foo f { 1, 3 }; // now, f.a = 1, f.b = 3, f.c = 0
return 0;
}Teraz wszystkie wartości inicjalizacyjne uległy przesunięciu, a co gorsza, kompilator może nie wykryć tego jako błędu (w końcu składnia jest nadal prawidłowa).
Aby tego uniknąć, C++20 dodaje nowy sposób inicjalizacji elementów składowych struktury, zwany wyznaczonymi inicjatorami Wyznaczone inicjatory pozwalają jawnie zdefiniować, do których wartości inicjalizacyjnych są mapowane Elementy członkowskie można inicjować za pomocą inicjalizacji listy lub kopiowania i muszą być inicjowane w tej samej kolejności, w jakiej zostały zadeklarowane w strukturze, w przeciwnym razie spowoduje to ostrzeżenie lub błąd. Elementy członkowskie, które nie są wyznaczone jako inicjator, zostaną zainicjowane jako wartości.
struct Foo
{
int a{ };
int b{ };
int c{ };
};
int main()
{
Foo f1{ .a{ 1 }, .c{ 3 } }; // ok: f1.a = 1, f1.b = 0 (value initialized), f1.c = 3
Foo f2{ .a = 1, .c = 3 }; // ok: f2.a = 1, f2.b = 0 (value initialized), f2.c = 3
Foo f3{ .b{ 2 }, .a{ 1 } }; // error: initialization order does not match order of declaration in struct
return 0;
}Dla użytkowników Clang
Podczas wykonywania wyznaczonych inicjatorów pojedynczych wartości przy użyciu nawiasów klamrowych, Clang nieprawidłowo wyświetla ostrzeżenie „nawiasy klamrowe wokół inicjatora skalarnego”. wkrótce.
Wyznaczone inicjatory są przydatne, ponieważ zapewniają pewien poziom samodokumentacji i pomagają uniknąć przypadkowego pomylenia kolejności wartości inicjalizacyjnych. Jednak wyznaczone inicjatory również znacznie zaśmiecają listę inicjatorów, dlatego nie zalecamy ich obecnie stosowania jako najlepszej praktyki.
Ponadto dlatego, że nie ma żadnego wymuszania, aby wyznaczone inicjatory były używane wszędzie, gdzie agregat jest używany. inicjowany, dobrze jest unikać dodawania nowych członków na środku istniejącej definicji agregacji, aby uniknąć ryzyka przesunięcia inicjatora.
Najlepsza praktyka
Dodając nowego członka do agregacji, najbezpieczniej jest dodać go na dół listy definicji, aby inicjatory pozostałych członków nie uległy przesunięciu.
Przypisanie z listą inicjatorów
Jak pokazano w poprzedniej lekcji, możemy przypisać wartości do poszczególnych elementów struktur:
struct Employee
{
int id {};
int age {};
double wage {};
};
int main()
{
Employee joe { 1, 32, 60000.0 };
joe.age = 33; // Joe had a birthday
joe.wage = 66000.0; // and got a raise
return 0;
}Jest to dobre rozwiązanie w przypadku pojedynczych elementów, ale nie jest idealne, gdy chcemy zaktualizować wiele elementów. Podobnie do inicjowania struktury za pomocą listy inicjatorów, możesz także przypisywać wartości do struktur za pomocą listy inicjatorów (która wykonuje przypisanie elementów):
struct Employee
{
int id {};
int age {};
double wage {};
};
int main()
{
Employee joe { 1, 32, 60000.0 };
joe = { joe.id, 33, 66000.0 }; // Joe had a birthday and got a raise
return 0;
}Pamiętaj, że ponieważ nie chcieliśmy zmieniać joe.id, musieliśmy podać bieżącą wartość for joe.id na naszej liście jako element zastępczy, tak aby przypisanie członków mogło przypisać joe.id Do joe.id. Jest to trochę brzydkie.
Przypisanie z wyznaczonymi inicjatorami C++20
Wyznaczone inicjatory mogą być również użyte w przypisaniu listy:
struct Employee
{
int id {};
int age {};
double wage {};
};
int main()
{
Employee joe { 1, 32, 60000.0 };
joe = { .id = joe.id, .age = 33, .wage = 66000.0 }; // Joe had a birthday and got a raise
return 0;
}Do wszystkich elementów, które nie są wyznaczone w takim przypisaniu, zostanie przypisana wartość, która byłaby używana jako wartość inicjalizacja. Gdybyśmy nie określili wyznaczonego inicjatora dla joe.id, joe.id przypisanoby mu wartość 0.
Inicjowanie struktury inną strukturą tego samego typu
Struktura może być również zainicjowana przy użyciu innej struktury tego samego typu:
#include <iostream>
struct Foo
{
int a{};
int b{};
int c{};
};
std::ostream& operator<<(std::ostream& out, const Foo& f)
{
out << f.a << ' ' << f.b << ' ' << f.c;
return out;
}
int main()
{
Foo foo { 1, 2, 3 };
Foo x = foo; // copy-initialization
Foo y(foo); // direct-initialization
Foo z {foo}; // direct-list-initialization
std::cout << x << '\n';
std::cout << y << '\n';
std::cout << z << '\n';
return 0;
}Powyższe wypisuje:
1 2 3 1 2 3 1 2 3
Zauważ, że używa to standardowych form inicjalizacji, które znamy (kopiowanie, bezpośrednie lub bezpośrednie inicjowanie listy), a nie inicjalizacja agregowana.
Jest to najczęściej spotykane podczas inicjowania struktury wartością zwracaną przez funkcję, która zwraca strukturę tego samego typu. Omówimy to bardziej szczegółowo w lekcji 13.10 -- Przekazywanie i zwracanie struktur.

