Wykład 3
Wskaźniki i tablice
Wskaźnik jest zmienną, która zawiera adres innej zmiennej. Stosowanie wskaźników zwykle prowadzi do bardziej zwartego i efektywnego kodu. Miejmy jednak na uwadze, że użyte nieostrożnie moga stać się przyczyną wielu trudnych do znaleznienia błędów. Jak pokażemy dalej ze wskaźnikami bardzo silnie związane są tablice.

Wskaźniki i adresy

Wskaźnik jest zmienną służącą do wskazywania zmiennych określonego typu. Ogólna postać deklaracji wskaźnika jest następująca

typ *nazwaWskaznika;

gdzie typ jest pewnym typem, na który będzie mógł pokazywać wskaźnik nazwaWskaznika, na przykład

int *wskI; // wskI jest wskaźnikiem do obiektów typu int
float *wskF; // wskF jest wskaźnikiem do obiektów typu float

Tak więc od wskaźnika wymaga się wskazywania obiektu określonego typu.

Aby wskaźnik mógł coś pokazywać, musimy jemu ,,powiedzieć'' co ma pokazywać. Inaczej mówiąc, musimy do wskaźnika przypisać adres jakiejś zmiennej (ale konkretnie ustalonego typu). Służy do tego jednoargumentowy operator &. Jeśli założymy, że intI jest wskaźnikiem do typu int oraz x zmienną typu int, to przypisanie adresu zmiennej x do wskaźnika wskI nastąpi po wykonaniu poniższej instrukcji

wskI=&x;

Operator & można stosować tylko do zmiennych i elementów tablic, nie można go stosować do wyrażeń, stałych i zmiennych rejestrowych.

Jednoargumetnowy operator * oznacza adresowanie pośrednie lub odwołanie pośrednie. Zastosowany do wskaźnika daje zawartość obiektu wskazywanego przez ten wskaźnik. Zatem aby odczytać wartość zmiennej x możemy teraz napisać *wskI


#include <stdio.h>

int main(void)
{
  int x=1,y=5;
  int *wsk;
  
  printf("x=%d y=%d\n",x,y);
  wsk=&x;
  y=*wsk;
  printf("x=%d y=%d\n",x,y);
  *wsk=7;
  printf("x=%d y=%d\n",x,y);
  
  return 0;
}
Listing 3.1

x=1 y=5
x=1 y=1
x=7 y=1
Output 3.1 Efekt działania programu z listingu 3.1

#include <stdio.h>

int main(void)
{
  int x=1,y=5;
  int *wsk;
  
  printf("%d %d %d\n",x,y,*wsk);
  wsk=&x;
  printf("%d %d %d\n",x,y,*wsk);
  (*wsk)++;
  printf("%d %d %d\n",x,y,*wsk);
  wsk=&y;
  printf("%d %d %d\n",x,y,*wsk);
  (*wsk)+=2;
  printf("%d %d %d\n",x,y,*wsk);
  
  return 0;
}
Listing 3.2

1 5 807409035
1 5 1
2 5 2
2 5 5
2 7 7
Output 3.2 Efekt działania programu z listingu 3.2

Wskaźniki i argumenty funkcji

Jak wiemy z poprzedniego wykładu, w języku C argumenty są przekazywane przez wartość. Nie ma więc bezpośredniego sposobu aby wywołana funkcja mogła zmienić wartość zmiennej należącej do funkcji wywołującej. Jedynym sposobem aby to osiągnąć jest stosowanie wskaźników. Poniżej przedstawiamy zmodyfikowaną wersję funkcji swap z poprzedniego wykładu. Jak można zobaczyć, deklarując parametry jako wskaźniki, za ich pomocą otrzymujemy pośredni dostęp do rzeczywistych argumentów.


#include <stdio.h>

void swap(int *x, int *y)
{
  int temp;
  
  temp=*x;
  *x=*y;
  *y=temp;
}

int main(void)
{
  int a=3, b=5;
  
  printf("a=%d, b=%d\n",a,b);
  swap(&a,&b);
  printf("a=%d, b=%d\n",a,b);
  
  return 0;
}
Listing 3.3

a=3, b=5
a=5, b=3
Output 3.3 Efekt działania programu z listingu 3.3

Wskaźniki, tablice i arytmetyka na adresach

W języku C występuje ścisła zależność pomiędzy wskaźnikami i tablicami. Każdą operację, którą można przeprowadzić przy pomocy indeksowania tablicy, można również wykonać za pomocą wskaźników.

Deklaracja

int tab[3];

definiuje tablicę tab o rozmiarze 3 a więc blok 3 kolejnych obiektów nazwanych tab[0], tab[1], tab[2]. Zapis tab[i] oznacza t-ty element tablicy tab. Tablica może zostać zainicjalizowana za pomocą listy inicjalizatorów zwierającej ztałe początkowe wartości każdego elementu tablicy

int tab[3]={1,3,7};

