6.1 — Pierwszeństwo i łączenie operatorów

Wprowadzenie do rozdziału

Ten rozdział opiera się na koncepcjach z lekcji 1.9 -- Wprowadzenie do literałów i operatorów. Oto krótki przegląd:

An operacja to proces matematyczny obejmujący zero lub więcej wartości wejściowych (zwanych operandów), który generuje nową wartość (zwaną wartością wyjściową). Konkretna operacja, którą należy wykonać, jest oznaczona konstrukcją (zazwyczaj symbolem lub parą symboli) zwaną operator.

Na przykład, jako dzieci, wszyscy dowiadujemy się, że 2 + 3 jestrówna 5 literały 2 i 3 są operandami, a symbol + jest operatorem, który nakazuje nam zastosować dodawanie matematyczne na operandach w celu uzyskania nowej wartości 5. Ponieważ używany jest tutaj tylko jeden operator, jest to proste.

W tym rozdziale omówimy tematy związane z operatorami i poznamy wiele popularnych operatorów obsługiwanych przez C++.

Ocena wyrażeń złożonych

Rozważmy teraz wyrażenie złożone, takie jak 4 + 2 * 3. Czy należy to pogrupować jako (4 + 2) * 3 co daje 18 lub 4 + (2 * 3) co daje 10? Używając normalnych matematycznych reguł pierwszeństwa (które stwierdzają, że mnożenie jest rozwiązywane przed dodawaniem), wiemy, że powyższe wyrażenie należy pogrupować jako 4 + (2 * 3) w celu wygenerowania wartości 10. Ale skąd kompilator o tym wie?

Aby ocenić wyrażenie, kompilator musi wykonać dwie rzeczy:

  • W czasie kompilacji kompilator musi przeanalizować wyrażenie i określić, w jaki sposób operandy są pogrupowane za pomocą operatorów. Odbywa się to poprzez reguły pierwszeństwa i skojarzeń, które omówimy za chwilę.
  • W czasie kompilacji lub w czasie wykonywania operandy są oceniane, a operacje wykonywane w celu uzyskania wyniku.

Pierwszeństwo operatorów

Aby pomóc w analizowaniu wyrażenia złożonego, wszystkim operatorom przypisany jest poziom pierwszeństwa. Operatory z wyższym poziomem pierwszeństwa są najpierw grupowane za pomocą operandów.

W poniższej tabeli widać, że mnożenie i dzielenie (poziom pierwszeństwa 5) mają wyższy poziom pierwszeństwa niż dodawanie i odejmowanie (poziom pierwszeństwa 6). Zatem mnożenie i dzielenie zostaną zgrupowane z operandami przed dodawaniem i odejmowaniem. Innymi słowy, 4 + 2 * 3 zostaną pogrupowane jako 4 + (2 * 3).

Skojarzenie operatorów

Rozważ wyrażenie złożone, takie jak 7 - 4 - 1. Czy należy to pogrupować jako (7 - 4) - 1 co daje 2 lub 7 - (4 - 1), co daje 4? Ponieważ oba operatory odejmowania mają ten sam poziom pierwszeństwa, kompilator nie może użyć samego pierwszeństwa do określenia sposobu grupowania.

Jeśli dwa operatory o tym samym poziomie pierwszeństwa sąsiadują ze sobą w wyrażeniu, skojarzenie operatora mówi kompilatorowi, czy ma oceniać operatory (nie operandy!) od lewej do prawej, czy od prawej do lewej. Odejmowanie ma poziom pierwszeństwa 6, a operatory na poziomie pierwszeństwa 6 mają łączność od lewej do prawej. Zatem to wyrażenie jest pogrupowane od lewej do prawej: (7 - 4) - 1.

Tabela pierwszeństwa i powiązania operatorów

Poniższa tabela ma przede wszystkim służyć jako wykres referencyjny, do którego można się odwołać w przyszłości, aby rozwiązać wszelkie pojawiające się pytania dotyczące pierwszeństwa lub powiązania.

Uwagi:

  • Poziom pierwszeństwa 1 to najwyższy poziom pierwszeństwa, a poziom 17 to najwyższy poziom pierwszeństwa najniższy. Operandy o wyższym poziomie pierwszeństwa grupowane są w pierwszej kolejności.
  • L->R oznacza łączenie od lewej do prawej.
  • R->L oznacza łączenie od prawej do lewej.

