Ta lekcja stanowi kontynuację naszej eksploracji instrukcji switch, którą rozpoczęliśmy w poprzedniej lekcji 8.5 - Podstawy instrukcji Switch. Na poprzedniej lekcji wspomnieliśmy, że każdy zestaw instrukcji pod etykietą powinien kończyć się na break statement lub return statement.
W tej lekcji zbadamy dlaczego i omówimy pewne problemy z zakresem przełączników, które czasami napotykają nowych programistów.
Przejście
Gdy wyrażenie switch pasuje do etykiety case lub opcjonalnej etykiety domyślnej, wykonanie rozpoczyna się od pierwszej instrukcji następującej po pasującej etykiecie. Wykonywanie będzie następnie kontynuowane, aż spełni się jeden z następujących warunków zakończenia:
- Osiągnięty zostanie koniec bloku przełączającego.
- Kolejna instrukcja przepływu sterowania (zazwyczaj
breaklubreturn) powoduje wyjście bloku przełączającego lub funkcji. - Coś innego zakłóca normalny przebieg programu (np. system operacyjny zamyka program, wszechświat imploduje, itd…)
Zauważ, że obecność innej etykiety przypadku jest nie jednym z tych warunków zakończenia — zatem bez break lub return wykonanie przeniesie się na kolejne przypadki.
Oto program, który wykazuje takie zachowanie:
#include <iostream>
int main()
{
switch (2)
{
case 1: // Nie pasuje
std::cout << 1 << '\n'; // Skipped
case 2: // Match!
std::cout << 2 << '\n'; // Tutaj rozpoczyna się wykonywanie
case 3:
std::cout << 3 << '\n'; // To jest również wykonywane
case 4:
std::cout << 4 << '\n'; // To jest również wykonywane
default:
std::cout << 5 << '\n'; // To jest również wykonywane
}
return 0;
}Ten program wyświetla następujące informacje:
2 3 4 5
Prawdopodobnie nie tego chcieliśmy! Kiedy wykonywanie przebiega z instrukcji znajdującej się pod etykietą do instrukcji pod kolejną etykietą, nazywa się to przejściem.
Ostrzeżenie
Gdy instrukcje znajdujące się pod przypadkiem lub etykietą domyślną zaczną się wykonywać, przeniosą się one do kolejnych przypadków. Aby temu zapobiec, zwykle używa się instrukcji break lub return.
Ponieważ awaria jest rzadko pożądana lub zamierzona, wiele kompilatorów i narzędzi do analizy kodu oznaczy awarię jako ostrzeżenie.
Atrybut [[fallthrough]]
Komentowanie celowego awarii jest powszechną konwencją informowania innych programistów, że awaria jest zamierzona. Chociaż działa to w przypadku innych programistów, kompilator i narzędzia do analizy kodu nie wiedzą, jak interpretować komentarze, więc nie pozbędą się ostrzeżeń.
Aby rozwiązać ten problem, C++17 dodaje nowy atrybut o nazwie [[fallthrough]].
Atrybuty to nowoczesna funkcja C++, która pozwala programiście dostarczyć kompilatorowi dodatkowych danych o kodzie. Aby określić atrybut, nazwę atrybutu umieszcza się w podwójnych nawiasach. Atrybuty nie są instrukcjami — raczej można ich używać niemal wszędzie tam, gdzie są istotne kontekstowo.
Klasa [[fallthrough]] atrybut modyfikuje null statement aby wskazać, że awaria jest zamierzona (i nie powinny zostać wywołane żadne ostrzeżenia):
#include <iostream>
int main()
{
switch (2)
{
case 1:
std::cout << 1 << '\n';
break;
case 2:
std::cout << 2 << '\n'; // Tutaj rozpoczyna się wykonywanie
[[fallthrough]]; // celowe przejście -- zwróć uwagę na średnik wskazujący instrukcję zerową
case 3:
std::cout << 3 << '\n'; // To jest również wykonywane
break;
}
return 0;
}Ten program wypisuje:
2 3
I nie powinien generować żadnych ostrzeżeń o awarii.
Najlepsza praktyka
Użyj [[fallthrough]] atrybut (wraz z instrukcją zerową) wskazujący zamierzenie fallthrough.
Etykiety przypadków sekwencyjnych
Możesz użyć operatora logicznego OR, aby połączyć wiele testów w jedną instrukcję:
bool isVowel(char c)
{
return (c=='a' || c=='e' || c=='i' || c=='o' || c=='u' ||
c=='A' || c=='E' || c=='I' || c=='O' || c=='U');
}Występuje te same wyzwania, które przedstawiliśmy we wstępie do instrukcji switch: c jest oceniany wiele razy i czytelnik musi się upewnić, że c jest oceniany za każdym razem.
Możesz zrób coś podobnego, używając instrukcji switch, umieszczając wiele etykiet case w sekwencji:
bool isVowel(char c)
{
switch (c)
{
case 'a': // jeśli c jest 'a'
case 'e': // lub jeśli c to „e”
case 'i': // lub jeśli c to „i”
case 'o': // lub jeśli c to „o”
case 'u': // lub jeśli c to „u”
case 'A': // lub jeśli c to „A”
case 'E': // lub jeśli c to „E”
case 'I': // lub jeśli c to „I”
case 'O': // lub jeśli c to „O”
case 'U': // lub jeśli c to „U”
return true;
default:
return false;
}
}Pamiętaj, wykonanie rozpoczyna się od pierwszej instrukcji po pasującej etykiecie case. Etykiety przypadków nie są instrukcjami (są etykietami), więc się nie liczą.
Pierwsza instrukcja po uniknąć instrukcjach przypadków w powyższym programie to return true, więc jeśli którekolwiek etykiety przypadków pasują, funkcja zwróci true.
W ten sposób możemy „nakładać” etykiety spraw, aby wszystkie te etykiety spraw później miały ten sam zestaw instrukcji. Nie jest to uważane za zachowanie awaryjne, więc użycie komentarzy lub [[fallthrough]] nie jest tutaj potrzebne.
Etykiety nie definiują nowego zakresu
Przy if statements, po warunku if może znajdować się tylko jedna instrukcja, a instrukcja ta jest uważana za znajdującą się domyślnie w bloku:
if (x > 10)
std::cout << x << " is greater than 10\n"; // ta linia domyślnie uważana za znajdującą się wewnątrz a blokJednak w przypadku instrukcji switch wszystkie instrukcje znajdujące się po etykietach są ograniczone do bloku switch. Nie są tworzone żadne ukryte bloki.
switch (1)
{
case 1: // nie tajnego tajnego prywatnego
foo(); // to jest część zakresu przełącznika, a nie ukryty blok dla przypadku 1
break; // to jest część zakresu przełącznika, a nie ukryty blok dla przypadku 1
default:
std::cout << "default case\n";
break;
}W powyższym przykładzie 2 instrukcje pomiędzy case 1 a etykietą domyślną są objęte zakresem bloku przełącznika, a nie bloku ukrytego dla case 1.
Deklaracja i inicjalizacja zmiennej wewnątrz instrukcji case
Możesz zadeklarować lub zdefiniować (ale nie inicjować) zmienne wewnątrz przełącznika, zarówno przed, jak i po etykietach case:
switch (1)
{
int a; // OK: definicja jest dozwolona przed etykietami przypadków
int b{ 5 }; // illegal: inicjalizacja nie jest dozwolona przed etykietami przypadków
case 1:
int y; // OK, ale zła praktyka: definicja jest dozwolona w obrębie przypadku
y = 4; // OK: przypisanie jest technologiczne
break;
case 2:
int z{ 4 }; // illegal: inicjalizacja nie jest dozwolona, jeśli istnieją kolejne przypadki
y = 5; // ok: y zostało zadeklarowane powyżej, więc możemy go użyć również tutaj
break;
case 3:
break;
}Chociaż zmienna y została zdefiniowana w case 1, był on również używany w case 2 . Wszystkie instrukcje wewnątrz przełącznika są uważane za część tego samego zakresu. Zatem zmienna zadeklarowana lub zdefiniowana w jednym przypadku może zostać wykorzystana w późniejszym przypadku, nawet jeśli przypadek, w którym zdefiniowano zmienną, nigdy nie zostanie wykonany (ponieważ przełącznik ją przeskoczył)!
Jednak inicjalizacja zmiennych ma wymaga wykonania definicji. Inicjowanie zmiennych jest niedozwolone w każdym przypadku, który nie jest ostatnim przypadkiem (ponieważ przełącznik mógłby przeskoczyć inicjator, jeśli zdefiniowany został kolejny przypadek, w którym to przypadku zmienna byłaby niezdefiniowana, a dostęp do niej skutkowałby niezdefiniowanym zachowaniem). Inicjalizacja jest również niedozwolona przed pierwszym przypadkiem, ponieważ te instrukcje nigdy nie zostaną wykonane, ponieważ przełącznik nie ma możliwości dotarcia do nich.
Jeśli przypadek wymaga zdefiniowania i/lub zainicjowania nowej zmiennej, najlepszą praktyką jest zrobienie tego w jawnym bloku pod instrukcją case:
switch (1)
{
case 1:
{ // zwróć uwagę na dodanie tutaj wyraźnego bloku
int x{ 4 }; // okej, zmienne mogą być inicjalizowane wewnątrz bloku wewnątrz case
std::cout << x;
break;
}
default:
std::cout << "default case\n";
break;
}Najlepsza praktyka
Jeśli definiujesz zmienne używane w instrukcji case, zrób to w bloku wewnątrz case.
Czas quizu
Pytanie nr 1
Napisz funkcję o nazwie Calc() który przyjmuje dwie liczby całkowite i znak reprezentujący jedną z następujących operacji matematycznych: +, -, *, / lub % (reszta). Użyj instrukcji switch, aby wykonać odpowiednią operację matematyczną na liczbach całkowitych i zwrócić wynik. Jeśli do funkcji zostanie przekazany nieprawidłowy operator, funkcja powinna wyświetlić komunikat o błędzie. W przypadku operatora dzielenia wykonaj dzielenie na liczbach całkowitych i nie przejmuj się dzieleniem przez zero.
Wskazówka: „operator” to słowo kluczowe, zmiennych nie można nazwać „operatorem”.

