8.6 — Przełączanie przełączania i określanie zakresu

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 break lub return) 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 blok

Jednak 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”.

Pokaż rozwiązanie

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