C/Operatory

Z Wikibooks, biblioteki wolnych podręczników.
< C
Skocz do: nawigacji, wyszukiwania

Przypisanie[edytuj]

Operator przypisania ("="), jak sama nazwa wskazuje, przypisuje wartość prawego argumentu lewemu, np.:

int a = 5, b;
b = a;
printf("%d\n", b); /* wypisze 5 */

Operator ten ma łączność prawostronną tzn. obliczanie przypisań następuje z prawa na lewo i zwraca on przypisaną wartość, dzięki czemu może być użyty kaskadowo:

int a, b, c;
a = b = c = 3;
printf("%d %d %d\n", a, b, c);  /* wypisze "3 3 3" */

Skrócony zapis[edytuj]

C umożliwia też skrócony zapis postaci a #= b;, gdzie # jest jednym z operatorów: +, -, *, /, %, &, |, ^, << lub >> (opisanych niżej). Ogólnie rzecz ujmując zapis a #= b; jest równoważny zapisowi a = a # (b);, np.:

int a = 1;
a += 5;     /* to samo, co a = a + 5;       */
a /= a + 2; /* to samo, co a = a / (a + 2); */
a %= 2;     /* to samo, co a = a % 2;       */
Porada Początkowo skrócona notacja miała następującą składnię: a =# b, co często prowadziło do niejasności, np. i =-1 (i = -1 czy też i = i-1?). Dlatego też zdecydowano się zmienić kolejność operatorów.

Rzutowanie[edytuj]

Zadaniem rzutowania jest konwersja danej jednego typu na daną innego typu. Konwersja może być niejawna (domyślna konwersja przyjęta przez kompilator) lub jawna (podana explicite przez programistę). Oto kilka przykładów konwersji niejawnej:

int i = 42.7;            /* konwersja z double do int */
float f = i;             /* konwersja z int do float */
double d = f;            /* konwersja z float do double */
unsigned u = i;          /* konwersja z int do unsigned int */
f = 4.2;                 /* konwersja z double do float */
i = d;                   /* konwersja z double do int */
char *str = "foo";       /* konwersja z const char* do char*  [1] */
const char *cstr = str;  /* konwersja z char* do const char* */
void *ptr = str;         /* konwersja z char* do void* */

Podczas konwersji zmiennych zawierających większe ilości danych do typów prostszych (np. double do int) musimy liczyć się z utratą informacji, jak to miało miejsce w pierwszej linijce - zmienna int nie może przechowywać części ułamkowej toteż została ona odcięta i w rezultacie zmiennej została przypisana wartość 42.

Zaskakująca może się wydać linijka oznaczona przez [1]. Niejawna konwersja z typu const char* do typu char* nie jest dopuszczana przez standard C. Jednak literały napisowe (które są typu const char*) stanowią tutaj wyjątek. Wynika on z faktu, że były one używane na długo przed wprowadzeniem słówka const do języka i brak wspomnianego wyjątku spowodowałby, że duża część kodu zostałaby nagle zakwalifikowana jako niepoprawny kod.

Do jawnego wymuszenia konwersji służy jednoargumentowy operator rzutowania, np.:

double d = 3.14;
int pi = (int)d;         /* 1 */
pi = (unsigned)pi >> 4;  /* 2 */

W pierwszym przypadku operator został użyty, by zwrócić uwagę na utratę precyzji. W drugim, dlatego że bez niego operator przesunięcia bitowego zachowuje się trochę inaczej.

Obie konwersje przedstawione powyżej są dopuszczane przez standard jako jawne konwersje (tj. konwersja z double do int oraz z int do unsigned int), jednak niektóre konwersje są błędne, np.:

const char *cstr = "foo";
char *str = cstr;

W takich sytuacjach można użyć operatora rzutowania by wymusić konwersję:

const char *cstr = "foo";
char *str = (char*)cstr;

Należy unikać jednak takich sytuacji i nigdy nie stosować rzutowania by uciszyć kompilator. Zanim użyjemy operatora rzutowania należy się zastanowić co tak naprawdę będzie on robił i czy nie ma innego sposobu wykonania danej operacji, który nie wymagałby podejmowania tak drastycznych kroków.

