C/Zmienne

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

Procesor komputera stworzony jest tak, aby przetwarzał dane, znajdujące się w pamięci komputera. Z punktu widzenia programu napisanego w języku C (który jak wiadomo jest językiem wysokiego poziomu) dane umieszczane są w postaci tzw. zmiennych. Zmienne ułatwiają programiście pisanie programu. Dzięki nim programista nie musi się przejmować gdzie w pamięci owe zmienne się znajdują, tzn. nie operuje fizycznymi adresami pamięci, jak np. 0x14613467, tylko prostą do zapamiętania nazwą zmiennej.

Czym są zmienne?[edytuj]

Zmienna jest to pewien fragment pamięci o ustalonym rozmiarze, który posiada własny identyfikator (nazwę) oraz może przechowywać pewną wartość, zależną od typu zmiennej.

Deklaracja zmiennych[edytuj]

Aby móc skorzystać ze zmiennej należy ją przed użyciem zadeklarować, to znaczy poinformować kompilator, jak zmienna będzie się nazywać i jaki typ ma mieć. Zmienne deklaruje się w sposób następujący:

typ nazwa_zmiennej;

Oto deklaracja zmiennej o nazwie "wiek" typu "int" czyli liczby całkowitej:

int wiek;

Zmiennej w momencie zadeklarowania można od razu przypisać wartość:

int wiek = 17;
Porada W języku C zmienne deklaruje się na samym początku bloku (czyli przed pierwszą instrukcją).
{
   int wiek = 17;
   printf("%d\n", wiek);
   int kopia_wieku; /* tu stary kompilator C zgłosi błąd - deklaracja występuje po instrukcji (printf). */
   kopia_wieku = wiek;
}

Według nowszych standardów (C99) możliwe jest deklarowanie zmiennej w dowolnym miejscu programu (podobnie jak w języku C++), ale wtedy musimy pamiętać, aby zadeklarować zmienną przed jej użyciem. To znaczy, że taki kod jest niepoprawny:

{
   printf ("Przeliczam wiek...\n");
   printf ("Mam %d lat\n", wiek);
   int wiek = 17; /* deklaracja po użyciu - kompilator nigdy tego nie dopuści */
}

Należy go zapisać tak:

{
   printf ("Przeliczam wiek...\n");
   int wiek = 17; /* deklaracja w środku bloku - dopuszczalna w C99 */
   printf ("Mam %d lat\n", wiek);
}
Uwaga! Uwaga!
Język C nie inicjalizuje zmiennych lokalnych. Oznacza to, że w nowo zadeklarowanej zmiennej znajdują się śmieci - to, co wcześniej zawierał przydzielony zmiennej fragment pamięci. Aby uniknąć ciężkich do wykrycia błędów, dobrze jest inicjalizować (przypisywać wartość) wszystkie zmienne w momencie zadeklarowania.

Zasięg zmiennej[edytuj]

Zmienne mogą być dostępne dla wszystkich funkcji programu - nazywamy je wtedy zmiennymi globalnymi. Deklaruje się je przed wszystkimi funkcjami programu:

#include <stdio.h>
 
int a,b; /* nasze zmienne globalne */
 
void func1 ()
{
 /* instrukcje */
 a=3;
 /* dalsze instrukcje */
}
 
int main ()
{
 b=3;
 a=2;
 return 0;
}

Zmienne globalne, jeśli programista nie przypisze im innej wartości podczas definiowania, są zwyczajowo inicjalizowane wartością 0. Nie jest to jednak zawarte w specyfikacji języka C i może się zdarzyć, że dany kompilator nie wykona tej operacji. Tak samo niektóre kompilatory mają opcję wyłączenia inicjalizacji zmiennych globalnych. Dlatego też dobrą praktyką (dla przenośności kodu) jest jawne inicjalizowanie zmiennych globalnych wartościami domyślnymi (zwykle 0).

