Prawo Murphy'ego: Zawsze kiedy masz właśnie coś zrobić, okazuje się, że najpierw musisz zrobić coś innego.
C++: Zmienne i stałe
Typy danych   |   Typ enum   |   Tablice   |   Wskaźniki   |   Struktury   |   Pola bitowe   |   Własne typy danych   |   Stałe   |   Zasięg i czas życia zmiennych
Typy danych
Język C++ operuje pięcioma podstawowymi typami danych:
  • char - znak, numer znaku w kodzie ASCII
  • int - liczba całkowita
  • float - liczba zmiennoprzecinkowa;
  • double - liczba zmiennoprzecinkowa podwójnej precyzji;
  • void - nieokreślona.
Pełna lista typów zmiennych w języku C++:
Deklaracja Bajtów Zakres wartości
char 1 -128 ... 127
unsigned char 1 0 ... 255
int
signed
signed int**
short
short int
**
2 -32768 ... 32767
unsigned
unsigned int**
unsigned short
2 0 ... 65 535
long
long int**
signed long
4 -2 147 483 648 ... 2 147 483 647
unsigned long
unsigned long int**
4 0 ... 4 294 967 295
float 4 3.4E+-38* (7 cyfr)
double 8 1.7E+-308* (15 cyfr)
long double 10 3.4E-4932 ... 1.1E+4932*
enum 2 0 ... 65 535
void*** 0 brak
* Zapis 3.4E-38 ... 3.4E+38 oznacza: -3.4*1038 ... 0 ... 3.4*10^-38...+3.4*1038
** Słowo int może zostać opuszczone w deklaracji, gdyż jest przyjmowane domyślnie.
*** Typ void nie jest typem danych w ścisłym tego słowa znaczeniu, ponieważ nie można utworzyć zmiennej typu void. Służy on głownie do deklaracji typu funkcja, które nie zwracają żadnej wartości lub nie przyjmują żadnych parametrów, np.:
void main(void)

Dopuszczalne są deklaracje i definicje grupowe z zastosowaniem listy zmiennych. Zmienne na liście należy oddzielić przecinkami.

Składnia deklaracji zmiennej:

typ_danych nazwa_zmiennej;
Przykłady:
   int a;
   unsigned b;
   float c, d, e;

Podczas deklarowania zmiennych można je również inicjalizować. Przykłady:

   int a;
   unsigned long int x = 5;
   float y=3.48, z = 2.78;
Typ enum
enum jest typem wyliczeniowy, odpowiednikiem typu unsigned int (patrz zakres wartości). Umożliwia on nadawanie kolejne elementy (liczbom) własnych nazw.

Prześledźmy to na przykładach:

   #include <stdio.h>

   void main(void)
   {   enum {A, B, C, X=24, Y, Z} litery;
      litery = A;   printf("Wartość dla A = %d\n", litery);
      litery = B;   printf("Wartość dla B = %d\n", litery);
      litery = C;   printf("Wartość dla C = %d\n", litery);
      litery = X;   printf("Wartość dla X = %d\n", litery);
      litery = Y;   printf("Wartość dla Y = %d\n", litery);
      litery = Z;   printf("Wartość dla Z = %d\n", litery);
   }

Program wyświetli kolejno: 0 1 2 24 25 26. Dzieje się tak dlatego, że identyfikatorowi A odpowiada wartość 0, identyfikatorowi B - 1, a identyfikatorowi C - 2. Identyfikatorowi X przypisaliśmy wartość 24. W związku z tym kolejne elementy listy będą miały przypisane jako identyfikatory następne liczby naturalne: Y - 25, a Z - 26.

   #include <stdio.h>

   void main(void)
   {   enum {false=0, no=0, off=0, true=1, yes=1, on=1} boolean;
      boolean = false;   printf("Wartość logiczna = %d\n", boolean);
      boolean = true;    printf("Wartość logiczna = %d\n", boolean);
      boolean = no;      printf("Wartość logiczna = %d\n", boolean);
      boolean = yes;     printf("Wartość logiczna = %d\n", boolean);
      boolean = off;     printf("Wartość logiczna = %d\n", boolean);
      boolean = on;      printf("Wartość logiczna = %d\n", boolean);
   }