lub

int tab[]={1,3,7};

Jeśli teraz wsk będzie wskaźnikiem do obiektów typu int

int *wsk

to przypisanie

wsk=&tab[0]

ustawi wskaźnik wsk tak, aby wskazywał na pierwszy (zerowy) element tablicy tab (zawiera on więc adres elementu tab[0]. Teraz przypisanie

x=*wsk;

spowoduje skopiowanie zawartości tab[0] do x.

Jeśli wsk wskazuje na pewien element tablicy to wsk+1 wskazuje na element następny a wsk+i odnosi się do i-tego elementu po wsk. Jest tak zawsze bez względu na to ile fizycznie miejsca w pamięci zajmują obiekty jakiegoś typu (patrz rysunek poniżej)

W języku C nazwa tablicy jest stałym wskaźnikiem na jej pierwszy (zerowy) elment zatem przypisanie

wsk=&tab[0];

można zastąpić przypisaniem postaci

wsk=tab;

Konsekwencją tego faktu jest to, że odwołanie

tab[i]

można zapisać jako

*(a+i)

gdyż wyrażenie a[i] przy obliczaniu jest przekształcane bezpośrednio na (a+i); stądy wynika równoważność obu tych form.


#include <stdio.h>

int main(void)
{ 
  int tab[3]={1,2,3};
  int *wsk;
  
  wsk=&tab[0];
  printf("%d\n",*wsk);
  printf("%d\n",*(++wsk));
  wsk=tab;
  printf("%d\n",*wsk);
  printf("%d\n",*(++wsk));
  printf("%d %d\n",tab[2],*(tab+2));
  printf("%d %d\n",2[tab],*(2+tab));
  
  return 0;
}
Listing 3.4

1
2
1
2
3 3
3 3
Output 3.4 Efekt działania programu z listingu 3.4

Tablice wielowymiarowe

Deklaracja

int tab[2][3];

definiuje tablicę dwuwymiarową mogącą pomieścić 2x3=6 elementów typu int. Odniesienie do elementu w 2 wierszu i 1 kolumnie otrzymujemy pisząc

tab[2][1]

Aby nadać wartość początkową dla elementów tablicy tab należy napisać

int tab[2][3]={
               {1, 2, 3},
               {4, 5, 6}
              };
lub
int tab[][3]={
              {1, 2, 3},
              {4, 5, 6}
             };
Jak widać konieczne jest określenie zakresu wszystkich wymiarów z wyjatkiem pierwszego. Jesto to konsekwencją traktowania tablic wielowymiarowych jak jednowymiarowych tablic, których elementami są tablice.


#include <stdio.h>

#define MAXROW 3
#define MAXCOL 4

void F1(int t[MAXROW][MAXCOL])
{
  int i,j;
  
  for(i=0;i<MAXROW;i++)
  for(j=0;j<MAXCOL;j++)
    t[i][j]+=10;
}

void F2(int t[][MAXCOL])
{
  int i,j;
  
  for(i=0;i<MAXROW;i++)
  for(j=0;j<MAXCOL;j++)
    t[i][j]+=10;
}

void F3(int (*t)[MAXCOL])
{
  int i,j;

  for(i=0;i<MAXROW;i++)
  for(j=0;j<MAXCOL;j++)
    t[i][j]+=10;
}

void Wypisz(int t[MAXROW][MAXCOL])
{
  int i,j;
  
  for(i=0;i<MAXROW;i++)
  {
    printf("\n");
    for(j=0;j<MAXCOL;j++)
      printf("%2d ",t[i][j]);
  }
  printf("\n============\n");
}

int main(void)
{
  int tab[3][4]={
                 { 1, 2, 3, 4},
                 { 5, 6, 7, 8},
                 { 9,10,11,12}
                };
  
  Wypisz(tab);
  F1(tab);
  Wypisz(tab);
  F2(tab);
  Wypisz(tab);
  F3(tab);
  Wypisz(tab);
  
  return 0;
}
Listing 3.5

 1  2  3  4 
 5  6  7  8 
 9 10 11 12 
============

11 12 13 14 
15 16 17 18 
19 20 21 22 
============

21 22 23 24 
25 26 27 28 
29 30 31 32 
============

31 32 33 34 
35 36 37 38 
39 40 41 42 
============
Output 3.5 Efekt działania programu z listingu 3.5

Argumenty wywołania programu

W języku C działanie każdego programu rozpoczyna się od wywołania funkcji main z dwoma argumentami. Pierwszy z nich, umownie nazywany argc (ang. argument counter), jest liczbą argumentów z jaką program został wywołany. Drugi, argv (ang. argument vector) jest wskaźnikiem do tablicy zawierającej argumenty, każdy jako tekst. Argument o indeksie 0 jest scieżką wywołania programu, zatem licznik argc jest zawsze co najmniej równy 1. Dzieki temu możemy przekazać do programu podczas jego uruchamiania pewne parametry


#include <stdio.h>
#include <string.h>

int main (int argc, char **argv)
{
  int i,j,t;
  char *temp;
  
  for(i=1;i<argc;i++)
    printf("argv[%d]=%s\n",i,argv[i]);
  
  for(i=1;i0)
        t=j;
    }
    temp=argv[t];
    argv[t]=argv[i];
    argv[i]=temp;
  }
  
  for(i=1;i<argc;i++)
    printf("argv[%d]=%s\n",i,argv[i]);
      
  return 0;
}
Listing 3.6