Zmienne, które funkcja deklaruje do "własnych potrzeb" nazywamy zmiennymi lokalnymi. Nasuwa się pytanie: "czy będzie błędem nazwanie tą samą nazwą zmiennej globalnej i lokalnej?". Otóż odpowiedź może być zaskakująca: nie. Natomiast w danej funkcji da się używać tylko jej zmiennej lokalnej. Tej konstrukcji należy, z wiadomych względów, unikać.

int a=1; /* zmienna globalna */ 
 
int main()
{
 int a=2;         /* to już zmienna lokalna */
 printf("%d", a); /* wypisze 2 */
}

Czas życia[edytuj]

Czas życia to czas od momentu przydzielenia dla zmiennej miejsca w pamięci (stworzenie obiektu) do momentu zwolnienia miejsca w pamięci (likwidacja obiektu).

Zakres ważności to część programu, w której nazwa znana jest kompilatorowi.

main()
{
 int a = 10;
 {                            /* otwarcie lokalnego bloku */
   int b = 10;
   printf("%d %d", a, b);
 }                            /* zamknięcie lokalnego bloku, zmienna b jest usuwana */
 
 printf("%d %d", a, b);       /* BŁĄD: b juz nie istnieje */
}                               /* tu usuwana jest zmienna a */

Zdefiniowaliśmy dwie zmienne typu int. Zarówno a i b istnieją przez cały program (czas życia). Nazwa zmiennej a jest znana kompilatorowi przez cały program. Nazwa zmiennej b jest znana tylko w lokalnym bloku, dlatego nastąpi błąd w ostatniej instrukcji.

Uwaga! Uwaga!
Niektóre kompilatory (prawdopodobnie można tu zaliczyć Microsoft Visual C++ do wersji 2003) uznają powyższy kod za poprawny! W dodatku można ustawić w opcjach niektórych kompilatorów zachowanie w takiej sytuacji, włącznie z zachowaniami niezgodnymi ze standardem języka!

Możemy świadomie ograniczyć ważność zmiennej do kilku linijek programu (tak jak robiliśmy wyżej) tworząc blok. Nazwa zmiennej jest znana tylko w tym bloku.

{
  ...
}

Stałe[edytuj]

Stała, różni się od zmiennej tylko tym, że nie można jej przypisać innej wartości w trakcie działania programu. Wartość stałej ustala się w kodzie programu i nigdy ona nie ulega zmianie. Stałą deklaruje się z użyciem słowa kluczowego const w sposób następujący:

const typ nazwa_stałej=wartość;

Dobrze jest używać stałych w programie, ponieważ unikniemy wtedy przypadkowych pomyłek a kompilator może często zoptymalizować ich użycie (np. od razu podstawiając ich wartość do kodu).

const int WARTOSC_POCZATKOWA=5;
int i=WARTOSC_POCZATKOWA;
WARTOSC_POCZATKOWA=4;  /* tu kompilator zaprotestuje */
int j=WARTOSC_POCZATKOWA;

Przykład pokazuje dobry zwyczaj programistyczny, jakim jest zastępowanie umieszczonych na stałe w kodzie liczb stałymi. W ten sposób będziemy mieli większą kontrolę nad kodem - stałe umieszczone w jednym miejscu można łatwo modyfikować, zamiast szukać po całym kodzie liczb, które chcemy zmienić.

Nie mamy jednak pełnej gwarancji, że stała będzie miała tę samą wartość przez cały czas wykonania programu, możliwe jest bowiem dostanie się do wartości stałej (miejsca jej przechowywania w pamięci) pośrednio - za pomocą wskaźników. Można zatem dojść do wniosku, że słowo kluczowe const służy tylko do poinformowania kompilatora, aby ten nie zezwalał na jawną zmianę wartości stałej. Z drugiej strony, zgodnie ze standardem, próba modyfikacji wartości stałej ma niezdefiniowane działanie (tzw. undefined behaviour) i w związku z tym może się powieść lub nie, ale może też spowodować jakieś subtelne zmiany, które w efekcie spowodują, że program będzie źle działał.