Operatory arytmetyczne[edytuj]

Uwaga! Uwaga!
W arytmetyce komputerowej nie działa prawo łączności oraz rozdzielności. Wynika to z ograniczonego rozmiaru zmiennych, które przechowują wartości. Przykład dla zmiennych o długości 16 bitów (bez znaku). Maksymalna wartość, którą może przechowywać typ to: 2^16-1 = 65535. Zatem operacja typu 65530+10-20 zapisana jako (65530+10)-20 może zaowocować czymś zupełnie innym niż 65530+(10-20). W pierwszym przypadku zapewne dojdzie do tzw. przepełnienia — procesor nie będzie miał miejsca, aby zapisać dodatkowy bit. Zachowanie programu będzie w takim przypadku zależało od architektury procesora. Analogiczny przykład możemy podać dla rozdzielności mnożenia względem dodawania.

Język C definiuje następujące dwuargumentowe operatory arytmetyczne:

  • dodawanie ("+"),
  • odejmowanie ("-"),
  • mnożenie ("*"),
  • dzielenie ("/"),
  • reszta z dzielenia ("%") określona tylko dla liczb całkowitych (tzw. dzielenie modulo).


Dzielenie i mnożenie[edytuj]

Porada Zobacz również funkcję div
int a=7, b=2, c;
c = a % b;
printf ("%d\n",c); /* wypisze "1" */

Należy pamiętać, że (w pewnym uproszczeniu) wynik operacji jest typu takiego jak największy z argumentów. Oznacza to, że operacja wykonana na dwóch liczbach całkowitych nadal ma typ całkowity nawet jeżeli wynik przypiszemy do zmiennej rzeczywistej. Dla przykładu, poniższy kod:

float a = 7 / 2;
printf("%f\n", a);

wypisze (wbrew oczekiwaniu początkujących programistów) 3.0, a nie 3.5. Odnosi się to nie tylko do dzielenia, ale także mnożenia, np.:

float a = 1000 * 1000 * 1000 * 1000 * 1000 * 1000;
printf("%f\n", a);

prawdopodobnie da o wiele mniejszy wynik niż byśmy się spodziewali. Aby wymusić obliczenia rzeczywiste należy zmienić typ jednego z argumentów na liczbę rzeczywistą po prostu zmieniając literał lub korzystając z rzutowania, np.:

float a = 7.0 / 2; /* wcześniejszy zapis: float a = 7 / 2; */ 
float b = (float)1000 * 1000 * 1000 * 1000 * 1000 * 1000;
printf("%f\n", a);
printf("%f\n", b);


Dodawanie i odejmowanie[edytuj]

Operatory dodawania i odejmowania są określone również, gdy jednym z argumentów jest wskaźnik, a drugim liczba całkowita. Ten drugi jest także określony, gdy oba argumenty są wskaźnikami. O takim użyciu tych operatorów dowiesz się więcej w dalszej części książki.

Inkrementacja i dekrementacja[edytuj]

Aby skrócić zapis wprowadzono dodatkowe operatory: inkrementacji ("++") i dekrementacji ("--"), które dodatkowo mogą być pre- lub postfiksowe. W rezultacie mamy więc cztery operatory:

Operatory inkrementacji zwiększa, a dekrementacji zmniejsza argument o jeden. Ponadto operatory pre- zwracają nową wartość argumentu, natomiast post- starą wartość argumentu.

 int a, b, c;
 a = 3;
 b = a--; /* po operacji b=3 a=2 */
 c = --b; /* po operacji b=2 c=2 */

Czasami (szczególnie w C++) użycie operatorów stawianych za argumentem jest nieco mniej efektywne (bo kompilator musi stworzyć nową zmienną by przechować wartość tymczasową).

Uwaga! Uwaga!
Bardzo ważne jest, abyśmy poprawnie stosowali operatory dekrementacji i inkrementacji. Chodzi o to, aby w jednej instrukcji nie umieszczać kilku operatorów, które modyfikują ten sam obiekt (zmienną). Jeżeli taka sytuacja zaistnieje, to efekt działania instrukcji jest nieokreślony. Prostym przykładem mogą być następujące instrukcje:
int a = 1;
a = a++;
a = ++a;
a = a++ + ++a;
printf("%d %d\n", ++a, ++a);
printf("%d %d\n", a++, a++);