c:\prog35.exe celina ela aga dominika
argv[1]=celina
argv[2]=ela
argv[3]=aga
argv[4]=dominika
argv[1]=aga
argv[2]=celina
argv[3]=dominika
argv[4]=ela

Output 3.6 Efekt działania programu z listingu 3.6
(żółty tekst został wprowadzony z klawiatury przez użytkownika)

Wskaźniki do funkcji

Pomomo, że funkcja w języku C nie jest zmienną to istnieje możliwość definiowania wskaźników do funkcji. Takim wskaźnikom można nadawać wartość, umieszczać je w tablicach, przekazywać do funkcji, zwracać jako wartość funkcji. Nazwa funkcji stanowi stały wskaźnik do funkcji. Na przykład nazwa MojaFunkcja jest wskaźnikiem do funkcji MojaFunkcja()


#include <stdio.h>

void f1(void)
{
  printf("Jestem f1\n");
}

void f2(void)
{
  printf("Jestem f2\n");
}

int main(void)
{
  void (*f)(void);
  
  f=f1;
  f();
  f=f2;
  f();
  
  return 0;
}
Listing 3.7

Jestem f1
Jestem f2
Output 3.7 Efekt działania programu z listingu 3.7

W powyższym kodzie w deklaracji zmiennej tutu konieczne jest użycie pierwszej pary nawiasów. Bez niej zamiast deklaracji wskaźnika do funkcji tutu otrzymamy, że tutu jest funkcją tutu. Poniżej przedstawiamy jeszcze jeden przykład.


#include <stdio.h>
#include <stdlib.h> //atof
#include <math.h> //sqrt

void Brak(double a, double b, double d);
void Jeden(double a, double b, double d);
//nazwy parametrow w deklaracji nie musza byc 
//zgodne z nazwami w definicji
void Dwa(double x, double y, double z);
//co wiecej, wcale nie musza wystapic
int Delta(double, double, double, double *);

void (*result[3])(double, double, double) = {Brak,Jeden,Dwa};

int main( int argc, char* argv[])
{
  double a, b, c, delta;

  int r;
  if (argc>4)
  {
    fprintf(stderr,"Zla ilosc parametrow\n");
    return -1;
  }
      
  a=atof(argv[1]);
  b=atof(argv[2]);
  c=atof(argv[3]);
  
  r=Delta(a,b,c,&delta);
   
  if(r!=-1)
    result[r](a,b,delta);
  else
    printf("To nie jest trojmian kwadratowy\n");
    
  return 0;
}

void Brak(double a, double b, double d)
{
  printf("\nBrak rzeczywistych pierwiastkow\n");
}

void Jeden(double a, double b, double d)
{
  printf("\nJeden pierwiastek podwojny: x0=%f\n",(-b)/(a+a));
}

void Dwa(double a, double b, double d)
{
  double p = sqrt(d);

  printf("\nDwa pierwiastki:\nx1=%f\n",(-b+p)/(a+a));
  printf("x2=%f\n",(-b-p)/(a+a));
}

int Delta(double a, double b, double c, double *d)
{
  if(a==0)
    return -1;
  *d=b*b-4*a*c;
  if(*d<0) return 0;
  if(*d>0) return 2;
  
  return 1;
}
Listing 3.8

C:\prog38.exe 1 1 -6

Dwa pierwiastki:
x1=2.000000
x2=-3.000000

C:\prog38.exe 1 -6 9

Jeden pierwiastek podwojny: x0=3.000000

C:\prog38.exe 2 -2 1

Brak rzeczywistych pierwiastkow

Output 3.8 Efekt działania programu z listingu 3.8
(żółty tekst został wprowadzony z klawiatury przez użytkownika)

Trochę gimanstyki

Złożone deklaracje są zawsze interpretowane począwszy od deklarowanego identyfikatora. Następnie aż do przeanalizowania wszystkich operatorów, powtarzane są następujące kroki

  1. Interpretowana jest każda para nawiasów () lub [] znajdująca się po prawej stronie.
  2. Jeżeli nie ma nawiasów, to interpretowana jest każda gwiazdka znajdjąca się po lewej stronie.
Wszelkie operatory użyte w deklaracji (to znaczy *, () oraz []) mają takie same priorytety ja w wyrażeniach. W celu zgrupowania argumentów można używać również nawiasów. Oto kilka mniej lub bardziej oczywistych przykładów