Ćwiczenie 4
Wskaźniki, tablice
4.1. Wskaźnik na zmienną
Deklaracja int *pa; oznacza, że pa jest wskaźnikiem na obiekt typu int. Zmienna pa zawiera adres pamięci, zarezerwowanej na zmienną typu int. Chcąc dostać się do zmiennej wskazywanej przez pa używamy operatora wyłuskania (dereferencji) *.
Tak więc wyrażenie *pa jest zmienną, która jest wskazywana przez wskaźnik pa.
Deklaracja int p, y; oznacza, że p jest zmienną typu int. Stosując operator adresu & można dowiedzieć się, jaki jest adres zmiennej p. Adres ten można przypisać zmiennej pa (która jest wskaźnikiem na obiekt typu int): pa = &p;
Teraz *pa i p są tym samym obiektem. Tak więc instrukcje
y = p + 1; i y = *pa + 1; są równoważne.
Wartości wskaźników można wyprowadzić w postaci szesnastkowych liczb o wartościach segmentu i offsetu przy użyciu kodu konwersji %p np. printf("%p", pa);
Na wskaźnikach tego samego typu - innego niż void - można wykonywać następujące operacje:
porównanie dwóch wskaźników,
podstawienie jednego wskaźnika pod drugi,
odjęcie od siebie dwóch wskaźników,
dodanie lub odjęcie liczby całkowitej od wskaźnika. lub podstawienie dwóch wskaźników.
Program 4.1. //zmienna a wskaźnik
#include <stdio.h>
#include <conio.h>
int a, b; //zmienne globalne
int *pa, *pb; //wskaźniki globalne
void main(void)
{
clrscr();
printf("\n podaj wartosc liczby a\n");
scanf("%d",&a);
pa=&a; //podstawienie adresu pod wskaźnik
printf("\n a= %d",a); //wypisanie wartości zmiennej a
printf("jest pod adresem pa=%p", pa); //wypisanie adresu zmiennej a
*pa=*pa+2; //zwiększenie zmiennej a o 2
printf("\n a teraz a= %d",a);
printf("\n podaj wartosc liczby b\n");
scanf("%d",&b);
pb=&b; //podstawienie adresu pod wskaźnik
printf("\n b= %d",b);
printf(" jest pod adresem pb= %p",pb);
printf("\n pb-pa= %d",pb-pa); //odjęcie dwóch wskaźników od siebie
getch();
}
4.2. Wskaźnik na tekst
Stała łańcuchowa jest ciągiem znaków zawartych pomiędzy znakami cudzysłowu. Na jej końcu kompilator dodaje automatycznie znak '\0' oznaczający koniec tekstu. Stałe łańcuchowe mogą być definiowane za pomocą dyrektywy #define. Stałe łańcuchowe należą do klasy statycznej, tzn. są przechowywane przez cały czas działania programu i tylko w jednej kopii. Całe zdanie w cudzysłowie jest równocześnie stałym wskaźnikiem do miejsca, w którym jest zapisane. Można więc użyć zapisu wskaźnikowego char *tekst =" O co chodzi?" lub zadeklarować tablicę znakową zainicjowaną char tab[] = "O co chodzi?"
Do przeprowadzania operacji na tekstach można wykorzystywać następujące funkcje zawarte w pliku string.h:
strcpy(char *s1,char *s2) - kopiowanie tekstu z s2 do s1;
strcat(char *s1, char *s2) - dołączenie do tekstu z s1 tekstu z s2;
strcmp(char *s1,char *s2) - porównanie tekstów s1 i s2;
strchr(char *s1,char c)- odszukanie znaku c w tekście s1;
strstr(char *s1,char *s2)- odszukanie tekstu s2 w tekście s1;
strlen(char *s) - obliczenie długości tekstu s;
Program 4.2
#include <stdio.h>
#include <conio.h>
#include<string.h>
void main(void)
{ clrscr();
char pan[]="KAROL"; //deklaracja i inicjacja tablicy 6-znakowej
char *wsk="INKA"; // wskaźnik na tekst
puts(pan); //wypisanie tekstu z tablicy pan
puts(wsk); //wypisanie tekstu wskazywanego przez wsk
printf("\nrozmiar tablicy pan: %d\n", sizeof(pan));
printf("\ndlugosc tekstu wskazywanego przez wsk: %d\n", strlen(wsk));
printf("%c\n", pan[0]); //pierwszy znak w tekscie
printf("%s\n", &pan[0]); //tekst pod adresem &pan[0]
printf("adres poczatku tablicy pan %p\n", pan);
printf("%s\n", pan+3); //tekst pod adresem pan+3
printf("%c\n", wsk[2]); //trzeci znak w tekście
printf("%c\n", *(wsk+2)); //trzeci znak w tekście
printf("%s\n", wsk+2); //tekst pod adresem wsk+2
strcat(wsk, "OLA"); //dołączenie tekstu
printf("%s\n",wsk);
strcpy(wsk, "OLA"); //przekopiowanie tekstu
printf("%s\n",wsk);
strcpy(wsk, pan); //przekopiowanie tekstu
printf("%s\n",wsk);
strcpy(pan, "OLA"); //przekopiowanie tekstu
printf("%s\n",pan);
wsk=pan; //podstawienie adresu
puts (wsk);
puts(++wsk);
getch();
}
4.3. Tablice jednowymiarowe
Tablice jednowymiarowe deklaruje się podając typ elementów, nazwę tablicy i liczbę elementów ujętą w nawiasy kwadratowe: np. char t[10]; double tab[2]; Elementy tablicy numeruje się zaczynając od zera.
Tablica może być zainicjowana podczas deklaracji:
char t[10] = {'a','b','c','d','e','\o'};.
Para nawiasów [] jest dwuargumentowym, lewostronnie łącznym operatorem indeksacji, którego wynikiem jest element tablicy.
Program 4.3 //deklaracje i inicjacje tablic
#include <stdio.h>
#include <conio.h>
#define r 10
void main()
{ char c1[r] = {'b','i','a','l','y','\0'}; // lub {"zielony"};
int i1[r] = {1,2,3,4,5,4,5,4,6}; //tablica będzie uzupełniona zerami
float f1[r] = {1.25,0,0,23,-3.33,333,34.23,1};
clrscr();
for(int i=0;i<10;i++)
printf("\nc1[%d] = %c i1[%d] = %d f1[%d] = %8.2f",i,c1[i],i,i1[i],i,f1[i]);
getch();
}
4.4. Wskaźnik a tablica
W C istnieje ścisła zależność między wskaźnikami a tablicami. Każda operacja, która może być przeprowadzona przez indeksowanie tablicy, może być również dokonana za pomocą wskaźnika.
Deklaracja int A[10]; definiuje tablicę dziesięciu liczb typu int, a więc blok 10 kolejnych obiektów nazywających się A[0], A[1], ..., A[9]. Zapis A[i] oznacza element tablicy oddalony o i pozycji od jej początku.
Jeżeli pa będzie wskaźnikiem do obiektów całkowitych zadeklarowanym jako int *pa; wówczas przypisania takie, jak
pa = &A[0]; lub pa = A;
ustawiają pa tak, aby wskazywał na zerowy element tablicy A; to znaczy pa będzie zawierać adres elementu A[0].
Instrukcje *pa = 10; i A[0] = 10; są równoważne.
Jeśli pa wskazuje na pewien element tablicy A, to z definicji pa+1 wskazuje na element następny. Ogólnie pa-i wskazuje na i-ty element przed pa, a pa+i na i-ty element po pa. Zatem, jeżeli pa wskazuje na A[0], to *(pa+1) odnosi się do zawartości A[1]; pa+i jest adresem A[i], a *(pa+i) jest wartością A[i].
Istnieje jednak istotna różnica między nazwą tablicy a wskaźnikiem. Wskaźnik jest zmienną, wiec operacje pa=A i pa++ mają sens. Natomiast nazwa tablicy jest stałą wskaźnikową, a nie zmienną czyli konstrukcje takie, jak A = pa, A++ lub pa=&A są niedopuszczalne!!!
Jeśli uzupełnimy program 4.1 instrukcjami:
int *pa;
pa=i1;
puts("wyświetlenie zawartości tablicy i1 ");
for (i=0;i<10;i++) printf(" %d",*(pa+i));
to wyświetlimy zawartość tablicy i1 używając notacji wskaźnikowej.
4.5. Tablice wielowymiarowe
Tablice wielowymiarowe są reprezentowane jako tablice tablic. Np. deklaracja
char A[2][5];
wprowadza tablicę A z dwoma elementami, z których każdy jest tablicą typu char [5]. Tablica taka może być zainicjowana podczas deklaracji:
char A[2][5] = { {'a','b','c' }, {'0', '1', '2', '3', '4'}};
Nazwa tablicy A jest stałą wskazującą na pierwszy wiersz tablicy, czyli jej pierwszy element A[0].
A+i wskazuje na i-ty wiersz tablicy, czyli A[i]. Natomiast A[i]+j wskazuje na j-ty element i-tego wiersza, czyli A[i][j].
Program 4.5.
#include <stdio.h>
#include <conio.h>
#define M 10
void main ()
{
int A[M][M], i, j, w,k;
clrscr();
printf("Podaj ilosc wierszy ");
scanf("%d",&w);
printf("Podaj ilosc kolumn ");
scanf("%d",&k);
for(i=0;i<w;i++)
{
for (j=0;j<k;j++)
{
printf("A[%d",i);
printf(",%d",j);
printf("]=");
scanf("%d",&A[i][j]);
}
}
clrscr();
for(i=0;i<w;i++)
{
for (j=0;j<k;j++)
{
gotoxy(5+4*j,10+i);
printf("%d",A[i][j]);
}
}
getch();
}
Sprawdzić, że poniższe zapisy są równoważne:
A[0][0] i *A[0] i **A
A[1][2] i *(A[1]+2) i *(*(A+1)+2)
&A[1][2] i A[1]+2 i *(A+1)+2
4.6. Alokacja (przydział) pamięci na tablice dynamiczne
Wszystkie dane w programie należą do jednej z trzech klas: statycznych, automatycznych lub dynamicznych. Dane statyczne są definiowane w czasie kompilacji. Pamięć dla danych automatycznych jest przydzielana, niewidocznie dla użytkownika, w chwili ich utworzenia. Tylko dane dynamiczne mają przydzielaną pamięć (na stercie) na żądanie programisty. Stosuje się je głównie wtedy, gdy ich rozmiar nie jest znany w chwili pisania programu.
Przydziału pamięci na tablicę n liczb całkowitych dokonuje się za pomocą funkcji
void * malloc(n*sizeof(int)), lub void *calloc(n, sizeof(int)), zdefiniowanych w pliku nagłówkowym alloc.h, które zwracają wskaźnik do początku przydzielonego obszaru lub wskazanie puste NULL, gdy alokacja się nie powiodła. Ponadto funkcja calloc zeruje przydzieloną pamięć. Do zwalniania wcześniej przydzielonej pamięci służy funkcja free(wsk). Zwalnianie pamięci nie przydzielonej jest błędem.
Program 4.6.1 /*dynamiczne przydzielanie pamięci na tablicę jednowymiarową */
#include <stdio.h>
#include <conio.h>
#include <stdlib.h>
#include <time.h>
#include <alloc.h>
main()
{
int n,i, *wsk;
randomize();
clrscr();
printf("liczba elementow tablicy n=");
scanf("%d",&n);
wsk=(int*)calloc(n,sizeof(int));
if (wsk == NULL)
{
printf("brak miejsca w pamieci\n");
exit (1);
}
else
for (i=0;i<n;i++)
{
*(wsk+i)=random(100);
printf("%5d",*(wsk+i));
}
getch();
free(wsk);
return 0;
}
Program 4.6.2 /*dynamiczne przydzielanie pamięci na tablicę dwuwymiarową */
# include <stdio.h>
# include <conio.h>
# include <alloc.h>
void tworz_tab(int *a[10],int *w,int *k);
void wydruk_tab(int *a[10], int *w, int *k);
void main()
{
int w,k, nr;
int *tab[10]; //deklaracja tablicy zawierającej wskaźniki do początków wierszy
clrscr();
printf("liczba wierszy=");scanf("%d",&w);
printf("liczba kolumn=");scanf("%d",&k);
for (int i=0;i<w;i++)
tab[i]=(int *)calloc(k,2); //alokacja wierszy
tworz_tab(tab,&w,&k);
clrscr();
wydruk_tab(tab,&w,&k);
for (i=0;i<w;i++) free(*(tab+i)); //zwolnienie pamięci
getch();
}
void tworz_tab(int *a[10], int *w, int *k)
{
int i,j;
for (i=0;i<*w;i++)
for (j=0;j<*k;j++)
{
printf("tab[%d,%d]=",i,j);
scanf("%d",*(a+i)+j); // lub *(*(a+i)+j)=random(100);
}
}
void wydruk_tab(int *a[10], int *w, int *k)
{
int i, j;
for (i=0;i<*w;i++)
{
for(j=0;j<*k;j++)
printf("%5d",*(*(a+i)+j)); // *(a+i) - adres pocz. i-tego wiersza
printf("\n"); // *(a+i)+j - adres j-tego elementu i-tego wiersza
} // *(*(a+i)+j) - wartość pod powyższym adresem
}
4.7. Wskaźnik a funkcja
Argumenty wywołania funkcji (parametry aktualne) są przekazywane przez wartość. Jeśli argument ma być wykorzystany do wyprowadzenia wyniku, to ten argument musi być typu wskaźnikowego np. funkcja void fun(int *a, int*b) {int t=*a; *a=*b; *b=t;} wykonuje zamianę wartości wskazywanych przez wskaźniki będące jej argumentami.
Nazwa funkcji jest stałą wskaźnikową do tej funkcji. Jeśli funkcja ma prototyp
void fun(int*t, int r), to wskaźnik do niej musi mieć ten sam typ i identyczną listę argumentów: void (*wsk_fun)(int *t1, int r1). Możliwe jest wtedy przypisanie wsk_fun=fun. Zapisując wywołanie funkcji określonej przez wskaźnik, nie trzeba poprzedzać wskaźnika operatorem wyłuskania.
Program 4.7.
#include <stdio.h>
#include <conio.h>
int minimum(int *t, int n) //tablicę przekazuje się jako wskaźnik do zerowego elementu tablicy
{ int min=t[0];
for (int i=0;i<n;i++)
if(min>t[i]) min=t[i];
return min;
}
const int r=6;
int (*wsk_min)(int*,int) = minimum; //deklaracja wskaźnika do funkcji i zainicjowanie go
int tab[r]={3,5,7,4,1,7};
void main()
{ clrscr();
printf("\n 1 wywolanie: %d",minimum(tab,r));
printf("\n 2 wywolanie: %d",wsk_min(tab,r));
printf("\n 3 wywolanie: %d",(*wsk_min)(tab,r));
getch();
}
Wskaźniki do funkcji można przekazywać jako argumenty innym funkcjom, zwracać jako wyniki funkcji, porównywać ze wskazaniem pustym NULL. Nie wolno wykonywać na nich operacji arytmetycznych.
Stosuje się także tablice wskaźników do funkcji np. int(*rodzaje_Testów[10])(); jest deklaracją tablicy zawierającej 10 elementów, z których każdy jest wskaźnikiem do funkcji.
Programy do samodzielnego napisania:
Wskaźnik t wskazuje na tekst. Napisać funkcję hist(char *t, int *litery, int *cyfry), która policzy histogramy (liczbę wystąpień) wszystkich małych liter samogłosek i cyfr w podanym tekście. Funkcja jako pierwszy parametr dostaje wskaźnik do tekstu, który ma być analizowany, a jako drugi i trzeci wskaźniki do tablic, w których należy umieścić liczebności wystąpień liter i cyfr.
Dana jest tablica A zawierająca n liczb całkowitych. Do tablicy B przepisać z tablicy A elementy o indeksach parzystych. Do tablicy C przepisać z A elementy parzyste.
Dany jest ciąg n liczb rzeczywistych. Obliczyć wartość średnią jego elementów i wstawić ją na początek tego ciągu.
Zdefiniować funkcję znajdującą pozycję i wartość maksymalnego elementu danego ciągu liczbowego. Napisać program sprawdzający jej działanie.
Uporządkować rosnąco ciąg n liczb całkowitych używając notacji wskaźnikowej. Wskazania do trzech największych elementów umieścić w tablicy wskaźników.
Dwa uporządkowane rosnąco ciągi liczb scalić w jeden ciąg uporządkowany rosnąco.
Utworzyć tablicę dwuwymiarową o wymiarze n × n wypełnioną kolejnymi liczbami naturalnymi w następujący sposób:
Następnie wyzerować jej elementy leżące poniżej głównej przekątnej.
Napisać funkcję sklej, której parametrami są dwa wskaźniki na tekst s i t. Funkcja ma dopisać do tekstu s tekst t, inaczej mówiąc ma je połączyć. Funkcja ma operować na wskaźnikach do tekstu, a nie na tablicach i indeksowaniu.
Do programu 4.6.2 dopisać:
a) funkcję usuwającą wiersz z tablicy
b) funkcję dodającą jako ostatni wiersz zawierający sumy elementów poszczególnych kolumn
Sprawdzić działanie tych funkcji.
10. Napisać (przy użyciu wskaźników) program wykonujący operacje dodawania, odejmowania i mnożenia dwóch macierzy dwuwymiarowych.
Laboratorium Podstawy Programowania
5