W tym z kolei przykładzie wyświetlone zostaną kolejno: 0 1 0 1 0 1, gdyż zmienna o nazwie boolean przyjmuje wartość 1 dla identyfikatorów true, yes i on, a wartość 0 dla false, no i off.

Należy jednak pamiętać, że mimo faktu przypisywania zmiennej wartości poprzez nadane identyfikatory, to nadal są to zwykłe liczby.

Tablice
Tablic używa się w przypadku, gdy chcemy przechowywać dużą ilość danych tego samego typu przy zachowaniu łatwego do nich dostępu. Mimo, że tablica może przechowywać wiele danych jednego typu, odwołujemy się do niej za pomocą jednej nazwy. Aby móc określić, o który element chodzi, musimy użyć dodatkowo indeksu, czyli kolejnego (liczonego od 0) numeru elementu. Kolejne elementy są umiejscowione w pamięci komputera jeden za drugim. Adres tablicy jest stały. Raz przydzielony nie zmienia się przez cały czas działania programu. Składnia deklaracji tablicy wygląda następująco:

typ zmienna_tablicowa [ ilość_elementów ] ...;

Początek deklaracji jest identyczny, jak wszystkie dotychczas poznane. Czymś nowym jest ilość_elementów podana po nazwie zmiennej w nawiasach kwadratowych. Podaje ona ile elementów ma liczyć tablica. Musi to być liczba naturalna większa od 0. Tablice mogą być jedno- lub wielowymiarowe, jak np.:
   int t1[10]; //tablica o 10 elementach typu int
   int t2 [10] [5]; //tabilca o 10 wierszach po 5 elementów w każdym
W programie do kolejnych elementów odwołujemy się podając ich indeksy. Należ pamiętać, że indeksy tablicy liczone są od 0. Stąd dla tablicy 10-elementowej poprawnymi indeksami są liczby od 0 do 9.
Elementy tablicy mogą być dowolnego typu (w tym również struktury). Np:
   typedef struct
   {   int id;
       int ocena;
   } oceny;

   oceny lista_ocen[20];
Podczas deklarowania zmiennej tablicowej można również ją inicjować, jak pokazują poniższe przykłady:
   int tab1 [5] = { 1, 2, 3, 4, 5 };
gdzie kolejne elementy tablicy maja jako wartości przypisane kolejne liczby naturalne, lub:
   int tqb2 [3] [2] = { {1, 5}, {1, 4}, {1, 5} };
Tutaj tworzymy tablicę 2-wymiarową i inicjujemy jej wszystkie elementy. Zwróć uwagę, że elementy każdego wiersz stanowią wewnętrzne tablice.

Istniej również możliwość deklarowania tablic bez podawania ich wielkości. Kompilator sam ustala wtedy wielkość tablicy na podstawie danych podanych przy jej automatycznej inicjalizacji. Można tak deklarować każdy typ danych, ale najczęściej korzystamy z tego przy tworzeniu tablic zawierających łańcuchy znakowe. Np.:

   int tab [] = { 1, 2, 3, 4, 5 };
   char napis [] = "Tablica znakowa";

W pierwszym przypadku kompilator utworzy tablicę o 5 elementach typu int. W drugim - tablicę o 16 elementach typu char: 15 na znaki podanego napisu plus 1 element na stałą NULL kończącą każdą stałą łańcuchową.

Wskaźniki i referencje
Wskaźnik to zmienna, która zawiera adres innej zmiennej w pamięci komputera. Istnienie wskaźników umożliwia pośrednie odwoływanie się do wskazywanego obiektu (liczby, znaku, łańcucha znaków itp.) a także stosunkowo proste odwołanie się do obiektów sąsiadujących z nim w pamięci.

