Wykład 4 - 2godz.
Zakres tematyczny:
1. Typy pochodne zmiennych:
- wskaźniki
- wskaźniki a tablice
1. Wskaźniki
Najogólniej mówiąc, wskaźnik jest zmienną, która przechowuje adres innej zmiennej. Język C jest językiem tak zorganizowanym, że często posługuje się wskaźnikami. Są nimi argumenty wielu funkcji bibliotecznych, czy też wartości przez nie zwracane. Co prawda, istnienie wskaźników nie jest konieczne (można by się było bez nich obejść), ale właśnie dzięki nim język C tworzy tak zwarte i efektywne kody. Ponieważ wskaźniki i tablice są blisko ze sobą spokrewnione, przy okazji omawiania wskażników uzupełnimy nieco informacje o tablicach.
Wskaźnikami możemy posłużyć się wobec dowolnej zmiennej podstawowej, tablicy, funkcji, innego wskaźnika, struktury itp.Wskaźniki deklarujemy używając następującej składni:
typ_zmiennej * nazwa_zmiennej
np:
char * wsk1;
int *wsk2;
float *wsk3;
Wskaźniki są 16 - to lub 32 bitową liczbą, która przechowuje adres zmiennej danego typu: i tak czytając powyższe zapisy:
wsk1 jest wskaźnikiem do wskazywania (wskazujący) na zmienną typu char
wsk2 jest zmienną wskazującą na zmienną typy integer
wsk3 wskazuje na zmienną typu flaot
Przypomnijmy sobie na wstępie jak zorganizowana jest pamięć. Jest to tablica kolejno numerowanych lub adresowanych komórek pamięci: można nimi manipulować pojedyńczo lub posługując sie grupami sąsiednich komórek. Każda zmienna ma unikalny adres wskazujący początkowy obszar pamięci zajmowany przez tą zmienną. Ilość pamięci zajmowanej przez zmienną zalęży jak pamiętamy od typu zmiennej. Zamiast odwoływać się do wartości zmiennej, można mieć do niej dostęp poprzez manipulowanie zmiennymi, które zawierają ich adres, czyli wskaźniki. Przydaje się to szczególnie przy manipulowaniu na tablicach, czy strukturach.
Treścią wskaźnika jest informacja, gdzie wskazany obiekt się znajduje, a nie to co się w nim znajduje.
Operator * informuje nas, że mamy do czynienia ze wskaźnikiem. Podobnie jak nawiasy [] informowały nas o tablicach. Sam wskaźnik nazywa się tak jak nazwa_zmiennej. Typami pochodnymi są też referencje i tzw. pola bitowe. Referencja nie jest obiektem (jak np. tablica) i do niej nie ma wskaźników. Do poszczególnych pól bitowych też nie (ponieważ zwiększenie adresu o jeden powoduje przesunięcie się w pamięci o 8 bitów(bajt) a nie o jeden bit).
Należy też pamiętać, że wskaźnik stworzony do wskazywania na obiekty jednego typu nie może wskazywać na inne typy:
int *wsk;
float a;
wsk = &a; //nieprawidłowo.
Operator & jest operatorem adresu i może być stosowany tylko do obiektów zajmujacych pamięć: zmienne, elementy tablic. Nie można go stosować do wyrażeń, stałych i tzw. zmiennych register( o tym później). Pomaga on nadać zadeklarowanemu wskaźnikowi wartość początkową, czyli przypisać go do konkretnego obiektu. Kiedy wskaźnik pokazuje już na konkretnie miejsce możemy odnieść się do tego obiektu na który wskazuje( odczytać jego wartość, lub zapisać coś do niego- obiektu nie wskaźnika).
Operator * jest jak mówiłam operatorem adresowania pośredniego, ale także odwołania pośredniego: zastosowany do wskaźnika daje zawartość obiektu wskazanego przez ten wskaźnik np.:
int *wsk1 ;
float wsk2;
int zmienna1 = 10;
float zmienna2 = 1.2;
wsk1 = &zmienna1;
o a<<"Wskaźnik wsk1 wskazuje na zmiennąestedresie"<
<<"zawierającą zmienną = "<<*wsk1
wsk2 = &zmienna2;
cout<<"Wskaźnik wsk2 wskazuje na zmienną o adresie"<
<<"zawierającą zmienną = "<<*wsk2
Przypisanie adresu konkretnej zmiennej można skrócić. Zapis:
int *wsk;
int zmienna;
wsk = &zmienna;
jest równoważny:
int zmienna = 100;
int *wsk = &zmienna;
Teraz do zmiennej chcemy wpisać nową wartość:
*wsk = 200;
cout<
Czyli:Do obiektu można coś wpisać albo używając nazwy albo wskaźnika pokazującego na ten obiekt.
Jeśli wsk wskazuje na zmienną całkowitą, to *wsk może wystąpić wszędzie tam gdzie może wystąpic zmienna, a więc np:
int *wsk;
int zmienna = 10;
wsk = &zmienna;
*wsk = *wsk + 2; // zmienna = 12;
Ponieważ operatory * oraz & są silniejsze niż operatory arytmetyczne, to dla wyrażenia:
x = *wsk + 1;
nie trzeba nawiasu: najpierw pobierana jest wartość z adresu wsk i zwiększona o 1 zostaje zapisana do x.
*wsk +=1 ++*wsk (*wsk)++ są równoważne.
W ostatnim przypadku nawiasy są niezbędne bo jednoargumentowe operatory * i ++ wykonywane są od prawej do lewej. Czyli operacja wykonała by zwiększenie wskaźnika o jeden, a nie obiektu.
Ponieważ wskaźniki są zwykłymi zmiennymi można ich używać bez adresowania pośredniego:
wsk = wsk1;
Zapis ten mówi, że teraz wsk będzie wskazywał na to samo co wsk1.
Wskaźnik void
Przy okazji powiemy sobie o wskaźniku void. Jak pamiętamy, deklaracja wskaźnika niosła w sobie dwie informacje: adres jakiegoś miejsca w pamięci, oraz na jakiego typu zmienną pokazuje:int, char float itp.
Możemy jednak zdefiniować wskaźnik, który informacji o typie nie przenosi - jest to tzw. wskaźnik void. Definiujemy go następująco:
void *w;
Służy on najogólniej mówiąc do reprezentowania bloku pamięci np.
void *wsk_blok = malloc(100);
O funkcji malloc mówić będziemy przy okazji omawiania dynamicznej alokacji pamięci. Teraz omawiając zapis powyższy mówimy, że zmienna wsk_blok wskazuje adres 100 bajtowego bloku pamięci.
W związku z tym, że wskaźnik void nie określa typu zmiennej na jaki wskazuje, jest rzeczą oczywistą, że nie można nim posłużyć się do odczytania pamięci. Nie można się nim poruszać po sąsiednich adresach: nie wiemy co ile bajtów poruszać się po pamięci.
Aby móc teraz traktować ten obszar pamięci jako np tablicę znaków czy innych zmiennych, musimy dokonać odpowiednich konwersji do żądanych typów, przy pomocy operatora rzutowania, o którym mówiliśmy na poprzednich wykładach , i tak np:
void wsk_blok = malloc(100);
char *wsk;
// int *wsk1;
.................
wsk_blok = wsk;
// wsk_blok = wsk1;
Zapisy te oznaczają, że teraz wskaźnik typu void wskazuje na to samo, na co wskazuje wskaznik typu char (int).
Wskaźnik każdego typu można przypisać wskaźnikowi typu void, bez konieczności rzutowania.
Przy okazji możemy napomknąć, że operator rzutowania może być stosowany również do wskaźników np:
int *wsk1, *wsk2;
float *wsk;
...............
wsk1 = wsk2; //poprawny zapis
wsk = wsk1; //bład kompilatora, bo chcemy wskaźnikiem do float pokazywać //na int
wsk = (float *)wsk1; //świadomie każemy kompilatorowi to zrobi, kompilacja bez //błędu
Operatora rzutowania musimy użyć, gdy chcemy przypisać wskaźnikowi rzeczywistemy wskaźnik typu void:
float *wsk;
voiod *wsk1;
....................
wsk = wsk1; //błąd
wsk = (float *)wsk1; //poprawnie z operatorem rzutowania
Jest różnica między ANSII C a C++. W języku C niezależnie od tego po której stronie operatora przypisania stał wskaźnik void nie trzeba było używać rzutowania.
1a. Wskaźniki a tablice
Wskaźniki generalnie stosuje sie w 4 przypadkach:
- usprawnienie pracy z tablicami
- w funkcjach mogących zmieniać przychodzące do niej argumenty
- dostęp do specjalnych komórek pamięci
- rezerwacja pamięci
Najpowszechniej na poczatku stosuje się wskaźniki do pracy z tablicami. Zadeklarujmy wskaźnik i tablicę:
int *wsk;
int tab_a[10];
1. instrukcja:
wsk = &tab_a[n];
ustawia wskaźnik na n-tym elemencie tablicy. Operator & jest operatorem adresu. Typ wskaźnika zgadzać się musi z typem tablicy.
2. instrukcja:
wsk = &tab_a[0];
jest równoważna instrukcji:
wsk = tab_a;
i oznacza ustawienie wskaźnika na pierwszy element tablicy(na początek). Zapisy są równoważne, ponieważ jak już wspomnieliśmy, nazwa tablicy stanowi adres jej zerowego (pierwszego) elementu.
Po ustawieniu wskaźnika na n-ty element tablicy:
wsk = tab_a[7];
to przejście do następnego elementu tablicy umożliwia
instrukcja 3 :
wsk = wsk + 1; lub wsk ++;
aby przesunąć się o n elementów w tablicy napiszemy
instrukcję 4 :
wsk + = n;
Prostota zapisu jest zaskakująca. Można zapytac skąd wskaźnik wie, gdzie się przesunąć gdy np: zwiększamy go o 1. Otórz w definicji wskaźnika powiedzieliśmy, że np. wsk = jest wskaźnikiem na int. Stąd kompilator wnioskuje, że aby odnaleźć następny element typu int należy przesunąć się o 2 bajty. (typ int - 2 bajty)
Powyższe rysunki ilustrują mechanizm poruszania się wskaźnika do tablicy dla różnych typów zmiennych. Przepiszemy teraz program z poprzedniego wykładu tak, aby posługiwał się wskaźnikami, a nie klasycznym odwoływaniem sie do n-tego elementu tablicy:
Przykład 1.
#include
#include
#include
main()
{
int *wsk_a, *wsk_b,i, j,pom;
int a[4][4];
int b[4]={0,0,0,0};
clrscr();
wsk_a = a; //inicjowanie wskaźników
wsk_b = b;
for(i = 0;i<4;i++)
for(j = 0;j<4;j++)
{
printf("a[%i][%i] = ",i,j); //wprowadzenie danych do tablicy
scanf("%i",wsk_a++);
/* *wsk_a++ = i+1; */
}
printf("Tablica a:
");
for(i = 0,wsk_a = a;i<4;i++)
for(j = 0;j<4;j++) //Wydruk tablicy wprowadzonej
printf("
%i",*wsk_a++);
for(wsk_a = a,i = 0;i<4;i++)
{
pom = 0;
for(j = 0;j<4;j++)
pom += *wsk_a++; //sumowanie elementów wiersza
*wsk_b++ = pom;
}
for(j = 0,wsk_b = b;j<4;j++) //wydruk wyników
printf("%i ",*wsk_b++);
while(!kbhit());
return 0;
}
Jak widać, posłużenie się wskaźnikami nic szczególnego nam nie dało. Pozornie tylko. W efekcjie otrzymujemy szybszy od poprzedniego program. Zapis np a[5] powoduje żmudne liczenie adresu czygo przy wskaźnikach nie ma.
Pamiętajmy!!!
Mimo, że możemy zapiasć:
wsk = tab_a;
to o ile możemy postąpić:
wsk++;
to nie możemy napisać:
tab_a++;
Różnicą między wskaźnikiem a nazwą tablicy jest to, że wskaźnik jest obiektem w pamięci, w związku z tym można np. ustalić jego adres, natomiast nazwa tablicy nie jest obiektem i na niej nie można przeprowadzać żadnych operacji.
Dla tak zadeklarowanego wskaźnika:
int *wsk;
adresem tego wskaźnika jest wartość wyrażenia:
&wsk;
1b.Arytmetyka wskaźników
1.
Możemy dodawać i odejmować liczby całkowite do wskaźników tak, aby w potrzebny sposób przesuwać je po tablicy. Operacje te nie są sprawdzane przez kompilator, i wtedy możemy przesunąć wskaźnik poza zadeklarowaną tablicę i np. zniszczyć istniejące tam potrzebne dane. Błędy takie są najtrudniejsze do wykrycia, dlatego przy przesuwaniu wskaźników należy zachować daleko idącą ostrożność.
2.
Możemy odjąć dwa wskaźniki od siebie:
wsk_a - wsk_b
Gdy pokazują one na różne elementy tej samej tablicy to wynikiem takiej operacji jest liczba dzielących je elementów. Liczba może byc ze znakiem - lub +.
3.
Wskaźniki można ze sobą porównać. Do tego celu służą nam operatory:
== != < > <= >=
Dla dwóch wskaźników:
int *wsk1, *wsk2;
wyrażenie : wsk1 = wsk2 oznacza, że wskazują one na ten sam obiekt.
if(wsk1 == wsk2)
cout<<"Oba wskaźniki pokazują na ten sam obiekt";
Jeśli wskaźniki wskazują na jakieś elementy tej samej tablicy, to wyrażenie wsk1 < wsk2 oznacza, że wsk1 wskazuje na element tablicy o mniejszym indeksie.
4.
Każdy wskaźnik można porównać z adresem 0 zwanym NULL. Takie ustawienie wskaźnika:
wsk = 0; //lub wsk = NULL
informuje, że wskaźnik nie pokazuje na nic konkretnego (czasami niektóre funkcje biblioteczne zwracają wskaźnik NULL (null pointer) np funkcja gets(string)). Potem możemy łatwo sprawdzać:
if(wsk == 0) // if(wsk ==NULL) if(!wsk)
............. .............. .......
1c. Inicjowanie wskaźników
Przed rozpoczęciem pracy ze wskaźnikami należy pamiętać o tym , że przed ich pierwszym użyciem muszą być one ustawione.
W tym punkcie zbierzemy wszystkie sposoby ustawiania wskaźników i te o których mówilismy oraz te o których nie wspominaliśmy.
1. tak aby wskazywał na konkretny obiekt:
wsk = &obiekt;
2. można ustawić go na to samo na co wskazuje inny wskaźnik:
wsk = wsk1;
3. ustawić wskaźnik na początek jakiejś tablicy:
wsk = tab_a; // wsk = &tab_a[0];
4.ustawić wskaźnik na jakąś funkcję.
5.ustwić wskaźnik na zarezerwowany dynamicznie obszar
6. ustawić wskaźnik na konkretny adres którego np. zawartość chcemy sprawdzić
7. ustawienie wskaźnika na string
wsk = "to jest string";
1.d Tablice wskaźników
Pamiętamy, że tablica to ciągły obszar w pamięci, w którym przechowywany jest ciąg zmiennych tego samego typu. Skoro moga być tablice int , float , char , to dlaczego nie miałoby być tablic wskaźników. W końcu adres to też liczba. Np.:
float *wsk1[3];
int *wsk2[10];
powyższe zapisy czytamy:
- wsk1 jest tablicą trzy elementową wskaźników do float
- wsk2 jest tablicą 10 -elementową wskaźników na int.
1.e Wskaźniki a stringi
Wielokrotnie mówiliśmy,że string to tablica znaków zakończona znakiem NULL.
Jeśli jakiś wskaźnik ma pokazywac na ciąg znaków, to można go zadeklarować jako:
char *text;
i zainicjować:
text = "To jest próbny tekst";
lub razem w jednej linii:
char *text = "to jest próbny tekst";
Zapis 1 nie oznacza kopiowania, jest to przypisanie wskaźnika konkretnemu stringowi. W języku C nie ma instrukcji do obsługi ciągu znaków jako całości.
Porównajmy zapis:
char tab_text [] = "To jest próbny tekst";
char *text = "to jest próbny tekst";
Pierwsze jest tablicą, poszczególne znaki tablicy można zmieniać, ale nazwa tab_text zawsze będzie odwołaniem do tego samego miejsca w pamięci. Z drugiej strony text jest wskaźnikiem zainicjowanym tak, aby wskazywał na string. Wskaźnik można później zmieniać, tak aby wskazywał na cokolwiek, ale zmiana w stałej napisowej ma skutek nieokreślony
W jaki sposób można wykorzystać wskaźniki do stringu, zademonstrujemy na przykładzie funkcji append_string, dodającej dwa stringi do siebie:
Przykład 2.
append_string(char *string1, char *string2)
{
//znajdywanie końca stringu2 do ktorego dolączymy string1
while(*string2 != NULL)
string2++;
//dołączanie stringów
while((*string2 = *string1) != NULL)
{
string1++;
string2++;
}
}
W praktyce funkcję pisze się nieco inaczej. Można to zrobić tak:
append_string(char *string1, char *string2)
{
//znajdywanie końca stringu2 do ktorego dolączymy string1
while(*string2++)
;
//dołączanie stringów
while(*string2++ = *string1++)
;
}
Zwiększanie wskaźników string1 i string2 przeniesiono do części warunkowej. Jak mówiliśmy wcześniej, wartością operacji: *string1++ jest znak na który wskazywał wskaźnik przed zwiększeniem. Znak ze stringu 1 wstawiany jest na końcu stringu2 i na koniec dołączany jest znak NULL. Porównanie ze znakiem ' ' jest zbyteczne, bo dla string1 = NULL wartość wyrażenia = 0 co jest warunkiem zakończenia pętli. Po instrukcji while wykonywana jest instrukcja pusta, ponieważ wszystkie operacje wykonywane są w momencie sprawdzania warunku.
Podobnie jak w przypadku tablic wskaźników na int czy float, można również używać wskaźników do stringów. Jej elementami są wskaźniki mogące pokazywać na stringi:
char *wsk_tab_char[4];
Wskaźniki te można ustawiać podobnie jak do zwyklych stringów:
char *text[4] = {"pon", "wto", "sro", "czw"};
Elementami tej tablicy nie są oczywiscie stringi - nazwy dni tygodnia, ale adresy tych miejsc pamięci, gdzie umieszczone sa te stringi
1.f dostep do konkretnych komórek pamięci:
Czasami zachodzi konieczność uzyskania bezpośredniego dostępu do konkretnej komórki pamięci bez podania jej nazwy. Wtedy odnosimy sie do niej poprzez wskaźnik:
wsk = 0x1111;
podajemy poprostu do wskaźnika adres komórki i odtąd możemy posługiwać się tym wskaźnikiem
cout<<"wartość przechowywana pod adresem"<
Nie zawsze jednak ustawienie adresu jest takie proste: w Borlandzie C++ istnieje makro o nazwie: MK_FP (make far pointer) - zainteresowani muszą się sami z nim zapoznać (podaje się segment i offset).
1.g Dynamiczna alokacja pamięci
Wcześniej czy później, programista staje przed problemem dostępności pamięci. Jak intuicyjnie wiadomo, jest ona ograniczona i nie możemy do niej ładować dowolnej ilości dowolnie dużych obiektów. Stajemy więc przed problemem, jak tworzyć tymczasowe obiekty tak, aby gdy nie będą potrzebne łatwo pozbyć się ich z pamięci. Odpowiedź jest prosta - allokować pamięć.
W językach C i C++ podejście do problemu allokacji pamięci jest całkowicie inne.
W języku C pamięć dostępna dla programu w czasie jego uruchomienia nazywa się HEAP'em, w C++ - FREE STORE- pamięć wolna. Różnica między nimi dwoma leży tylko w funkcjach używanych do dostepu do tej pamięci.
W języku C, do alokacji pamięci służy grupa funkcji malloc. Omówimy pokrótce te funkcje.
Piersza to malloc
void *malloc(size_t n);
funkcja zwraca wskaźnik do n bajtów niezainicjowanej pamięci, lub NULL jeśli żądanie nie może być spełnione.
Druga funkcja to calloc
void *calloc(size_t n, size_t size);
zwraca wskaźnik do obszaru mogacego pomieścic tablicę n elementową, każdy element ma rozmiar size. Zwraca NULL, gdy pamięć nie może być przydzielona. Pamięć inicjowana jest zerami.
Po wykorzystaniu pamięci można ją zwolnić. Do tego celu służy funkcja free
void free(p);
zwalnia pamięć wskazaną przez p, przy czym p musi być wynikiem wcześniejszego wywołania funkcji malloc lub calloc. Nie ma ograniczeń na kolejność zwalniania pamięci, natomiast poważnym błędem jest zwalnianie czegoś, co nie było poprzednio przydzielone w/w funkcjami.
Zadaniem naszym będzie dynamiczne alokowanie struktury o nazwie "data":
struct date *dateptr;
dateptr = (struct date *) malloc(sizeof(struct date));
W przykładzie tym, funkcja malloc alokuje blok pamięci wystarczający do przechowania struktury i zwraca wskaźnik. Funkcja zwraca void pointer i dlatego jak pamietamy musimy go rzutować na odpowiedni typ kiedy chemy przypisać go do zmiennej dateptr. Po tej operacji blok pamięci możemy traktować jako strukturę date.
W języku C możemy korzystać z jednego z 6 standardowych modeli pamięci:tiny, small, medium,compact, large, huge ktore różnia się min. ilościa pamięci przeznaczonej na dane.Dla modelu compact, large i hudge, gdzie pamięć na dane jest ponad 64 kB, funkcja malloc zamienia na jest na funkcję farmalloc, farfree operujące na pamięci o długości ponad 1 segment.
Alternatywą do tych funkcji w języku C++ jest operator new i delete. Operator new tworzy obiekt, a operator delete usuwa obiekt z pamięci. Jeśli zdefiniujemy wskaźnik:
char *wsk;
to instrukcja
wsk = new char;
powoduje utworzenie nowego obiektu typu char. Nie ma on nazwy, ale możemy sie do niego odwoływać poprzez wskaźnik zawierający adres tego obiektu.
Natomiast:
delete wsk;
powoduje usunięcie obiektu wskazanego przez wsk z pamięci.
Jeśli chcemy utworzyć tablice w pamięci to postępujemy następująco:
int *wsk_tab;
wsk_tab = new int[10];
operator new utworzył 10-elementowa tablicę int. Kasowanie tablicy zarezerwowanej dynamicznie:
delete [] wsk_tab;
Zwróćmy uwagę na nawiasy kwadratowe.
Cechy obiektów utworzonych operatorem new
1. obiekty żyją od momentu utworzenia operatorem new aż do momentu usunięcia operatorem delete
2. obiekty nie mają nazwy. Operujemy na nich tylko przy pomocy wskaźników.
3. obiekty utworzone operatorem new nie są automatycznie inicjowane (są w nich śmieci)
Przykład 3;
#include
main()
{
int dl_tab,i;
cout<<"podaj rozmiar tablicy: "
cin>>dl_tab;
int *wsk_tab = new int[dl_tab];
for(i = 0;i
*wsk_tab++ = i;
//.......instrukcje wykorzystujące tablice
delete [] wsk_tab;
return 0;
}
Za pomoca operatora delete kasuje się tylko obiekty utworzone przy pomocy operatora new, przy czym nie należy kasować wczesniej skasowanego obiektu. Można kasować natomiast wskaźnik ustawiony na NULL:
wsk = NULL;
delete wsk;
W trakcie alokowania pamięci może zdarzyć się tak, że operator new zwróci NULL. Oznacza to, że wyczerpaliśmy pamięć dostępną na dane. W związku z tym w programach tworzących dużą liczbę dużych obiektów należy kontrolować poprawność operacji alokacji. Można tego dokonać albo poprzxez fragment programu:
int *wsk;
wsk = new int[10000];
if(!wsk)
error("pamięć się wyczerpała");
lub przy wykorzystaniu funkcji set_new-handler:
Przykład 4.
#include
#include
#include
void alarm();
long k;
main()
{
set_new_handler(alarm);
for(k = 0; ; k++)
new int;
}
void alarm()
{
cout<<"Brak pamięci przy k = "<
exit(1);
}
W funkcji main wykonuje się nieskończona pętla tworząca dynamicznie obiekty. Jeśli w którymś momencie zabraknie pamięci, sterowanie przejmuje automatycznie funkcja set_new_handler uruchamiająca funkcję alarmową napisaną przez użytkownika. Argumentem tej funkcji jest wskaźnik do funkcji alarm (dalej dowiemy sie, że nazwa funkcji jest jej adresem w pamięci).
Porównanie strych i nowych metod
- uniezalezniamy się od modelu pamięci
- w momencie kreacji obiektu własnego typu (klasy) uruchamiana jest specjalna funkcja nazwana konstruktorem ( nie ma tego przy wywolaniu funkcji malloc),
- przy kasowaniu delete uruchamiany jest automatycznie destruktor
O szcegółach mówić będziemy przy okazji omawiania w przyszłym semestrze dynamicznej alokacji obiektów zdefiniowanej przez programistę klasy.
1.h Stale wskaźniki i wskaźniki do stałych
Zadeklarujmy wskaźnik:
char *const text = "to jest proba tekstu";
Jest to tzw. stały wskaźnik. Musi być od razu ustawiony w momencie deklarowania. Będzie on wskazywał zawsze na to samo miejsce w pamięci, ale zmienna przez niego wskazywana może być zmieniana:
*text = 'a'; // OK
char str[10];
text = str; // nie poprawne wskźnik nie może być zmieniany, nie można się
//nim poruszać.
Deklaracja :
const int *wsk_int
jest to deklaracja wskaźnika do stałego obiektu. Nie musi być od razu ustawiany. Wskaźnik uznaje pokazany obiekt za stały, nie może więc go modyfukować:
int x[4] = {0,1,2,3};
int tmp, *w;
const int *wsk_do_st;
w = x;
wsk_do_st = x; //ustawienie wskaźnika na początek tablicy
tmp = *wsk_do_st; //odczytujemy 0 element tablicy
wsk_do_st ++; //poruszamy wskaźnik
*wsk_do_st = 0; //błąd -za pomoca tego wskaźnika obiektu modyfikować //nie wolno
Oba typy wskaźników można połączyć. Otrzymujemy wtedy stały wskaźnik do stałego obiektu:
const int *const wsk;
int m = 6,n = 4,tmp;
const int *const wsk = &m; //musimy go od razu ustawić bo jest to stały wskaźnik
tmp = *wsk;
*wsk = 15; //błąd nie można zapisać - wskaźnik traktuje swój obiekt jako //stały
w = &n; //błąd, nieruchomy wskaźnik, nie można nim pokazywać na inny //obiekt
Pozostałe informacje na temat wskaźników podamy przy okazji omawiania funkcji oraz innych typów pochodnych.