Programowanie w C++
Liczba odwołań : 17141
autor : Tomasz Czwarno
Funkcje bezrezultatowe i rezultatowe w C++.
- Funkcja bezrezultatowe.
- Funkcja rezultatowe.
- Wskaźniki i odnośniki w C++.
- Wskaźniki
- Odnośniki
- Zmienne statyczne i dynamiczne w C++.
- Zmienne statyczne
- Zmienne dynamiczne
- Funkcje rezultatowe zwykłe, wskaźnikowe i odnośnikowe w C++.
- Klasy
- Sekcje
- Konstruktory i Destruktory
- Metody
- Funkcje operatorowe
- Operator 1 argumentowy (!) umieszczony w klasie
- Operator 1 argumentowy (!) umieszczony poza klasą
- Operator 1 argumentowy następnikowy (++) umieszczony w klasie
- Operator 1 argumentowy następnikowy (++) umieszczony poza klasą
- Operator 1 argumentowy poprzednikowy (++) umieszczony w klasie
- Operator 1 argumentowy poprzednikowy (++) umieszczony poza klasą
- Operator 2 argumentowy (=) umieszczony w klasie
- Operator 2 argumentowy (+) umieszczony poza klasą
1.Funkcje bezrezultatowe i rezultatowe w C++.
O tym czy funkcja ma być, rezultatowe czy nie decyduje sama jej deklaracja.
Dla funkcji bezrezultatowe jest to 'void', wiąże się to z tym, że nie istnieje zmienna tego typu,
więc uznaje się ja za bezrezultatową. Natomiast funkcja rezultatowa jest to każda funkcja, której
typ rezultatu jest różny od 'void'.
a) Funkcja bezrezultatowa
Powrót
#include <iostream.h>
void fun(int &par) // typ rezultatu 'void' - bezrezultatowy
{
++par;
return;
}
int main(void)
{
int fix = 10;
fun(fix); // wywołanie funkcji
cout << fix; // 11
return 0;
}
Zastosowanie odnośnika jako parametru funkcji, powoduje faktyczne
operacje na skojarzonym z nim argumencie.I tak, jeśli wywołaliśmy funkcje
fun(fix) to operacja
++ptr da ten sam rezultat co
fix++.
b) Funkcja rezultatowa
Powrót
#include <iostream.h>
int fun(int par)
{
++par;
return;
}
int main(void)
{
int fix = 10;
cout << fun(fix); // 11
cout << fix; // 10
return 0;
}
2.Wskaźniki i odnośniki
Powrót
a) Wskażniki
Powrót
Wskaźnikiem jest zmienna, której można przypisać wskazanie.
Wyrażenie *ptr jest nazwą chwilową zmiennej i może wskazywać wiele zmiennych.
W jednej chwili *ptr wskazuje 1 zmienna ale zaraz może wskazywać 2 zmienną.
int main(void)
{
int fix1 = 10,
fix2 = 20;
int *ptr;
ptr = &fix1;
cout << *ptr; // 10
*ptr = 11; // ptr[0] = 11
cout << *ptr; // 11
cout << fix1; // 11
ptr = &fix2;
cout << *ptr; // 20
*ptr = 22;
cout << *ptr; // 22
cout << fix2; // 22
ptr = 0;
cout << *ptr; // Błąd !!!
return 0;
}
Kiedy przypiszemy wskaźnikowi wskazanie puste, użycie nazwy *ptr jest
zabronione do chwili przypisania mu nazwy zmiennej tego samego typu co wskaźnik.
Jeśli wskaźnik wskazuje element tablicy int arr = {10, 20, 30}; to :
// Przypisanie wskazania na 1 element tablicy
int *ptr = arr; // 10
// Przypisanie wskazania na 2 element tablicy
int *ptr = arr + 1; // 20
// Wskazanie 2 element tablicy
*(ptr + 1); // 20
// Wskazanie 2 element tablicy
ptr[1]; // 20
// Wskazanie 2 element tablicy
ptr += 1; // 20
Własności wskaźników i operacje na nich :
int main(void)
{
int arr[3] = {10, 20, 30};
int *ptr = arr;
// Własności int oraz operatora ++
int par = 1;
cout << par << endl; // 1
cout << par++ << endl; // 1
cout << par << endl << endl; // 2
cout << par << endl; // 2
cout << ++par << endl; // 3
cout << par << endl << endl; // 3
// Własności ptr oraz operatora ++
cout << *ptr << endl; // 10
// Wskazuje element nastepujący po wskazanym
// Opracowanie nie modifikuje wskaźnika
cout << *ptr++ << endl; // 10 // *ptr++ = ptr++[0]
cout << *ptr << endl << endl; // 20
cout << *ptr << endl; // 20
// Wskazuje element nastepny po wskazanym
// Opracowanie modifikuje wskaźnik
cout << *++ptr << endl; // 30 // *++ptr
cout << *ptr << endl << endl; // 30
cout << *ptr << endl; // 30
// Wskazuje ten sam element w tablicy
// Opracowanie modyfikuje wskazania
// Zwieksza wartość tego elementu w tablicy
cout << ++*ptr << endl; // 31 // ++*ptr = ++ptr[0]
cout << *ptr << endl << endl; // 31
cout << *ptr << endl; // 31
// Wskazuje ten sam element w tablicy
// Opracowanie nie modyfikuje wskazania
// Zwieksza wartość tego elementu w tablicy
cout << ptr[0]++ << endl; // 31 // ptr[0]++
cout << *ptr << endl << endl; // 32
// Wyświetlenie zawartości tablicy
ptr = arr;
for(int x = 0; x < 3; x++)
cout << *ptr++ << endl;
return 0;
}
b) Odnośniki
Powrót
Jeśli zainicjujemy odnośnik do zmiennej, to nazwa odnośnika stanie się
trwała nazwą zmiennej (synonimem), a każda operacja na odnośniku będzie operacja na zmiennej.
int main(void)
{
int fix = 10;
int &ref = fix;
ref = 20;
cout << ref << endl; // 20
cout << fix << endl; // 20
fix = 30;
cout << fix << endl; // 30
cout << ref << endl; // 30
return 0;
}
3. Zmiennych statycznych i dynamiczne
Powrót
Komputer na mikroprocesorze Intel 80-86 i działający w systemie
MS-DOS dzieli pamięć na obszary zwane segmentami. Segment ma maksymalny rozmiar 64kB
i jest podzielony na 3 obszary (Obszar danych statycznych, sterta i stos). Stos to
obszar zmiennych automatycznych. Sterta to obszar zmiennych dynamicznych.
Granica miedzy nimi jest ruchoma. W obszarze zmiennych statycznych przechowywane
są zmienne statyczne i zewnętrzne.
a) Zmienne statyczne
Powrót
Zmienne statyczne w momenci utworzenia istnieją w pamięci komputera aż
do końca działania programu. Przez cały jago czas.
Klasyczny przykład błędu, funkcja zwraca wartość zmienne
lokalnej po wyjściu z funkcji, ale zmienna lokalna już nie istnieje.
int &fun(void)
{
int loc = 10;
loc++;
return loc;
}
int main(void)
{
cout << fun() << endl; // Błąd !!!
return 0;
}
Zastosowanie zmiennej statycznej likwiduje ten błąd.
int &fun(void)
{
static int loc = 10;
loc++;
return loc;
}
int main(void)
{
cout << fun() << endl; // 11
cout << fun() << endl; // 12
return 0;
}
b) Zmienne dynamiczne
Powrót
Zmienne dynamiczne są szczególnie przydatne, w momencie kiedy nie
wiadomo od początku ile pamięci potrzeba. Pamięć zajętą przez zmienne dynamiczne
trzeba konieczne zwolnić.
int main(void)
{
int tab[3]; // Zainicjowanie 3 elementowej tablicy
return 0;
}
int main(void)
{
int x = 3;
int tab[x]; // Błąd !!!
return 0;
}
Błąd ten można zlikwidować inicjując tablice dynamicznie.
int main(void)
{
int x = 3;
int *tab = new int[x];
delete [] tab;
return 0;
}
4. Funkcje rezultatowe z rezultatem zwykłym, wskaźnikowym i odnośnikowym.
Powrót
Poniżej przedstawione są 3 funkcje których działanie jest identyczne.
#include <iostream.h>
// rozwiązanie zwykłe
int fun1(int *arr, int par)
{
return arr[sizeof(arr) - par];
}
// rozwiązanie wskaźnikowe
int *fun2(int *arr, int par)
{
return &arr[sizeof(arr) - par];
}
// rozwiązanie odnośnikowe
int &fun3(int *arr, int par)
{
return arr[sizeof(arr) - par];
}
int main(void)
{
int tab[] ={10, 20, 30, 40, 50};
cout << fun1(tab, 0) << endl;
cout << *fun2(tab, 0) << endl;
cout << fun3(tab, 0) << endl;
return 0;
}
5. Klasy
Powrót
a) Sekcje
Powrót
Składniki klasy możemy umieścić w 3 sekcjach
private, public, protected .
Wybór sekcji zależy od tego co zamierzamy zrobić z klasa. Gdy umieścimy
składniki w sekcji private uniemożliwimy
tym używanie ich poza klasą. W sekcji public składniki
tam umieszczone będą mogły być używane przez wszystkie funkcje i składniki.
Składniki w sekcji protected mogą być używane
przez składniki jego klasy, funkcje zaprzyjaźnione oraz składniki jego klas
pochodnej. W sekcjach umieszcza się składniki obiektu klasy oraz konstruktory,
destruktory, metody i funkcje operatorowe.
b) Konstruktory i Destruktory
Powrót
Konstruktor klasy służy do inicjowania elementów struktury (klasy).
W tym miejscu decydujemy jak będą wyglądały obiekty naszej klasy. Kiedy projektujemy
dosyć skomplikowaną klasę (np. używamy wskaźniki) należy wyposażyć ją w
konstruktor kopiujący, który wykona tzw. kopiowanie głębokie. Gdy nawet nie
umieścimy konstruktora kopiującego, domyślnie konstruktor kopiujący zostanie
umieszczony w klasie i wykonuje kopiowanie płytkie. Zalecane jest stosowanie
konstruktora kopiującego kiedy np składnikami obiektu klasy są wskaźniki na
jakieś elementy. Wiąże się to z destruktorami, które są składnikami wywołanymi
tuż przed zniszczeniem obiektów klasy przez system. Destruktory stosuje się również
kiedy stosujemy wskaźniki, gdyż kiedy system zniszczy np wskaźnik, który
jest składnikiem obiektu klasy, nie usunie wskazania tego wskaźnika, który będzie
zalegał pamięć. Z tego powodu stosuje się destruktory. Wracając do
konstruktora kopiującego i destruktora. Kiedy stworzymy jakiś obiekt klasy, który
jako składnik ma właśnie wskaźnik, to musimy tu zastosować konstruktor
kopiujący, który wykona kopiowanie głębokie. Kopiowanie płytkie odpada gdyż
kopiując obiekt klasy ze wskaźnikiem, skopiujemy tylko wskaźnik ale wskazanie
pozostanie takie samo dla dwóch obiektów. I kiedy destruktor zniszczy obiekt
pierwszy (zniszczy wskaźnik i wskazanie) ale kiedy będzie niszczył obiekt
drugi będzie niszczył coś co już nie istnieje. Może to spowodować do załamania
systemu.
c) Metody
Powrót
Metody są to funkcje klasy wywoływane na rzecz obiektu.
d) Funkcje operatorowe
Powrót
Funkcje operatorowe jedno lub dwu argumentowe służą do
zdefiniowania operacji operatorowych na obiektach klasy. Funkcje operatorowe mogą
być umieszczone w ciele funkcji lub poza nią, z tym że jeśli umieścimy je poza
klasa musimy musimy ją zaprzyjaźnić z klasą.
i) Operator 1 argumentowy (!) umieszczony w klasie Owad
Powrót
// np. !a - a.operator!()
void operator!(void)
{
x = 0;
}
ii) Operator 1 argumentowy (!) umieszczony poza klasą Owad.
Powrót
// np. !a - operator!(a)
friend void operator!(Owad &par) // Umieszczone gdzieś w klasie
void operator!(Owad &par)
{
par.im = 0;
}
iii) Operator 1 argumentowy następnikowy (++) umieszczony w klasie Owad.
Powrót
//np. a++ - a.operator++(0)
void operator++(int)
{
x = 0;
}
iv) Operator 1 argumentowy następnikowy (++) umieszczony poza klasą Owad.
Powrót
// np. a++ - operator++(a, 0)
friend void operator++(Owad &par, int) // Umieszczone gdzieś w klasie
void operator!(Owad &par)
{
par.x = 0;
}
v) Operator 1 argumentowy poprzednikowy (++) umieszczony w klasie Owad.
Powrót
// np. ++a - a.operator++()
void operator++(void)
{
x = 0;
}
vi) Operator 1 argumentowy poprzednikowy (++) umieszczony poza klasą Owad.
Powrót
// np. ++a - operator++(a)
friend void operator++(Owad &par) // Umieszczone gdzieś w klasie
void operator++(Owad &par)
{
par.x = 0;
}
vii) Operator 2 argumentowy (=) umieszczony w klasie Owad.
Powrót
// np. a = b - a.operator=(b)
Rozpatrzmy 3 aspekty: a = b, a = b = c, a = a.
Dla jasności Lewy element to this, a Prawy to par
// To rozwiązanie realizuje tylko a = b,
// przy a=b=c za b=c zostanie zwrócone void
&operator=(Owad &par)
{
// Użycie return *this wiąże się z & - &operator
this->x = par.x;
// Jest to typowe użycie wskaźnika,
// gdy chcemy zmienić wartość wejściową
return *this;
}
// To rozwiązanie jest już lepsze od poprzedniego gdyż
// przy a=b=c za b=c zostanie zwrócone b.
Owad &operator=(Owad &par)
{
this->x = par.x;
}
// To rozwiązanie jest kolejnym ulepszeniem
// zabezpieczającym przed operacja a = a
Owad &operator=(Owad &par)
{
if(this != &par) {
this->x = par.x;
return *this;
}
return *this;
}
viii) Operator 2 argumentowy (+) umieszczony poza klasą Owad.
Powrót
// np. a + b - operator+(a, b)
friend Owad &operator+(Owad &parL, Owad &parR) // Umieścic gdzieś w klasie
Owad operator+(Owad &parL, Owad &parR)
{
return parL.x + par.R;
}
|