Kompilator GCC potrafi ostrzegać przed takimi błędami - aby to czynił należy podać mu jako argument opcję -Wsequence-point lub -Wall.

Operacje bitowe[edytuj]

Oprócz operacji znanych z lekcji matematyki w podstawówce, język C został wyposażony także w operatory bitowe, zdefiniowane dla liczb całkowitych. Są to:

  • negacja bitowa ("~"),
  • koniunkcja bitowa ("&"),
  • alternatywa bitowa ("|") i
  • alternatywa rozłączna (XOR) ("^").

Działają one na poszczególnych bitach przez co mogą być szybsze od innych operacji. Działanie tych operatorów można zdefiniować za pomocą poniższych tabel:

 "~" | 0 1    "&" | 0 1    "|" | 0 1    "^" | 0 1
-----+-----  -----+-----  -----+-----  -----+-----
     | 1 0      0 | 0 0      0 | 0 1      0 | 0 1
                1 | 0 1      1 | 1 1      1 | 1 0

   a   | 0101  =  5
   b   | 0011  =  3
-------+------
  ~a   | 1010  = 10
  ~b   | 1100  = 12
 a & b | 0001  =  1
 a | b | 0111  =  7
 a ^ b | 0110  =  6

Lub bardziej opisowo:

  • negacja bitowa daje w wyniku liczbę, która ma bity równe jeden tylko na tych pozycjach, na których argument miał bity równe zero;
  • koniunkcja bitowa daje w wyniku liczbę, która ma bity równe jeden tylko na tych pozycjach, na których oba argumenty miały bity równe jeden (mnemonik: 1 gdy wszystkie 1);
  • alternatywa bitowa daje w wyniku liczbę, która ma bity równe jeden na wszystkich tych pozycjach, na których jeden z argumentów miał bit równy jeden (mnemonik: 1 jeśli jest 1);
  • alternatywa rozłączna daje w wyniku liczbę, która ma bity równe jeden tylko na tych pozycjach, na których tylko jeden z argumentów miał bit równy jeden (mnemonik: 1 gdy różne).

Przy okazji warto zauważyć, że a ^ b ^ b to po prostu a. Właściwość ta została wykorzystana w różnych algorytmach szyfrowania oraz funkcjach haszujących. Alternatywę wyłączną stosuje się np. do szyfrowania kodu wirusów polimorficznych.

Przesunięcie bitowe[edytuj]

Dodatkowo, język C wyposażony jest w operatory przesunięcia bitowego w lewo ("<<") i prawo (">>"). Przesuwają one w danym kierunku bity lewego argumentu o liczbę pozycji podaną jako prawy argument. Brzmi to może strasznie, ale wcale takie nie jest. Rozważmy 4-bitowe liczby bez znaku (taki hipotetyczny unsigned int), wówczas:

  a   | a<<1 | a<<2 | a>>1 | a>>2
------+------+------+------+------
 0001 | 0010 | 0100 | 0000 | 0000
 0011 | 0110 | 1100 | 0001 | 0000
 0101 | 1010 | 0100 | 0010 | 0001
 1000 | 0000 | 0000 | 0100 | 0010
 1111 | 1110 | 1100 | 0111 | 0011
 1001 | 0010 | 0100 | 0100 | 0010

Nie jest to zatem takie straszne na jakie wygląda. Widać, że bity będące na skraju są tracone, a w "puste" miejsca wpisywane są zera.

Inaczej rzecz się ma jeżeli lewy argument jest liczbą ze znakiem. Dla przesunięcia bitowego w lewo a << b jeżeli a jest nieujemna i wartość a \cdot 2^b mieści się w zakresie liczby to jest to wynikiem operacji. W przeciwnym wypadku działanie jest niezdefiniowane[1].

Dla przesunięcia bitowego w lewo, jeżeli lewy argument jest nieujemny to operacja zachowuje się tak jak w przypadku liczb bez znaku. Jeżeli jest on ujemny to zachowanie jest zależne od implementacji.

