Programowanie komputerów
mgr Teresa Podhorska
2.3. Wskaźniki 2.3.1. Wskaźniki a tablice
2.3.2. Wskaźnik void
2.3.3. Arytmetyka wskaźników
2.3.4. Podsumowanie inicjowania wskaźników
2.3.5. Wskaźniki a stringi
2.3.6. Stale wskaźniki i wskaźniki do stałych
2.3. Wskaźniki
Każda zmienna ma unikalny adres wskazujący początkowy obszar pamięci zajmowany przez tą zmienną. Adres można przechowywać, zmienna która przechowuje adres obiektu nazywa się wskaźnikiem.
typ_zmiennej *nazwa_zmiennej
int *wsk; - wsk jest wskaźnikiem do pokazywania na obiekty typu int
wsk
&wsk
model tiny, small, medium - wskaźnik zajmuje 2 bajty pamięci
model compact, large, huge - wskaźnik zajmuje 4 bajty pamięci
Przykłady:
char *wsk; - wsk jest wskaźnikiem do pokazywania na obiekty typu char
float *wsk;- wsk jest wskaźnikiem do pokazywania na obiekty typu float
void *wsk; - wsk jest wskaźnikiem do pokazywania na obiektu nieznanego typu
float *wsk_tab[10]; - tablica 10 wskaźników do liczb rzeczywistych
float (*wsk_tab)[10]; - wskaźnik do tablicy 10 liczb rzeczywistych
int (*wsk) (int, int); - wskaźnik do funkcji o argumentach typu (int, int) przekazującej wynik typu całowitego
int *wsk (int, int); - funkcja o argumentach typu (int, int) przekazująca wskaźnik do typu całkowitego
char ** wsk; - wskaźnik do wskaźnika do znaku (adres do adresu)
int *wsk; float a; wsk = &a; //nieprawidłowo. Chcemy wskaźnikiem do int pokazywać na float |
int *wsk; int i; wsk = &i; //prawidłowo. |
int *wsk; int i;
wsk
wsk = &i;
wsk
printf (″ %p″, wsk); printf (″ %p″, &i);
FFF4 FFF4
Podstawową operacją na wskaźniku jest wyłuskanie, czyli odwołanie się do obiektu wskazywanego przez wskaźnik. Operacja ta nazywa się adresowaniem pośrednim. Operatorem adresowania pośredniego jest jednoargumentowa * zapisywana jako przedrostek.
Zastosowana do wskaźnika daje zawartość obiektu wskazanego przez ten wskaźnik np.:
int *wsk ;
int j, i=3;
wsk = &i;
wsk przechowuje adres zmiennej i.
cout<<*adres zmiennej i wynosi: *<< wsk << * wartość zmiennej i wynosi: *<<*wsk;
Ekran: adres zmiennej i wynosi : 0xFFDA wartość zmiennej i wynosi: 3
int i, j;
int *wsk;
i=3;
wsk=&i;
j =*wsk;
= *wsk - pobierz wartość spod adresu
(1) j =*wsk; - pobranie wartości spod adresu i przypisanie tej wartośći zmiennej j
po wykonaniu intsrukcji (1) j=3;
*wsk = 200; wstaw wartość 200 pod adres wskazywany przez zmienną wsk
po wykonaniu intsrukcji (2) i = 200;
Zmieni się wartość zmiennej i z wartości 3 na 200. i=200 j=3
int *wsk=&i; int *ptr=&i;
int **p; int **p=&ptr;
*p=wsk;
Jeśli wsk wskazuje na zmienną całkowitą, to *wsk może wystąpić wszędzie tam gdzie może wystąpić zmienna, więc np:
int *wsk;
int zm = 10, x;
wsk = &zm;
x = *wsk + 1; // x= zm + 1 = 12 + 1;
*wsk = *wsk + 2; // zm = zm + 2 = 12 + 2;
Ponieważ operatory * oraz & są silniejsze niż operatory arytmetyczne, to w wyrażeniach:
*wsk = *wsk + 2;
x = *wsk + 1;
nie trzeba używać nawiasu: najpierw pobierana jest zawartość spod adresu wsk i zwiększona o 1 zostaje zapisana do zmiennej x.
int i=2, x; int *wsk=&i;
x= ++(*wsk); x = ; //wartość spod adresu wsk zostaje zwiększona o 1
x= ++*wsk; x = ; //wartość spod adresu wsk zostaje zwiększona o 1
x= (*wsk)++; x = ; // wartość spod adresu wsk zostaje zwiększona o 1
x= *wsk++; x = ; //pobierz wartość spod adresu i zwiększ adres o 1
wsk wsk++
FFE2 |
FFE4 |
|
|
wsk++ przesunięcie adresu o sizeof (int), ponieważ wsk jest adresem zmiennej typu całkowitego.
*wsk++ pobranie obiektu przed zwiększeniem wskaźnika i zwiększenie wskaźnika o jeden
*++wsk zwiększenie wskaźnika o jeden i pobranie obiektu po zwiększeniu wskaźnika.
wsk = wsk1;
Zapis ten mówi, że teraz wsk będzie wskazywał na to samo co wsk2.
2.3.1. Wskaźniki a tablice
Nazwa tablicy jest adresem początku obszaru zarezewowanego dla tablicy, jest to równocześnie wskaźnik do jej zerowego elementu.
Zadeklarujemy tablicę:
int tab[4]={-1,-20, 3, 5};
tab(&tab[0]) &tab[1] &tab[2] &tab[3]
tab[0] -> *(tab)
tab[1] -> *(tab+1)
tab[2] -> *(tab+2)
tab[3] -> *(tab+3)
adresy: 2000 2002 2004 2006
wartości: |
-1 |
-20 |
3 |
5 |
tab
&tab[0]
tab + 1
&tab[1]
tab + 2
&tab[2]
tab + 3
&tab[3]
cout<<*tab << * to wyświetlenie wartości tab[0]\n*;
cout<<*(tab+1) << * to wyświetlwnie wartości tab[1]\n*;
cout<<*( tab +2) << * to wyświetlwnie wartości tab[2]\n*;
int *wsk;
instrukcja: wsk = &tab[n];
ustawia wskaźnik na n-tym elemencie tablicy.
instrukcja: wsk = &tab[0]; jest równoważna instrukcji: wsk = tab;
oznacza ustawienie wskaźnika na pierwszy element tablicy czyli na jej początek
cout<<wsk[0]<< *\n*<<wsk[1] <<*\n*<<wsk[2];
Możemy również zmieniać adres dodając do wskaźnika liczbę całkowitą.
Przejście do następnego elementu tablicy:
wsk= wsk + 1; lub wsk ++; //gubimy adres początkowy
Aby przesunąć się o n elementów w tablicy piszemy
instrukcja: wsk = wsk+n;
Z definicji wskaźnika wynika że wsk jest wskaźnikiem do int. Stąd kompilator wnioskuje, że aby odnaleźć następny element typu int należy przesunąć się o sizeof (int).
int *wsk, i;
int tab[10]={1, 3, 5, 6, 7, 8, 9};
wsk = tab; //inicjowanie wskaźnika
cout<<*wsk << * to wyświetlenie wartości tab[0]*;
cout<<*(wsk+1)<<* to wyświetlenie wartości tab[1]*;
cout<<wsk[0] << * to wyświetlenie wartości tab[0]*;
cout<<wsk[1]<< * to wyświetlenie wartości tab[1]*;
for(i = 0; i<10; i++) printf("%d\n", *wsk++);
for(i = 0; i<10; i++) printf("%d\n", wsk[i]);
Uwaga!!!
Mimo, że możemy zapisać:
wsk = tab;
to o ile możemy zapisać:
wsk++;
to nie możemy napisać:
tab++;
Dla tak zadeklarowanego wskaźnika:
int *wsk;
adresem tego wskaźnika jest wartość wyrażenia:
&wsk;
2.3.2. Wskaźnik void
Deklaracja wskaźnika niesie w sobie dwie informacje: adres miejsca w pamięci, oraz typ obiektu na który te adres wskazuje.
Przy deklaracji: void *wsk;
Wskaźnik wsk wskazuje jedynie na konkretne miejsce w pamięci nie informując o typie obiektów tam przechowywanych.
Wskaźnik void służy on do przekazywania wskaźników do funkcji które nie mogą czynić założeń co do typu przekazywanego obiektu i przy zwracania przez funkcje obiektów nieznanego typu.
void *ptr;
char *wsk;
int *wsk1;
ptr = wsk; lub ptr = wsk1;
Wskaźnik każdego typu można przypisać wskaźnikowi typu void, bez konieczności konwersji.
Odwrotne przypisanie wymaga stosowania operatora konwersji.
float *wsk;
void *ptr;
wsk = ptr; //błąd, bez konwersji
wsk = (float *) ptr; //poprawnie z operatorem rzutowania
void *malloc (size_t size);
wsk = (float *) malloc (100 * sizeof (float)); - operator rzutowania
2.3.3. Arytmetyka wskaźników
Dopuszczalne operacje na wskaźniku:
dodawanie
odejmowanie
porównywanie
Możemy dodawać i odejmować liczby całkowite od wskaźników tak, aby w potrzebny sposób przesuwać je po tablicy. Operacje te nie są sprawdzane przez kompilator.
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 być ze znakiem.
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;
if (wsk1 = = wsk2) cout<<"Oba wskaźniki pokazują na ten sam obiekt";
W operacjach dodawania i odejmowania na wskaźniku, bierze udział tylko część offsetowa adresu.
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 (niektóre funkcje biblioteczne zwracają wskaźnik NULL (null pointer), możemy go użyć do kontroli np:
if (!wsk) // if(wsk = =NULL) if(wsk= =0)
2.3.4. Inicjowanie wskaźników
W tym punkcie zbierzemy wszystkie sposoby inicjowania wskaźników:
1. można przypisać adres konkretnego obiektu:
int *wsk, obiekt, tab[12];
wsk = &obiekt;
wsk = tab; // wsk = &tab[0]; wsk = &tab[2];
2. ustawić wskaźnik na adres funkcji.
int fill (char) {return 1;}
int (*wsk) (char);
wsk = fill;
wywołanie funkcji wsk ('*'); lub (*wsk) ('*');
char znak='*'; wsk (znak); lub (*wsk) (znak);
3. zarezerwować obszar dynamicznie
int *wsk;
wsk = new int[k];
wsk = (int*) malloc (k * sizeof (int));
4. można przypisać inny wskaźnik:
int *wsk, *ptr;
wsk = new int; ptr=wsk;
5. ustawić wskaźnik na konkretny adres którego np. zawartość chcemy sprawdzić np: wsk = FFDA;
Dynamiczna alokacja pamięci .
Funkcje przydziału i zwalaniania pamięci w obszarze bliskiego stosu.
malloc(), calloc(), realloc(), free ()
Alokacja pamięci.
void *malloc (size_t n); typedef unsigned size_t
Funkcja zwraca wskaźnik do n bajtów niezainicjowanej pamięci, lub NULL jeśli żądanie nie może być spełnione.
void *calloc (size_t n, size_t size); Pamięć inicjowana jest zerami.
char *wsk = (char*) malloc (100 * sizeof (char));
char *wsk = (char*) calloc (100, sizeof (char));
if (wsk= =NULL) cout<<″brak pamięci″;
void *realloc (void *block, size_t size);
wsk = (char*) realloc (wsk, 300 * sizeof (char));
Funkcja może zwiększać lub zmniejszać rozmiar pamięci przydzielonej poprzednio za pomocą funkcji malloc lub calloc zachowując w miarę możliwości zawartość.
Zwalnianie pamięci.
Po wykorzystaniu pamięci można ją zwolnić. Do tego celu służy funkcja free().
void free ( void *p);
Zwalnia pamięć wskazaną przez p, przy czym p musi być wynikiem wcześniejszego wywołania funkcji malloc() lub calloc(). Błędem jest zwalnianie obszary, który nie był poprzednio przydzielony w/w funkcjami.
free (ptr); free (wsk);
W języku C możemy korzystać z jednego z 6 standardowych modeli pamięci:
tiny, small, medium, compact, large, huge
które różnią się min. ilością pamięci przeznaczonej na dane.
Funkcje przydziału i zwalaniania pamięci w obszarze dalekiego stosu.
farmalloc(), farcalloc, farrealoc() - funkcje pozwalają na przydział bloków pamięci powyżej 64kB. Są bezużyteczne w model tiny.
farfree() - zwalnianie pamięci.
W modelach compact, large, huge - istnieje obszar dalekiego stosu zmiennych dynamicznych
W modelach small, medium istnieją dwa stosy zmiennych dynamicznych bliski i daleki.
Operatory new i delete.
Operator new tworzy obiekt, a operator delete usuwa obiekt z pamięci.
char *wsk;
Alokacja pamięci operatorem new.
wsk = new char;
powoduje utworzenie nowego obiektu typu char. Nie ma on nazwy, ale możemy się do niego odwoływać poprzez wskaźnik zawierający adres tego obiektu.
int *wsk_tab;
wsk_tab = new int[k];
operator new utworzył k-elementowa tablicę typu int.
for (i=0; i<k; i++) wsk_tab[i] = 1; lub
for (i=0; i<k; i++) *wsk_tab++ = 1; lub
for (i=0; i<k; i++) *(wsk_tab+i) = 1;
int (*wsk_tab)[k];
wsk_tab = new int[w][k];
for (i=0; i<w; i++)
for (j=0; j<k; j++) wsk_tab[i][j]=1;
lub
for (i=0; i<w; i++)
for (j=0; j<k; j++) *(wsk_tab[i] + j) =1;
lub
for (i=0; i<w; i++)
for (j=0; j<k; j++) *(*(wsk_tab+i) +j )=1;
int (*wsk)[k];
wsk = new int[w][k];
Przykład:
wsk[w][k] - tablice wielowymiarowe są traktowane jak wskaźniki do wskaźników
*(wsk+w) - początek w-tego wiersza
*(wsk+w) + k - wskazanie kolumny w tablicy
W tablicach jednowymiarowych dostęp za pomocą wskaźnika jest czytelny natomiast w tablicach wielowymiarowych raczej nie.
*(*(wsk+0)+0) |
*(*(wsk+0)+1) |
*(*(wsk+0) +2) |
wsk[0][0] |
wsk[0][1] |
wsk[0][2] |
*(*(wsk+1) +0) |
*(*(wsk+1) +1) |
*(*(wsk+1) +2) |
wsk[1][0] |
wsk[1][1] |
wsk[1][2] |
Zwalnianie pamięci.
delete wsk; delete [] wsk_tab;
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
Nie należy kasować wcześniej 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. Należy kontrolować poprawność operacji alokacji.
int *wsk;
wsk = new int[10000];
if(!wsk) cout<<"pamięć się wyczerpała";
lub przy wykorzystaniu funkcji set_new-handler():
Przykład
#include<iostram.h>
#include<new.h> //set_new_handler()
void alarm(){
cout<<"Brak pamięci przy k = "<<k;
}
long k;
void main(){
set_new_handler(alarm);
for(k = 0; ; k++)
new int;
}
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.
Dostęp 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 się do niej poprzez wskaźnik:
wsk = 0xF11D;
podajemy po prostu do wskaźnika adres komórki i odtąd możemy posługiwać się tym wskaźnikiem
cout<<"wartość przechowywana pod adresem"<<wsk <<*wsk;
2.3.5. Wskaźniki a stringi
String to tablica znaków zakończona znakiem NULL.
Jeżeli wskaźnik ma pokazywać na ciąg znaków, to można go zadeklarować jako:
char *text;
można go też inicjalizować:
(1) char *text = "to jest próbny tekst";
Porównajmy zapis:
(2) char text_tab [] = "To jest próbny tekst";
text_tab++ - błąd
text++ - poprawnie
Przykład:
char text[]=* BORLAND C++*;
char *cel, *zrodlo;
zrodlo = text;
il = strlen (zrodlo);
cel = new [il] +1;
(1) while ((*zrodlo) != NULL) {
*cel = *zrodlo;
cel++; zrodlo++;
}
*cel=NULL; // wskaźnik cel wskazuje na koniec stringu.
(2) while(*cel++ = *zrodlo++); kopiujemy razem ze znakiem NULL
Funkcje bilblioteczne:
strcpy (cel, zrodlo); - kopiowanie łańcucha zrodlo do celu, kończy operację po osiągnięciu znaku NULL.
memcpy (cel, zrodlo, strlen(zrodlo)+1); kopiowanien bajtów z obszaru wskazywango przez zrodlo do obszaru wskazywanego przez cel.
Inicjowanie tablicy wskaźników do stringów. Jej elementami są wskaźniki mogące pokazywać na stringi:
char *text[5]; // 5 wskaźników.
Wskaźniki te można inicjalizować:
char *text[5] = {"poniedzialek", "wtorek", "sroda", "czwartek", *piątek*};
char *text[] = {"poniedzialek", "wtorek", "sroda", "czwartek", *piątek*};
Elementami tej tablicy są adresy tych miejsc pamięci, gdzie umieszczone są te stringi,
(1) printf (*\ntext: %s*,text[0]); // poniedzialek
(2) printf (*\ntext: %s*,text[1]); // wtorek
dostęp do poszczególnych znaków:
(3) printf (*\nznak: %c*,text[0][0]); //p
(4) printf (*\nznak: %c*,text[0][1]); //o
char tab[2][15]={″Ala″, ″Irek″};
char (*wsk)[15];
wsk=tab; printf (″%s″, wsk); //Ala
char *ptr;
ptr=tab[0]; printf (″%s″, ptr); //Ala
ptr=tab[1]; printf (″%s″, ptr); // Irek
ptr=tab; //błąd char [15]* char *
2.3.6. Stale wskaźniki i wskaźniki do stałych
Deklaracja:
int *const wsk = tab;
Jest to deklaracja stałego wskaźnika. Musi być inicjowany podczas deklarowania. Wskaźnik jest deklarowany jako stały więc nie można go modyfikować, ale zmienna przez niego wskazywana może być zmieniana:
int tab[3];
*wsk = 3; // OK
wsk++; // wskaźnik nie może być zmieniany, wskaźnik const
Deklaracja :
const int *wsk;
Jest to deklaracja wskaźnika do stałego obiektu. Nie musi być od razu inicjalizowany.
Wskaźnik uznaje wskazywany obiekt za stały, więc używając tego wskaźnika obiektu nie można modyfikować, natomiast wskaźnik może być zmieniany.
int tab[3];
wsk = tab; // ustawienie wskaźnika na początek tablicy
*wsk=9; // błąd, dla tego wskaźnika obiekt const
wsk ++; // wskaźnik może być zmieniany
Deklaracja :
int tab[3];
const int *const wsk=tab;
Sały wskaźnik do stałego obiektu. Musi być inicjowany podczas deklarowania. Wskaźnik jest deklarowany jako stały więc nie można go modyfikować, uznaje wskazywany obiekt za stały, więc używając tego wskaźnika obiektu też nie można modyfikować.
*wsk=9; // błąd,za pomoca tego wskaźnika obiektu modyfikować nie wolno, obiekt const
wsk ++; //błąd, wskaźnik nie może być zmieniany, wskaźnik const
Programowanie komputerów C/C++
4
wykład 5
Wykład 5
semestr III - studia dzienne magistereskie
Wykład 5
semestr III - studia dzienne zawodowe
Zajmuje 2 lub 4 bajty pamięci w zależności od modelu pamięci
Przechowuje adres do zmiennej typu int
&i - adres zmiennej i
FFF4
Miejsce na adres do zmiennej typu int
FFF4
adres początku obszaru zarezerwowanego dla tablicy