Podobnie do zdefiniowania stałej możemy użyć dyrektywy preprocesora #define (opisanej w dalszej części podręcznika). Tak zdefiniowaną stałą nazywamy stałą symboliczną. W przeciwieństwie do stałej zadeklarowanej z użyciem słowa const stała zdefiniowana przy użyciu #define jest zastępowana daną wartością w każdym miejscu, gdzie występuje, dlatego też może być używana w miejscach, gdzie "normalna" stała nie mogłaby dobrze spełnić swej roli.

W przeciwieństwie do języka C++, w C stała to cały czas zmienna, której kompilator pilnuje, by nie zmieniła się.

Typy zmiennych[edytuj]

Każdy program w C operuje na zmiennych - wydzielonych w pamięci komputera obszarach, które mogą reprezentować obiekty nam znane, takie jak liczby, znaki, czy też bardziej złożone obiekty. Jednak dla komputera każdy obszar w pamięci jest taki sam - to ciąg zer i jedynek, w takiej postaci zupełnie nieprzydatny dla programisty i użytkownika. Podczas pisania programu musimy wskazać, w jaki sposób ten ciąg ma być interpretowany.

Typ zmiennej wskazuje właśnie sposób, w jaki pamięć, w której znajduje się zmienna będzie wykorzystywana. Określając go przekazuje się kompilatorowi informację, ile pamięci trzeba zarezerwować dla zmiennej, a także w jaki sposób wykonywać na niej operacje.

Każda zmienna musi mieć określony swój typ w miejscu deklaracji i tego typu nie może już zmienić. Lecz co jeśli mamy zmienną jednego typu, ale potrzebujemy w pewnym miejscu programu innego typu danych? W takim wypadku stosujemy konwersję (rzutowanie) jednej zmiennej na inną zmienną. Rzutowanie zostanie opisane później, w rozdziale Operatory.

Istnieją wbudowane i zdefiniowane przez użytkownika typy danych. Wbudowane typy danych to te, które zna kompilator, są one w nim bezpośrednio "zaszyte". Można też tworzyć własne typy danych, ale należy je kompilatorowi opisać. Więcej informacji znajduje się w rozdziale Typy złożone.

W języku C wyróżniamy 4 podstawowe typy zmiennych. Są to:

char - jednobajtowe liczby całkowite, służy do przechowywania znaków;
int- typ całkowity, o długości domyślnej dla danej architektury komputera;
float - typ zmiennopozycyjny (zwany również zmiennoprzecinkowym), reprezentujący liczby rzeczywiste (4 bajty);
double - typ zmiennopozycyjny podwójnej precyzji (8 bajtów);
bool (tylko C99) (wymaga dołączenia stdbool.h) - typ logiczny

Typy zmiennoprzecinkowe zostały dokładnie opisane w IEEE 754.

Porada W języku C nie jest możliwe przekazywanie typu jako argumentu

int[edytuj]

Ten typ przeznaczony jest do liczb całkowitych. Liczby te możemy zapisać na kilka sposobów:

  • System dziesiętny
12 ; 13 ; 45 ; 35 itd
  • System ósemkowy (oktalny)
010    czyli 8
016    czyli 8 + 6 = 14
018    BŁĄD

System ten operuje na cyfrach od 0 do 7. Tak wiec 8 jest niedozwolona. Jeżeli chcemy użyć takiego zapisu musimy zacząć liczbę od 0.

  • System szesnastkowy (heksadecymalny)
0x10   czyli 1*16 + 0 = 16
0x12   czyli 1*16 + 2 = 18
0xff   czyli 15*16 + 15 = 255

W tym systemie możliwe cyfry to 0...9 i dodatkowo a, b, c, d, e, f, które oznaczają 10, 11, 12, 13, 14, 15. Aby użyć takiego systemu musimy poprzedzić liczbę ciągiem 0x. Wielkość znaków w takich literałach nie ma znaczenia.

