W poprzedniej lekcji (1.3 -- Wprowadzenie do obiektów i zmiennych), omówiliśmy, jak zdefiniować zmienną, której możemy używać do przechowywania wartości. Podczas tej lekcji omówimy, jak właściwie umieszczać wartości w zmiennych.
Dla przypomnienia, oto krótki program, który najpierw przydziela pojedynczą zmienną całkowitą o nazwie x, a następnie przydziela dwie kolejne zmienne całkowite o nazwach y i z:
int main()
{
int x; // define an integer variable named x (preferred)
int y, z; // define two integer variables, named y and z
return 0;
}W ramach przypomnienia zaleca się definiowanie jednej zmiennej w każdym wierszu. Do przypadków, w których definiujemy wiele zmiennych, wrócimy w dalszej części tej lekcji.
Przypisanie zmiennej
Po zdefiniowaniu zmiennej możesz nadać jej wartość (w osobnej instrukcji) za pomocą operatora = . Proces ten nazywa się operatorem przypisania, a = nazywa się operatorem przypisania.
int width; // define an integer variable named width
width = 5; // assignment of value 5 into variable width
// variable width now has value 5Domyślnie przypisanie kopiuje wartość po prawej stronie operatora = do zmiennej po lewej stronie operatora. Nazywa się to przypisaniem kopiowania.
Po nadaniu zmiennej wartości wartość tej zmiennej można wydrukować za pomocą std::cout i << operator.
Przypisania można użyć zawsze, gdy chcemy zmienić wartość przechowywaną przez zmienną. Oto przykład, w którym dwukrotnie użyliśmy przypisania:
#include <iostream>
int main()
{
int width; // define a variable named width
width = 5; // copy assignment of value 5 into variable width
std::cout << width; // prints 5
width = 7; // change value stored in variable width to 7
std::cout << width; // prints 7
return 0;
}Wypisuje:
57
Gdy ten program jest uruchomiony, wykonanie rozpoczyna się od góry funkcji main i przebiega sekwencyjnie. Najpierw przydzielana jest pamięć dla zmiennej width . Następnie przypisujemy width wartość 5. Kiedy wyprowadzimy wartość width, drukuje 5 do konsoli. Kiedy następnie przypiszemy wartość 7 Do width, każda wcześniejsza wartość (w tym przypadku 5) zostanie nadpisana. Zatem kiedy ponownie wyprowadzimy width , tym razem wypisuje 7.
Zwykłe zmienne mogą przechowywać tylko jedną wartość na raz.
Ostrzeżenie
Jednym z najczęstszych błędów popełnianych przez nowych programistów jest mylenie operatora przypisania (=) z operatorem równości (==). Przypisanie (=) służy do przypisania wartości zmiennej. Równość (==) służy do sprawdzania, czy dwa operandy mają taką samą wartość.
Inicjalizacja zmiennej
Jedną wadą przypisania jest to, że przypisanie wartości do właśnie zdefiniowanego obiektu wymaga dwóch instrukcji: jednej do zdefiniowania zmiennej i drugiej do przypisania wartości.
Te dwa kroki można połączyć. Po zdefiniowaniu obiektu można opcjonalnie podać wartość początkową dla obiektu. Proces określenia wartości początkowej obiektu nazywa się inicjowaniem, a składnia używana do inicjalizacji obiektu nazywa się inicjator. Nieformalnie wartość początkowa jest często nazywana również „inicjatorem”.
Na przykład poniższa instrukcja zarówno definiuje zmienną o nazwie width (typu int), jak i inicjuje ją wartością 5:
#include <iostream>
int main()
{
int width { 5 }; // define variable width and initialize with initial value 5
std::cout << width; // prints 5
return 0;
}W powyższej inicjalizacji zmiennej width, { 5 } jest inicjatorem, a 5 jest inicjatorem wartość.
Kluczowa informacja
Inicjalizacja zapewnia wartość początkową zmiennej. Pomyśl o „inicjalizacji”.
Różne formy inicjalizacji
W przeciwieństwie do przypisania (które jest ogólnie proste), inicjalizacja w C++ jest zaskakująco złożona. Na początek przedstawimy tutaj uproszczony widok.
Istnieje 5 typowych form inicjalizacji w C++:
int a; // default-initialization (no initializer)
// Traditional initialization forms:
int b = 5; // copy-initialization (initial value after equals sign)
int c ( 6 ); // direct-initialization (initial value in parenthesis)
// Modern initialization forms (preferred):
int d { 7 }; // direct-list-initialization (initial value in braces)
int e {}; // value-initialization (empty braces)Możesz zobaczyć powyższe formularze napisane z różnymi odstępami (np. int b=5; int c(6);, int d{7};, int e{};). To, czy używasz dodatkowych spacji dla czytelności, czy nie, jest kwestią osobistych preferencji.
Od C++ 17 inicjalizacja kopiowania, inicjalizacja bezpośrednia i inicjalizacja bezpośrednia listy zachowują się w większości przypadków identycznie. Najbardziej odpowiedni przypadek, w którym się różnią, omówimy poniżej.
Powiązana treść
Resztę różnic między inicjalizacją kopiowania, inicjalizacją bezpośrednią i inicjalizacją listy omówimy na lekcji 14.15 — Inicjalizacja klasy i elizja kopiowania.
Dla zaawansowanych czytelników
Inne formy inicjalizacji obejmują:
- Inicjalizacja agregowana (patrz 13.8 -- Inicjowanie agregatu Struct).
- Inicjalizacja listy-kopiowania (omówione poniżej).
- Inicjalizacja odniesienia (patrz 12.3 -- Odniesienia do wartości).
- Inicjalizacja statyczna, inicjalizacja stała i inicjalizacja dynamiczna (patrz 7.8 — Dlaczego (nie stałe) zmienne globalne są złe).
- Inicjalizacja zera (omówione poniżej).
Domyślna inicjalizacja
Gdy nie jest podany inicjator (np. dla zmiennej a powyżej), nazywa się to inicjalizacją domyślną. W wielu przypadkach inicjalizacja domyślna nie wykonuje inicjalizacji i pozostawia zmienną nieokreśloną wartość (wartość, której nie można przewidzieć, czasami nazywaną „wartością śmieciową”).
Omówimy ten przypadek w dalszej części lekcji (1.6 — Niezainicjowane zmienne i niezdefiniowane zachowanie).
Inicjalizacja kopiowania
Gdy wartość początkowa jest podana po znaku równości, nazywa się to inicjalizacją kopiowania. Ta forma inicjalizacji została odziedziczona z języka C język.
int width = 5; // copy-initialization of value 5 into variable widthPodobnie jak przypisanie kopii, kopiuje to wartość po prawej stronie równości do zmiennej tworzonej po lewej stronie. W powyższym fragmencie, zmienna width zostanie zainicjowana wartością 5.
inicjowanie kopiowania wypadła z łask we współczesnym C++ ze względu na mniejszą wydajność niż inne formy inicjalizacji dla niektórych typów złożonych. Jednakże C++ 17 rozwiązało większość tych problemów i inicjowanie kopiowania jest teraz znajdziesz nowych zwolenników. Można go również znaleźć w starszym kodzie (zwłaszcza w kodzie przeniesionym z C) lub przez programistów, którzy po prostu uważają, że wygląda bardziej naturalnie i jest łatwiejszy do odczytania.
Dla zaawansowanych czytelników
Inicjalizacja kopiowania jest również używana za każdym razem, gdy wartości są kopiowane niejawnie, na przykład podczas przekazywania argumentów do funkcji przez wartość, powrotu z funkcji przez wartość lub przechwytywania wyjątków według wartości.
Inicjalizacja bezpośrednia
Gdy wartość początkowa jest podana w nawiasie, jest to zwana inicjalizacją bezpośrednią.
int width ( 5 ); // direct initialization of value 5 into variable widthInicjalizacja bezpośrednia została początkowo wprowadzona, aby umożliwić bardziej efektywną inicjalizację obiektów złożonych (tych z typami klasowymi, które omówimy w następnym rozdziale). Podobnie jak inicjalizacja kopiowania, inicjalizacja bezpośrednia wypadła z łask we współczesnym C++, głównie z powodu jej zastąpienia przez bezpośrednią inicjalizację listy ma swój własny sposób, dlatego w niektórych przypadkach ponownie znajduje zastosowanie inicjalizacja bezpośrednia.
Dla zaawansowanych czytelników
Inicjalizacja bezpośrednia jest również używana, gdy wartości są jawnie rzutowane na inny typ (np. poprzez static_cast).
Inicjowanie listy
Nowoczesnym sposobem inicjowania obiektów w C++ jest użycie formy inicjalizacji wykorzystującej nawiasy klamrowe. Jest to zwana inicjowaniem listy (lub inicjalizacja jednolita lub inicjalizacja nawiasów ).
Inicjowanie listy występuje w dwóch postaciach:
int width { 5 }; // direct-list-initialization of initial value 5 into variable width (preferred)
int height = { 6 }; // copy-list-initialization of initial value 6 into variable height (rarely used)Wcześniej niż C++ 11 niektóre typy inicjalizacji wymagane przy użyciu inicjalizacji kopiowania i inne typy inicjalizacji wymagane przy użyciu inicjalizacji bezpośredniej mogą być trudne do odróżnienia przypisanie kopii (ponieważ oba używają =), a bezpośrednia inicjalizacja może być trudna do odróżnienia od operacji związanych z funkcjami (ponieważ oba używają nawiasów).
Inicjowanie listy zostało wprowadzone, aby zapewnić składnię inicjującą, która działa prawie we wszystkich przypadkach, zachowuje się spójnie i ma jednoznaczną składnię, która ułatwia określenie, gdzie inicjujemy obiekt.
Kluczowa informacja
Kiedy widzimy curly nawiasami klamrowymi, wiemy, że inicjujemy obiekt za pomocą listy.
Dodatkowo inicjowanie listy umożliwia także inicjowanie obiektów listą wartości, a nie pojedynczą wartością (dlatego nazywa się to „inicjowaniem listy”). Przykład tego pokażemy w lekcji 16.2 — Wprowadzenie do std::vector i konstruktorów list.
Inicjowanie listy nie pozwala na zawężanie konwersji
Jedna z głównych zalet inicjowania listy. dla nowych programistów C++ jest to, że „konwersje zawężające” są niedozwolone. Oznacza to, że jeśli spróbujesz zainicjować zmienną na liście przy użyciu wartości, której zmienna nie może bezpiecznie przechowywać, kompilator musi wygenerować komunikat diagnostyczny (błąd kompilacji lub ostrzeżenie), aby cię powiadomić. Na przykład:
int main()
{
// An integer can only hold non-fractional values.
// Initializing an int with fractional value 4.5 requires the compiler to convert 4.5 to a value an int can hold.
// Such a conversion is a narrowing conversion, since the fractional part of the value will be lost.
int w1 { 4.5 }; // compile error: list-init does not allow narrowing conversion
int w2 = 4.5; // compiles: w2 copy-initialized to value 4
int w3 (4.5); // compiles: w3 direct-initialized to value 4
return 0;
}W linii 7 powyższego programu używamy wartości (4.5) ze składnikiem ułamkowym (.5) do zainicjowania listy zmiennej całkowitej (która może przechowywać tylko wartości nieułamkowe). Ponieważ jest to konwersja zawężająca, w takich przypadkach kompilator musi wygenerować diagnostykę.
Inicjalizacja kopiowania (linia 9) i inicjalizacja bezpośrednia (linia 10) po cichu usuwają .5 i inicjują zmienną wartością 4 (co prawdopodobnie nie jest tym, czego chcemy). Twój kompilator może Cię o tym ostrzec (ponieważ utrata danych jest rzadko pożądana), ale może też tego nie robić.
Zauważ, że to ograniczenie zawężania konwersji dotyczy tylko inicjalizacji listy, a nie jakichkolwiek kolejnych przypisań do zmiennej:
int main()
{
int w1 { 4.5 }; // compile error: list-init does not allow narrowing conversion of 4.5 to 4
w1 = 4.5; // okay: copy-assignment allows narrowing conversion of 4.5 to 4
return 0;
}Inicjalizacja wartości i inicjalizacja zera
Gdy zmienna jest inicjowana przy użyciu pustego zestawu nawiasów klamrowych, obowiązuje specjalna forma inicjalizacji listy zwana inicjalizacją wartości ma miejsce. W większości przypadków inicjalizacja wartości domyślnie zainicjuje zmienną na zero (lub jakąkolwiek wartość najbliższą zeru dla danego typu). W przypadku wystąpienia zerowania nazywa się to inicjowaniem zera.
int width {}; // value-initialization / zero-initialization to value 0Dla zaawansowanych czytelników
W przypadku typów klas inicjalizacja wartości (i inicjalizacja domyślna) może zamiast tego zainicjować obiekt do predefiniowanych wartości domyślnych, które mogą być niezerowe.
Inicjowanie listy jest preferowaną formą inicjalizacji we współczesnym C++
Inicjowanie listy (w tym inicjalizacja wartości) jest ogólnie preferowana w porównaniu z innymi formami inicjalizacji, ponieważ działa w większości przypadków (i dlatego jest najbardziej spójna), uniemożliwia zawężanie konwersji (czego normalnie nie chcemy) i obsługuje inicjalizację za pomocą listy wartości (coś, co omówimy w przyszłej lekcji).
Najlepsza praktyka
Preferuj bezpośrednią inicjalizację listy lub inicjalizację wartości do inicjacji zmiennych.
Nota autora
Bjarne Stroustrup (twórca C++) i Herb Sutter (ekspert C++) również zalecają używanie inicjalizacji listy do inicjowania zmiennych.
We współczesnym C++ są pewne przypadki, w których inicjowanie listy nie działa zgodnie z oczekiwaniami. Jeden taki przypadek omawiamy w lekcji 16.2 — Wprowadzenie do std::vector i konstruktorów list. Z powodu takich dziwactw niektórzy doświadczeni programiści zalecają obecnie stosowanie kombinacji kopiowania, bezpośredniej i inicjalizacji listy, w zależności od okoliczności. Kiedy już zaznajomisz się z językiem na tyle, aby zrozumieć niuanse każdego typu inicjalizacji i uzasadnienie takich zaleceń, możesz samodzielnie ocenić, czy te argumenty są dla Ciebie przekonujące.
P: Kiedy powinienem inicjować za pomocą { 0 } zamiast {}?
Użyj bezpośredniej inicjalizacji listy, gdy faktycznie używasz wartości początkowej:
int x { 0 }; // direct-list-initialization with initial value 0
std::cout << x; // we're using that 0 value hereUżyj inicjalizacja wartości, gdy wartość obiektu jest tymczasowa i zostanie zastąpiona:
int x {}; // value initialization
std::cin >> x; // we're immediately replacing that value so an explicit 0 would be meaninglessZainicjuj swoje zmienne
Zainicjuj swoje zmienne podczas tworzenia. W końcu może się zdarzyć, że zechcesz zignorować tę radę z konkretnego powodu (np. sekcji kodu krytycznej dla wydajności, która wykorzystuje wiele zmiennych) i nie ma w tym nic złego, pod warunkiem, że wybór zostanie dokonany celowo.
Powiązana treść
W celu dalszej dyskusji na ten temat Bjarne Stroustrup (twórca języka C++) i Herb Sutter (ekspert C++) sami przedstawiają to zalecenie tutaj.
Badamy, co się stanie, jeśli spróbujesz użyć zmiennej, która nie ma dobrze zdefiniowanej wartości w lekcji 1.6 — Niezainicjowane zmienne i niezdefiniowane zachowanie.
Najlepsza praktyka
Zainicjuj zmienne po utworzeniu.
Instancja
Termin instancją to fantazyjne słowo, które oznacza, że zmienna została utworzona (przydzielona) i zainicjowana (obejmuje to domyślną inicjalizację). Obiekt utworzony jest czasami nazywany instancją. Najczęściej termin ten jest stosowany do obiektów typu klasowego, ale czasami jest również stosowany do obiektów innych typów.
Inicjowanie wielu zmiennych
W ostatniej sekcji zauważyliśmy, że możliwe jest zdefiniowanie wielu zmiennych tego samego typu w jednej instrukcji, oddzielając ich nazwy przecinkiem:
int a, b; // create variables a and b, but do not initialize themZauważyliśmy również, że najlepszą praktyką jest całkowite unikanie tej składni. Ponieważ jednak możesz napotkać inny kod korzystający z tego stylu, nadal warto porozmawiać o nim trochę więcej, choćby z innego powodu niż dla podkreślenia kilku powodów, dla których powinieneś go unikać.
Możesz inicjować wiele zmiennych zdefiniowanych w tej samej linii:
int a = 5, b = 6; // copy-initialization
int c ( 7 ), d ( 8 ); // direct-initialization
int e { 9 }, f { 10 }; // direct-list-initialization
int i {}, j {}; // value-initializationNiestety, istnieje tu częsta pułapka, która może wystąpić, gdy programista omyłkowo spróbuje zainicjować obie zmienne za pomocą jednej inicjalizacji instrukcja:
int a, b = 5; // wrong: a is not initialized to 5!
int a = 5, b = 5; // correct: a and b are initialized to 5W górnej instrukcji zmienna a zostanie niezainicjowana, a kompilator może złożyć skargę lub nie. Jeśli tak nie jest, jest to świetny sposób na sporadyczne awarie programu lub generowanie sporadycznych wyników. Porozmawiamy więcej o tym, co się stanie, jeśli wkrótce użyjesz niezainicjowanych zmiennych.
Najlepszym sposobem, aby pamiętać, że jest to błędne, jest zwrócenie uwagi, że każda zmienna może zostać zainicjowana tylko przez własny inicjator:
int a = 4, b = 5; // correct: a and b both have initializers
int a, b = 5; // wrong: a doesn't have its own initializerOstrzeżenia o nieużywanych zainicjalizowanych zmiennych
Nowoczesne kompilatory zazwyczaj generują ostrzeżenia, jeśli zmienna jest inicjalizowana, ale nie jest używana (ponieważ jest to rzadko pożądane). A jeśli włączona jest opcja „traktuj ostrzeżenia jako błędy”, ostrzeżenia te zostaną zaliczone do błędów i spowodują niepowodzenie kompilacji.
Rozważmy następujący niewinnie wyglądający program:
int main()
{
int x { 5 }; // variable x defined
// but not used anywhere
return 0;
}Podczas kompilacji z GCC i włączoną opcją „traktuj ostrzeżenia jako błędy”, generowany jest następujący błąd:
prog.cc: In function 'int main()': prog.cc:3:9: error: unused variable 'x' [-Werror=unused-variable]
i program się nie kompiluje.
Istnieje kilka prostych sposobów naprawienia tego to.
- Jeśli zmienna rzeczywiście jest nieużywana i nie jest Ci potrzebna, to najprościej będzie usunąć definicję
x(lub ją skomentować). W końcu, jeśli nie jest używana, usunięcie jej nie będzie miało na nic wpływu. - Inną opcją jest po prostu użycie gdzieś zmiennej:
#include <iostream>
int main()
{
int x { 5 };
std::cout << x; // variable now used somewhere
return 0;
}Ale wymaga to pewnego wysiłku, aby napisać kod, który z niej korzysta, a wadą jest potencjalna zmiana zachowania programu.
Klasa [[maybe_unused]] atrybut C++17
W niektórych przypadkach żadna z powyższych opcji nie jest pożądana. Rozważmy przypadek, w którym mamy zestaw wartości matematycznych/fizycznych, których używamy w wielu różnych programach:
#include <iostream>
int main()
{
// Here's some math/physics values that we copy-pasted from elsewhere
double pi { 3.14159 };
double gravity { 9.8 };
double phi { 1.61803 };
std::cout << pi << '\n'; // pi is used
std::cout << phi << '\n'; // phi is used
// The compiler will likely complain about gravity being defined but unused
return 0;
}Jeśli często używamy tych wartości, prawdopodobnie mamy je gdzieś zapisane i kopiujemy/wklejamy/importujemy je wszystkie razem.
Jednak w każdym programie, w którym nie używamy uniknąć tych wartości, kompilator prawdopodobnie będzie narzekał na każdą zmienną, która w rzeczywistości nie jest używana. W powyższym przykładzie moglibyśmy po prostu usunąć definicję gravity. Ale co by było, gdyby zamiast 3 było 20 lub 30 zmiennych? A co jeśli użyjemy ich w wielu miejscach? Przeglądanie listy zmiennych w celu usunięcia/komentowania nieużywanych wymaga czasu i energii. A później, jeśli będziemy potrzebować takiego, który wcześniej usunęliśmy, będziemy musieli poświęcić więcej czasu i energii, aby wrócić i ponownie go dodać/odkomentować.
Aby rozwiązać takie przypadki, w C++17 wprowadzono atrybut [[maybe_unused]] , który pozwala nam powiedzieć kompilatorowi, że nie przeszkadza nam nieużywanie zmiennej. Kompilator nie będzie generował ostrzeżeń o nieużywanych zmiennych dla takich zmiennych.
Następujący program nie powinien generować żadnych ostrzeżeń/błędów:
#include <iostream>
int main()
{
[[maybe_unused]] double pi { 3.14159 }; // Don't complain if pi is unused
[[maybe_unused]] double gravity { 9.8 }; // Don't complain if gravity is unused
[[maybe_unused]] double phi { 1.61803 }; // Don't complain if phi is unused
std::cout << pi << '\n';
std::cout << phi << '\n';
// The compiler will no longer warn about gravity not being used
return 0;
}Dodatkowo kompilator prawdopodobnie zoptymalizuje te zmienne poza programem, więc nie będą miały one wpływu na wydajność.
Klasa [[maybe_unused]] Atrybut powinien być stosowany selektywnie tylko do zmiennych, które mają konkretny i uzasadniony powód nieużywania (np. ponieważ potrzebna jest lista nazwane wartości, ale które konkretne wartości są faktycznie używane w danym programie, mogą się różnić). W przeciwnym razie nieużywane zmienne należy usunąć z programu.
Nota autora
Na przyszłych lekcjach często będziemy definiować zmienne, których nie będziemy używać ponownie, aby zademonstrować składnię pewnych pojęć. Użycie [[maybe_unused]] pozwala nam to zrobić bez ostrzeżeń/błędów kompilacji.
Czas quizu
Pytanie nr 1
Jaka jest różnica pomiędzy inicjalizacją a przypisaniem?
Pytanie nr 2
Jaką formę inicjalizacji powinieneś preferować, jeśli chcesz zainicjować zmienną o określonej wartości?
Pytanie nr 3
Co to jest inicjalizacja domyślna i inicjalizacja wartości? Jakie jest zachowanie każdego z nich? Co wolisz?

