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
Język C++ operuje pięcioma podstawowymi typami danych:
Pełna lista typów zmiennych w języku C++:
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;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. 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żdymW 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ź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:
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:
Możliwa jest także sytuacja odwrotna: y = *(px + 1);Ponieważ operator () ma wyższy priorytet niż * , więc:
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.
*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 10Skąd to się bierze? Po uruchomieniu programu przydział pamięci dla zmiennych wygląda tak: 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... 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 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. 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ł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 tekstowejW zależności od zasięgu ("czasu ich życia") zmienne dzielimy na:
Zmienne lokalne
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
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
|