Zazwyczaj operacja przesunięcia w lewo zachowuje się tak samo jak dla liczb bez znaku, natomiast przy przesuwaniu w prawo bit znaku nie zmienia się[2]:

  a   | a>>1 | a>>2
------+------+------
 0001 | 0000 | 0000
 0011 | 0001 | 0000
 0101 | 0010 | 0001
 1000 | 1100 | 1110
 1111 | 1111 | 1111
 1001 | 1100 | 1110

Przesunięcie bitowe w lewo odpowiada pomnożeniu, natomiast przesunięcie bitowe w prawo podzieleniu liczby przez dwa do potęgi jaką wyznacza prawy argument. Stąd przykładowo :

 d1 = (d << 1);

oznacza to samo co :

 d1 = d*2;

Jeżeli prawy argument jest ujemny lub większy lub równy liczbie bitów w typie, działanie jest niezdefiniowane.

#include <stdio.h>
 
int main ()
{
 int a = 6;
 printf ("6 << 2 = %d\n", a<<2);  /* wypisze 24 */
 printf ("6 >> 2 = %d\n", a>>2);  /* wypisze 1 */
 return 0;
}

Porównanie[edytuj]

W języku C występują następujące operatory porównania:

  • równe ("=="),
  • różne ("!="),
  • mniejsze ("<"),
  • większe (">"),
  • mniejsze lub równe ("<=") i
  • większe lub równe (">=").

Wykonują one odpowiednie porównanie swoich argumentów i zwracają jedynkę jeżeli warunek jest spełniony lub zero jeżeli nie jest.

Częste błędy[edytuj]

Uwaga! Uwaga!
Osoby, które poprzednio uczyły się innych języków programowania, często mają nawyk używania w instrukcjach logicznych zamiast operatora porównania "==", operatora przypisania "=". Ma to często zgubne efekty, gdyż przypisanie zwraca wartość przypisaną lewemu argumentowi.

Porównajmy ze sobą dwa warunki:

(a = 1)
(a == 1)

Pierwszy z nich zawsze będzie prawdziwy, niezależnie od wartości zmiennej a! Dzieje się tak, ponieważ zostaje wykonane przypisanie do a wartości 1 a następnie jako wartość jest zwracane to, co zostało przypisane - czyli jeden. Drugi natomiast będzie prawdziwy tylko, gdy a jest równe 1.

W celu uniknięcia takich błędów niektórzy programiści zamiast pisać a == 1 piszą 1 == a, dzięki czemu pomyłka spowoduje, że kompilator zgłosi błąd.

Warto zauważyć, że kompilator GCC potrafi w pewnych sytuacjach wychwycić taki błąd. Aby zaczął to robić należy podać mu argument -Wparentheses.

Innym błędem jest użycie zwykłych operatorów porównania do sprawdzania relacji pomiędzy liczbami rzeczywistymi. Ponieważ operacje zmiennoprzecinkowe wykonywane są z pewnym przybliżeniem rzadko kiedy dwie zmienne typu float czy double są sobie równe. Dla przykładu:

#include <stdio.h>
 
int main ()
{
 float a, b, c;
 a = 1e10;   /* tj. 10 do potęgi 10 */
 b = 1e-10;  /* tj. 10 do potęgi -10 */
 c = b;      /* c = b */
 c = c + a;  /* c = b + a          (teoretycznie) */
 c = c - a;  /* c = b + a - a = b  (teoretycznie) */
 printf("%d\n", c == b); /* wypisze 0 */
}

Obejściem jest porównywanie modułu różnicy liczb. Również i takie błędy kompilator GCC potrafi wykrywać - aby to robił należy podać mu argument -Wfloat-equal.

Operatory logiczne[edytuj]

Analogicznie do części operatorów bitowych, w C definiuje się operatory logiczne, mianowicie:

  • negację (zaprzeczenie): "!"
  • koniunkcję ("i"): "&&"
  • alternatywę ("lub"): "||"

Działają one bardzo podobnie do operatorów bitowych, jednak zamiast operować na poszczególnych bitach, biorą pod uwagę wartość logiczną argumentów.