Porada Ponadto w niektórych kompilatorach przeznaczonych głównie do mikrokontrolerów spotyka się jeszcze użycie systemu binarnego. Zazwyczaj dodaje się przedrostek 0b przed liczbą (analogicznie do zapisu spotykanego w języku Python). W tym systemie możemy oczywiście używać tylko i wyłącznie cyfr 0 i 1. Tego typu rozszerzenie bardzo ułatwia programowanie niskopoziomowe układów. Należy jednak pamiętać, że jest to tylko i wyłącznie rozszerzenie.

float[edytuj]

Ten typ oznacza liczby zmiennoprzecinkowe czyli ułamki. Istnieją dwa sposoby zapisu:

  • System dziesiętny
  3.14 ; 45.644 ; 23.54 ; 3.21 itd
  • System "naukowy" - wykładniczy
  6e2 czyli 6 * 102 czyli 600
  1.5e3 czyli 1.5 * 103 czyli 1500
  3.4e-3 czyli 3.4 * 10(-3) czyli 0.0034

Należy wziąć pod uwagę, że reprezentacja liczb rzeczywistych w komputerze jest niedoskonała i możemy otrzymywać wyniki o zauważalnej niedokładności.[1]

double[edytuj]

Double - czyli "podwójny" - oznacza liczby zmiennoprzecinkowe podwójnej precyzji. Oznacza to, że liczba taka zajmuje zazwyczaj w pamięci dwa razy więcej miejsca niż float (np. 64 bity wobec 32 dla float), ale ma też dwa razy lepszą dokładność.

Domyślnie ułamki wpisane w kodzie są typu double. Możemy to zmienić dodając na końcu literę "f":

  1.5f   (float)
  1.5    (double)

char[edytuj]

Jest to typ znakowy, umożliwiający zapis znaków ASCII. Może też być traktowany jako liczba z zakresu 0..255. Znaki zapisujemy w pojedynczych cudzysłowach (czasami nazywanymi apostrofami), by odróżnić je od łańcuchów tekstowych (pisanych w podwójnych cudzysłowach).

   'a' ; '7' ; '!' ; '$'

Pojedynczy cudzysłów ' zapisujemy tak: '\'' a null (czyli zero, które między innymi kończy napisy) tak: '\0'. Więcej znaków specjalnych.

Warto zauważyć, że typ char to zwykły typ liczbowy i można go używać tak samo jak typu int (zazwyczaj ma jednak mniejszy zakres). Co więcej literały znakowe (np. 'a') są traktowane jako liczby i w języku C są typu int (w języku C++ są typu char).

void[edytuj]

Słowa kluczowego void można w określonych sytuacjach użyć tam, gdzie oczekiwana jest nazwa typu. void nie jest właściwym typem, bo nie można utworzyć zmiennej takiego typu; jest to "pusty" typ (ang. void znaczy "pusty"). Typ void przydaje się do zaznaczania, że funkcja nie zwraca żadnej wartości lub że nie przyjmuje żadnych parametrów (więcej o tym w rozdziale Funkcje). Można też tworzyć zmienne będące typu "wskaźnik na void"

Specyfikatory[edytuj]

Specyfikatory to słowa kluczowe, które postawione przy typie danych zmieniają jego znaczenie.

signed i unsigned[edytuj]

Na początku zastanówmy się, jak komputer może przechować liczbę ujemną. Otóż w przypadku przechowywania liczb ujemnych musimy w zmiennej przechować jeszcze jej znak. Jak wiadomo, zmienna składa się z szeregu bitów. W przypadku użycia zmiennej pierwszy bit z lewej strony (nazywany także bitem najbardziej znaczącym) przechowuje znak liczby. Efektem tego jest spadek "pojemności" zmiennej, czyli zmniejszenie największej wartości, którą możemy przechować w zmiennej.

Signed oznacza liczbę ze znakiem, unsigned - bez znaku (nieujemną). Mogą być zastosowane do typów: char i int i łączone ze specyfikatorami short i long (gdy ma to sens).