Rozpatrzmy to na przykładzie. Załóżmy, że:
x - jest zmienną typu int umieszczoną gdzieś w pamięci komputera (zajmuje 2 kolejne bajty pamięci);
px - jest wskaźnikiem do zmiennej x.
Jednoargumentowy operator & podaje adres obiektu, a zatem instrukcja:

   px = &x;
przypisuje wskaźnikowi px adres zmiennej x. Mówimy, że:
px wskazuje na zmienną x lub px jest wskaźnikiem (ang. pointer) do zmiennej x.

Operator wyłuskani (*) powoduje, że zmienna z tym operatorem jest traktowana jako adres pewnego obiektu. Zatem, jeśli przyjmiemy, że y jest zmienną typu int, to działania:

   y = x;
oraz
   px = &x;
   y = *px;
będą mieć identyczny skutek. Zapis y = x oznacza:
"Nadaj zmiennej y dotychczasową wartość zmiennej x";
a zapis y=*px oznacza:
"Nadaj zmiennej y dotychczasową wartość zmiennej,
której adres w pamięci wskazuje wskaźnik px;" (czyli właśnie x).

Zmienne wskaźnikowe także wymagają deklaracji. Poprawna deklaracja w opisanym powyżej przykładzie powinna wyglądać tak:

   int x,y;
   int *px;
Zapis int *px; oznacza:
"px jest wskaźnikiem i będzie wskazywać na liczby typu int".
Wskaźniki do zmiennych mogą zamiast zmiennych pojawiać się w wyrażeniach po prawej stronie, np. poprawny jest poniższy zapisy:
   int x,y;
   int *px;
   px = &x;
   y = *px + 1;       // równoważne y = x + 1

   printf("%d", *px); // równoważne printf("%d", x);
   y = sqrt(*px);     // pierwiastek kwadratowy z x

Operatory & i * mają wyższy priorytet niż operatory arytmetyczne, dzięki czemu:

  • najpierw następuje pobranie spod wskazanego przez wskaźnik adresu zmiennej;
  • potem następuje wykonanie operacji arytmetycznej (operacja nie jest więc wykonywana na wskaźniku, a na wskazywanej zmiennej!).

Możliwa jest także sytuacja odwrotna:

   y = *(px + 1);
Ponieważ operator () ma wyższy priorytet niż * , więc:
  • najpierw wskaźnik zostaje zwiększony o 1;
  • potem zostaje pobrana z pamięci wartość znajdująca się pod wskazanym adresem (nie jest to już adres zmiennej x, a obiektu "następnego" w pamięci) i przypisana zmiennej y.

Taki sposób poruszania się po pamięci jest szczególnie wygodny, jeśli pod kolejnymi adresami pamięci rozmieścimy np. kolejne wyrazy z tablicy, czy kolejne znaki tekstu.
Przyjrzyjmy się wyrażeniom, w których wskaźnik występuje po lewej stronie. Podane zapisy są równoważne (mają identyczne działanie):

   *px = 0;      <==>    x = 0;
   *px += 1;     <==>    x += 1;
   (*px)++;      <==>    x++;
Na zakończenie wskaźników mały program, który powinien wyjaśnić do końca, o co tu chodzi:
   #include <stdio.h>
   #include <conio.h>

   int a=1, b=2, c=3, d=4, e=5, f=6, g=7, h=8, i=9, j=10, n;
   int *ptr1 = &a;
   long int *ptr2 = (long *)&a;

   void main()
   {   clrscr();
       printf("Skok o 2Bajty   Skok o 4Bajty");

       for(n=0; n<=9; n++)
       {   printf("\n%d", *(ptr1+n));
           printf("\t\t%d",  *(ptr2+n));
       }
       getch();
   }
Po uruchomieniu tego programu powinieneś otrzymać taki mniej więcej ekran:
Skok o 2Bajty        Skok o 4Bajty
    1                    1
    2                    3
    3                    5
    4                    7
    5                    9
    6                 2344
    7                -5688
    8                 5686
    9               -34455
   10
Skąd to się bierze? Po uruchomieniu programu przydział pamięci dla zmiennych wygląda tak:

Mapka pamięci