"Prawda" i "fałsz" w języku C[edytuj]

Język C nie przewiduje specjalnego typu danych do operacji logicznych — operatory logiczne można stosować do liczb (np. typu int), tak samo jak operatory bitowe albo arytmetyczne.

Wyrażenie ma wartość logiczną:

  • 0, czyli jest "fałszywe" wtedy i tylko wtedy, gdy jest równe 0
  • 1 czyli jest "prawdziwe", gdy jest różne od zera

Operatory logiczne w wyniku dają zawsze albo 0 albo 1.

Żeby w pełni uzmysłowić sobie, co to to oznacza, spójrzmy na wynik wykonania poniższych trzech linijek:

printf("koniunkcja: %d\n", 18 && 19);
printf("alternatywa: %d\n", 'a' || 'b');
printf("negacja: %d\n", !20);
koniunkcja: 1
alternatywa: 1
negacja: 0

Liczba 18 nie jest równa 0, więc ma wartość logiczną 1. Podobnie 19 ma wartość logiczną 1. Dlatego ich koniunkcja jest równa 1. Znaki 'a' i 'b' zostaną w wyrażeniu logicznym potraktowane jako liczby o wartości odpowiadającej kodowi ASCII znaku — czyli oba będą miały wartość logiczną 1. Liczba 20 również ma wartość logiczną 1 (bo nie jest zerem), dlatego jej negacja to 0 czyli fałsz.

Dalsze przykłady w dziale o instrukcjach sterujących.

Skrócone obliczanie wyrażeń logicznych[edytuj]

Język C wykonuje skrócone obliczanie wyrażeń logicznych - to znaczy, oblicza wyrażenie tylko tak długo, jak nie wie, jaka będzie jego ostateczna wartość. To znaczy, idzie od lewej do prawej obliczając kolejne wyrażenia (dodatkowo na kolejność wpływ mają nawiasy) i gdy będzie miał na tyle informacji, by obliczyć wartość całości, nie liczy reszty. Może to wydawać się niejasne, ale przyjrzyjmy się wyrażeniom logicznym:

A && B
A || B

Jeśli A jest fałszywe, nie trzeba obliczać B w pierwszym wyrażeniu, bo koniunkcja fałszu i dowolnego wyrażenia zawsze da fałsz. Analogicznie, w drugim przykładzie, jeśli A jest prawdziwe, to całe wyrażenie jest prawdziwe i wartość B nie ma znaczenia.

Poza zwiększoną szybkością zysk z takiego rozwiązania polega na możliwości stosowania efektów ubocznych. Idea efektu ubocznego opiera się na tym, że w wyrażeniu można wywołać funkcje, które będą robiły poza zwracaniem wyniku inne rzeczy, oraz używać podstawień. Popatrzmy na poniższy przykład:

( (a > 0) || (a < 0) || (a = 1) )

Jeśli a będzie większe od 0 to obliczona zostanie tylko wartość wyrażenia (a > 0) - da ono prawdę, czyli reszta obliczeń nie będzie potrzebna. Jeśli a będzie mniejsze od zera, najpierw zostanie obliczone pierwsze podwyrażenie a następnie drugie, które da prawdę. Ciekawy będzie jednak przypadek, gdy a będzie równe zero - do a zostanie wtedy podstawiona jedynka i całość wyrażenia zwróci prawdę (bo 1 jest traktowane jak prawda).

Efekty uboczne pozwalają na różne szaleństwa i wykonywanie złożonych operacji w samych warunkach logicznych, jednak przesadne używanie tego typu konstrukcji powoduje, że kod staje się nieczytelny i jest uważane za zły styl programistyczny.

Operator wyrażenia warunkowego[edytuj]

C posiada szczególny rodzaj operatora - to operator ?: zwany też operatorem wyrażenia warunkowego. Jest to jedyny operator w tym języku przyjmujący trzy argumenty.

a ? b : c

Jego działanie wygląda następująco: najpierw oceniana jest wartość logiczna wyrażenia a; jeśli jest ono prawdziwe, to zwracana jest wartość b, jeśli natomiast wyrażenie a jest nieprawdziwe, zwracana jest wartość c.