Jeśli przy signed lub unsigned nie napiszemy, o jaki typ nam chodzi, kompilator przyjmie wartość domyślną czyli int.

Przykładowo dla zmiennej char(zajmującej 8 bitów zapisanej w formacie uzupełnień do dwóch) wygląda to tak:

signed char a;      /* zmienna a przyjmuje wartości od -128 do 127 */
unsigned char b;    /* zmienna b przyjmuje wartości od 0 do 255    */
unsigned short c;
unsigned long int d;

Jeżeli nie podamy żadnego ze specyfikatora wtedy liczba jest domyślnie przyjmowana jako signed (nie dotyczy to typu char, dla którego jest to zależne od kompilatora).

signed int i = 0;
// jest równoznaczne z:
int i = 0;

Liczby bez znaku pozwalają nam zapisać większe liczby przy tej samej wielkości zmiennej - ale trzeba uważać, by nie zejść z nimi poniżej zera - wtedy "przewijają" się na sam koniec zakresu, co może powodować trudne do wykrycia błędy w programach.

size_t[edytuj]

Typ size_t jest zdefiniowany w nagłówku stddef.h[2][3] jako alias do liczby całkowitej bez znaku


typedef unsigned int size_t;

Użycie size_t może poprawić przenośność, wydajność i czytelność kodu.[4]

short i long[edytuj]

Short i long są wskazówkami dla kompilatora, by zarezerwował dla danego typu mniej (odpowiednio — więcej) pamięci. Mogą być zastosowane do dwóch typów: int i double (tylko long), mając różne znaczenie.

Jeśli przy short lub long nie napiszemy, o jaki typ nam chodzi, kompilator przyjmie wartość domyślną czyli int.

Należy pamiętać, że to jedynie życzenie wobec kompilatora - w wielu kompilatorach typy int i long int mają ten sam rozmiar. Standard języka C nakłada jedynie na kompilatory następujące ograniczenia:

int - nie może być krótszy niż 16 bitów;
int - musi być dłuższy lub równy short a nie może być dłuższy niż long;
short int - nie może być krótszy niż 16 bitów;
long int - nie może być krótszy niż 32 bity;

Zazwyczaj typ int jest typem danych o długości odpowiadającej wielkości rejestrów procesora, czyli na procesorze szesnastobitowym ma 16 bitów, na trzydziestodwubitowym - 32 itd.[5] Z tego powodu, jeśli to tylko możliwe, do reprezentacji liczb całkowitych preferowane jest użycie typu int bez żadnych specyfikatorów rozmiaru.

Modyfikatory[edytuj]

volatile[edytuj]

volatile znaczy ulotny. Oznacza to, że kompilator wyłączy dla takiej zmiennej optymalizacje typu zastąpienia przez stałą lub zawartość rejestru, za to wygeneruje kod, który będzie odwoływał się zawsze do komórek pamięci danego obiektu. Zapobiegnie to błędowi, gdy obiekt zostaje zmieniony przez część programu, która nie ma zauważalnego dla kompilatora związku z danym fragmentem kodu lub nawet przez zupełnie inny proces.

volatile float liczba1;
float liczba2;
{ 
 printf ("%f\n%f\n", liczba1, liczba2);
 /* instrukcje nie związane ze zmiennymi */ 
 printf ("%f\n%f", liczba1, liczba2);
}

Jeżeli zmienne liczba1 i liczba2 zmienią się niezauważalnie dla kompilatora to odczytując:

  • liczba1 - nastąpi odwołanie do komórek pamięci. Kompilator pobierze nową wartość zmiennej.
  • liczba2 - kompilator może wypisać poprzednią wartość, którą przechowywał w rejestrze.

