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; // zdefiniuj zmienną całkowitą o nazwie x (preferowaną)
int y, z; // zdefiniuj dwie zmienne całkowite, nazwane y i 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, = nazywa się operatorem przypisania.
int width; // zdefiniuj zmienną całkowitą o nazwie szerokość
width = 5; // przypisanie wartości 5 do zmiennej szerokości
// szerokość zmiennej ma teraz wartość 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; // zdefiniuj zmienną o nazwie szerokość
width = 5; // skopiuj przypisanie wartości 5 do zmiennej szerokości
std::cout << width; // prints 5
width = 7; // zmienić wartość przechowywaną w zmiennej szerokość na 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 }; // zdefiniuj szerokość zmiennej i zainicjuj jej wartością początkową 5
std::cout << width; // prints 5
return 0;
}W powyższej inicjalizacji zmiennej width, { 5 } jest inicjatorem, 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)
// Tradycyjne formy inicjalizacji:
int b = 5; // kopiuj-inicjalizację (wartość początkowa po znaku równości)
int c ( 6 ); // bezpośrednia inicjalizacja (wartość początkowa w nawias)
// Modern initialization forms (preferred):
int d { 7 }; // bezpośrednia-inicjalizacja-listy (wartość początkowa w nawiasach klamrowych)
int e {}; // inicjalizacja wartości (puste nawiasy klamrowe)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; // skopiuj-inicjalizację wartości 5 do zmiennej szerokościPodobnie 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 ); // bezpośrednia inicjalizacja wartości 5 do zmiennej szerokościInicjalizacja 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 }; // bezpośrednia-inicjalizacja-listy o wartości początkowej 5 do zmiennej szerokości (preferowane)
int height = { 6 }; // skopiuj-listę-inicjalizacja wartości początkowej 6 do zmiennej wysokości (rzadko używana)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()
{
// Liczba całkowita może przechowywać tylko wartości nieułamkowe.
// Inicjowanie int o wartości ułamkowej 4.5 wymaga od kompilatora konwersji 4.5 na wartość, którą może przechowywać int.
// Taka konwersja jest konwersją zawężającą, ponieważ część ułamkowa wartości będzie utracone.
int w1 { 4.5 }; // błąd kompilacji: list-init nie pozwala na zawężającą konwersję
int w2 = 4.5; // kompiluje: w2 kopiowanie inicjowane do wartości 4
int w3 (4.5); // kompiluje: w3 bezpośrednio inicjowane do wartości 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 }; // błąd kompilacji: list-init nie pozwala na zawężającą konwersję z 4.5 do 4
w1 = 4.5; // ok: przypisanie kopii umożliwia zawężającą konwersję z 4,5 do 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 {}; // inicjalizacja-wartości / inicjalizacja zera do wartości 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 }; // bezpośrednia-inicjalizacja-listy o wartości początkowej 0
std::cout << x; // używamy tutaj tej wartości 0Użyj inicjalizacja wartości, gdy wartość obiektu jest tymczasowa i zostanie zastąpiona:
int x {}; // inicjalizacja wartości
std::cin >> x; // od razu zastępujemy tę wartość, więc jawne 0 nie miałoby sensuZainicjuj 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; // utwórz zmienne aib, ale nie rób tego zainicjuj jeZauważ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; // kopiuj-inicjalizację
int c ( 7 ), d ( 8 ); // direct-initialization
int e { 9 }, f { 10 }; // bezpośrednia inicjalizacja listy
int i {}, j {}; // inicjalizacja wartościNiestety, 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; // źle: a nie jest inicjowane na 5!
int a = 5, b = 5; // poprawna: a i b są inicjalizowane do 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; // poprawne: a i b mają inicjatory
int a, b = 5; // źle: a nie ma własnego inicjatoraOstrzeż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
// ale nie jest nigdzie używany
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; // zmienna teraz gdzieś używana
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()
{
// Oto kilka wartości matematycznych/fizycznych, które skopiowaliśmy z innego miejsca
double pi { 3.14159 };
double gravity { 9.8 };
double phi { 1.61803 };
std::cout << pi << '\n'; // pi jest używane
std::cout << phi << '\n'; // phi jest używane
// Kompilator prawdopodobnie będzie narzekał, że grawitacja jest zdefiniowana, ale nieużywana
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 }; // Nie narzekaj, jeśli pi jest unused
[[maybe_unused]] double gravity { 9.8 }; // Nie narzekaj, jeśli grawitacja nie jest używana
[[maybe_unused]] double phi { 1.61803 }; // Nie narzekaj, jeśli phi jest niewykorzystane
std::cout << pi << '\n';
std::cout << phi << '\n';
// Kompilator nie będzie już ostrzegać o nieużywaniu grawitacji
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?