Zmienne a do j mają przydzielone kolejne komórki pamięci (każda po 2 bajty) i przypisane wartości początkowe. Zmienna n zajmuje kolejne 2 bajty, ale już bez inicjacji (nie wiadomo, co się w niej znajdzie). Zmienne ptr1 i ptr2 mają przypisany adres pamięci przydzielony zmiennej a, z tym, że ptr1 wskazuje na typ int (2-bajtowy), a ptr2 na typ long (4-bajtowy). Obie instrukcje printf wyświetlają liczby całkowite typu int, w każdym kolejnym przejściu pętli pobierając je z kolejnej lokacji pamięci. Ponieważ ptr1 wskazuje na typ int, to kolejne jego odwołania *(ptr1+n) pobierają kolejne nasze zmienne. Zmienna ptr2 wskazuje na typ long, dlatego kolejne pobrania *(ptr2+n) przesuwają adres o 4 bajty i w konsekwencji pobieramy co drugą zmienną, a po wartości 9 idziemy poza obszar naszych zmiennych...

Struktury
Struktura jest złożonym typie danych tworzonym przez programistę. Stanowi kombinację wcześniej zdefiniowanych typów: typów prostych oraz innych typów zdefiniowanych przez programistę (także inne struktury).

Ogólna postać definicji typu strukturalnego wygląda następująco:
 

typedef struct {
  typ nazwa_pola1;
  typ nazwa_pola2;
  ...
  typ nazwa_polaN;
} nazwa_struktury;

lub
 
struct nazwa_struktury {
  typ nazwa_pola1;
  typ nazwa_pola2;
  ...
  typ nazwa_polaN;
};

  Można również zdeklarować zmienną strukturalną bez definiowania typu strukturalnego. Robimy to tak:
 
struct {
  typ nazwa_pola1;
  typ nazwa_pola2;
  ...
  typ nazwa_polaN;
} nazwa_struktury;

  W pierwszych dwóch definicjach deklarujemy typ strukturalny, który możemy następnie wykorzystać do tworzenia zmiennych. Różnica między tymi składniami deklaracji polega na późniejszym odwołaniu do typu podczas tworzenia zmiennych. Jeżeli użyliśmy słowa typedef, to deklaracja zmiennej wygląda tak:
 
nazwa_struktury nazwa_zmiennej;

  W drugim przypadku zmienna musimy deklarować tak:
 
struct nazwa_struktury nazwa_zmiennej;

  Poza tym faktem, oba sposoby deklaracji niczym się nie różnią.

Użycie trzeciej składni nie tworzy nowego typu, ale od razu zmienną, do której można się odwoływać w kodzie programu. Używamy go, gdy nie zależy nam na utworzeniu nowego typu, a tylko zmiennej.

Przykład:
   include <stdio.h>

   typedef struct
   {   int hh;
       int mm;
       int ss;
   } czas;

   void main(void)
   {   czas godzina;

       godzina.hh = 15;
       godzina.mm = 45;
       godzina.ss = 20;

       printf("Teraz jest godzina: %d:%d:%d\n", godzina.hh, godzina.mm, godzina.ss);
   }
W przykładzie zdefiniowaliśmy strukturę o nazwie czas zawierającą 3 pola typu int: hh, mm i ss do przechowywania informacji o czasie. Moglibyśmy te same informacje przechowywać w 3 osobnych zmiennych, ale co jeśli chcielibyśmy mieć dane o dwóch różnych godzinach? Musielibyśmy dodać 3 nowe zmienne, co przy konieczności zapamiętania wielu godzin doprowadziłoby do chaosu. Struktura pozwala nam przechowywać potrzebne informacje, przy czym wszystko znajduje sie w jednym miejscu - zamiast 3, mamy tylko 1 zmienną. Na początku funkcji main zadeklarowaliśmy zmienną typu czas o nazwie godzina, która będzie przechowywać informacje. W następnych 3 wierszach pokazano w jaki sposób odwołujemy się do poszczególnych pól struktury: podajemy nazwę zmiennej (godzina), potem stawiamy kropkę, a następnie podajemy nazwę pola, do którego się odnosimy.

