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;       */
d >>= 2; // Przesuń d w prawo * ang. right shift) o 2 pozycje i przypisz z powrotem do d

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

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
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);


Dzielenie

  • całkowite ( ang. integer division)[1][2]
  • zmiennoprzecinkowe ( ang. float division)

Zasady dzielenia liczb całkowitych i zmiennoprzecinkowych w C[3]

  • integer / integer = integer
  • float / integer = float
  • integer / float = float

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ą).

Operatory bitowe

edytuj

Oprócz operacji znanych z lekcji matematyki w podstawówce, język C został wyposażony także w operatory bitowe[4][5][6][7], . Są to:

  • negacja bitowa (NOT)("~"),
  • koniunkcja bitowa (AND)("&"),
  • alternatywa bitowa (OR)("|") 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 prawdy ( matryc logicznych):

W pierwszej tabeli a i b oznaczają bity ( albo pole bitowe o długości 1) , a nie liczby typu całkowitego

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


W drugiej tabeli a i b oznaczają 4 bitowe pole ( c nie ma typu 4 bitowego)

   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.


Negacja bitowa

edytuj

Jak wpływa długość liczby binarnej na wynik negacji ?

1 bit 4 bity 8 bitów
a      
~a     Parser nie mógł rozpoznać (SVG (MathML może zostać włączone przez wtyczkę w przeglądarce): Nieprawidłowa odpowiedź („Math extension cannot connect to Restbase.”) z serwera „http://localhost:6011/pl.wikibooks.org/v1/”:): {\displaystyle {11111110}_2 = 254_{10}}

Przykładowy program działajacy na liczbie 8 bitowej ( unsigned char ):

// gcc b.c -Wall
// ./a.out
#include <stdio.h>

/*     
https://stackoverflow.com/questions/699968/display-the-binary-representation-of-a-number-in-c
Chris Lutz
*/
void PrintBitsOfUChar(unsigned char v) {
  int i; // for C89 compatability
  for(i = 7; i >= 0; i--) putchar('0' + ((v >> i) & 1));
}



void TestBitwiseNot (unsigned char a ){

	printf ("decimal number  a = %3u;\t ", a );
	printf ("it's binary expansion = ");
	PrintBitsOfUChar(a);
	printf("\n"); 
	printf ("decimal number ~a = %3u;\t ", (unsigned char) ~a );
	printf ("it's binary expansion = ");
	PrintBitsOfUChar((unsigned char) ~a);
	printf("\n\n"); 
	
}






int main ()
{
 //unsigned char a;
 //char buffer[8];
 
 printf("unsigned char	has size = 1 byte = 8 bits and range from 0 to 255\n\n");
 
 TestBitwiseNot(0);
 TestBitwiseNot(255);
 TestBitwiseNot(5);
 TestBitwiseNot(3);
    
 
 return 0;
}


Wynik

unsigned char	has size = 1 byte = 8 bits and range from 0 to 255

decimal number  a =   0;	 it's binary expansion = 00000000
decimal number ~a = 255;	 it's binary expansion = 11111111

decimal number  a = 255;	 it's binary expansion = 11111111
decimal number ~a =   0;	 it's binary expansion = 00000000

decimal number  a =   5;	 it's binary expansion = 00000101
decimal number ~a = 250;	 it's binary expansion = 11111010

decimal number  a =   3;	 it's binary expansion = 00000011
decimal number ~a = 252;	 it's binary expansion = 11111100

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.

  [variable]<<[numberOfPlaces]

Brzmi to może strasznie, ale wcale takie nie jest. Rozważmy operacje przesunięcia na liczbach 4-bitowych :

  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.

Przesunięcie w lewą stronę oznacza przemieszczenie wszystkich bitów argumentu w lewo o określoną liczbę miejsc oraz wprowadzenie z prawej strony takiej samej ilości zer.

Przesunięcie w prawo oznacza przemieszczenie wszystkich bitów argumentu w prawo o określoną liczbę miejsc oraz powielenie najstarszego bitu na skrajnej lewej pozycji.[8]:

#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;
}

Inny przykład

#include <stdio.h>

int main() {
    unsigned int d = 0b11010110; // Binary: 11010110, Decimal: 214
    int k = 2;

    printf("Before shift: %u\n", d);

    d >>= k; // Right shift d by 2 positions and assign back to d

    printf("After shift: %u\n", d);

    return 0;
}


W tym przykładzie początkowa wartość d wynosi 214 (w formacie binarnym: 11010110).