Prec/AssOperatorDescriptionWzór
1 L->R::
::
Zakres globalny (jednoargumentowy)
Zakres przestrzeni nazw (binarny)
::nazwa
nazwa_klasy::nazwa_członka
2 L->R()
()
type()
type{
[]
.
->
++
––
typ
const_cast
dynamic_cast
reinterpret_cast
static_cast
sizeof…
noexcept
wyrównanie
Nawiasy
Wywołanie funkcji
Obsada funkcjonalna
Lista obiektu tymczasowego init (C++11)
Array indeks dolny
Dostęp elementu z obiektu
Dostęp elementu z obiektu ptr
Po-zwiększeniu
Po zmniejszeniu
Informacje o typie wykonania
Odrzuć const
Rzutuj w czasie wykonywania ze sprawdzaniem typu
Rzutuj jeden typ na inny
Rzutuj w czasie kompilacji ze sprawdzaniem typu
Uzyskaj rozmiar pakietu parametrów
Sprawdzanie wyjątków w czasie kompilacji
Uzyskaj typ wyrównanie
(wyrażenie)
nazwa_funkcji(argumenty)
typ(wyrażenie)
typ{wyrażenie
wskaźnik[e xpression]
obiekt.nazwa_elementu
wskaźnik_obiektu->nazwa_elementu
lwartość++
lwartość––
typid(typ) lub typeid(wyrażenie)
const_cast<typ>(wyrażenie)
dynamic_cast<typ>(wyrażenie)
reinterpret_cast<typ>(wyrażone sion)
static_cast<type>(wyrażenie)
rozmiar…(wyrażenie)
noexcept(wyrażenie)
alignof(typ)
3 R->L+
-
++
––
!
nie
~
(type)
operator sizeof
co_await
&
*
nowego
new[]
delete
delete[]
Jednoargumentowy plus
Jednoargumentowy minus
Wstępny przyrost
Wstępny spadek
NIELogiczne
NIELogiczne
Bitowo NOT
w stylu C
Rozmiar w bajtach
Oczekuj asynchronicznie wywołanie
Adres
Dereferencja
Dynamiczna alokacja pamięci
Dynamiczna alokacja tablicy
Dynamiczne kasowanie pamięci
Dynamiczna alokacja tablicy usunięcie
+wyrażenie
-wyrażenie
++lwartość
––lwartość
!wyrażenie
nie wyrażenie
~wyrażenie
(nowy_typ)wyrażenie
rozmiar(typ) lub rozmiar(wyrażenie)
wyrażenie co_await (C++20)
&lwartość
*wyrażenie
nowy typ
nowy typ[wyrażenie]
usuń wskaźnik
usuń[] wskaźnik
4 L->R->*
.*
Selektor wskaźnika elementu składowego
Selektor obiektu elementu składowego
object_pointer->*pointer_to_member
object.*pointer_to_ember
5 L->R*
/
%
Mnożenie
Dzielenie
Reszta
wyrażenie * wyrażenie
wyrażenie / wyrażenie
wyrażenie % wyrażenie
6 L->R+
-
Dodawanie
Odejmowanie
wyrażenie + wyrażenie
wyrażenie - wyrażenie
7 L->R<<
>>
Przesunięcie bitowe w lewo / Wstawienie
Przesunięcie bitowe w prawo / Wyodrębnienie
wyrażenie << wyrażenie
wyrażenie >> wyrażenie
8 L->R<=>Porównanie trójczynnikowe (C++20)wyrażenie <=> wyrażenie
9 L->R<
<=
>
>=
Porównanie mniejsze niż
Porównanie mniejsze niż lub równa się
Porównanie większe niż
Porównanie większe lub równe
wyrażenie < wyrażenie
wyrażenie <= wyrażenie
wyrażenie > wyrażenie
wyrażenie >= wyrażenie
10 L->R==
!=
Równość
Nierówność
wyrażenie == wyrażenie
wyrażenie != wyrażenie
11 L->R&Bitowe ANDwyrażenie i wyrażenie
12 L->R^Bitowego XORwyrażenie ^ wyrażenie
13 L->R|Bitowo LUBwyrażenie | wyrażenie
14 L->R&&
i
Logiczne AND
Logiczne AND
wyrażenie && wyrażenie
wyrażenie i wyrażenie

15 L->R||
lub
Logiczne LUB
Logiczne LUB
wyrażenie || wyrażenie
wyrażenie lub wyrażenie
16 R->Lthrow
co_yield
?:
=
*=
/=
%=
+=
-=
<<=
>>=
&=
|=
^=
wyrażenie rzutu
wyrażenie plonu (C++20)
Warunkowe
Przypisanie
Przypisanie mnożenia
Przypisanie przez dzielenie
Przypisanie reszty
Przypisanie dodawania
Odejmowanie
Przypisanie z przesunięciem bitowym w lewo
Przypisanie z przesunięciem bitowym w prawo
Przypisanie bitowe AND
Przypisanie bitowe OR
Przypisanie bitowe XOR przypisanie
wyrażenie rzutu
wyrażenie współ_wyrażenia
wyrażenie ? wyrażenie: wyrażenie
lwartość = wyrażenie
lwartość *= wyrażenie
lwartość /= wyrażenie
lwartość %= wyrażenie
lwartość += wyrażenie
lwartość -= wyrażenie
lwartość <<= wyrażenie
lwartość >>= wyrażenie
lwartość &= wyrażenie
lwartość |= wyrażenie
lwartość ^= wyrażenie
17 L->R,Operator przecinkawyrażenie, wyrażenie

Powinieneś już rozpoznaj kilka z tych operatorów, takich jak +, -, *, /, (), I sizeof. Jeśli jednak nie masz doświadczenia z innym językiem programowania, większość operatorów w tej tabeli będzie prawdopodobnie dla Ciebie niezrozumiała. Można się tego spodziewać w tym momencie. Wiele z nich omówimy w tym rozdziale, a pozostałe zostaną wprowadzone, jeśli będzie taka potrzeba.

P: Gdzie jest operator wykładnika?

C++ nie zawiera operatora do potęgowania (operator^ ma inną funkcję w C++). Potęgowanie omawiamy szerzej na lekcji 6.3 -- Reszta i potęgowanie.

Zauważ to operator<< obsługuje zarówno bitowe przesunięcie w lewo, jak i wstawianie, oraz operator>> obsługuje zarówno bitowe przesunięcie w prawo, jak i ekstrakcję. Kompilator może określić, jaką operację wykonać na podstawie typów operandów.

Nawiasy

Ze względu na zasady pierwszeństwa, 4 + 2 * 3 zostaną pogrupowane jako 4 + (2 * 3). A co jeśli faktycznie mieliśmy na myśli (4 + 2) * 3? Podobnie jak w normalnej matematyce, w C++ możemy jawnie używać nawiasów, aby ustawić grupowanie operandów według własnego uznania. Działa to, ponieważ nawiasy mają jeden z najwyższych poziomów pierwszeństwa, zatem nawiasy zazwyczaj oceniają przed tym, co się w nich znajduje.

Użyj nawiasów, aby ułatwić zrozumienie wyrażeń złożonych

Rozważ teraz wyrażenie takie jak x && y || z. Czy to jest oceniane jako (x && y) || z lub x && (y || z)? Możesz zajrzeć do tabeli i zobaczyć, że && ma pierwszeństwo przed ||. Ale jest tak wiele operatorów i poziomów pierwszeństwa, że ​​trudno je wszystkie zapamiętać. Nie chcesz też cały czas wyszukiwać operatorów, aby zrozumieć, jak obliczane jest wyrażenie złożone.

Aby ograniczyć błędy i ułatwić zrozumienie kodu bez odwoływania się do tabeli pierwszeństwa, dobrym pomysłem jest umieszczenie w nawiasach wszelkich nietrywialnych wyrażeń złożonych, aby było jasne, jaki jest Twój zamiar.

Najlepsza praktyka

Użyj nawiasów, aby było jasne, jak powinno oceniać nietrywialne wyrażenie złożone (nawet jeśli są technicznie niepotrzebne).

Dobrą zasadą jest: umieszczaj w nawiasach wszystko z wyjątkiem dodawania, odejmowania, mnożenia i dzielenia.

Istnieje jeden dodatkowy wyjątek od powyższej najlepszej praktyki: Wyrażenia, które mają pojedynczy operator przypisania (bez operatora przecinka) nie muszą mieć prawego operandu przypisania owiniętego w nawias.

Na przykład:

x = (y + z + w);   // instead of this
x = y + z + w;     // it's okay to do this

x = ((y || z) && w); // instead of this
x = (y || z) && w;   // it's okay to do this

x = (y *= z); // expressions with multiple assignments still benefit from parenthesis

Operatory przypisania mają drugi najniższy priorytet (tylko operator przecinka) jest niższy i rzadko używany). Dlatego też, dopóki istnieje tylko jedno przypisanie (i nie ma przecinków), wiemy, że prawy operand zostanie w pełni obliczony przed przypisaniem.

Najlepsza praktyka

Wyrażenia z pojedynczym operatorem przypisania nie muszą mieć prawego operandu przypisania owiniętego w nawias.

Obliczanie wartości operacji

W standardzie C++ używany jest termin wartość obliczenia oznaczające wykonanie operatorów w wyrażeniu w celu wygenerowania wartości. Reguły pierwszeństwa i asocjacji określają kolejność obliczania wartości.

Na przykład, biorąc pod uwagę wyrażenie 4 + 2 * 3, ze względu na reguły pierwszeństwa grupuje się je jako 4 + (2 * 3). Obliczenie wartości dla (2 * 3) musi nastąpić najpierw, aby można było zakończyć obliczenie wartości dla 4 + 6 .

Ocena argumentów

Standard C++ (przeważnie) używa terminu oceną w odniesieniu do oceny operandów (a nie oceny operatorów lub wyrażeń!). Na przykład dane wyrażenie a + b, a zostanie ocenione pod kątem wygenerowania pewnej wartości, a b zostanie ocenione w celu wygenerowania pewnej wartości. Wartości te można następnie wykorzystać jako argumenty operator+ do obliczenia wartości.

Nomenklatura

Nieformalnie zwykle używamy terminu „ocena” w odniesieniu do oceny całego wyrażenia (obliczenia wartości), a nie tylko argumentów wyrażenia.

Kolejność obliczania argumentów (w tym argumentów funkcji) jest w większości nieokreślona

W większości przypadków kolejność oceny operandów i argumentów funkcji jest nieokreślona, co oznacza, że mogą być oceniane w dowolnej kolejności.

Rozważmy następujące wyrażenie:

a * b + c * d

Z powyższych reguł pierwszeństwa i kojarzenia wiemy, że to wyrażenie zostanie pogrupowane tak, jak gdybyśmy wpisali:

(a * b) + (c * d)

Jeśli a Jest 1, b Jest 2, c Jest 3, I d Jest 4, to wyrażenie zawsze obliczy wartość 14.

Jednak reguły pierwszeństwa i łączenia mówią nam tylko, w jaki sposób operatory i operandy są pogrupowane i kolejność, w jakiej nastąpi obliczanie wartości. Nie mówią nam o kolejności, w jakiej oceniane są operandy lub podwyrażenia. Kompilator może dowolnie oceniać argumenty a, b, c lub d w dowolnej kolejności. Kompilator może również wykonać a * b lub c * d najpierw obliczenia.

W przypadku większości wyrażeń nie ma to znaczenia. W powyższym przykładowym wyrażeniu nie ma znaczenia, w jakiej kolejności zmienne a, b, c lub d są oceniane pod kątem ich wartości: obliczona wartość zawsze będzie wynosić 14. Nie ma tu żadnej dwuznaczności.

Można jednak pisać wyrażenia, w których kolejność obliczania ma znaczenie. Rozważmy ten program, który zawiera błąd często popełniany przez nowych programistów C++:

#include <iostream>

int getValue()
{
    std::cout << "Enter an integer: ";

    int x{};
    std::cin >> x;
    return x;
}

void printCalculation(int x, int y, int z)
{
    std::cout << x + (y * z);
}

int main()
{
    printCalculation(getValue(), getValue(), getValue()); // this line is ambiguous

    return 0;
}

Jeśli uruchomisz ten program i wprowadzisz dane wejściowe 1, 2, I 3, możesz założyć, że program ten obliczy 1 + (2 * 3) i wydrukuje 7. Ale to zakłada, że ​​argumenty printCalculation() będą oceniane w kolejności od lewej do prawej (więc parametr x otrzymuje wartość 1, y otrzymuje wartość 2, I z otrzymuje wartość 3). Jeśli zamiast tego argumenty są oceniane w kolejności od prawej do lewej (więc parametr z otrzymuje wartość 1, y otrzymuje wartość 2, I x otrzymuje wartość 3), wówczas program wypisze 5 .

Wskazówka

Kompilator Clang ocenia argumenty w kolejności od lewej do prawej. Kompilator GCC ocenia argumenty w kolejności od prawej do lewej.

Jeśli chcesz zobaczyć to zachowanie na własne oczy, możesz to zrobić na Wandbox. Wklej powyższy program, wejdź w 1 2 3 w klasie zakładkę Stdin , wybierz GCC lub Clang, a następnie skompiluj program. Dane wyjściowe pojawią się na dole strony (być może trzeba będzie przewinąć w dół, aby je zobaczyć). Zauważysz, że dane wyjściowe dla GCC i Clang różnią się!

Powyższy program można ujednoznacznić, czyniąc każde wywołanie funkcji getValue() oddzielną instrukcją:

#include <iostream>

int getValue()
{
    std::cout << "Enter an integer: ";

    int x{};
    std::cin >> x;
    return x;
}

void printCalculation(int x, int y, int z)
{
    std::cout << x + (y * z);
}

int main()
{
    int a{ getValue() }; // will execute first
    int b{ getValue() }; // will execute second
    int c{ getValue() }; // will execute third

    printCalculation(a, b, c); // this line is now unambiguous

    return 0;
}

W tej wersji a zawsze będzie miało wartość 1, b będzie miało wartość 2, I c będzie miało wartość 3. Kiedy argumenty printCalculation() są oceniane, nie ma znaczenia, w jakiej kolejności następuje ocena argumentów - parametr x zawsze otrzyma wartość 1, y otrzyma wartość 2, I z otrzyma wartość 3. Ta wersja będzie deterministycznie drukowana 7.

Kluczowa informacja

Operandy, argumenty funkcji i podwyrażenia mogą być oceniane w dowolnej kolejności.

Częstym błędem jest przekonanie, że pierwszeństwo operatorów i łączność wpływają na kolejność oceniania. Pierwszeństwo i łączenie są używane tylko do określenia sposobu grupowania operandów za pomocą operatorów i kolejności obliczania wartości.

Ostrzeżenie

Upewnij się, że pisane wyrażenia (lub wywołania funkcji) nie są zależne od kolejności obliczania operandów (lub argumentów).

Powiązana treść

Operatory wywołujące skutki uboczne mogą również powodować nieoczekiwane wyniki oceny. Omawiamy to na lekcji 6.4 — Operatory zwiększania/zmniejszania i skutki uboczne.

Czas quizu

Pytanie nr 1

Z codziennej matematyki wiesz, że wyrażenia w nawiasach są oceniane w pierwszej kolejności. Na przykład w wyrażeniu (2 + 3) * 4, instrukcja (2 + 3) najpierw obliczana jest część.

W tym ćwiczeniu podany jest zestaw wyrażeń bez nawiasów. Korzystając z zasad pierwszeństwa operatorów i skojarzeń z powyższej tabeli, dodaj nawiasy do każdego wyrażenia, aby było jasne, w jaki sposób kompilator oceni wyrażenie.

Pokaż wskazówkę

Przykładowy problem: x = 2 + 3% 4

Operator binarny % ma wyższy priorytet niż operator + lub operator =, więc jest oceniany jako pierwszy:

x = 2 + (3 % 4)

Operator binarny + ma wyższy priorytet niż operator =, więc jest oceniany dalej:

Ostateczna odpowiedź: x = (2 + (3 % 4))

Nie potrzebujemy już powyższej tabeli, aby zrozumieć, jak to wyrażenie będzie obliczane.

a) x = 3 + 4 + 5;

Pokaż rozwiązanie

b) x = y = z;

Pokaż rozwiązanie

c) z *= ++y + 5;

Pokaż rozwiązanie

d) a || b i& c || d;

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