Praktyczne zastosowanie - znajdowanie większej z dwóch liczb:

a = (b>=c) ? b : c;     /* Jeśli b jest większe bądź równe c, to zwróć b. 
                           W przeciwnym wypadku zwróć c. */

lub zwracanie modułu liczby:

a = a < 0 ? -a : a;

Wartości wyrażeń są przy tym operatorze obliczane tylko jeżeli zachodzi taka potrzeba, np. w wyrażeniu 1 ? 1 : foo() funkcja foo() nie zostanie wywołana.

Operator przecinek[edytuj]

Operator przecinek jest dość dziwnym operatorem. Powoduje on obliczanie wartości wyrażeń od lewej do prawej po czym zwrócenie wartości ostatniego wyrażenia. W zasadzie, w normalnym kodzie programu ma on niewielkie zastosowanie, gdyż zamiast niego lepiej rozdzielać instrukcje zwykłymi średnikami. Ma on jednak zastosowanie w instrukcji sterującej for.

Operator sizeof[edytuj]

Operator sizeof zwraca rozmiar w bajtach (gdzie bajtem jest zmienna typu char) podanego typu lub typu podanego wyrażenia. Ma on dwa rodzaje: sizeof(typ) lub sizeof wyrażenie. Przykładowo:

#include <stdio.h>
 
int main()
{
 printf(" sizeof(short)=%lu\n sizeof(int)=%lu\n sizeof(long)=%lu\n", sizeof(short), sizeof(int), sizeof(long));
 return 0;
}

Operator ten jest często wykorzystywany przy dynamicznej alokacji pamięci, co zostanie opisane w rozdziale poświęconym wskaźnikom.

Pomimo, że w swej budowie operator sizeof bardzo przypomina funkcję, to jednak nią nie jest. Wynika to z trudności w implementacji takowej funkcji - jej specyfika musiałaby odnosić się bezpośrednio do kompilatora. Ponadto jej argumentem musiałyby być typy, a nie zmienne. W języku C nie jest możliwe przekazywanie typu jako argumentu. Ponadto często zdarza się, że rozmiar zmiennej musi być wiadomy jeszcze w czasie kompilacji - to ewidentnie wyklucza implementację sizeof() jako funkcji.

Wynik operatora sizeof jest typu size_t

Inne operatory[edytuj]

Poza wyżej opisanymi operatorami istnieją jeszcze:

  • operator "[]" opisany przy okazji opisywania tablic;
  • jednoargumentowe operatory "*" i "&" opisane przy okazji opisywania wskaźników;
  • operatory "." i "->" opisywane przy okazji opisywania struktur i unii;
  • operator "()" będący operatorem wywołania funkcji,
  • operator "()" grupujący wyrażenia (np. w celu zmiany kolejności obliczania)

Priorytety i kolejność obliczeń[edytuj]

Jak w matematyce, również i w języku C obowiązuje pewna ustalona kolejność działań. Aby móc ją określić należy ustalić dwa parametry danego operatora: jego priorytet oraz łączność. Przykładowo operator mnożenia ma wyższy priorytet niż operator dodawania i z tego powodu w wyrażeniu 2 + 2 \cdot 2 najpierw wykonuje się mnożenie, a dopiero potem dodawanie.

Drugim parametrem jest łączność - określa ona od której strony wykonywane są działania w przypadku połączenia operatorów o tym samym priorytecie. Na przykład odejmowanie ma łączność lewostronną i 2 - 2 - 2\, da w wyniku -2. Gdyby miało łączność prawostronną, wynikiem byłoby 2. Przykładem matematycznego operatora, który ma łączność prawostronną jest potęgowanie, np. 4^{3^2} jest równe 4^9 = 262144 (łączność lewostronna dałaby wynik 64^2 = 4096).

W języku C występuje dużo poziomów operatorów. Poniżej przedstawiamy tabelkę ze wszystkimi operatorami poczynając od tych z najwyższym priorytetem (wykonywanych na początku).

