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/Ass | Operator | Description | Wzó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 AND | wyrażenie i wyrażenie |
| 12 L->R | ^ | Bitowego XOR | wyrażenie ^ wyrażenie |
| 13 L->R | | | Bitowo LUB | wyraż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->L | throw 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 przecinka | wyraż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 parenthesisOperatory 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 * dZ 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.
| Przykładowy problem: x = 2 + 3% 4 Operator binarny x = 2 + (3 % 4) Operator binarny 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;
b) x = y = z;
c) z *= ++y + 5;
d) a || b i& c || d;