Modyfikator volatile jest rzadko stosowany i przydaje się w wąskich zastosowaniach, jak współbieżność i współdzielenie zasobów oraz przerwania systemowe. Często jest stosowany przy tworzeniu programów na mikrokontrolery. Kompilatory często tak optymalizują kod, aby wszystkie operacje wykonywały się w rejestrach, przez co wartość zmiennej w pamięci może być przez dłuższy czas nieuaktualniana. Zastosowanie volatile zmusi kompilator do każdorazowego odwołania do pamięci w przypadku operowania na zmiennych.

register[edytuj]

Jeżeli utworzymy zmienną, której będziemy używać w swoim programie bardzo często, możemy wykorzystać modyfikator register. Kompilator może wtedy umieścić zmienną w rejestrze, do którego ma szybki dostęp, co przyśpieszy odwołania do tej zmiennej

register int liczba;

W nowoczesnych kompilatorach ten modyfikator praktycznie nie ma wpływu na program. Optymalizator sam decyduje czy i co należy umieścić w rejestrze. Nie mamy żadnej gwarancji, że zmienna tak zadeklarowana rzeczywiście się tam znajdzie, chociaż dostęp do niej może zostać przyspieszony w inny sposób. Raczej powinno się unikać tego typu konstrukcji w programie.

static[edytuj]

Pozwala na zdefiniowanie zmiennej statycznej. "Statyczność" polega na zachowaniu wartości pomiędzy kolejnymi definicjami tej samej zmiennej. Jest to przede wszystkim przydatne w funkcjach. Gdy zdefiniujemy zmienną w ciele funkcji, to zmienna ta będzie od nowa definiowana wraz z domyślną wartością (jeżeli taką podano). W wypadku zmiennej określonej jako statyczna, jej wartość się nie zmieni przy ponownym wywołaniu funkcji. Na przykład:

void dodaj(int liczba)
{
 int zmienna = 0;     /* bez static*/
 zmienna = zmienna + liczba;
 printf ("Wartosc zmiennej %d\n", zmienna);
}

Gdy wywołamy tę funkcję np. 3 razy w ten sposób:

 dodaj(3);
 dodaj(5);
 dodaj(4);

to ujrzymy na ekranie:

Wartosc zmiennej 3
Wartosc zmiennej 5
Wartosc zmiennej 4

jeżeli jednak deklarację zmiennej zmienimy na static int zmienna = 0, to wartość zmiennej zostanie zachowana i po ponownym wykonaniu funkcji powinnyśmy ujrzeć:

Wartosc zmiennej 3
Wartosc zmiennej 8
Wartosc zmiennej 12


Zupełnie co innego oznacza static zastosowane dla zmiennej globalnej. Jest ona wtedy widoczna tylko w jednym pliku. Zobacz też: rozdział Biblioteki.

extern[edytuj]

Przez extern oznacza się zmienne globalne zadeklarowane w innych plikach - informujemy w ten sposób kompilator, żeby nie szukał jej w aktualnym pliku. Zobacz też: rozdział Biblioteki.

auto[edytuj]

Zupełnym archaizmem jest modyfikator auto, który oznacza tyle, że zmienna jest lokalna. Ponieważ zmienna zadeklarowana w dowolnym bloku zawsze jest lokalna, modyfikator ten nie ma obecnie żadnego zastosowania praktycznego. auto jest spadkiem po wcześniejszych językach programowania, na których oparty jest C (np. B).

Przypisy

  1. The Floating-Point Guide
  2. opis size_t w cpp0x
  3. About size_t and ptrdiff_t, Andrey Karpov
  4. Why size_t matters, Dan Saks
  5. Wiąże się to z pewnymi uwarunkowaniami historycznymi. Podręcznik do języka C duetu K&R zakładał, że typ int miał się odnosić do typowej dla danego procesora długości liczby całkowitej. Natomiast jeśli procesor mógł obsługiwać typy dłuższe lub krótsze stosownego znaczenia nabierały modyfikatory short i long. Dobrym przykładem może być architektura i386, która umożliwia obliczenia na liczbach 16-bitowych. Dlatego też modyfikator short powoduje skrócenie zmiennej do 16 bitów.