>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 a 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 a 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: // Does not match
        std::cout << 1 << '\n'; // Skipped
    case 2: // Match!
        std::cout << 2 << '\n'; // Execution begins here
    case 3:
        std::cout << 3 << '\n'; // This is also executed
    case 4:
        std::cout << 4 << '\n'; // This is also executed
    default:
        std::cout << 5 << '\n'; // This is also executed
    }

    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'; // Execution begins here
        [[fallthrough]]; // intentional fallthrough -- note the semicolon to indicate the null statement
    case 3:
        std::cout << 3 << '\n'; // This is also executed
        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': // if c is 'a'
    case 'e': // or if c is 'e'
    case 'i': // or if c is 'i'
    case 'o': // or if c is 'o'
    case 'u': // or if c is 'u'
    case 'A': // or if c is 'A'
    case 'E': // or if c is 'E'
    case 'I': // or if c is 'I'
    case 'O': // or if c is 'O'
    case 'U': // or if c is '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"; // this line implicitly considered to be inside a block

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: // does not create an implicit block
    foo(); // this is part of the switch scope, not an implicit block to case 1
    break; // this is part of the switch scope, not an implicit block to case 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; // okay: definition is allowed before the case labels
    int b{ 5 }; // illegal: initialization is not allowed before the case labels

case 1:
    int y; // okay but bad practice: definition is allowed within a case
    y = 4; // okay: assignment is allowed
    break;

case 2:
    int z{ 4 }; // illegal: initialization is not allowed if subsequent cases exist
    y = 5; // okay: y was declared above, so we can use it here too
    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:
{ // note addition of explicit block here
    int x{ 4 }; // okay, variables can be initialized inside a block inside a 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