Obiekty jako zmienne w pamięci
dynamicznej komputera –
abstrahowanie zmiennych
wykład 3
Składniki klasy-podsumowanie
• Składnikami klasy mogą, i najczęściej muszą być
deklarowane
zmienne,
funkcje,
nazywane
wówczas funkcjami składowymi, a także obiekty
innych klas i deklarowane konstruktory obiektów
własnych klasy. Składnikiem klasy będzie też
konstruktor
domniemany,
kopiujący
i
konwertujący występujące niejawnie na rzecz
obiektów klasy.
• Funkcje wchodzące w skład klasy w tym
znaczeniu, że wykonują czynności na rzecz
obiektów klasy mogą być definiowane wewnątrz
klasy albo na zewnątrz po bloku main() tak, jak
inne funkcje. Mogą funkcje składowe klasy być
także funkcjami biblioteki standardowej i wtedy
odpowiednie pliki nagłówkowe, zawierające te
funkcje, należy dołączyc preprocesorem. W
przykładach omawianych była to na przykład
funkcja strcpy() z pliku bibliotecznego string.h.
Funkcja wykorzystywana dalej jako metoda
obiektów może się znaleźć w pliku nagłówkowym
i nie spowoduje to kłopotów z linkowaniem
programu, kiedy będzie się ona wielokrotnie
wywoływana w programie. Tak samo zachowują
się funkcje z biblioteki standardowej. Jest to
możliwe dlatego, że taka funkcja składowa klasy
jest po kompilacji traktowana jako funkcja typu
inline, czyli wywoływana jednokrotnie. Każde jej
następne wywołanie to po prostu wykonanie ciała
funkcji bez konieczności poszukiwania jej adresu
przy każdym wywołaniu.
Przykład:
Plik osoba.h
-------------------------------------------------------------------
#include<iostream.h>
#include<string.h>
class Osoba{
char nazwisko[80];
int wiek;
public:
void zapamietaj(char *napis, int lata);
void wypisz() {
cout<<”\t”<<nazwisko<<”,lat:”<<wiek<<endl;}
/// FUNKCJA wypisz JEST WEWNATRZ CIALA KLASY
};
Zdefiniowany został plik nagłówkowy, który będzie włączany
wszędzie tam, gdzie w programie stosowana będzie klasa
Osoba.
Funkcja składowa klasy nazwana wypisz (...) wszędzie tam,
gdzie zostanie zastosowana będzie miała tylko formę:
cout<<”\t”<<nazwisko<<”,lat:”<<wiek<<endl;
Nie będzie każdorazowo wywoływana. Gdyby jednak funkcja
była
zdefiniowana poza ciałem klasy to nie może wystąpić w
pliku nagłówkowym
bo pojawią się błędy linkowania
(konsolidacji).
Jeśli funkcji składowych klasy jest wiele to warto przygotować
plik nagłówkowy, w którym są one definiowane wewnątrz
klasy.
Funkcja i wskaźnik this
Rozpatrzmy funkcję klasy Osoba opisaną poniżej
void Osoba::zapamietaj(char *napis, int lata)
{
strcpy(nazwisko,napis);
wiek=lata;
}
Powołajmy do życia kilka obiektów klasy Osoba:
Osoba student1, student2, profesor, technik, inżynier
Mamy pięć obiektów i chcemy, aby do konkretnego wpisany
został wiek =23.
Wykonujemy to za pomocą podanej wyżej funkcji na przykład
następująco:
student2.zapamietaj(’’Tomek Gawin’’,23);
Funkcja musi odszukać obiekt student2 aby wykonać operację na
jego rzecz. Robi to tak, że korzysta z ukrytego przed nami wskaźnika
this, który jest inicjalizowany w momencie pojawienia się operatora
kropki po nazwie obiektu. Ten wskaźnik pokazuje funkcji na którym
egzemplarzu (konkrecie, instancji) obiektów klasy ma wykonać
swoje czynności. Oznacza to, że postać funkcji należy rozumieć tak:
void Osoba::zapamietaj(char *napis, int lata)
{
strcpy(this->nazwisko, napis);
this->wiek=lata;
}
Tablica nazwisko i zmienna wiek zostają „podczepione” pod
wskaźnik obiektu.
Zauważcie, że zmienna typu char jest w opisie funkcji wskaźnikiem
do zmiennej napis. To zapewni bezbłędne zadziałanie operacji this.
Wskaźnik this jest wstawiany we właściwe miejsce poza nasza
wiedzą. Jest to wskaźnik stały czyli typu
X const*
gdzie X to nasz obiekt.
this - WSKAŹNIK SPECJALNY
• Każdej funkcji - metodzie zadeklarowanej
wewnątrz klasy zostaje w momencie
wywołania w niejawny sposób (ang.
implicitly) przekazany wskaźnik do obiektu (w
stosunku do którego funkcja ma zadziałać).
Pointer wskazuje funkcji w pamięci ten
obiekt, którego członkiem jest dana funkcja.
Bez istnienia takiego właśnie wskaźnika nie
moglibyśmy stosować spokojnie funkcji, nie
moglibyśmy odwoływać się do pola obiektu,
gdybyśmy nie wiedzieli
jednoznacznie, o który obiekt chodzi.
this - WSKAŹNIK SPECJALNY, c.d.
• Program posługuje się automatycznie niejawnym
wskaźnikiem do obiektu (ang. implicit pointer). Możemy
wykorzystać ten istniejący, choć do tej pory nie widoczny dla
nas pointer posługując się słowem kluczowym this (ten). This
pointer wskazuje na obiekt, do którego należy funkcja.
Korzystając z tego wskaźnika funkcja może bez cienia
wątpliwości zidentyfikować właśnie ten obiekt, z którym
pracuje a nie obiekt przypadkowy.
[!!!] FUNKCJE KATEGORII static NIE OTRZYMUJĄ POINTERA
this.
Należy pamiętać, że wskaźnik this istnieje wyłącznie podczas
wykonywania metod (ang. class member function execution),
za
wyjątkiem funkcji statycznych.
Obiekt jako zmienna – przesyłanie
obiektu do funkcji
Przesyłanie obiektu przez wartość
Przykład:
Plik prezentacja.c
///////
#include<iostream.h>
#include ”osoba.h”
///////
void prezentacja (Osoba);
//
nazwa klasy jest w tej deklaracji typem
argumentu funkcji
///////
void main()
{
Osoba kompozytor, autor;
//
wywolanie obiektow klasy
Osoba do zycia
kompozytor.zapamietaj(”Aleksander Borodin”, 54);
autor.zapamietaj(”Alosza Jerofiejew”, 33);
///
teraz wywołamy obiekty poprzez wywołania funkcji, w której
sa one argumentami
//////
prezentacja(kompozytor);
//
argument podany jawnie czyli przez
wartosc
prezentacja(autor);
}
/////////
void prezentacja(Osoba ktos)
//
to jest opis ciala funkcji i tu
argument ma już nazwe poza typem
{
cout<<”\n przedstawiam panstwu , oto we wlasnej osobie:”;
ktos.wypisz();
}
Co wytworzymy na ekranie poprzez wykonanie tego
programu?
przedstawiam panstwu, oto we wlasnej osobie
Aleksander Borodin, lat:54
przedstawiam panstwu, oto we wlasnej osobie Alosza
Jerofiejew, lat:33
Wystąpiło tu
stosowanie obiektu jako argumentu
funkcji.
Obiekt inicjalizował się jako kopia wewnątrz
funkcji. Przy obiektach o skomplikowanej budowie nie
jest to dobry sposób, bo tworzenie kopii obiektu
wewnątrz funkcji musi trwać odpowiednio długo.
Wypisanie na ekran nastąpiło za pomocą funkcji
dołączonej w pliku nagłówkowym osoba.h.
Przesyłanie obiektu przez referencję
Przesyłanie przez referencję działa tak samo jak dla wszystkich
innych zmiennych w programie.
Referencja działa jak przezwisko
zmiennej
pokazujące, gdzie jej szukać. Nie jest tworzona kopia
zmiennej w funkcji, a tylko podawane miejsce jej ulokowania. W
podanym przykładzie wystarczy zmienić postać funkcji prezentacja.
Przykład:
Plik prezentacja.c
///////
#include<iostream.h>
#include <osoba.h>
///////
void prezentacja (Osoba
&
);
///////
main()
{
Osoba kompozytor, autor;
kompozytor.zapamietaj(’’Aleksander Borodin”, 54);
autor.zapamietaj(’’Alosza Jerofiejew”, 33);
/// teraz wywołamy obiekty poprzez wywołania funkcji
//////
prezentacja(kompozytor);
prezentacja(autor);
}
/////////
void prezentacja(Osoba
&
ktos)
{
cout<<”\n przedstawiam panstwu ,oto we wlasnej osobie:”;
ktos.wypisz();
}
Czyli argument funkcji jest deklarowany przez referencję i obiekt i to
wystarcza aby funkcja pracowała na oryginale obiektu w miejscu jego
ulokowania na stosie RAM. Tworzenie kopii nie jest teraz konieczne.
Operatory new i delete
Podczas deklarowania zmiennych rezerwowany jest dla ich obszar
pamięci. Jeśli się do zadeklarowanych zmiennych odwołamy, to
program musi wiedzieć, gdzie dokładnie szukać ich wartości w
pamięci operacyjnej.
Szczególne kłopoty powstają wówczas, kiedy deklarujemy zmienną,
która ma wiele elementów o różnych lokalizacjach w RAM. Taką
zmienną jest np. tablica. Jak pamiętamy, nie możemy się do niej
odwołać w programie jeśli w momencie odwołania nie jest znany
rozmiar tablicy.
Możemy zastosować odwołanie dynamiczne do pamięci za pomocą
operatora new. Wtedy wystarczy nazwa zmiennej, w tym tablicy i nie
musimy znać jej rozmiaru, a mimo to wszystkie elementy zostaną
poprawnie odszukane do dalszego zastosowania w programie.
Operator new musi działać nie na zmiennej tylko na wskaźniku do
zmiennej, czyli na wskazaniu miejsca (adresu) w pamięci
operacyjnej. Mówimy potocznie, że jest to miejsce na stercie (heap).
Przykład: program tworzy listę osób z nazwiskami i imionami. Dane
o osobach umieszczane są w tablicach. W jednej umieszcza się
nazwiska, a w drugiej imiona. Do opisu posłuży obiekt, który ma
adres zwracany przez operator new.
#include <conio.h>
#include <stdlib.h>
#include <string.h>
class Osoba {
public:
char imie[80], nazw[80];
};
void main()
{
//void clrscr();
Osoba *p1=new Osoba, *p2=new Osoba;
if(!p1||!p2)
{
cprintf(“\n\rp1 i/lub p2?”);
getch();
exit(0);
}
strcpy(p1->imię,”Jan”);
strcpy(p1->nazw,”Krol”);
*p2= *p1;
strcpy(p1->imię,”Ewa Nowak-”);
cprintf( ”\n\r%s%s”,p1->imie, p1->nazw);
cprintf( ”\n\r%s%s”,p2->imie, p2->nazw);
delete p1;delete p2;
getch();
}
Sposób
użycia
operatora
new do
obiektów
Co zobaczymy na ekranie?
Jan Krol
Ewa Nowak-Krol
Jak to działa?
Pliki nagłówkowe: conio.h zawierają biblioteki do funkcji cprintf();
string.h zawiera biblioteczna funkcje strcpy();.
#include <conio.h>
#include <stdlib.h>
#include <string.h>
Niektóre pliki nagłówkowe nie musza być pisane z
rozszerzeniem h.
Klasa Osoba nie zawiera konstruktora. Po kompilacji mamy więc
konstruktor Osoba domniemany. Dostępne są dla każdego obiektu
tego konstruktora zmienne tablicowe: imie[], nazw[].
class Osoba {
public:
char imie[80], nazw[80];
};
Obiekty p1 i p2 jako konkrety klasy Osoba zostają przywołane
poprzez wskaźniki o adresach zwracanych operatorem new. Ten
operator przydziela jednocześnie wskaźnikom *p1 oraz *p2 miejsca
w pamięci. Może się zdarzyć, że w RAM nie będzie miejsca na
przydzielenie pamięci dla p1 oraz p2. Wtedy zostanie im nadana
przez operator new wartość NULL. Gdyby tak się zdarzyło, to
zostanie to wykryte przez if(!p1||!p2), a potem wydrukowane w
formie wartości p1 oraz p2 na ekranie w kolejnym wierszu
programu. Następnie wykonana by była instrukcja exit(0), co
oznacza zakończenie programu poprze wyjście poza ostatnia linię
kodu. Funkcja exit() jest umieszczona w pliku stdlib.h.
Osoba *p1=new Osoba, *p2=new Osoba;
if(!p1||!p2)
{
cprintf(“\n\rp1 i/lub p2?”);
getch();
exit(0);
}
Funkcja strcpy() z biblioteki nagłówkowej string.h kopiuje
łańcuchy Jan oraz Krol do tablic imie[] i nazw[]. Tablice są w
obiekcie wskazywanym przez p1. Jeśli obiekt jest wskazywany przez
wskaźnik, to odwołujemy się do niego operatorem strzałki, a nie
kropki jak w poprzednich przykładach.
Pod adres wskaźnika p2 zostaje wpisana deferencja czyli zawartość
spod adresu *p1.
strcpy(p1->imię,”Jan”);
strcpy(p1->nazw,”Krol”);
*p1= *p2;
Do tablicy imie[] wskazywanej przez p2 kopiowany jest łańcuch Ewa
Nowak-.
Drukowane są tablice z obiektów p1 oraz p2.
Operator delete usuwa obiekty p1 oraz p2 ze sterty pamięci
dynamicznej i tym samym zwalnia pamięć.
strcpy(p1->imię,”Ewa Nowak-”);
cprintf( ’’\n\r%s%s”, p1->imie,p1->nazw);
cprintf( ’’\n\r%s%s”, p2->imie,p2->nazw);
delete p1;delete p2;
getch();
}
Wskaźniki do obiektów
• Wskaźniki do obiektów funkcjonują podobnie jak wskaźniki
do
struktur. Operator -> pozwala na dostęp zarówno do danych
jak i
do funkcji. Dla przykładu wykorzystamy obiekt naszej
prywatnej
klasy Licznik.
class Licznik
{
public:
char moja_litera;
int ile;
Licznik(char znak) { moja_litera = z; ile = 0; }
void Skok_licznika(void) { ile++; }
};
Wskaźniki do obiektów, c.d.
• Aby w programie można było odwołać się do obiektu nie
poprzez
nazwę a przy pomocy wskaźnika, zadeklarujemy wskaźnik
do
obiektów klasy Licznik:
Licznik *p;
Wskaźnik w programie możemy zastosować np. tak:
p->Skok_licznika();
(czytaj: Wywołaj metodę "Skok_licznika()" w stosunku do
obiektu
wskazywanego w danym momencie przez wskaźnik p)
Wskaźniki do obiektów, c.d.
• Trzeba pamiętać, że sama deklaracja w
przypadku referencji i wskaźników nie
wystarcza. Przed użyciem należy jeszcze
zainicjować wskaźnik w taki sposób, by
wskazywał na nasz obiekt-licznik.
Wskaźnik do obiektu inicjujemy w taki sam
sposób jak każdy inny pointer:
p = &Obiekt;