Poza tym, że do poszczególnych pól odwołujemy się w nowy sposób, możemy z nich korzystać tak jakby byłaby to normalna zmienna o danym typie: możemy przypisywać wartość, czy używać wszelkich operatorów.

Pola bitowe
Pola bitowe mają zastosowanie przy definicji struktur - przy pomocy tej konstrukcji możemy zadeklarować pole, którego wielkość będzie mniejsza niż 1 bajt (1 lub kilka bitów - stąd nazwa).

Zobaczymy to na przykładzie:

   include <stdio.h>

   typedef struct
   {   unsigned char mx : 4;
       unsigned char co : 1;
       unsigned char cw : 1;
   } mieszkanie;

   void main(void)
   {   mieszkanie info;

       info.mx = 3;
       info.co = 1;
       info.cw = 1;

       printf("Typ mieszkania : M-%d\n", info.mx);
       if(info.co) printf("Posiada centralne ogrzewanie.\n");
       if(info.cw) printf("Posiada ciepłą wodę.\n");
   }

Program przechowuje i wyświetla informacje o mieszkaniu: o typie i o tym, czy posiada co i cw. Normalnie potrzebowalibyśmy zadeklarować strukturę o 3 polach, która - przy zastosowaniu 1-bajtowego typu char, zajęłaby 3 bajty pamięci. Dzięki zastosowaniu pól bitowych wszystkie informacje zajmują 1 bajt pamięci. Jak to możliwe? Otóż ograniczyliśmy zakres poszczególnych pól. Ile potrzeba miejsca w pamięci, aby przechować informację o fakcie wyposażenia, bądź nie, mieszkania w ciepłą wodę? Są 2 możliwe stany: jest lub nie ma. Czyli innymi słowy 1 albo 0 - wystarczy 1 bit. To samo dotyczy ogrzewania. Jeśli chodzi o typ mieszkania, to na 4 bitach można zapisać liczbę 15 - chyba nie ma mieszkań większych niż M-15? W ten sposób zamiast używać 3, struktura używa zaledwie 1 bajtu. Dostęp do pól bitowych jest identyczny, jak do tych "normalnych".

Jedyną różnicą jest sposób ich deklaracji - tzn. deklaruje się je tak samo, ale po typie i nazwie pola podaje się dodatkowo: dwukropek i ilość bitów przeznaczonych na dane pole.

Własne typy danych
Język C++, poza wbudowanymi typami danych, umożliwia definicję własnych typów danych. Do deklaracji nowego typu danych służy instrukcja typedef. Deklarację własnego typu wykorzystujemy do zmiany nazwy istniejącego typu na bardziej poręczną lub do definiowania własnych złożonych struktur danych.
Składnia deklaracji wygląda następująco:
 
typedef definicja_typu nazwa_nowego_typu;

 

Przykłady jej użycia typedef:

   typedef float rzeczywista;
   typedef char znak;
   void main(void)
   {   rzeczywista a  = 1.25;
       znak        zn = 'A';
       ...
   }

 

W przykład zadeklarowaliśmy typy danych rzeczywista i znak. Określiliśmy, że nowe typy będą po prostu typami float i char tylko ze zmienionymi nazwami. Następnie w funkcji main zadeklarowaliśmy zmienne własnych typów.

   typedef osoba
   {   char nazwisko [15];
       char imie [15];
       char wiek;
   }

   void main(void)
   {   osoba   ktos, grupa[20];
       ...
   }

 

Tutaj zadeklarowaliśmy typ strukturalny o nazwie osoba. Następnie w funkcji main zadeklarowaliśmy zmienną osoba jako strukturę zdefiniowanego przez nas typu oraz tablicę 20 takich struktur o nazwie grupa.