Po przesunięciu w prawo o 2 pozycje (k wynosi 2) wynikiem jest 00110101, czyli 53 w systemie dziesiętnym.

Zatem wynikiem będzie:


Before shift: 214
After shift: 53

Zastosowania

edytuj

Operatorów bitowych używamy do:

  • operacji na zbiorach [9]
  • szyfrowania: XOR Encryption[10]
  • kompresji danych ( najmniejszy typ liczbowy ma 1 bajt = 8 bitów )[11]
  • szybkich obliczeń [12][13]
    • kod Graya [14]
    • obliczania liczb losowych
  • programowaniu systemów wbudowanych (ang. embedded system) [15]
  • programowania grafiki
    • OpenGl: pola bitowe w arumentach funkcji
  • sieciach komputerowych ( adres IP, Maska podsieci )
  • (a << b) jest równoważne pomnożeniu a przez 2^b (2 podniesione do potęgi b)[16]
  • (a>>b)‘ jest równoznaczne z podzieleniem a przez 2^b

Porównanie

edytuj

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

  • 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.


równe

edytuj
// http://blogs.msdn.com/b/nativeconcurrency/archive/2012/10/11/floating-point-arithmetic-intricacies-in-c-amp.aspx

#include <stdio.h>

int main()
{if (0.0f == -0.0f)
    printf("equal\n");
    else printf("not equal ");

    return 0;
}

Częste błędy

edytuj

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

W języku C mamy 3 możliwości korzystania z "prawdy" i "fałszu":[18]

  • poprzez wartość wyrażenie : fałsz to zero a prawdy to wszystkie inne wartości
  • z użyciem dyrektywy preprocesora define : #define FALSE 0 ... #define TRUE !(FALSE)
  • użyciem typu bool


Typ bool

edytuj

Logiczny typ danych[19] ( ang. Boolean data type ) składa się z dokładnie dwóch elementów: prawdy (ang. true, 1, +) i fałszu (ang. false, 0, -).

Typu bool możemy używać na 4 sposoby : [20]

  • w C99 ( nie w C90 [21]) poprzez dodanie dyrektywy #include <stdbool.h>
  • trzy sposoby z użyciem dyrektyw prepocesora :
typedef int bool;
#define true 1
#define false 0
typedef int bool;
enum { false, true };
typedef enum { false, true } bool;

Wartość wyrażenia

edytuj

W tym przypadku nie używamy 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ść.[22] 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.[23] 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)

Wyrażenie

edytuj

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   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   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.   jest równe   (łączność lewostronna dałaby wynik  ).

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[24]. 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));
}

Jak czytać wyrażenia?

edytuj


Reguła spiralna wg Davida Anderson[26], która umożliwia każdemu programiście C przeanalizowanie dowolnej deklaracji C

  • Zacznij od nieznanego elementu : opisz go
  • poruszaj się spiralnie, zgodnie z ruchem wskazówek zegara
  • spotykając kolejne elementy, zastąp je odpowiednim opisem
  • Kontynuuj to w kierunku spiralnym/zgodnym z ruchem wskazówek zegara, aż wszystkie elementy zostaną opisane
  • Zawsze najpierw rozwiązuj wszystko, co jest w nawiasach!

Przykład 1

edytuj
char *str[10]

Co to jest str ?

Zaczynamy od środka :

str jest to 

Poruszamy się spiralnie w kierunku zgodnym z ruchem wskazówek zegara, zaczynając od `str', a pierwszym znakiem, który widzimy, jest `[', co oznacza, że mamy tablicę, więc...

str to tablica zawierająca 10 elementów

Kontynuuj w kierunku spiralnym, i dochodzimy do "char *"

 str to tablica zawierająca 10 wskaźników na char
 

Odpowiedź z cdcl :

declare str as array 10 of pointer to char


                     +-------+
                     | +-+   |
                     | ^ |   |
                char *str[10];
                 ^   ^   |   |
                 |   +---+   |
                 +-----------+

Przykład 2

                     +--------------------+
                     | +---+              |
                     | |+-+|              |
                     | |^ ||              |
                char *(*fp)( int, float *);
                 ^   ^ ^  ||              |
                 |   | +--+|              |
                 |   +-----+              |
                 +------------------------+

Przykład 3

                      +-----------------------------+
                      |                  +---+      |
                      |  +---+           |+-+|      |
                      |  ^   |           |^ ||      |
                void (*signal(int, void (*fp)(int)))(int);
                 ^    ^      |      ^    ^  ||      |
                 |    +------+      |    +--+|      |
                 |                  +--------+      |
                 +----------------------------------+
  • 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