Zajęcia wyrównawcze z informatyki |
Semestr 4 Grupa 1 Sekcja 36 |
Wskaźniki, referencje i podprogramy
Czym jest wskaźnik?
Wskaźnik jest to zmienna przechowująca adres komórki pamięci. Używając wskaźników możemy bezpośrednio manipulować zawartością pamięci przez co uzyskujemy możliwość budowy wydajnych algorytmów. Wskaźników używamy z dwóch powodów:
Niektórych algorytmów nie można zapisać bez użycia wskaźników.
Zastosowanie wskaźników prowadzi do uzyskania bardziej efektywnego kodu.
Aby zrozumieć ideę posługiwania się wskaźnikami należy zrozumieć w jaki sposób programista korzysta z pamięci.
Pamięć komputera
Pamięć komputera zorganizowana jest w ciąg występujących po sobie komórek. Każda komórka ma rozmiar 1 bajta (8 bitów) i ma niepowtarzalny adres, który spełnia rolę jej identyfikatora. Pierwsza komórka pamięci ma adres 0. Każda następna ma adres o jeden większy od swej poprzedniczki. Adres ostatniej komórki zależy od wielkości pamięci zainstalowanej w komputerze (pamięć RAM i plik wymiany). Na rysunku 1 przedstawiono fragment pamięci.
Rysunek 1. Fragment pamięci komputera
Jeżeli zmienna wskaźnikowa ma wartość zero oznacza to, że nie wskazuje na żaden obszar pamięci. Komórka pamięci o adresie 0 jest zarezerwowana dla systemu operacyjnego i nie może być wykorzystywana przez aplikacje użytkownika. Do oznaczania tej komórki pamięci w języku C została zadeklarowana specjalna stała o nazwie NULL.
Deklaracja zmiennej
Deklaracja zmiennej to w istocie rezerwowanie dla niej miejsca w pamięci. Po rezerwacji możemy do tego obszaru zapisywać informacje jak również z niego je odczytywać. Dostęp do tych danych może być realizowany na dwa sposoby:
przez nazwę zmiennej,
przez adres pod którym te dane się znajdują.
Przykład 1:
// Rezerwacja pamięci dla zmiennych A, B, C
int main()
{
int A;
char C;
float B;
}
Na rysunku 2 przedstawiono fragment pamięci przed (rys. 2a) i po deklaracji trzech zmiennych A, B, C (rys. 2b).
Rysunek 2. Przykładowy wynik deklaracji zmiennych
W zależności od rodzaju zmiennej, komórki pamięci, które zostają przydzielone tej zmiennej, są zerowane (zmienne statyczne) lub pozostawiane bez zmian — zawierają przypadkowe wartości (zmienne automatyczne).
Operator wyłuskania *
Operator wyłuskania jest zwany także operatorem dostępu pośredniego lub dereferencją. Ogólna postać wywołania jest następująca:
*identyfikator_zmiennej;
Jeżeli operator wyłuskania * użyty jest przed zmienną podczas jej deklaracji to oznacza, że deklarowana zmienna jest wskaźnikiem do danego typu.
Przykład 2:
int *p; //zmienna p jest wskaźnikiem do danych typu int
Jeżeli operator wyłuskania * znajduje się przed zmienną, która już wcześniej została zadeklarowana to oznacza wartość przechowywaną pod adresem.
Przykład 3:
int *p; //zmienna p jest wskaźnikiem do danych typu int
.
.
*p = 12; //do obszaru pamięci wskazywanego przez zmienną
//wskaźnikową p zostanie wpisana wartość 12
Operator adresu &
Operator adresu & służy do pozyskiwania adresu zmiennej. Ogólna postać wywołania jest następująca:
&identyfikator_zmiennej;
Jeżeli operator adresu & użyty jest przed zmienną podczas jej deklaracji to oznacza, że deklarowana zmienna jest referencja (inną nazwą tej samej zmiennej).
Przykład 4:
int main()
{
float a; //deklaracja zmiennej a typu float
float *p; //deklaracja zmiennej wskaźnikowej p typu float
float &r=a; //deklaracja referencji r do zmiennej a;
}
UWAGA!!! Nie należy mylić jednoargumentowych operatorów & i * z dwuargumentowymi operatorami & (bitowy iloczyn logiczny) i * (operator mnożenia).
Przykład 5:
int main()
{
float a=3.14; //(1)
float *p=NULL; //(2)
p = &a; //(3)
printf("%f",*p); //(4)
}
Na rysunku 3a przedstawiono zawartość pamięci po wykonaniu (1) i (2) linii kodu (przykład 5). Rysunek 3b przedstawia zawartość pamięci po wykonaniu całego kodu. Podane w przykładzie numery komórek pamięci są tylko przykładowe.
Rysunek 3. Ilustracja przykładu 5
Referencje
Referencja jest aliasem (inną nazwą). Referencja zostaje utworzona przez zainicjowanie jej nazwą innej zmiennej, będącej celem referencji. Od tego momentu referencja działa jak alternatywna nazwa celu. Wszystkie operacje związane z referencją w rzeczywistości dotyczy jej obiektu docelowego. Referencje mają prawie te same możliwości, co wskaźniki, ale posiadają dużo prostszą składnię.
Tworzenie referencji
Referencję tworzy się, zapisując typ obiektu docelowego, operator referencji (&) oraz nazwę referencji.
typ &nazwa_referencji = cel_referencji;
Przykład 6:
#include <stdio.h>
#include <conio.h>
int main()
{
int A;
int &rA = A;
A = 3;
printf(" A = %d\nrA = %d", A, rA);
rA = 4;
printf(" A = %d\nrA = %d", A, rA);
_getch();
return 0;
}
Własności referencji.
Nie można zmieniać przypisania referencji, gdyż przypisanie powoduje nadanie nowej wartości obiektowi docelowemu.
Gdy pobieramy adres referencji, uzyskujemy adres jej celu.
Referencje w odróżnieniu od wskaźników nie mogą przyjmować wartości NULL.
Czym jest funkcja?
Funkcja jest to podprogram, operujący na danych i zwracający wartość. Każdy program w języku C (C++) zawiera przynajmniej jedną funkcję, main(). Gdy program rozpoczyna działanie, funkcja main() jest wywoływana automatycznie. Może ona wywoływać inne funkcje, które z kolei mogą wywoływać kolejne funkcje. Każda funkcja posiada nazwę, gdy ta nazwa zostanie napotkana przez program, przechodzi on do wykonywania kodu zawartego wewnątrz tej funkcji. Nazywa się to wywołaniem funkcji. Gdy zakończy się wykonywanie funkcji działanie programu jest wznawiane od instrukcji następującej po wywołaniu tej funkcji.
Parametry do funkcji można przekazywać poprzez:
wartość,
wskaźnik,
referencję.
Poszczególne elementy listy należy rozdzielać przecinkami.
Deklaracja funkcji
Deklaracja funkcji jest bardzo podobna do definicji funkcji z tą jednak różnicą, że nie umieszczamy w niej ciała funkcji (klamry również są pomijana) a na końcu wstawiam średnik.
Deklaracja funkcji ma postać:
Typ_wyniku nazwa_funkcji ( lista parametrów );
Przykład 7:
int silnia(int n); //deklaracja funkcji - prototyp
int main()
{
int n,w;
w=silnia(n); // wywołanie funkcji
prntf("%d\n",w);
return 0;
}
int silnia(int n) //definicja funkcji
{
int i,k=1;
for(i=2;i<=n;i++)
{
k=k*i;
}
return k;
}
Deklaracja funkcji jest elementem opcjonalny, to znaczy jej występowanie w programie nie jest obowiązkowe. Niemniej są sytuacje, gdy oprócz definicji funkcji w programie musimy umieścić jej deklarację (inna nazwa to prototyp). Taka sytuacja ma miejsce, gdy tworzymy swój własny zbiór funkcji (moduł), aby zawarte w nim funkcje były dostępne w innych modułach na początku modułu musimy umieścić deklaracje funkcji. Inny przypadek występuje wtedy, gdy definicja funkcji znajduje się w tym samym pliku, ale poniżej wywołania funkcji (przykład 7). W takiej sytuacji, aby funkcja była widoczna musimy na początku pliku umieścić jej prototyp.
Definicja funkcji ma postać
Definicja funkcji ma postać:
Typ_wyniku nazwa_funkcji ( lista parametrów )
{
deklaracje;
instrukcje;
return wynik;
}
Definicja funkcji składa się z nagłówka i ciała funkcji. Nagłówek zawiera typ wyniku, nazwę funkcji i listę parametrów. Ciało funkcji to wszystkie instrukcje znajdujące się pomiędzy nawiasami klamrowymi. Ciało funkcji może zawierać deklaracje stałych, deklaracje zmiennych, deklaracje używanych przestrzeni nazw, wywołania innych funkcji. Jeżeli funkcja zwraca wynik to musi również zawierać słowo kluczowe return. W przypadku wykonywania instrukcji zawartych wewnątrz funkcji natrafienie na słowo return powoduje zakończenie wykonywania funkcji.
Dobrym zwyczajem jest nadawanie funkcji nazwy sugerującej jej przeznaczenie bądź zagadnienie, które realizuje. Nie jest to jednak wymóg formalny a jedynie zalecenie pomagające w łatwiejszym posługiwaniu się funkcjami.
Przekazywanie parametrów przez wartość
Jeżeli do funkcji chcemy przekazać parametr poprzez wartość to element listy parametrów powinien mieć postać:
typ_zmiennej identyfikator_zmiennej
Przykład 8:
int silnia(int n)
{
//pozostałe instrukcje
}
float suma(float a, float b)
{
//pozostałe instrukcje
}
Przekazywanie parametrów przez wskaźnik
Jeżeli do funkcji chcemy przekazać parametry przez wskaźnik to element listy parametrów powinien mieć postać:
typ_zmiennej *identyfikator_zmiennej
Przykład 9:
int silnia(int *n)
{
//pozostałe instrukcje
}
float suma(float *a , float *b)
{
//pozostałe instrukcje
}
Przekazywanie parametrów przez referencję.
Jeżeli do funkcji chcemy przekazać parametry przez referencję to element listy parametrów powinien mieć postać:
typ_zmiennej &identyfikator_zmiennej
Przykład 10:
int silnia(int &n)
{
//pozostałe instrukcje
}
float suma(float &a , float &b)
{
//pozostałe instrukcje
}
Tabela 1. Porównanie różnych sposobów przekazywania parametrów.
Przekazywanie parametrów przez |
||
wartości |
wskaźniki |
referencje |
float suma(float a, float b) { return a+b; }
|
float suma(float *a, float *b) { return *a+*b; }
|
float suma(float &a, float &b) { return a+b; }
|
void sqr(float x) { x=x*x; }
|
void sqr(float *x) { *x=(*x)*(*x); }
|
void sqr(float &x) { x=x*x; }
|
Jakie znaczenie ma sposób przekazywania parametrów?
Jeżeli pewien parametr przekazywany jest poprzez wartość oznacza to, że w momencie wywołania funkcji tworzona jest kopia tego parametru i to właśnie na tej kopii wykonywane są wszystkie operacje wewnątrz funkcji. Natomiast oryginalny parametr nie podlega modyfikacji.
Tabela 2. Wyniki wywołania funkcji sqr() dla różnych sposobów przekazywania parametrów.
Definicja funkcji |
||
void sqr(float x) { x=x*x; } |
void sqr(float *x) { *x=(*x)*(*x); } |
void sqr(float &x) { x=x*x; } |
Wywołanie funkcji |
||
int main() { x=3; sqr(x); printf("x = %f",x); getch(); return 0; } |
int main() { x=3; sqr(&x); printf("x = %f",x); getch(); return 0; } |
int main() { x=3; sqr(x); printf("x = %f",x); getch(); return 0; } |
Wynik |
||
x = 3 |
x = 9 |
x = 9 |
Jeżeli pewien parametr przekazywany jest poprzez wskaźnik lub referencję to również tworzona jest kopia parametru z tym tylko, że zawiera ona adres komórki pamięci. Jeżeli dokonamy modyfikacji danych znajdujących się pod tym adresem to oczywiście zmiany te będą widoczne również poza funkcją. Własność tą można wykorzystać do przekazywania z funkcji więcej niż jednego wyniku. Jednakże, jeżeli zostanie ona użyta niewłaściwie może doprowadzić do błędnego działania programu. W niektórych sytuacjach błędy te mogą być bardzo trudne do znalezienia i usunięcia.
Podprogramy nie zwracająca wyniku — procedury.
Jeżeli chcemy zbudować podprogram, który nie zwraca wyniku to należy jako typ podać void. W tak zdefiniowanym podprogramie można używać słowa kluczowego return bez parametrów.
Przykład 11:
void WyswInt(int A)
{
printf("%d\n",A);
}
Ponieważ funkcja nie zwraca wyniku, więc również nie możemy wykorzystywać jej do tworzenia wyrażeń.
Sposób wywołania funkcji z przykładu 11 przedstawiono w przykładzie 12.
Przykład 12:
#include <stdio.h>
int main()
{
int A=10;
WyswInt(17);
WyswInt(A);
return 0;
}
Podprogramy bezargumentowe
Jest to szczególny przypadek podprogramu, który do prawidłowego działania nie wymaga wprowadzania danych. Przykładem może być funkcja _getch().
Przykład 13:
float Pi(void)
{
return (M_PI);
}
Jeżeli chcemy wywołać funkcję bez parametrową to nawiasy występujące po jej nazwie pozostawiamy puste. Nawiasów tych nie można pominąć gdyż służą one kompilatorowi do rozróżnienia funkcji od innych elementów języka takich jak stałe i zmienne.
Przykład 14:
#include <conio.h>
int main()
{
getch();
return 0;
}
Przydatne funkcje matematyczne
W pliku nagłówkowym math.h lub cmath znajdują się deklaracje przydatnych funkcji matematycznych. W tabeli 3 zostały one wymienione i krótko opisane.
Tabela 3. Funkcje matematyczne zadeklarowane w pliku nagłówkowym math.h
Funkcja |
Opis |
sin |
sinus |
cos |
cosinus |
tan |
tangens |
sinh |
sinus hiperboliczny |
cosh |
cosinus hiperboliczny |
tanh |
tangens hiperboliczny |
acos |
arkuc cosinus |
asin |
arkus sinus |
atan |
arkus tangens |
atan2 |
arkus tangens |
ceil |
zaokrągla do liczby całkowitej większej od argumentu |
floor |
zaokrągla do liczby całkowitej mniejszej od argumentu |
exp |
funkcja wykładnicza ex |
sqrt |
pierwiastek |
pow |
potęgoa |
log |
logarytm naturalny |
log10 |
logarytm dziesiętny |
fabs |
wartość bezwzględna |
fmod |
reszta z dzielenia |
Przykładowy program wykorzystujący wskaźniki, referencje i funkcje.
//--- Deklaracja modulow ----------------------------------------------------
#include "stdafx.h"
#include <conio.h>
#include <math.h>
//--- Deklaracje podprogramów -----------------------------------------------
float WczytajDane(char * Napis);
bool SprTrojkat(float a, float b, float c);
float PoleTrojkata(float a, float b, float c);
void wPoleIObwod(float a, float b, float c, float *p, float *o);
void rPoleIObwod(float a, float b, float c, float &p, float &o);
void Menu(void);
//--- Definicja funkcji main -------------------------------------------------
int _tmain(int argc, _TCHAR* argv[])
{
float a,b,c,p,o;
char z;
printf("Program obliczajacy pole trojkata.\n\n");
printf("Podaj dlugosci bokow potwierdzając kazda z nich naciskajac enter.\n\n");
a=WczytajDane("A = ");
b=WczytajDane("B = ");
c=WczytajDane("C = ");
if(SprTrojkat(a,b,c))
{
while(true)
{
Menu();
z=getch();
switch(z)
{
case '1': p=PoleTrojkata(a,b,c);
printf("\nPole trojkata = %10.3f\n",p);
break;
case '2': wPoleIObwod(a,b,c,&p,&o);
printf("\nPole trojkata = %10.3f\n",p);
printf("\nObwod trojkata = %10.3f\n",o);
break;
case '3': rPoleIObwod(a,b,c,p,o);
printf("\nPole trojkata = %10.3f\n",p);
printf("\nObwod trojkata = %10.3f\n",o);
break;
case 'k':
case 'K': return 0;
default: printf("Bledny wybor.");
}
}
}
else
{
printf("\nZ odcinkow o podanych dlugosciach nie mozna zbudowac trojkat");
printf("\nProgram zakonczony - nacisnij dowolny klawisz.");
_getch();
}
return 0;
}
//--- Definicje funkcji -----------------------------------------------------
//---------------------------------------------------------------------------
//funkcja przeznaczona do wczytywania danych typu float
//napis - opis wczytywanej wartosci przekazywany przez wskaznik
//wynik zwracany przez return
float WczytajDane(char * Napis)
{
float d;
do
{
printf("\t");
printf(Napis);
scanf("%f",&d);
}
while(d<=0);
return d;
}
//---------------------------------------------------------------------------
//funkcja sprawdzajaca czy z podanych bokow mozna zbudowac trojkat
//a, b, c - dlugosci bokow przekazywane przez wartosc
//wynik zwracany przez return
bool SprTrojkat(float a, float b, float c)
{
bool w=true;
if (a+b<=c) w = false;
if (a+c<=b) w = false;
if (b+c<=a) w = false;
return w;
}
//---------------------------------------------------------------------------
//funkcja obliczajaca pole trojkata
//a, b, c - dlugosci bokow przekazywane przez wartosc
//wynik zwracany przez return
float PoleTrojkata(float a, float b, float c)
{
float s,p;
p = (a+b+c)/2;
s = sqrt(p*(p-a)*(p-b)*(p-c));
return s;
}
//---------------------------------------------------------------------------
//funkcja obliczajaca pole i obwod trojkata
//a, b, c - dlugosci bokow przekazywane przez wartosc
//s - pole powierzchni przekazywane przez wskaznik
//o - obwod trojkata przekazywany przez wskaznik
void wPoleIObwod(float a, float b, float c, float *s, float *o)
{
float p;
*o = (a+b+c);
p=*o/2;
*s = sqrt(p*(p-a)*(p-b)*(p-c));
}
//---------------------------------------------------------------------------
//funkcja obliczajaca pole i obwod trojkata
//a, b, c - dlugosci bokow przekazywane przez wartosc
//s - pole powierzchni przekazywane przez referencje
//o - obwod trojkata przekazywany przez referencje
void rPoleIObwod(float a, float b, float c, float &s, float &o)
{
float p;
o = (a+b+c);
p=o/2;
s = sqrt(p*(p-a)*(p-b)*(p-c));
}
//---------------------------------------------------------------------------
//procedura wyswietlajaca menu
//nie wymaga danych wejciowych
//nie zwraca wyniku
void Menu(void)
{
printf("\nWybierz opcje:\n\n");
printf("\t1 - Oblicz pole trojkata.\n");
printf("\t2 - Oblicz pole i obwod trojkata. (wskaznik)\n");
printf("\t3 - Oblicz pole i obwod trojkata. (referencja)\n");
printf("\tk - Zakoncz program.\n");
}
Zadania do samodzielnego wykonania
Zadanie 1
Napisz podprogram obliczajcy największy wspólny podzielnik dwóch liczb. Następnie wykorzystaj go do znalezienia największego wspólnego podzielnkia trzech liczb (wywołując go ponownie - argumentami powinnien być wynik pierwszego wywołanai oraz trzecia liczba).
Zadanie 2
Napisz podprogram obliczający całkę funkcji jednej zmiennej zadanej wzorem f(x) = a sin(t) + b a następnie użyj go w funkcji main. Współczynniki a, b, poczatek i koniec całkowania oraz lizbe przedziałów na które ma zostać podzielona funkcja podcałkowa użytkownik ma podawać z klawiatury.
Zadanie 3
Napisz funkcje do rezerwacji i zwalniania pamięci dla tablicy jednowymiarowej oraz do wczytywania danych z klawiatury i wyświetlania zawartości tablicy na ekranie.
Zadanie 4
Napisz funkcje do rezerwacji i zwalniania pamięci dla tablicy dwuwymiarowej oraz do wczytywania danych z klawiatury i wyświetlania zawartości tablicy na ekranie.
Zadanie 5
Napisz podprogram zwracajacy największy i najmniejszy element macierzy oraz pozycje na których się znajdują.
Zadanie 6
Napisz funkcję realizujacą mnożenie macierzy. Parametrami wejściowymi powinny być obie macierze (argumenty) oraz ich wymiary. Wynikiem natomiast iloczyn tych macierzy. Ponadto w programie wykorystaj wcześniej stworzone funkcje do rezerwacji, zwalnianai, wczytywania i wyświetlania macierzy.