Stałe
Stała to taka zmienna, której wartość można przypisać tylko 1 raz. Z punktu widzenia komputera stała niczym nie różni się od zmiennej: musi mieć miejsce w pamięci odpowiednie do zadeklarowanego typu, musi być zapamiętany identyfikator i adres. Jedyna praktyczna różnica polega na tym, że zmiennej zadeklarowanej jako stała, nie można przypisać w programie żadnej innej wartości. Deklaracja stałej wygląda identycznie jak deklaracja zmiennej, z tym, że nazwa typu jest poprzedzana słowem kluczowym const.
Zapis:
const float PI = 3.1416;
jest jednocześnie deklaracją, definicją i inicjacją stałej PI.

Przykłady:

   const int STO = 100; // stała typu int
   const int TAB[4]={1,2,3,4}; //wskaźnik do tablicy liczb całkowitych
   const char *IMIE = "Jasio"; // wskaźnik do stałej tekstowej
Zasięg i "czas życia" zmiennych
W zależności od zasięgu ("czasu ich życia") zmienne dzielimy na:
  • lokalne;
  • globalne;
  • statyczne.

Zmienne lokalne
Wszystkie zmienne deklarowane wewnątrz funkcji - czy to main, czy też funkcja zdefiniowana przez użytkownika są prywatne, czyli lokalne dla danej funkcji. Żadna inna funkcja nie ma do nich dostępu. Każda zmienna lokalna funkcji, zaczyna jest tworzona w chwili wywołania funkcji i likwidowana po jej zakończeniu. Jeżeli po raz drugi wywołamy funkcję, to zmienne te będą ponownie tworzone, ale nie ma pewności, że znajdą się w tym samym miejscu w pamięci co poprzednio. Również nie zachowają swojej poprzedniej wartości (z poprzedniego wywołania) i muszą być jawnie określane od nowa przy każdym wejściu do funkcji. Ponieważ zmienne nie są automatycznie zerowane, to początkowo mają przypadkową wartość. Należy pamiętać, aby nie operować na tych zmiennych zanim w nich czegoś sensownego nie zapiszemy. Z tego typu zmiennymi wiąże się słowo kluczowe auto, np.:

auto int x;
Jest ono rzadko używane, gdyż zmienne są definiowane domyślnie jako automatyczne. Jeśli więc w bloku funkcji wystąpi taka deklaracja jest ona równoważna:
int x;

Zmienne globalne
Przeciwieństwem zmiennych lokalnych są zmienne zewnętrzne w stosunku do wszystkich funkcji, zwane globalnymi. Są one dostępne poprzez nazwę we wszystkich funkcjach pliku. Deklaracje takich zmiennych umieszcza się na początku programu (pliku). Ze względu na to, że zmienne globalne są ogólnie dostępne, mogą być używane zamiast listy parametrów przy przekazywaniu danych między funkcjami. Ponieważ istnieją stale, a nie pojawiają się i znikają razem z wywołaniem funkcji, zachowują swoje wartości nawet po zakończeniu działania funkcji, która nadała (zmieniła) im wartość. Zmienna globalna może być zdefiniowana tylko jeden raz, na zewnątrz wszystkich funkcji. Zmienne globalne są wstępnie inicjowane zerami.

Uwaga: jeżeli wewnątrz funkcji zadeklarujemy zmienną lokalną o nazwie identycznej ze zmienną globalną, to odpowiednia zmienna globalna zostanie przesłonięta przez zmienną lokalną. Oznacza to, że zmienna globalna nie będzie widziana wewnątrz tej funkcji.

Zmienne statyczne
Są to zmienne deklarowane przy pomocy słowa kluczowego static. Można ją stosować zarówno do zmiennych lokalnych jak i globalnych. W stosunku do zmiennych globalnych klauzula static nie powoduje żadnych konsekwencji. Istotne jest jej użycie dla zmiennych lokalnych: powoduje ona, że po wyjściu z funkcji zmienna nie jest niszczona, ale pozostaje z dotychczasowym przydziałem pamięci oraz zachowuje dotychczasową wartość. Oznacza to, że przy kolejnym wywołaniu funkcji wartość zmienna jest znana: jest taka sama, jak przy poprzednim wyjściu z funkcji.

« wstecz   dalej »