Operator Łączność
nawiasy nie dotyczy
jednoargumentowe przyrostkowe: [] . -> wywołanie funkcji postinkrementacja postdekrementacja lewostronna
jednoargumentowe przedrostkowe: ! ~ + - * & sizeof preinkrementacja predekrementacja rzutowanie prawostronna
* / % lewostronna
+ - lewostronna
<< >> lewostronna
< <= > >= lewostronna
== != lewostronna
& lewostronna
^ lewostronna
| lewostronna
&& lewostronna
|| lewostronna
?: prawostronna
operatory przypisania prawostronna
, lewostronna

Duża liczba poziomów pozwala czasami zaoszczędzić trochę milisekund w trakcie pisania programu i bajtów na dysku, gdyż często nawiasy nie są potrzebne, nie należy jednak z tym przesadzać, gdyż kod programu może stać się mylący nie tylko dla innych, ale po latach (czy nawet i dniach) również dla nas.

Warto także podkreślić, że operator koniunkcji ma niższy priorytet niż operator porównania[3]. Oznacza to, że kod

if (flags & FL_MASK == FL_FOO)

zazwyczaj da rezultat inny od oczekiwanego. Najpierw bowiem wykona się porównanie wartości FL_MASK z wartością FL_FOO, a dopiero potem koniunkcja bitowa. W takich sytuacjach należy pamiętać o użyciu nawiasów:

if ((flags & FL_MASK) == FL_FOO)

Kompilator GCC potrafi wykrywać takie błędy i aby to robił należy podać mu argument -Wparentheses.

Kolejność wyliczania argumentów operatora[edytuj]

W przypadku większości operatorów (wyjątkami są tu &&, || i przecinek) nie da się określić, która wartość argumentu zostanie obliczona najpierw. W większości przypadków nie ma to większego znaczenia, lecz w przypadku wyrażeń, które mają efekty uboczne, wymuszenie konkretnej kolejności może być potrzebne. Weźmy dla przykładu program

#include <stdio.h>
 
int foo(int a) 
{
 printf("%d\n", a);
 return 0;
}
 
int main(void) 
{
 return foo(1) + foo(2);
}

Otóż nie wiemy czy najpierw zostanie wywołana funkcja foo z parametrem jeden, czy dwa. Jeżeli ma to znaczenie należy użyć zmiennych pomocniczych, zmieniając definicję funkcji main na:

int main(void) 
{
 int tmp = foo(1);
 return tmp + foo(2);
}

Teraz już na pewno najpierw zostanie wypisana jedynka, a potem dopiero dwójka. Sytuacja jeszcze bardziej się komplikuje, gdy używamy wyrażeń z efektami ubocznymi jako argumentów funkcji, np.:

#include <stdio.h>
 
int foo(int a) 
{
 printf("%d\n", a);
 return 0;
}
 
int bar(int a, int b, int c, int d) 
{
 return a + b + c + d;
}
 
int main(void) 
{
 return bar(foo(1), foo(2), foo(3), foo(4));
}

Teraz też nie wiemy, która z 24 permutacji liczb 1, 2, 3 i 4 zostanie wypisana i ponownie należy pomóc sobie zmiennymi tymczasowymi, jeżeli zależy nam na konkretnej kolejności:

int main(void) 
{
 int tmp1 = foo(1);
 int tmp2 = foo(2);
 int tmp3 = foo(3);
 return bar(tmp1, tmp2, tmp3, foo(4));
}

Uwagi[edytuj]

  • W języku C++ wprowadzony został dodatkowo inny sposób zapisu rzutowania, który pozwala na łatwiejsze znalezienie w kodzie miejsc, w których dokonujemy rzutowania. Więcej na stronie C++/Zmienne.

Zobacz też[edytuj]

Przypisy

  1. Niezdefiniowane w takim samym sensie jak niezdefiniowane jest zachowanie programu, gdy próbujemy odwołać się do wartości wskazywanej przez wartość NULL czy do zmiennych poza tablicą.
  2. ale jeżeli zależy Ci na przenośności kodu nie możesz na tym polegać
  3. Jest to zaszłość historyczna z czasów, gdy nie było logicznych operatorów && oraz || i zamiast nich stosowano operatory bitowe & oraz |.