programowanie C++, Psychologia , Rozwój Osobisty , NLP, Infomatyka


0x08 graphic

Zakres tematyczny:

1. Wprowadzenie - historia języka C/C++.

2. Pierwszy program :struktura programu jednosegmentowego, operacje wejścia /wyjścia - porównanie języka C i C++.

  1. Wprowadzenie.

Na dzisiejszym wykładzie rozpoczynamy omawianie jednego z wielu języków programowania, a mianowicie języka C i jego potomka języka C++. Celowo nie ograniczymy się jedynie do bardzo modnego ostatnio języka C++, albowiem początkującym zwłaszcza programistom programowanie samo w sobie sprawia wiele problemów, nie mówiąc już o programowaniu obiektowo zorientowanym. Poza tym, spotkacie się Państwo w literaturze z programami napisanymi w języku C i ich analiza mogłaby sprawić trudności. Na wykładach będziemy kolejno omawiać poszczególne elementy języka C i ich ewentualne uzupełnienia w języku C++. Stosunkowo niewiele miejsca poświęcimy programowaniu obiektowemu, ponieważ zagadnieniu temu poświęcony będzie odrębny przedmiot w późniejszych semestrach. Na początek kilka słów o historii języka C.
Język C opracowany został ponad 20 lat temu przez Kernighana i Ritchiego, którzy są jednocześnie autorami najpopularniejszego opracowania na temat standardu tego języka. Pierwowzorem tego języka był język B (wywodzący się z języka BCPL) stworzony dla pierwszego systemu UNIX. W toku rozwoju powstało kilka standardów języka, z których obecnie najpopularniejszy to ANSII C. Jest to jednak standard umożliwiający tylko programowanie strukturalne. Wraz z rozwojem obiektowych elementów języka powstał standard AT&T Bell Laboratories, obejmujący oba elementy języka. Język C jest językiem ogólnego stosowania. Był on ściśle związany z systemem UNIX dla którego został opracowany, ponieważ zarówno system operacyjny jak i większość programów działających pod jego kontrolą napisano w C. Ideą, która przyświecała twórcom języka C, było stworzenie takiego języka, który skupiałby w sobie cechy języków niskiego poziomu, a więc: szybkość i efektywność kodu wynikowego programów, jak i wysokiego poziomu: prostszy zapis algorytmów, większa przenośność programów między różnymi typami komputerów. Tak powstał język C dziedziczący część cech języków asemblerowych jak i języków wysokiego poziomu.
W istocie, rdzeń języka C można określić jako stosunkowo niskiego poziomu, ponieważ:
a) posługuje się tymi samymi typami obiektów co większość komputerów: a więc znakami, liczbami, adresami. Obiekty te mogą być łączone lub przemieszczane za pomocą zwykłych operacji arytmetycznych i logicznych dostępnych na istniejących maszynach. Nie istnieją tu operacje na obiektach złożonych jak ciągi znaków, zbiory, listy, tablice;

b) nie zawiera w przeciwieństwie do np.Pascala (który z pewnością większość z Państwa zna), instrukcji pozwalających wykonywać operacje wejścia/wyjścia i innych specjalistycznych funkcji, będących standardowymi elementami języków wysokiego poziomu. W zamian za to, otrzymujemy niewielką liczbę instrukcji, znaczną liczbę różnorodnych typów danych i operatorów, umożliwiających efektywny zapis algorytmów.

Takie postępowanie można wytłumaczyć chęcią stworzenia języka zapewniającego lepszą przenośność programów pomiędzy komputerami różnych typów. Wiadomo bowiem, że sposób przeprowadzania np. operacji we/wy zależy od danego typu systemów komputerowych i zwykle bywa zaprogramowany z uwzględnieniem specyfiki konkretnego systemu. Problem braku instrukcji (wydawałoby się niezbędnych) do realizacji tych operacji rozwiązano dostarczając odpowiednie funkcje w standardowych bibliotekach języka. Stanowią one integralną część języka bez której nie można się obejść przy programowaniu. Niestety Ci z państwa, którzy programowali wcześniej w innych językach, szczególnie w Pascalu, mogą napotkać pewne problemy wynikające ze znacznie gorszej niż w tym języku diagnostyki błędów. Tak więc trzeba będzie zwrócić baczniejszą uwagę na sprawy składni i analizy błędów logicznych wynikających z błędnego wykorzystania elementów języka do zapisu algorytmu.

  1. Pierwszy program.

Rozpoczynamy teraz praktyczną naukę programowania w języku C/C++. Na początek powiemy sobie króciutko o kodzie źródłowym programu. W C/C++ tworzenie programu odbywa się w dwóch etapach:

  1. Opracowanie kodu źródłowego.

  2. Generowanie kodu wynikowego.

Pierwszy etap polega na zapisaniu algorytmu za pomocą instrukcji języka. W efekcie programista otrzymuje kod źródłowy programu, który jest plikiem tekstowym zawierającym zapis algorytmu.

Drugi etap polega na przetłumaczeniu konstrukcji właściwych językowi wyższego poziomu na rozkazy procesora i doprowadzenie programu do postaci wykonywanej przez komputer. Operacja ta składa się z dwóch faz: kompilacji i linkowania: czyli łączenia. Nas interesuje etap pierwszy, czyli generowanie kodu źródłowego. Ponieważ każdy programista wyznaje zasadę : ABY PISAĆ TRZEBA PISAĆ , zaczynamy naukę programowania od najprostszego programu:

Program 1

#include
main()
{
printf("Witamy w krainie języka C");

}

Program 2

#include
main()
{
cout<<"Witamy w krainie języka C++";

}
Wykonanie tych programów spowoduje wyświetlenie na ekranie monitora napisu: "Witamy w krainie języka C" dla programu 1 i "Witamy w krainie języka C++" dla programu 2.
Jak widać, oba programy mają takie samo zadanie: wypisanie tekstu na ekranie, ale różnią się nieznacznie: pierwszy napisany został zgodnie ze standardem języka C (dalej mówić będziemy napisany w C), drugi napisany został w języku C++. O różnicach będziemy mówić za chwilę, teraz omówimy wspólne cechy, a mianowicie strukturę programu w języku C/C++, która jest taka sama.

Każdy program C/C++ jest zbudowany z funkcji i zmiennych. Funkcja zawiera instrukcje określające jakie operacje obliczeniowe należy wykonać, zmienne natomiast przechowują wartości używane podczas tego procesu. Program musi zawierać funkcję zwaną main. Funkcjom języka C/C++ można nadawać dowolne nazwy lecz main jest nazwą specjalną. Od niej rozpoczyna się wykonywanie programu. Treść tej funkcji (zwana ciałem funkcji), czyli innymi słowy instrukcje wykonywane w ramach tej funkcji, zawarte są między dwoma nawiasami klamrowymi {...} (tak jak w każdej innej funkcji). W naszych programach funkcja main ma tylko jedną instrukcję:

  1. printf("Witamy w krainie języka C");

  2. cout<<"Witamy w krainie języka C++";

która sprawia, że na standardowym urządzeniu wyjściowym czyli po prostu na ekranie zostanie wypisany napis ujęty w znaki cudzysłowia. Umieszczony na końcu średnik jest znakiem końca instrukcji. Każda instrukcja musi być zakończona średnikiem. Język C/C++ jest językiem o tzw. wolnym formacie. Wszystko może znaleźć się w każdym miejscu linii, a nawet rozpisane na kilka linii. Poza nielicznymi sytuacjami, w dowolnym miejscu instrukcji można przejść do innej linii i tam kontynuować pisanie, ponieważ koniec instrukcji określa nie koniec linii, ale średnik.
Dla wygody programisty lepiej jest każdą instrukcję pisać w osobnej linii, co po pierwsze zwiększa czytelność programów, a po drugie, korzystając z takich narzędzi jak debugger mamy możliwość śledzenia programu linia po linii. Wracając do naszego programu. Na ogół main, aby wykonać działanie woła na pomoc (inaczej wywołuje) inne funkcje napisane przez programistę lub pochodzące z bibliotek dostarczonych wraz z kompilatorem. Przyj żyjmy się pierwszym linijkom naszych programów. Zlecają one kompilatorowi dołączenie do programu informacji o standardowej bibliotece we/wy. Jak pamiętamy, operacje te nie są częścią definicji języka. Dla języka C będzie to biblioteka stdio, a dla C++ iostream. Dokładniej operacje we/wy będziemy omawiać w dalszej części wykładu. Jedną z metod komunikacji miedzy funkcjami jest przekazywanie danych. Funkcja wywołująca tworzy listę wartości czyli tzw. argumentów i dostarcza je funkcji wywoływanej. Jest ona umieszczana w nawiasach okrągłych, bezpośrednio po nazwie funkcji. W naszym przypadku main jest funkcją bezparametrową, o czym informuje nas pusty nawias po nazwie main. W szczególnych przypadkach funkcja main może posiadać parametry. Przykładem funkcji z jednym argumentem jest funkcja printf. Argumentem jest tutaj stała napisowa ujęta w znaki cudzysłowia. Zarówno w 1-szym jak i 2-gim programie w stałych napisowych mogą występować pewne znaki umożliwiające sterowanie wypisywaniem na ekran. Zmodyfikujmy nasze programy w instrukcjach odpowiadających za wyświetlenie napisu:

1. printf("Witamy w krainie języka C");

2. cout<<"Witamy w krainie języka C++";

Wstawienie w środek tekstu znaku powoduje przerwanie wypisywania w bieżącym wierszu i jego kontynuację od początku w nowym wierszu:

Witamy w krainie języka C(C++)

Miejsce wstawienia tego znaku zależy od sposobu w jakim chcemy wypisać komunikat. Oprócz znaku dostępne są inne znaki formatujące np.: - wstawienie znaku tabulacji, _ - wstawienie znaku cofania (backspace) i inne. Poprzednie programy wykonywały tylko podstawowe operacje wyjściowe - wypisywanie komunikatu. W naszym następnym programie spotkamy się z inną operacją we/wy - wczytaniem danych z klawiatury. Wprowadzimy też kilka nowych pojęć takich jak: komentarz, deklaracja, zmienne, formatowane wypisywanie danych.

Program 3

#include
/* Program do wyznaczania pola powierzchni kuli o promieniu R */

main() { int r, p; // p-pole kuli, r-promien kuli

printf("Wprowadź promień kuli r = ");

scanf("%i",&r);
p = 4 * pi * r * r;

printf("pole kuli o promieniu r = %i jest równe p = %i",r,p);

}

Program 4

#include
/* Program do wyznaczania pola powierzchni kuli o promieniu R */

main() { int r, p; // p-pole kuli, r-promien kuli

cout<<"Wprowadz promień kuli r = ";

cin>>r;
p = 4 * pi * r * r;

cout <<"pole kuli o promieniu r = <<R<<" jest równe p="<<p;


}


W programach tych zastosowano komentarze tzn. opisy przydatne programiście, a ignorowane przez kompilator. W języku C++ komentarze mogą być umieszczane dwojako:


- ograniczenie tekstu znakami /*......*/. Komentarz taki może ciągnąć się przez wiele linijek, lecz nie może być zagnieżdżony, tzn. nie można zapisać: /*........./*....*/......*/


- zastosowanie znaków //. Kompilator po napotkaniu takich znaków ignoruje resztę linii do końca. Jest to komentarz charakterystyczny tylko dla C++.


Komentarze /* i // mogą się zagnieżdżać. Dobrym zwyczajem jest opisywanie komentarzami wszystkiego czego się da w programie: funkcji, zmiennych itp. Przydadzą się po powrocie po dłuższym czasie do programu napisanego dawno temu.


Wróćmy do naszego programu. W funkcji main znajduje się linijka: int r,p; Są to tzw. definicje zmiennych. Nazwaliśmy je r i p. Nazwą może być dowolnie długi ciąg liter, cyfr oraz znaków podkreślenia. Nazwa nie może zaczynać się od cyfry oraz od znaku podkreślenia, ponieważ takie nazwy często występują w programach bibliotecznych. W języku C rozróżnia się duże i małe litery. Słowa kluczowe są unikalne i muszą być pisane małymi literami. Słowami kluczowymi są np.: int, class, public, for, return itp.


W języku C/C++ wszystkie zmienne muszą być zadeklarowane przed ich użyciem. Deklaracja informuje komputer jak dużo miejsca w pamięci musi zarezerwować na daną zmienną. Deklaracja składa się z nazwy typu i listy zmiennych danego typu. Oprócz poznanych już liczb całkowitych mamy do dyspozycji typy rzeczywiste float, znakowe char i inne. Dokładnie zapoznamy się z nimi w dalszej części wykładu. W C występują oprócz zwykłych zmiennych także tablice, struktury i unie. O nich także później. Przyjrzyjmy się teraz innym nie znanym nam jeszcze instrukcjom:

  1. scanf(" %i?,&r);

  2. cin>>r;

Są one instrukcjami odpowiadającymi za operacje związane z klawiaturą, czyli standardowym urządzeniem wejściowym (C-onsole IN-put). Umożliwiają one wczytanie z klawiatury liczby, której wartość zostaje przypisana zmiennej r. Pierwsza instrukcja jest charakterystyczna dla C, druga dla C++. Już na pierwszy rzut oka widać, że druga instrukcja jest wyraźnie prostsza. W funkcji scanf musimy pamiętać o typie wczytywanej liczby, i o tym czy mamy wpisać znak & czy też nie. Po wczytaniu danych następuje część obliczeniowa. Następnie instrukcje odpowiadające za wydrukowanie wartości wyniku. Używamy do tego celu znanych już nam konstrukcji cout i printf.

1.printf("pole kuli o promieniu r = %i jest równe p = %i",r,p); 2.cout <<"pole kuli o promieniu r = <<R<<" jest równe p="<<p;


Instrukcje te spowodują wyświetlenie na ekranie zarówno testów umieszczonych w znakach cudzysłowia, jak i wartości zmiennych r i p, które jak widać nie są ujęte w znaki " for(i=0;(i<80) && ((ch=getchar()) != EOF) && (ch !='

');i++)
buffer[i] = ch;

buffer[i] = '';

printf("%s
",buffer);
}

Program ten używa funkcji getchar() do wprowadzania z klawiatury jednej linii i gromadzenia jej w tablicy znaków buffer i kończy string znakiem specjalnym zanim wyświetli go na ekranie monitora.
Do wyprowadzania na ekran jednego znaku o określonym kodzie służy funkcja putchar():
int putchar(int)

Funkcja zwraca kod wypisanego znaku lub EOF gdy powstał błąd. Przykład zastosowania funkcji :

#include
#include
main()
{
int ch;

while((ch = getchar()) != EOF)

putchar(tolower(ch));
return 0;

}
Program zamienia małe litery tekstu wejściowego na duże. Na ćwiczeniach napiszemy kilka programów wykorzystujących te funkcje, które później będziecie państwo sprawdzać na laboratorium. Przejdziemy teraz do następnej grupy funkcji obsługujących tzw.stringi czyli ciągi znaków. Są to funkcje:gets i puts. Służą one do pobierania łańcucha znaków z klawiatury i wysłania na ekran zapamiętanego wcześniej w tablicy znaków.

Funkcja gets(string) czyta łańcuch znaków z klawiatury i zapisuje go do zmiennej buffer:
char *gets(buffer);

char * buffer;

Pierwszy napotkany znak nowej linii '

' zamienia na NULL przed zwróceniem stringu.

Jeśli odczyt przeprowadzony był bez błędu, funkcja zwraca adres tablicy znaków (swój argument), jeśli nie zwraca null pointer


Przykład 6

#include
int main()

{
char imie[20];

char *res;

printf("Cześć jestem IBM .Podaj mi swoje imie");

res = gets(imie); /* może być tylko gets(imie); */

printf("Milo mi Cię poznać %s",res);

return 0;

}
Program ten jak widać prosi o podanie imienia osoby uruchamiającej program, zapisując to imię przy pomocy funkcji gets do tablicy znaków imie i odpowiada jej imieniem wypisując zawartość wskaźnika res . Wprowadziliśmy tu kilka nowych konstrukcji. Pierwsza to linia :
int main()

Poprzednio nie wstawialiśmy słowa kluczowego int przed nazwa main. W standardzie przyjęło się, że każda funkcja jeśli nie zadeklarowaliśmy inaczej jest typu int. Można wtedy typ funkcji opuścić, jeśli natomiast napiszemy np. int wtedy w przedostatniej linii musimy napisać instrukcję określającą wartość jaką zwraca funkcja tu: return 0. Gdy funkcja nie zwraca wartości wtedy zamiast int main() piszemy void main() i pomijamy linię return 0; Drugą nowością jest linia
char imie[20].

Jest to deklaracja 20-elementowej tablicy typu char (znak). Jak mówiliśmy string to po prostu tablica znaków.

Funkcja puts(string) zapisuje string na ekran:

int puts(string);

const char *string;

Zamienia znak końca stringu na znak nowej linii. Zwraca 0 jeśli operacja przeszła bez błędu, i wartość niezerową gdy wystąpił błąd.


#include
int result;

main()
{
/* write a prompt to stdout */

result = puts("Insert data disk and strike any key");

}
Do realizacji tzw. formatowanego we/wy służy grupa funkcji scanf i printf. Pamiętamy z poprzednich programów, że funkcja printf() służy do wypisywania na ekranie monitora ciągu znaków i wartości zmiennych, przy czym obie te funkcje można łączyć razem np.:
printf("Wartość zmiennej a = % 2i zmiennej b = %4.2f

zmiennej c = %c zmiennej d = %s

",a,b,c,d);
Składnia tej funkcji jest następująca:

int printf(format [,argument]...);

const char *format;

Zmienna format zawiera zwykłe znaki kopiowane do strumienia wyjściowego oraz specyfikacje przekształceń, z których każde wskazuje sposób przekształcenia i wypisania kolejnego argumentu funkcji printf. Każdą specyfikację przekształcenia rozpoczyna znak % a kończy znak charakterystyczny dla tego przekształcenia. Między % i znakiem przekształcenia mogą w następującej kolejności występować:


W miejsce formatu wpisywane są wartości zmiennych w kolejności wskazanej w liście zmiennych występującej po formacie. Aby móc wydrukować znak % trzeba napisać: %%
Funkcja scanf służy do realizacji formatowanego wejścia. Podobnie jak w funkcji printf funkcja scanf zawiera łańcuch formatujący określający typ wprowadzanych zmiennych. Składnia tej funkcji jest następująca:

int scanfformat[,argument]...);

const char* format;

Funkcja umożliwia większość tych samych przekształceń co funkcja printf lecz w przeciwnym kierunku. Wczytuje znaki ze standardowego wejścia, interpretuje je zgodnie ze specyfikacjami zawartymi w formacie i zapamiętuje wyniki w miejscach określonych przez pozostałe argumenty. Argumenty muszą być wskaźnikami (znak &) i określają, gdzie dane mają być przekazane.

int i,j;

scanf("%d%d",&i,&j);
Format zawiera specyfikatory przekształceń . Mogą tu wystąpić:

Dokładniej o tych funkcjach musicie państwo przeczytać w literaturze. To były podstawowe funkcje we/wy w ujęciu proceduralnym, charakterystycznym dla języka C. Obecnie omówimy metody realizacji operacji we/wy na standardowych urządzeniach we/wy w języku C++. Zanim do tego przejdziemy musimy na chwilę zatrzymać się przy pojęciu strumienia. Wprowadzanie i wyprowadzanie informacji można potraktować jako strumień bajtów płynący od źródła do ujścia. Np:jeśli chcemy do zmiennej x wczytać z klawiatury jakąś liczbę, wtedy strumień bajtów płynie od urządzenia zewnętrznego - klawiatura do tego miejsca w pamięci gdzie mieści się zmienna x. Dla operacji we/wy kompilator predefiniuje kilka strumieni (predefiniuje czyli zakłada, otwiera strumień i po zakończeniu programu zamyka go) : cout, cin, cerr. Aby skorzystać z tych strumieni w programie trzeba umieścić dyrektywę

#include
podobnie jak dla C .

Ze strumieniem wyjściowym cout został stowarzyszony operator << (z ang. insertion operator), który wskazuje skąd dokąd zostanie wysłana informacja (odpowiada za wysłanie informacji do strumienia), czyli program:

Przykład 7

#include
void main()

{
int x = 123;

cout<
}
powoduje wysłanie do strumienia wyjściowego (który kończy się na ekranie) liczby x = 123.
Można wysłać różne typy zmiennych i stałych przy pomocy jednej instrukcji np.
int x=123;

cout<<"Wartość x = "<< x <<'.'

W przykładzie tym, za pomocą jednej instrukcji przesłano na ekran komunikat, wartość zmiennej i pojedynczy znak. Na ekranie pojawi się napis: Wartość x = 123.

Linijka int x=123 odpowiada za deklaracje zmiennych połączoną z jej inicjacją, czyli nadaniem wartości początkowej. Zwróćmy uwagę, że poszczególne typy zmiennych oddzielone są operatorami <<.

Jak dotąd przykłady podane przez nas nie pokazywały jak przesłać formatowane dane do cout. Przypuśćmy, że chcemy wyświetlić wartość całkowitą używając notacji 16-tkowej. Funkcja printf umożliwiała taką operację. Ale jak to zrobić w C++ za pomocą strumienia cout? Na marginesie pamiętajmy, że C jest częścią C++. Jeśli nie można czegoś zrobić w C++ lub jest to bardziej skomplikowane to w każdej chwili możemy odwołać się do C (w naszym przypadku wystarczy dołączyć , aby użyć funkcji printf). W C++ ze strumieniem cout współpracuje zestaw manipulatorów. Zmieniają one bieżący format dla całkowitego argumentu. Są to: dec, oct, hex.

Przykład 8

#include
main()
{
int x=123;

cout<<dec<<x<<' '

<<oct<<x<<' '

<<HEX<<X;
}
Przykład pokazuje jak można wyświetlić wartość całkowitą w trzech systemach:dec, hex, oct. Wynik działania programu to :123 173 7b.

Ze strumieniem wejściowym cin został stowarzyszony operator >>, odpowiadający za wczytanie informacji ze strumienia wejściowego (którego początek jest na klawiaturze), czyli program

Przykład 9

void main()


{
int x;

cout<<"wprowadz wartość x :

";
cin>>x;
cout<<"wartość x = "<<X;

}
powoduje wczytanie ze strumienia wejściowego (klawiatury) wartości do zmiennej x.
Jak widać operacje we/wy dla standardowych typów realizowane są bardzo prosto. Nie musimy pamiętać o formacie wprowadzania i wyprowadzania wyników, a strumienie cout i cin interpretują je prawidłowo. Trochę trudniej przedstawiają się te operacje dla typów zdefiniowanych przez użytkownika, ale o tym opowiemy dużo później.

Zakres tematyczny:

  1. Dynamiczna alokacja obiektów.

  2. Operacje we/wy - obsługa zbiorów.

  3. Obsługa zbiorów - operacje strumieniowe - podejście języka C.

  1. Dynamiczna alokacja obiektów.

W poprzednim semestrze omawialiśmy zagadnienia dynamicznej alokacji dla standardowych typów zmiennych, w podejściu standardowym i języka C++. Po omówieniu podstawowych informacji o obiektach, pora wspomnieć o tym zagadnieniu w odniesieniu do typów zdefiniowanych przez użytkownika. Przypomnijmy, że dla dużych obiektów (zmiennych, tablic, struktur) deklarowanie ich "na sztywno" w programie może prowadzić do problemów z umieszczeniem ich w pamięci. Jedynym rozsądnym wyjściem jest wtedy czasowe pobranie (zarezerwowanie) pewnego obszaru pamięci w którym chwilowo przechowywane będą dane , a po ich użyciu zwolnienie. W języku C obszar ten nazwano heap'em, C++ free store. Różnica między nimi polegała jedynie na funkcjach używanych do dostępu do tej pamięci. W języku C, do dynamicznej alokacji pamięci używaliśmy funkcji typu malloc, alloc, calloc, oraz free do zwalniania pamięci.. Musieliśmy jednak pamiętać z jakim modelem pamięci pracujemy. Dla innego niż small, tiny musieliśmy używać funkcji farmalloc, faralloc, farfree. Przypomnijmy jak to robiliśmy na przykładzie struktury zdefiniowanej na poprzednim wykładzie:

struct Date *dateptr;

dateptr = (struct date *) malloc(sizeof(struct date));

W języku C++ uniezależniamy się od modelu pamięci poprzez operowanie operatorami new i jego uzupełnieniem - operatorem delete. O ile w stosunku do zmiennych podstawowych (także struktur i tablic) można wymiennie stosować oba podejścia, tak do dynamicznej alokacji obiektów (przykładów klas zdefiniowanych przez użytkownika ) możliwe jest jedynie użycie operatorów new i delete. Dlaczego? Otóż gdybyśmy chcieli użyć funkcji malloc do tworzenia takiego nowego (dynamicznego co prawda, ale jednak) obiektu, wówczas nie uruchamiany by był konstruktor tworzący egzemplarze klas. Wówczas funkcja malloc zwracała by wskaźnik do niezainicjowanego bloku pamięci. Moglibyśmy co prawda używać wtedy funkcji składowych klasy, ale odnosiły by się one do nieprawidłowo skonstruowanego obiektu, co najprawdopodobniej spowodowało by sytuacje błędna. Np.

Date *dateptr;

int i;

datepte = (Date *) malloc(sizeof(Date));

i = dateptr->getmonth(); //zwraca niezdefiniowana wartość miesiąca.

W takim przypadku tracimy wszystkie możliwości jakie przewidzieliśmy dla konstruktora. Lepszym wyjściem jest tu użycie operatora new. Np.

Date *firstptr, secondptr;

int i;

firstptr = new Date; //wywołanie konstruktora

i = firstptr->getmonth(); //zwraca 1, bo tak było przewidziane w konstruktorze


secondptr = new Date(3,15,1996); //wywołanie konstruktora z zainicjowaniem //pamięci
i = secondptr->getmonth();

Operator new wywołuje konstruktor niezależnie od tego, czy użyliśmy argumentów czy nie, co gwarantuje, że każdy alokowany obiekt jest prawidłowo skonstruowany !!!

Zwalnianie pamięci zarezerwowanej operatorem new wykonuje operator delete. Dla obiektów alokowanych w ostatnim przykładzie zwolnienie pamięci odbywa się w następujący sposób:
delete firstptr;

delete secondptr;

Operator ten automatycznie wywołuje destruktor dla obiektu zanim alokuje go z pamięci.
Jak widać użycie obu operatorów dla obiektów jest typowe i podobne jak dla zmiennych podstawowych. Pamiętajmy, że użycie tego operatora do zwalniania pamięci nie przydzielonej operatorem new lub powtórne skasowanie już skasowanego wskaźnika powoduje, że program będzie zachowywał się dziwnie, lub po prostu zawiesi się !!!

Operator new i wskaźniki składowe klas - klasy ze wskaźnikami

Przypuśćmy, że chcemy zapisać klasę o nazwie String, gdzie każdy obiekt zawiera ciąg znaków. Przechowywanie stringów w tablicach nie jest odpowiednie, ponieważ nie wiemy jak długie będą te stringi. Zamiast tego każdemu obiektowi możemy włączyć wskaźnik do char jako daną składową i dynamicznie alokować odpowiednią pamięć dla każdego obiektu. Prześledźmy to na przykładzie.
#include
#include
class String

{
public:
String();
String(const char *s);

String(char c, int n);

void set(int index, chat newchar);

char get(int index) const;

int getlength()const {return length;}

void display()const {cout<<BUF;}

~String();
private:
int length;

char *buf;

}
String::String() { buf = 0; length = 0; }

String::String(const char *s) { length = strlen(s); buf = new char[length+1]; strcpy(buf,s); }
String::String(char c, int n) { length=n; buf=newchar[length+1]; memset(buf,c,length);
buf[length]=''; }

void String::set(int index, char newchar)

{
if((index>0) && (index<=length)) buf[index-1]=newchar;

}
char String::get(int index)const

{
if((index>0) &&(inex<=length)) return buf[index-1];

else return 0;

}
String::~String() { delete buf;} //pracuje nawet dla pustego stringu. Kasowanie wskaźnika //NULL jest bezpieczne;

main()
{
string mystring("oto mój string");

mystring.set(1,'O'); //zmienia znak na odpowiedniej pozycji

}
Konstruktor, który pobiera wskaźnik do char ,używa operatora new do alokowania bufora zawierającego string. Potem kopiuje zawartość stringu do bufora. W rezultacie obiekt typu string nie jest ciągłym blokiem pamięci, jak to się ma w przypadku struktury. Każdy obiekt składa się z dwóch bloków pamięci. Jeden zawiera length i buf, drugi zawiera wprowadzone znaki.
Wywołanie operatora sizeof() dla obiektu klasy string poda jedynie rozmiar pierwszego bloku. Jednak różne obiekty tej samej klasy mogą mieć bufory na znaki różnej długości.

PROBLEMY !!!

Przypuśćmy, że mamy funkcję main, która ma postać:

main()
{
string yourstring("oto twój string");

string mystring("oto mój string");

yourstring = mystring;

}
Jest oczywistym co robi taki program. Tworzy dwa obiekty yourstring i mystring i przypisuje yourstring zawartość obiektu mystring. Wygląda to nieszkodliwie, ale...

Kiedy przypisujemy jeden obiekt drugiemu, kompilator wykonuje przypisanie wszystkich składowych:
yourstring.length = mystring.length;

yourstring.buf = mystring.buf;

Pierwsze przypisanie nie jest problemem. Ale buf jest wskaźnikiem. Przypisanie ich oznacza, że mystring.buf wskazuje na ten sam obszar co yourstring.buf. Dwa obiekty operują na tym samym buforze.
Oznacza to, że modyfikacja jednego obiektu pociąga za sobą modyfikację drugiego, co nie zawsze jest zjawiskiem pożądanym. Jednak poważniejszy problem powstaje, gdy alokowany obiekt ma być likwidowany. Wówczas blok pierwszy pamięci zawierający length i buf są delokowane automatycznie, jednak bufor znaków alokowany był przy pomocy operatora new, a więc musi być usuwany operatorem delete. Dlatego też obiekty takich klas jak string, zawierające wskaźniki jako dane składowe muszą posiadać destruktory, likwidujące alokowane obszary. Jeśli klasa nie miałaby zdefiniowanego destruktora, zajęty obszar pamięci nie byłby zwalniany i program mógłby się zakończyć z komunikatem"out of memory".

Dynamiczna alokacja tablic obiektów .

Podobnie jak tablice typów wbudowanych możemy tworzyć tablice obiektów:

Date birthday[10];

Podczas deklarowania tablicy obiektów konstruktor wywoływany jest dla każdego elementu tablicy.
Gdy deklarujemy tablice bez inicjacji konstruktor wywoływany jest bez argumentów (efault constructor).
Gdy chcemy inicjować każdy element tablicy osobno, wywołujemy konstruktor z innymi argumentami:
Date birthday[10] = { Date(1,12,1986), Date(11,10,1986),

Date(1,1,1986)};
W powyższym przykładzie brakujące obiekty utworzone zostaną domyślnym konstruktorem (bez argumentów).
Tablice obiektów alokujemy taj jak dla innych typów:

String *text;

text = new string[5];

W tym przypadku nie możemy inicjować obiektów, wołany jest jedynie konstruktor default.
Przy delokacji tablicy obiektów operatorem delete konieczne jest stosowanie pustych nawiasów wskazujących na to, że jest to tablica obiektów. Gdybyśmy je pominęli delokowany byłby jedynie obszar wskazany przez wskaźnik np. text, czyli zerowy element tablicy) tylko obiekt pierwszy .
delete []text;

przy takim zapisie destruktor wywoływany jest dla każdego obiektu osobno. Wtedy kompilator delokuje obszar pamięci wskazany przez text.

Przy użyciu klas bez destruktorów np. Date można kasować tablice bez []:

Date *mydate;

mydate=new Date[5];

// użycie klasy

delete mydate;

W tym przypadku kompilator rozpozna, że ma do czynienia z klasa bez destruktora i automatycznie od razu zwolni obszar wskazany przez mydate. Ponieważ obiekt nie ma alokowanych buforów nie ma problemu związanego z brakiem destruktorów.

Jednak dobrym zwyczajem jest trzymanie się konsekwentnie zapisu z []. Zawsze bowiem klasa może być reimplementowana tak, że będzie mogła wykonywać dynamiczna alokacje pamięci i wymagać destruktora. Wówczas nie będziemy musieli poprawiać całego programu.

2. Operacje we/wy - obsługa zbiorów.

Procedury we/wy ze standardowej biblioteki C pozwalają zapisać dane i je odczytać ze zbiorów i urządzeń we/wy.

W języku C nie ma predefiniowanej struktury wewnętrznej zbiorów jak np. w Clipperze, gdzie dane zapisywane są w porcjach zwanych rekordami, i gdzie dostęp do danych odbywa się poprzez licznik rekordów. W C wszystkie dane traktowane są jako sekwencja bajtów. Wszystkie funkcje we/wy można zasadniczo podzielić na 3 grupy (które pokrótce omówimy na wykładzie):

Funkcje strumieniowe traktują zbiór danych i elementy zbioru danych jako ciąg pojedynczych znaków ( nie jako jakieś określone struktury danych), które mogą być później grupowane poprzez formatowanie, w formatowanych operacjach we/wy. Poprzez wybranie spośród wielu funkcji strumieniowych można przetwarzać dane w różnych rozmiarach i formatach, od pojedynczego znaku do dużych struktur danych. Aby mieć możliwość operowania na danych zapisanych w zbiorach, w pierwszej kolejności dany zbiór otworzyć (lub stworzyć jeśli nie istnieje). Kiedy otwieramy zbiór przy pomocy funkcji strumieniowych, otwarty zbiór jest kojarzony ze strukturą typu FILE(zdefiniowana w stdio.h) zawierającą podstawowe informacje dotyczące otwieranego zbioru. Po otwarciu zbioru wskaźnik do tej struktury jest zwracany przez funkcje otwierającą zbiór. Pozostałe funkcje operujące na zbiorze używają tego wskaźnika do komunikowania się ze zbiorem.
Strumieniowe operacje we/wy dostarczają: buforowanych, formatowanych i nieformatowanych operacji we/wy. Podczas buforowania strumienia, dane czytane ze strumienia lub do niego zapisywane są przechowywane w pewnym obszarze pamięci zwanej buforem. Przy zapisie - zawartość bufora wyjściowego jest przepisywana do miejsca przeznaczenia po zapełnieniu się bufora lub w momencie zamykania strumienia lub w czasie normalnego zakończenia programu (nie na skutek błędu wykonania). Potem bufor jest opróżniany. Przy czytaniu, blok danych umieszczany jest w buforze wejściowym, a dane czytane są z bufora. Kiedy bufor wejściowy jest pusty, następny blok danych przesyłany jest do bufora. Operacje buforowane są operacjami bardzo efektywnymi ponieważ system przesyła duże bloki danych w jednej operacji, a nie jak przy niebuforowanych, gdy wykonuje się zapis i odczyt pojedynczych elementów danych. Jednak w przypadku błędu wykonania i nienormalnego zakończenia programu podczas opróżniania bufora dane są bezpowrotnie tracony.

Operacje we/wy niskiego poziomu nie buforują ani nie formatują danych. Wykorzystują bezpośrednio możliwości operacji we/wy systemu operacyjnego. Przez te funkcje mamy dostęp do zbiorów i urządzeń zewnętrznych na niższym poziomie niż funkcje strumieniowe. Funkcje wykonują się szybciej bo omijany jest jak gdyby interfejs pomiędzy systemem operacyjnym a użytkownikiem. Jednak posługiwanie nimi jest dużo trudniejsze (przynajmniej na początku). W momencie otwarcia zbioru na poziomie niższym , ze zbiorem kojarzona jest zmienna typu integer - tzw. "file handle". Poprzez ta daną podobnie jak poprzez wskaźnik FILE, funkcje odwołują się do konkretnego zbioru.

Operacje we/wy na konsoli i portach mogą być traktowane jako rozszerzenie (rozwinięcie) funkcji strumieniowych. Pozwalają czytać lub pisać na konsolę lub porty we/wy (np. port drukarki, RS). Operacje czytają dane w bajtach. Można operacjom tym nadawać różne opcje np. wyświetlanie danych przy czytaniu itp.

UWAGA !!!!

Funkcje niskiego poziomu i strumieniowe są generalnie niezgodne, tak więc na jednym zbiorze muszą być wykonywane operacje jednego typu. Ponieważ strumieniowe operacje we/wy są buforowane, a niskiego poziomu nie, próba dostępu do zbioru przez dwie różne metody może spowodować utratę danych w buforze.

3. Operacje we/wy - podejście języka C - funkcje strumieniowe.

Aby używać funkcji strumieniowych musimy w zbiorze źródłowym dołączyć zbiór . W zbiorze tym zdefiniowane są stałe, typy i struktury (np. FILE) używane w funkcjach strumieniowych, zawiera deklaracje funkcji i makr do obsługi strumieni. Ze stałych, które mogą się przydać zdefiniowane są:
EOF - zdefiniowanej jako stała zwracające koniec zbioru

NULL - wskaźnik zerowy

BUFSIZE - bieżący rozmiar bufora strumieniowego w bajtach


OTWARCIE STRUMIENIA

Strumień przed jego użyciem musi być otwarty. Do tego celu służy jedna z funkcji: fdopen, fopen lub freopen. Po otwarciu może on być ustawiony w trybie: do odczytu, do zapisu, lub do aktualizacji, jako strumień tekstowy lub binarny. Wszystkie funkcje otwierające zwracają wskaźnik do struktury FILE, który służy do komunikowania się funkcji z konkretnym zbiorem danych
fp = fopen("name","mode")

name - nazwa zbioru

mode - tryb ustawienia zbioru

r - do czytania. Zbiór musi istnieć

w - otwiera pusty zbiór do zapisu, jeśli plik istnieje kasuje jego zawartość

a - do zapisu na końcu zbioru. Jeśli zbiór nie istnieje tworzy nowy

r+ - otwiera istniejący zbiór do zapisu i odczytu

w+ - otwiera pusty zbiór do zapisu i odczytu. Istniejący zbiór kasuje

a+ - otwiera do odczytu i zapisu na końcu. Nie istniejący zbiór tworzy

a i a+ - zapisuje zawsze na końcu mimo użycia funkcji pozycjonującej fseek

w i w+ - zawsze kasuje istniejący zbiór

b - otwiera zbiór w trybie binarnym

Funkcja zwraca wskaźnik fp lub NULL - przy błędzie otwarcia.


fp = fdopen(handle, "name")

Funkcja łączy strumień wejściowy ze zbiorem identyfikowanym przez stałą (file handle) -handle. Umożliwia wykonywanie operacji buforownych i formatowanych dla zbiorów otwartych w trybie "niskiego poziomu". Najpierw musi być otwarty taki zbiór, a potem użyte fdopen:
FILE *fp;

int fh = open("data",O_RDONLY);

fp = fdopen(fh,"r");

//operacje na zbiorze


fp = freopen(path,type,stream)

Funkcja zamyka zbiór stowarzyszony z file pointerem - stream i stowarzysza go ze zbiorem path. Jest zwykle wykorzystywany do przełączania standardowych we/wy: stdin, stdout, stdoux, stdprn na zbiór wyszczególniony przez programistę.

Zwraca wskaźnik do nowo otwartego zbioru. Jeśli powstał błąd, to zbiór jest zamykany i zwracany NULL
stdin, stdout- standardowe pliki otwierane w momencie rozpoczęcia programu

stdoux - port COM1

stdprn- drukarka

Poniższy przykład przypisuje stdout zbiorowi data2 i zapisuje linie do tego zbioru:
FILE *stream, *errstream;

main()
{
stream=freopen("data2", "w", stdout);

if(stream == NULL)

fprintf("error on freopen

");
else
{
frpintf(stream, "To będzie zapoisane do zbioru data2");

system("type data2");

}
}

STEROWANIE BUFOROWANIEM

Zbiór otwarty w/w funkcjami jest z założenia buforowany. Predefiniowane stderr i stdoux nie są buforowane z założenia, chyba że używane są w rodzinie funkcji printf, scanf, gdzie przypisane są do tymczasowych buforów. Strumienie stdin, stdout, stdprn są buforowane z założenia.

void setbuf(fp, buf)

fp - wskaźnik do otwartego zbioru

buf- tablica -char buf[BUFSIZE] dla operacji buforowanych

NULL - dla operacji niebuforowanych

Funkcja pozwala kontrolować buforowanie strumienia


int setvbuf(fp,buf,type,size)

fp i buf -jak w poprzedniej funkcji

size - rozmiar bufora

type - _IONBF bez buforowania: size i buf są ignorowane

_IOFBF pełne buforowanie

_IOLBFbuforowanie wierszy pliku tekstowego

Funkcja kontroluje buforowanie i rozmiar bufora dla strumienia

char buf[100];

size = sizeof(buf);

Zwraca 0 jeśli OK, != 0 jeśli ustalony nieprawidłowy rozmiar bufora lub tryb buforowania

ZAPIS I ODCZYT DANYCH

Funkcje strumieniowe jak już wspomnieliśmy wcześniej pozwalają przesyłać dane na różne sposoby. Można czytać lub pisać binarnie, znak po znaku liniami, lub w skomplikowanych formatach.
Operacje odczytu i zapisu odbywają się od bieżącej pozycji strumienia "file pointer" nie mylić z "FILE pointerem". File pointer zmienia pozycję pliku po zakończeniu zapisu lub odczytu. Np. jeśli czytamy jeden znak ze strumienia to file pointer rośnie o jeden i wskazuje na pierwszy nie przeczytany znak .

FUNKCJE ODCZYTU I ZAPISU FORMATOWANE WE/WY BEZPOŚREDNI ZAPIS/ODCZYT
Najprostszymi funkcjami są funkcje realizujące we/wy znakowe. Są nimi:


FORMATOWANE WE/WY

CZYTANIE ZNAKU ZE STRUMIENIA

int fgetc(fp);

int fgetchar(void);

Fgetc czyta znak ze strumienia jako unsigned char i przekształca do int. Fgetchar jest ekwiwalentem funkcji fgetc(stdin) - czyli czyta ze stdin

Zwraca znak czytany lub EOF jeśli powstał błąd lub napotkano EOF.

ZAPIS ZNAKU DO STRUMIENIA

int fputc(c,fp);

int fputchar(c);

Fputc zapisuje pojedynczy znak do zbioru. Fputchar jest ekwiwalentem fputc(c, stdout).

Zwraca pisany znak lub EOF (jak wyżej).


CZYTANIE STRINGU ZE STRUMIENIA

char *fgets(string, n, fp);

Odczytuje ze strumienia fp ciąg znaków aż do:

- pierwszego znaku '

i wstawia go do stringu dołączając na końcu '' Zwraca string lub NULL(jak wyżej).

ZAPIS STRINGU DO STRUMIENIA

int fputs(string ,fp);

Kopiuje string do zbioru fp, bez końcowego znaku ''.

Zwraca 0 jeśli OK, != 0jesli powstał błąd.

Przykład - dodaj zawartość zbioru 1 do zbioru 2.

#include
void fwriteln(FILE *fp, char *string)

{
while(*string != EOL && *string != NULL)

fputc(*string, fp);

fputc(NEW_LINE,fp);
}
#define MAX_STRING 100

void main()

{
FILE *if, *of;

CHAR STRING{MAX_STRING];


if((if = fopen("data1","r")) == NULL)

{
printf("Blad otwarcia zbioru we");

exit(1);
}
else
if((of=fopen("data2","a")) == NULL)

{
printf("Blad otwarcia zbioru wy");

exit(1);
}
else
{
while(fgets(string,MAX_STRING,if))
fwiteln(of,string);
}
fcloseall();
}

FORMATOWANY ZAPIS DO ZBIORU

int fprintf(fp, format,argument);

Funkcja formatuje i wpisuje do zbioru dane. Jest podobna do standardowej funkcji printf ma jedyni dodany fp identyfikujący zbiór.

Zwraca liczbę zapisanych znaków lub -1 gdy wystąpił błąd.


FORMATOWANY ODCZYT ZE ZBIORU

int fscanf(fp, format,argument)

Funkcja czyta dane ze zbioru od bieżącej pozycji, do argumentu w podanym formacie.
Zwraca liczbę przepisanych danych wejściowych, Eof po napotkaniu końca zbioru lub gdy wystąpi błąd.

BEZPOŚREDNIE WE/WY

ODCZYT ZE ZBIORU

size_t fread(buffer, size, count,fp)

Wczytuje do bufora co najwyżej count obiektów o rozmiarze size.

Zwraca liczbę przeczytanych obiektów (a nie bajtów), chyba że wystąpił błąd lub EOF.

ZAPIS DO ZBIORU

size_t fwrite(buffer, size,count,fp);

Zapisuje z bufora do pliku co najwyżej count obiektów o długości size.

Zwraca liczbę wpisanych obiektów, chyba że wystąpił błąd.

File pointer w przypadku powodzenia w obu operacjach przesuwa się o liczbę bajtów przeczytanych/zapisanych ze/do zbioru.

FUNKCJE DODATKOWE

int fseek(fp, offset, origin)

Funkcja przesuwa file pointer do nowego miejsca to jest o "offset" (typu long) bajtów od origin.
Origin:
SEEK_SET - początek pliku

\SEEK_CUR - bieżąca pozycja

SEEK_END koniec zbioru

Zwraca 0 jeśli OK, lub != 0 jeśli błąd.

Dla plików tekstowych fseek ma ograniczone użycie, bo zamiana CR i LF na jeden znak może spowodować , ze fseek da nieoczekiwane rezultaty.


long ftell(fp);

Podaje bieżącą pozycję pliku, liczona jako przesunięcie względem początku zbioru.
Zwraca -1L jeśli błąd


void rewind(fp);

Ustawia fp, na początek zbioru. Kasuje wskaźnik końca zbioru i błędów. Może być zastąpiona odpowiednio ustawioną funkcja fseek która jednak nie kasuje wskazań błędów i EOF

int fgetpos(fp, pos)

Zapamiętuje w pos bieżącą pozycję file pointera.

Zwraca 0 lub != 0 gdy błąd.


int fsetpos(fp, pos)

Ustawia w pos bieżącą pozycję pliku.

Zwraca 0 lub != 0 gdy błąd.


Obie ostatnie funkcje ustawiają stałą errno na:

EINVAL - nieprawidłowe FILE * (nie wskazuje na strukturę)

EBADF - wskazania na błędna strukturę lub zbiór niedostępny


ZAMYKANIE ZBIORU

PO ZAKOŃCZENIU PRACY ZAMYKAMY STRUMIENIE !!!

Z wyjątkiem predefiniowanych które automatycznie zamykane są po zakończeniu programu. Tak tez się dzieje z innymi, ale należy pamiętać, iż ilość otwartych w tym samym czasie strumieni jest ograniczona.
int fclose(fp);

int fcloseall();


OBSŁUGA BŁĘDÓW

Wiele funkcji ustawia wskaźniki stanu pliku po wystąpieniu błędu lub po napotkaniu końca zbioru. Mogą być przydatne funkcje :

clearerr(fp) - kasuje znacznik EOF i błędu dla pliku

feof(fp) - zwraca !=0 gdy ustawiony jest znacznik końca pliku itp.

Zakres tematyczny:

  1. Funkcje pomocnicze cd.

  2. Operacje we/wy niskiego poziomu.

  3. Operacje we/wy na konsoli i portach.

  4. Operacje we/wy w ujęciu obiektowym.

  1. Funkcje pomocnicze cd.

long ftell(fp);

Podaje bieżącą pozycję pliku, liczoną jako przesunięcie względem początku zbioru.
Zwraca -1L jeśli błąd


void rewind(fp);

Ustawia fp, na początek zbioru. Kasuje wskaźnik końca zbioru i błędów. Może być zastąpiona odpowiednio ustawioną funkcja fseek która jednak nie kasuje wskazań błędów i EOF

int fgetpos(fp, pos)

Zapamiętuje w pos bieżącą pozycję file pointera.

Zwraca 0 lub != 0 gdy błąd.


int fsetpos(fp, pos)

Ustawia w pos bieżącą pozycję pliku.

Zwraca 0 lub != 0 gdy błąd.


Obie ostatnie funkcje ustawiają stałą errno na:

EINVAL - nieprawidłowe FILE * (nie wskazuje na strukturę)

EBADF - wskazania na błędną strukturę lub zbiór niedostępny


ZAMYKANIE ZBIORU

PO ZAKOŃCZENIU PRACY ZAMYKAMY STRUMIENIE !!!

Z wyjątkiem predefiniowanych które automatycznie zamykane są po zakończeniu programu. Tak tez się dzieje z innymi, ale należy pamiętać, iż ilość otwartych w tym samym czasie strumieni jest ograniczona.
int fclose(fp);

int fcloseall();


Przykład - konwersja zbiorów mazow na latin

#include
#include

#define BYTE unsigned char


FILE *fi_inp, *fp_out;

BYTE mazow[18] = { 141,134,...,}; // 18 liczb >128

latin[18] = {157, 146,...,);

BYTE buf;

main()
{
int i;

clrscr();
if((fp_inp=fopen("inp_text.txt","rb")) == NULL)

{
printf("Bład otwarcia zbioru wejściowego");

exit(1);

}

if((fp_out=fopen("out_text.txt","rb")) == NULL)

{
printf("Bład otwarcia zbioru wyjściowego");

exit(1);
}
while(!feof(fp_inp))
{
fread(&buf,sizeof(BYTE),1,fp_inp);
for (i=0;i<18;i++)

if (buf==mazow[i])

{
buf = latin[i];

break;
}
fwrite(&buf,sizeof(BYTE),1,fp_out);
}
fseek(fp_out,0,SEEK_SET); //na początek zbioru

for( ; !feof(fp_out); )

{
fread(&buf, sizeof(BYTE), 1, fp_out);

putchar(buf);
}
fcloseall();
exit(0);
}

OBSŁUGA BŁĘDÓW

W początkowej fazie używania operacji we/wy pojawia się wiele błędów, dających dość nieoczekiwane efekty. Dla tego, pożytecznym jest nawyk sprawdzania poprawności wykonywanych operacji, w celu łatwiejszego wykrywania błędnych działań programu. Gdy jakaś operacja nie powiodła się, pojawia się błąd i dla danego strumienia ustawiany jest wskaźnik błędu.
Do testowania wskaźnika błędu i sprawdzania, czy w danej operacji zapisu/odczytu powstał błąd służy funkcja:

int ferror(fp);


zwraca 0 jeśli nie stwierdzono błędu, !0 w przeciwnym przypadku

Jeśli powstał błąd, to wskaźnik błędu pozostaje ustawiony aż do momentu zamknięcia strumienia, lub użycia funkcji:

void clearerr(fp);

ustawia wskaźnik błędu lub EOF na 0;

lub omówionej wcześniej funkcji :

rewind(fp);

Przykład - otwarcie do odczytu i próba zapisu

#include

FILE *fp;

char *string = "This should never be written";


main()
{
fp = fopen("data","r");

fprintf(fp, "%s

",string);
if (ferror(fp))

{
fprintf(stderr, "Write error

");
clearerr(fp);
}

2. Operacje we/wy niskiego poziomu.

Operacje tego typu - "low-level" manipulują na danych nieformatowanych i niebuforowanych. Zbiory otwarte poprzez wywołanie funkcji niskiego poziomu kojarzone są ze zmienną "file handle", całkowita zmienną poprzez która system operacyjny dociera do zbioru.

Chociaż operacje niskiego poziomu nie wymagają bibliotek , to jednak operują pewnymi stałymi np. EOF zdefiniowanymi tamże. W związku z tym, w programie używającym predefiniowane zmienne biblioteka ya musi być dołączona. Deklaracje funkcji low-level zawarte są w zbiorze

OTWARCIE ZBIORU

Podobnie jak to było dla funkcji strumieniowych, przed przystąpieniem do pracy ze zbiorem, zbiór ten należy otworzyć jedną z funkcji: open, sopen, create. Wszystkie te funkcje zwracają "file handle"
int open("nazwa",mode,pmode);

Otwiera zbiór o podanej nazwie w jednym z trybów mode:

O_APPEND - ustawia wskaźnik fp na koniec zbioru przed zapisem

O_BINARY - otwiera w trybie binarnym

O_CREATE - tworzy i otwiera nowy zbiór. Nie działa gdy zbiór istnieje.

O_RDONLY - tylko do odczytu

O_WRONLY 0 tylko do zapisu

i inne

połączenie trybów:

O_WRONLY | O_BINARY

parametr pmode działa tylko wtedy gdy ustawiony jest O_CREATE:

S_IWRITE - dozwolony zapis

S_IREAD - dozwolony odczyt

S_IWRITE | S_IREAD - dozwolony odczyt/zapis


int create("nazwa",tryb);

Funkcja zarówno tworzy nowy zbiór, lub obcina istniejący zbiór. Jeśli zbiór nie istnieje, jest tworzony i otwierany do zapisu. Jeśli istnieje, kasuje jego poprzednia zawartość i otwiera do zapisu.
pmode jak wyżej

Predefiniowane "file handle"

W momencie rozpoczynania programu do standardowych we/wy przypisywanych jest 5 "file handle":
stdin 0 stderr 2 stdprn 4

stdout 1 stdoux 3

Standardowe zbiory otwierane są automatycznie w momencie rozpoczęcia programu.

ODCZYT I ZAPIS DO ZBIORU

W przeciwieństwie do funkcji strumieniowych mamy dwie funkcje write, read. Operacje we/wy wykonywane są od bieżącego fp, który aktualizowany jest po każdej operacji zapisu i odczytu. Do testowania warunku końca zbioru może być użyta procedura eof. W razie wystąpienia błędu, operacje te ustawiają errno.

int read(fh,buf,count);

Czyta do bufora count bajtów ze zbioru skojarzonego z fh.

int write(fh,buf,count);

zapisuje z bufora do zbioru skojarzonego zfh count bajtów

Obie funkcje zwracają ilość zapisanych/odczytanych bajtów. Jeśli jest < niż count to oznacza błąd w realizacji funkcji.


FUNKCJE POMOCNICZE

long seek(fh,offset,origin)

przesuwa fp do nowej pozycji oddalonej od origin o offset bajtów.

Zwraca offset (w bajtach)


long tell(fh);

podaje bieżącą pozycję fp w zbiorze skojarzonym przez fh. Pozycja to liczba bajtów od początku zbioru.

ZAMYKANIE ZBIORU

int close(fh);

zwraca 0 gdy pomyślnie zamknie zbiór, lub -1 gdy powstał błąd.

Przykład - zapis do zbioru

#include
#include
#include
char buffer[100] = "To jest zapis do zbioru";

main()
{
int fh;

unsigned int nbytes = 100, byteswritten;


if((fh=open(a:dane.dat",O_WRONLY)) == -1)

{
perror("błąd otwarcia");

exit(1);
}
if((byteswritten =write(fh,buffer,nbytes) ) == -1)

perror(" ");

else
printf("Wrote %u bytes to file

",byteswritten);
}

UWAGA !!!

Urządzenia zewnętrzne (np.konsola) nie maja wskaźników do pliku, więc rezultat funkcji seek lub tell jest niezdefiniowany.

3. Operacje we/wy na konsoli i portach.

Funkcje wykonujące operacje we/wy na konsoli lub wyspecyfikowanym porcie są deklarowane w zbiorze . Funkcje takie jak: cgets, cscanf, getche, kbhit czytają z konsoli, podczas gdy : cprintf, cputs, putche zapisują na konsolę.

Konsola lub port nie mogą być otwierane i zamykane przed lub po wykonaniu operacji we/wy, dlatego w grupie tych funkcji nie ma funkcji open i close.

Do zapisu i odczytu pojedynczych bajtów służy funkcja inp oraz outp.

Do odczytu lub zapisu słów służy funkcja inpw i outpw.

Funkcje inp, inpw zwracają słowo lub bajt odczytany ze zbioru.

Funkcje outp, outpw zwracają słowo lub bajt zapisany do zbioru.


Przykład - zapis i odczyt z portu

#include
#include
/* odczyt z portu 0 */

unsigned int port = 0;

char result,res;

main()
{
result = inp(port);

printf("Wartość odczytana z portu %d = %d

",port,result);

port = 1;

res = outp(port,result);

printf("Wartość zapisana do %d = %d

",port,result);
}

4. Operacje we/wy w ujęciu obiektowym.

Aby móc używać poprzednio omawianych funkcji realizujących operacje we/wy w ujęciu standardowym, musieliśmy dołączać biblioteki funkcji realizujących określone operacje. Inaczej się ma w przypadku C++. Poznaliśmy już zgrubsza, co to są klasy, i jak tworzy się obiekty danej klasy (czyli jej realizację). Idąc dalej, język C++ rozszerzył biblioteki funkcji z języka C i stworzył biblioteki klas. Biblioteka klas, powstaje zwykle przez utworzenie plików nagłówkowych zawierających deklaracje klas i definicje odpowiednich funkcji składowych. Plik nagłówkowy, może być później dołączony do programu w standardowy sposób udostępniając zawarte w nim klasy.
W tym punkcie, po poznaniu pewnej minimalnej części biblioteki funkcji we/wy w języku C, przystąpimy do omawiania biblioteki klas we/wy języka C++, która zawiera klasy tworzące obiekty ułatwiające obsługę we/wy urządzenia i pliki). Poznaliśmy już wcześniej niektóre elementy systemu we/wy ( tj. << i >>), ale do poznania zostało nam jeszcze trochę.


4.1. Biblioteka iostream.

Przy omawianiu tej biblioteki spotkamy się z trzema problemami:

  1. Wprowadzanie i wyprowadzanie danych ze standardowych urządzeń we/wy jak ekran i klawiatura .

  2. Operacje na plikach danych znajdujących się na nośnikach zewnętrznych

  3. Formatowanie wewnętrzne

Aby móc skorzystać z tej biblioteki, musimy znaną dyrektywą #include dołączyć do programu pliki nagłówkowe zawierające odpowiednie deklaracje.

Jeśli dodatkowo w programie będziemy wykonywali operacje we/wy na plikach musimy dołączyć dodatkowo plik #include. Natomiast jeśli przeprowadzać będziemy formatowanie wewnętrzne, plik #include .

Niektóre realizacje biblioteki iostream takie jak Borland C++ są tak skonstruowane, że przy używaniu operacji we/wy na plikach, wystarczy dołączyć jedynie fstream.h, ponieważ on sam dołącza niezbędny mu plik iostream.h. Jeśli jednak sami go dołączymy nie spowoduje to błędu.

4.2. Pliki obiektów.

Ponieważ mamy możliwość wyświetlania na ekranie obiektów oraz pobieranie ich danych z klawiatury, sensowne jest rozszerzenie obiektów we/wy C++ również na pliki. Jak mówiliśmy, klasy obsługujące pliki zadeklarowane są w pliku fsream.h. Są to klasy:

ofstream - output file stream - zapis do pliku

ifstream - input file stream - odczyt z dysku

fstream - file stream - oba powyższe


Idee leżące u podstaw operacji we/wy na plikach w CC (użycie klas strumieni) nie różnią się za bardzo od analogicznych operacji w standardowym języku C. Jednak w typ przypadku mamy do czynienia z klasą strumienia i utworzeniem obiektu tej klasy (tzn. rzeczywistego, fizycznego strumienia) , a następnie dla otwarcia pliku posługujemy się funkcjami składowymi tego obiektu (tj. strumienia). Gdy otwieramy plik przy pomocy funkcji składowej strumienia open(), następuje automatyczne powiązanie pliku z tym strumieniem. Zobaczmy jak to się dzieje na przykładzie:

4.2.1. Zapis pliku w postaci ASCII.

#include
#include

/* przykład operacji otwarcia , zapisu zamknięcia pliku */

void main(void)

{
ofstream my_file; (1)


my_file.open("Hello.dat",ios::out,0); (2)

my_file << "Hello guy

"; (3)

my_file.close(); (4)

}

Gdy otwieramy plik do zapisu posługujemy się klasą ofstream.

  1. Zaczynamy od utworzenia małego pliku o nazwie "hello.dat", który będzie zawierał tylko napis "hello guy". Aby to wykonać, potrzebny będzie obsługujący zapis strumień klasy ofstream (1). Strumień ten nazwaliśmy my_file. Potrzebny będzie wtedy, gdy zaczniemy wykonywać na nim operacje.

  2. Tworzymy rzeczywisty plik dyskowy o nazwie "hello.dat". Do tego celu w klasie ofstrem przewidziano funkcję składową open() (2). W ten sposób połączyliśmy strumień my_file z plikiem dyskowym (strumień ten jest jak gdyby fp lub fh w klasycznym C).

void open(char * nazwa, int tryb,int atrybut_pliku);

tryb:
io::app - dopis danych do pliku

io::ate - przejście na koniec pliku

io::in - otwarcie pliku do odczytu

io::nocreate - jeśli plik nie istnieje nie otwieraj go

io::out - otwarcie pliku do zapisu

io::trunc - obcięcie długości pliku do zera, następnie jego otwarcie


atrybut pliku:

0 - czysty tekst

1 - tylko odczyt

2 - plik ukryty

4 - plik systemowy

8 - ustawienie bitu archiwum


W powyższym przykładzie, otworzony został plik tekstowy (atrybut pliku = 0) do zapisu (ios::out). Ponieważ parametry te są domyślne, można tą linie zapisać jako:

my_file.open("Hello.dat");
Możemy zrobić znacznie więcej. Ponieważ połączenie pliku ze strumieniem odbywa się w momencie otwarcia pliku, dwie pierwsze linie możemy połączyć w jedna :

ofstream my_file("Hello.dat");

przesyłając do konstruktora strumienie te same parametry co do funkcji open(). Uprościmy więc program do postaci:


ofstream my_file("Hello.dat");


my_file << "Hello guy

";

my_file.close();
Inaczej mówiąc możemy jednocześnie zadeklarować obiekt i powiązać go z plikiem dyskowym.

  1. Teraz możemy wysłać do niego dane (3). Robimy to w znany sposób. Operator << może być przeciążony.

  2. Teraz nie pozostaje nic innego jak zamknąć plik (4). Możemy to zrobić za pomocą funkcji składowej klasy ofstream close():

      1. Odczyt pliku w formacie ASCII.

#include
#include
#include

/* Odczyt pliku ASCII */


void main(void)

{
char napis1[20], napis2[20]; (1)


ifstream my_file("Hello.dat"); (2)


my_file>>napis1; (3)

my_file>>napis2; (3)


Mamy już na dysku plik. Możemy więc go np. odczytać. Zapisaliśmy go jako łańcuch, posługując się przeciążonym operatorem <<.

Przy odczycie tego łańcucha przy pomocy operatora >> odstęp między słowami będzie traktowany jako znak spacji dlatego do pobrania dwóch wyrazów potrzebny jest dwukrotny odczyt z pliku (1) i (3).
1. Deklarujemy więc dwa buforowe łańcuchy (1).

  1. Otwieramy plik przy pomocy funkcji składowej klasy tym razem ifstream open(). (2)

  2. Odczytujemy zawartość strumienia (3).


Należ zwrócić uwagę na to iż w przypadku operacji na plikach tekstowych, kompilator C++ dokonuje różnych przekształceń i interpretacji. Z jedną mieliśmy już do czynienia. Teraz omówimy jeszcze jeden przypadek. Znak '

' przy zapisie zostaje zamieniany na dwa znaki: ' ' oraz ''. Przy odczycie zachodzi przekształcenie odwrotne. Nie stało by się to w przypadku, gdy potraktowalibyśmy nasze dane jako binarne. W typ przypadku nie zachodzą żadne przekształcenia.


4.2.3. Zapis i odczyt pliku w postaci binarnej.

#include
#include
#include

void main(void)

{
char znak, napis[]="Hello guy";

int i = 0;


/* Zapis pliku binarnego */


ofstream my_file_out("Hello.dat");


while (napis[i])

my_file_out.put(napis[i++]);
my_file_out.close();

/* Odczyt pliku binarnego */


ifstream my_file_inp("Hello.dat");


while (my_file_inp)

{
my_file_inp.get(znak);
cout<<ZNAK;
}
my_file_inp.close();

getch();
}
Dwoma najważniejszymi funkcjami składowymi strumienia pliku dla operacji we/wy na plikach binarnych są get() i put():

int stream.get(char c);

Odczytuje ze strumienia znak i zapisuje do zmiennej c. Odczytuje nawet spacje (gdy odczytuje z cin) podczas gdy cin>>c tego nie odczyta. Zwraca int, aby oddać EOF przy wyjęciu tego znaku ze strumienia. Inną formą tej funkcji jest:

void stream.get(char c);


void stream.put(char &c);

wstawia do strumienia jeden znak.

Obie te funkcje operują na pojedynczych bitach nie interpretując jednak ich znaczenia. Przykładowy program w pierwszej kolejności otwiera plik do zapisu. Po czym przy pomocy funkcji put() zapisuje łańcuch "hello guy" znak po znaku. Po zamknięciu tego pliku jest on ponownie otwarty tym razem do odczytu. Utworzony strumień my_file_inp połączony jest w tej chwili z rzeczywistymi danymi zawartymi w pliku Hello.dat. Funkcja get odczytuje znak po znaku zawartość pliku aż do wyczerpania jego zawartości. Po osiągnięciu końca pliku strumień zwraca sam 0 więc testowanie końca pliku można przeprowadzić tak jak w przykładzie, lub np.:
while(my_file_inp.get(znak))
ponieważ get zwraca strumień na którym pracuje.

Innym sposobem śledzenia osiągnięcia końca pliku jest użycie funkcji int eof(), zwracającej tru gdy znajdujemy się na końcu pliku, false w przeciwnym przypadku.

Chociaż powyższe funkcje są dobre, to jednak nie umożliwiają one zapisu i odczytu obiektu.
Omówimy teraz funkcje write, read pozwalające odczytać i zapisać bloki danych, a nie pojedyncze bajty.

4.2.4. Zapis i odczyt pliku z użyciem funkcji read - write.

stream.read (unsigned char *bufor, int ilość_znaków);

stream.write(const char *bufor, int ilość_znaków);


#include
#include
#include
#include

void main(void)

{
char bufor[]="Hello guy"; (1)


/* Zapis pliku binarnego */


ofstream my_file_out ("Hello.dat"); (2)

my_file_out.write(bufor,sizeof(bufor));

my_file.close();
/* Odczyt pliku binarnego */

ifstream my_file_inp("Hello.dat");

my_file_inp.read(bufor,sizeof(bufor));

my_file_inp.close();

getche();
}
Posługując się funkcjami put i get musieliśmy operować poszczególnymi bajtami. Używając funkcji write-read operacje te wykonujemy niejako za jednym zamachem przekazując wskaźnik do łańcucha (czyli nazwę tablicy), oraz ilość bajtów które należy przesłać. Funkcji tych możemy używać także do odczytu i zapisu obiektów.

      1. Zapis i odczyt obiektów.

Niech dana będzie klasa:

class bank{

public:
char nazwisko[20];

float suma;

};
Poniższy program zapisze do pliku max.15 obiektów tej klasy:


#include
#include
#include
#include
#define INDEKS 15

class bank{

public:
char nazwisko[20];

float suma;

}dane[INDEKS];

/* Zapis tablicy obiektów */

void main(void)

{
ofstream my_file("DANE.dat");


strcpy(dane[0].nazwisko,"Kowalski");
dane[0].suma = 100.8;

strcpy(dane[1].nazwisko,"Kowal");
dane[1].suma = 1990.8;

.....................
my_file.write((char *)dane, INDEKS * sizeof(bamk));

my_file.close;
}

#include
#include
#define INDEKS 15


class bank{

public:
char nazwisko[20];

float suma;

}dane[INDEKS];

/* Odczyt z pliku tablicy obiektów */

void main(void)

{
ifstream my_file("DANE.dat");

my_file.read((char *)dane, INDEKS * sizeof(bamk));


cout<< dane[0].nazwisko <<DANE[0].SUMA;

cout<< dane[1].nazwisko <<DANE[1].SUMA;

.....................
my_file.close;
}

4.2.6. Informacje o pozycji wskaźników do pliku.

Często jest tak, że chcemy z pliku odczytać jeden konkretny obiekt. Np.: chcemy odczytać 9-ty obiekt z poprzedniego przykładu. Nie musimy w tym celu czytać całego pliku i dopiero po tym wyselekcjonować interesującej nas informacji.

Zdefiniowano bowiem dwa wskaźniki get i put. Położenie wskaźnika get określa dane które zostaną odczytane z pliku, natomiast położenie wskaźnika put informuje nas gdzie zostaną zapisane dane przy najbliższej operacji zapisu. Do ustawiania tych wskaźników służą funkcje:


stream.seekg( pozycja w pliku,seek_dir)

pozycjonuje wskaźnik get

stream.seekp(pozycja_w_pliku, seek_dir)

pozycjonuje wskaźnik put

pos: pokazuje na który bajt ma wskazać wskaźnik względem Seek_dir

seek_dir:
ios::beg - początek pliku

ios::end - koniec pliku

ios::cur - bieżąca pozycja pliku.


#include
#include
#define INDEKS 15


class bank{

public:
char nazwisko[20];

float suma;

}dane[INDEKS],record;

/* Odczyt z pliku 9-tego obiektu */

void main(void)

{
ifstream my_file("DANE.dat");


my_file.seekg(9*sizeof(bank),ios:beg);
my_file.read((char *)&rekord, INDEKS * sizeof(bamk));


cout<< rekord.nazwisko <<REKORD.SUMA;

.....................
my_file.close;
}

4.2.7. Inne pomocnicze funkcje.

strempos pos = tellg(void)

pokazuje pozycje wskaźnika do czytania get


streampos pos = tellp(void)

pokazuje pozycje wskaźnika do zapisu put

Zakres tematyczny:

  1. Operatory języka C/C++.

  2. Typy zmiennych.

  1. Operatory języka C/C++

Język C/C++ w porównaniu z innymi językami dostarcza programiście znaczną liczbę różnorodnych operatorów, umożliwiających bardzo efektywny zapis algorytmów. Aby jednak uniknąć późniejszych bardzo frustrujących i trudnych do wykrycia błędów, należy poświęcić nieco czasu na zapoznanie się dokładnie z tymi operatorami i zasadami konstrukcji wyrażeń. Duża różnorodność tych operatorów decyduje właśnie o unikalności języka C. Bo czy w innych językach programowania byłaby dopuszczalna sekwencja jak najbardziej prawidłowa w języku C:?
a = i +++j;

++x*=(a!=0)?a:b++;
Jak widać, wyrażenia w języku C/C++ są unikalne i nie przypominają Państwu nic wcześniej poznanego (chociażby z Pascala).

Kluczową rolę w konstrukcji wyrażeń w każdym języku odgrywają operatory. To właśnie one określają, co w jaki sposób i w jakiej kolejności zostanie obliczone. Operatory należą do jednej z dwu grup:

Język C++ zawiera wszystkie operatory języka C, a ponadto dołącza jeszcze pięć charakterystycznych dla siebie operatorów. W dalszej części wykładu omawiać będziemy kolejno wszystkie operatory.


Operatory arytmetyczne

- operatory : + - * /

nie wymagają szczegółowego wyjaśnienia. Należy tylko dodać, że należą one do grupy operatorów binarnych (ponieważ operują na dwóch obiektach - operandach).

Jest on podobnie jak poprzednie operatorem binarnym. W wyniki działania tego operatora otrzymujemy resztę z dzielenia operandów stojących po obu stronach operatora:
45 % 6 -> 3

Operatora tego, nie można stosować dla typów: float i double. Dla ujemnych argumentów operacji kierunek zaokrąglania wyniku jak i znak liczby która jest wynikiem dzielenia modulo zależne są od maszyny.
Priorytet operatorów *, /, % jest wyższy niż operatorów +, -.

Te operatory też są znane. Operator + właściwie nic nie robi, natomiast operator - zmienia wartość danego wyrażenia na przeciwną :

-(2*a + b)

Pamiętajmy, że nie chodzi tu o żadne odejmowanie, ale o operację stworzenia liczby przeciwnej.
- operatory inkrementacji i dekrementacji

Operacje zwiększania i zmniejszania o jeden, występują w programowaniu niezmiernie często. Ze względu na to, większość procesorów wykonuje ją w jednym rozkazie. Język C/C++ aby to zrealizować, zapewnił osobny operator umożliwiający wykonanie tych działań. Z wyrażeniami np.:
i = i +1;

i = i - 1;

mamy szczególnie często do czynienia w pętlach. Można je dla prostoty zastąpić poprzez wyrażenia:
i++;
i--;
Operatory dekrementacji i inkrementacji mogą mieć dwie formy:

W obu przypadkach wynikiem jest zmiana wartości i, ale wyrażenie ++i zwiększa i przed użyciem jej wartości, natomiast wyrażenie i++ zwiększa zmienną i dopiero po użyciu jej poprzedniej wartości. W kontekście, w którym ważna jest wartość zmiennej i, a nie tylko jej zmiana, wyrażenia ++i oraz i++ są różne np.:

i = 10;

x = i++;

cout<<" X = "<<X;

daje w rezultacie X = 10, ale

i = 10;

x = ++i;

cout<<" X = "<<X;

daje w rezultacie X = 11.


!!! Zadanie:

int m=3,n=1,r;

r = (m++) + (++n);

Jaka jest wartość r? ( r = 5);


Ponieważ są to operatory unarne (jednoargumentowe) to wyrażenie :

(i+j)++

jest błędne;

W przypadku, gdy chodzi tylko o sam efekt zwiększania o jeden, operatory post i prefiks-owe są równoważne.
- operator przypisania =

Powoduje on, że do obiektu stojącego po lewej stronie operatora przypisania zostaje zapisana wartość wyrażenia stojącego po prawej stronie. Jest to operator dwuargumentowy. Z założenia, operandy stojące po obu stronach powinny mieć taki sam typ. Jeśli natomiast nie mają, to jeśli to jest możliwe wykonana zostaje niejawna konwersja typów. O konwersji typów mówić będziemy osobno po omówieniu pozostałych typów zmiennych.

Operatory logiczne

Są to operatory: > >= < <=

W wyniku działania tych operatorów otrzymujemy odpowiedź: prawda (true - 1) lub fałsz (false - 0). Priorytet tych operatorów jest taki sam .

Są to operatory == !=.

Ich priorytet jest niższy niż priorytet operatorów relacji

Obie te grupy operatorów znalazły zastosowanie przy sprawdzaniu warunków np. w przypadku instrukcji if. Bardzo częstym błędem jest w przypadku używania operatorów przypisania pisanie jednego znaku = . Może to doprowadzić do powstania błędu, który niekoniecznie musi być wychwytywany przez kompilator (błąd logiczny). Weźmy np. fragment programu:
Przykład 1

#include
main()
{
int x = 3, x1 = 10;

if(x = x1) //błąd - przypisanie - nawias = 100

// 100 != 0 - czyli wynik - TRUE

cout<<"x = x1";

else
cout<<"x != x1);

return 0;

}
W efekcie program twierdzi, że 3 = 10. Co za absurd. Niektóre kompilatory między innymi i Borland C++ wychwytują takie błędy, gdy po if występuje operacja przypisania, a nie operator przyrównania. Tak na wszelki przypadek.

Są to operatory:

|| - realizujący sumę logiczną (LUB - alternatywa)

&& - realizujący iloczyn logiczny (I - koniunkcja)

Przykład 2

#include
main()
{
int ch;

ch = getchar();

if((ch >='a' && ch <='z')) || (ch >='A' && ch <='Z'))

cout<<"wprowadzony znak jest literą";

else
cout<<"Wprowadzony znak jest innym znakiem";

return 0;

}
Przypomnijmy, że wynik wyrażenia logicznego "prawda" daje 1, a "fałsz" daje 0. Wyrażenia logiczne obliczane są od lewej do prawej. Jest zasada, że komputer oblicza wartości wyrażeń dopóty, dopóki nie jest pewien wyniku. Weźmy np. fragment kodu:

(x1 == 0) && (x1 == 15) && (z>10)

Komputer oblicza wartość wyrażenia od lewej do prawej. Jeśli zdarzy się taki przypadek, że już pierwsze wyrażenie jest nieprawdziwe, to komputer nie oblicza dalej, ponieważ jest już pewien, że taka koniunkcja nie może być prawdziwa. Na tą cechę operacji trzeba bardzo uważać np.:
int i = 6,d = 4;

if((i>0) && (d++))

{
...............
}
W instrukcji tej nie tylko wykonywana jest operacja logiczna , ale także chcemy zwiększyć wartość zmiennej d. Jest to swego rodzaju pułapka. Bo gdyby jak mówiliśmy, wartość (i>0) nie była prawdziwa to nie dojdzie do zwiększenia d, na czym nam mogło szczególnie zależeć. Podobne pułapki mogą dotyczyć i alternatywy zgodnie z zasadami matematyki.

Priorytet operatora && jest większy niż operatora ||, ale oba są niższe niż operatorów przyrównania i relacji, więc w wyrażeniu

i < gr - 1 && (c = getchar()) != '

' 77 c!= EOF

nie potrzeba dodatkowych nawiasów. Ponieważ jednak priorytet operatora != jest wyższy niż przypisania, więc w zwrocie (c=getchar()) != '

potrzebne są nawiasy. Najpierw bowiem ma zostać przypisana wartość zmiennej c, a potem dopiero przyrównana do '

'.
- operator negacji !

Jest to operator jednoargumentowy i stoi zawsze po lewej stronie operandu:

!n
Jeśli n = 0, to wartością wyrażenia jest PRAWDA.

Instrukcja: if(!n) jest równoważna instrukcji: if(n == 0)

Przykład 4

int i = 0;

if(!i)
cout<<"Wartość i jest zerowa";


Operatory bitowe

Operatory te są charakterystyczne dla sposobu przechowywania informacji w komputerze (w postaci binarnej). Wiadomo, że w komputerze informacja gromadzona jest w postaci zero-jedynkowej. Te elementarne jednostki informacji (0 lub 1) nazywane są bitami. Komputer jednak nie odnosi się najczęściej do poszczególnych bitów, lecz gromadzi je w słowa, najczęściej 2 bajtowe. Informacje zapisane w słowie nie koniecznie muszą oznaczać konkretnych liczb. W jednym słowie można przechowywać szereg informacji na poszczególnych bitach słowa. Do pracy na poszczególnych bitach słowa służą właśnie operatory bitowe:

<< - przesunięcie w lewo

>> - przesunięcie w prawo

& - bitowy iloczyn logiczny

| - bitowa suma logiczna

^ - bitowa różnica symetryczna (bitowe exclusive OR)

~ - bitowa negacja

Jest to operator unarny, operujący na operandach całkowitych (niezależnie od znaku):
zmienna << ile miejsc

Służy do przesuwania bitów operandu stojącego po lewej stronie operatora o liczbę pozycji określoną przez drugi operand (jego wartość musi być dodatnia). Zwolnione bity zostają uzupełnione zerami:

int x = 0x1010

int wynik;

wynik = x << 2;

Powyższy fragment programu przesunie bity liczby x o 2 miejsca w lewo:

x = 0001 0000 0001 0000

wynik = 0100 0000 0100 0000

W powyższym przykładzie sam obiekt - zmienna x nie została zmieniona. Posłużył on jako wartość początkowa, a rezultat operacji został przechowany w zmiennej wynik.

Jeśli chodzi nam o to, aby przesunąć bity w danej zmiennej i tam też tą operację zapisać, to

działamy podobnie jak w przypadku wyrażenia : a = a + 5, czyli:

x = x << 2.

Operacja ta ( x<<2) jest równoważna pomnożeniu zmiennej przez cztery:

x 0001 = 1

x<<2 0100 = 4

Podobnie jak poprzedni operator, działa jedynie na operandach całkowitych. Przesuwa bity operandu stojącego po lewej stronie operatora o ilość bitów wskazaną przez operand prawy.
Bity z prawego brzegu są gubione. Jest jedna cecha tego operatora, która różni go od poprzedniego:
Jeśli operator pracuje na danej unsigned( bez znaku) , lub signed( ze znakiem) ale jest to liczba ujemna, to bity z lewego brzegu są uzupełnione zerami lub jedynkami zależnie od implementacji.
unsigned int x = 0x0ff0;

unsigned int wynik;

wynik = x >>2;

x 0000 1111 1111 0000

wynik 0000 0011 1111 1100

signed int f = 0xff00;

signed int wynik;

wynik = x>>2;

x 1111 1111 0000 0000

wynik 0011 1111 1100 0000

wynik1 1111 1111 1100 0000

Implementacja Borland C++ daje wynik1.

Operatory te także działają na operandach całkowitych.

int x1 = 0x0f0f;

int x2 = 0x0ff0;

x1 0000 1111 0000 1111

x2 0000 1111 1111 0000

x1 & x2 0000 1111 0000 0000 zasłania pewną grupę bitów

x1 | x2 0000 1111 1111 1111 służy do ustawiania bitów

x1 ^ x2 0000 0000 1111 1111 tam gdzie bity operandów są takie same ustawia 0, a gdzie różne 1 Operacja XOR( różnica symetr.)

~ x1 1111 0000 1111 0000 zamienia bit 1 na 0 i odwrotnie.

Teraz podam Państwu przykład zastosowania operatorów bitowych w konkretnym prostym programie:

Przykład 5

/* funkcja getbits wycina n bitów ze zmiennej x od poz. p */

unsigned getbits(unsigned x, int p, int n)

{
return(x >> (p+1-n)) & ~(~0<<N);

}
Funkcja zwraca n -bitowe pole wycięte ze zmiennej x od pozycji p, dosunięte do prawej strony wyniku. Wyrażenie x>>(p+1-n) dosuwa wybrane pole do prawego końca słowa. Wyrażenie ~0 oznacza same jedynki; przesunięcie ich w lewo o n pozycji bitowych (~0<

Należy pamiętać, że wynikiem działania operatorów logicznych jest wynik "prawda lub fałsz". Kompilatora nie interesuje analiza poszczególnych bitów, sprawdza tylko, czy jest w operandach zapisana 0 czy != 0. Na tych dwóch wartościach typu TRUE , FALSE dokonuje koniunkcji, a wynik jest = 0 lub 1.

Natomiast operatory bitowe wkraczają do wnętrza słowa , analizują poszczególne bity. Na nich dokonują np. koniunkcji bit po bicie i przepisują wynik 0 lub 1 na odpowiadający im bit zmiennej wynikowej. W rezultacie otrzymujemy wynik, będący specyficznym układem bitów, które można traktować jako liczbę, której wartość można np. wydrukować:

cout<< (x1 & x2); // = 3840

Są to bardzo proste operatory. Wyrażeniem podobnym do:

i = i + 2;

jest bardziej zwięzły zapis:

i + = 2;

Taki zapis można stosować dla większości operatorów binarnych. Czyli np.:

+=, -=, *= itd.

Jednak takie skondensowane zapisy dla początkujących mogą być źródłem wielu błędów logicznych wynikających z błędnego wykorzystania tych operatorów. I tak np.:
x *= y + 1;

jest odpowiednikiem wyrażenia:

x = x * (y+1);

a nie : x = x * y + 1;

Działa tu bowiem zasada:

wyr1 op = wyr2 to samo co wyr1 = (wyr1) op (wyr2)

Operatory te w przypadku skomplikowanych wyrażeń umożliwiają czytelniejszy zapis, poza tym pomagają kompilatorowi generowanie efektywniejszego kodu wynikowego, zmniejsza ilość koniecznych do wykonania operacji z dwóch do jednej.

Podam teraz przykład zastosowania operatorów w funkcji zliczającej bitowe jedynki argumentu całkowitego.

Przykład 6

int bitcount( unsigned x)

{
int b;

for(b = 0; x!=0; x>>=1)

if(x & 01)

b++;
return b;

}
Zadeklarowanie argumentu jako unsigned upewnia, że podczas przesuwania w prawo miejsca uzupełniane są zerami nie bitem znaku liczby.


Wyrażenie warunkowe

Widzimy, że język C idąc w kierunku zapewnienia efektywności kodu wynikowego programu, stosuje bardzo wiele skrótów. Innym skrótem jest badanie warunków przy pomocy wyrażenia warunkowego.
Instrukcja :

if( a > b)

z = a;

else
z = b;

może być zastąpiona trzyargumentowym operatorem " ? : " :

z = (a>b) ? a : b;

Nawiasy w wyrażeniu warunkowym nie są konieczne, ze względu na bardzo niski priorytet operatora ?. Ale lepiej je stosować dla uwypuklenia tego wyrażenia. Ogólnie można wyrażenie warunkowe zapisać:

wyr1 ? wyr2 : wyr3;

Wyr2 i wyr3 powinny mieć taki sam typ, jeśli nie to wykonywane jest przekształcenie typów.
Wyrażenia warunkowe często wpływają na zwięzłość programu np.:
printf("Masz %d częś%s.

",n,n ==1 ? "ć" : "ci");

lub:
for(i = 0; i < n; i++)

printf("%d6dc",a[i],(i%10 == 9 || i == n-1) ? '

' : ' ');

Znak nowego wiersza w powyższym przykładzie wypisywany jest po każdym dziesiątym elemencie tablicy, natomiast po innych wstawiany jest znak spacji.


Przekształcanie typów

Jeżeli argumentami operatora są obiekty różnych typów, to są one przekształcane do jednego wspólnego typu według kilku reguł. Ogólna zasada mówi że: automatycznie wykonuje się tylko takie przekształcenia, w których argument "ciaśniejszy jest zamieniany na obszerniejszy bez utraty informacji: np. zamiana całkowitej na zmiennopozycyjną.

Niedozwolone jest indeksowanie tablic zmiennymi float ( nie odbywa się konwersja).
Wyrażenia w których może wystąpić utrata informacji, jak np. przypisanie wartości o dłuższym typie całkowitym zmiennej krótszego typu, czy po zmianie zmiennopozycyjnej na całkowitą, mogą powodować wypisanie ostrzeżenia, ale nie jest niedozwolone.

Obiekty char są krótkimi liczbami całkowitymi, zapewniającymi znaczną elastyczność przy różnego rodzaju przekształceniach.

Przykład 7

int atoi(char s[])

{

int i,n;

n = 0;

for(i = 0;s[i] >='0' && s[i]<='9'; ++i)

n = 10 * n + (s[i] - '0'); // konwersja char na int;

return ;

}
Wyrażenie s[i] - '0' daje numeryczną wartość cyfry zawartej w s[i].

Ogólnie konwersja wykonywana jest w następujący sposób (zasady powinny być stosowane wg kolejności wymieniania):

Sposoby wykonywania konwersji

Oprócz automatycznego przekształcania typów, w dowolnym wyrażeniu można jawnie wymusić przekształcenie typów za pomocą:

Operator ten może mieć dwie formy:

(nazwa_typu) zmienna //forma języka C/C++

nazwa_typu(zmienna) //forma C++

Operator ten upewnia niejako komputer, że rzeczywiście chcemy przeprowadzić "niedozwoloną"

operacje konwersji np.: int do char;


Przykład 8

/* rand : generowanie losowej liczby całkowitej z przedziału 0...32767 */

unsigned long int next = 1

int rand (void)

{
next = next * 1103515245 + 12345;

return(unsigned int)(next/65536) % 32768;

}
/* srand zarodek dla f-cji rand */

void srand(unsigned int seed)

{

next = seed;

}

Operator sizeof()

Jak wspomnieliśmy przy okazji omawiania typów podstawowych, rozmiary niektórych zmiennych są uzależnione od implementacji.

Poza tym, język C++ wprowadził możliwość definiowania przez użytkownika własnych typów obiektów. Często ważne jest znanie rozmiarów tych typów.

Do tych celów zaimplementowano w języku C/C++ bardzo wygodny operator: sizeof(), podający rozmiary:

a) typów zmiennych sizeof(nazwa_typu)

b) obiektów sizeof(nazwa_obiektu)


Przykład 9

#include
main()
{
int tab_of_int[10];

cout <<"Podaj rozmiar typu int " << sizeof(int);

cout<<"
A teraz podaj rozmiar tablicy int-ów tab_of_int " << sizeof(tab_of_int;

return 0;

}

Operator przecinek

Ostatnim operatorem języka C/C++ jest przecinek. Jeśli kilka wyrażeń stoi obok siebie i są oddzielone przecinkami, to całość też jest wyrażeniem, a wartością tego wyrażenia jest wartością prawego argumentu.

(2+4, a*4, 3<6, 77+2) wart.wyrażenia: 79

Poszczególne wyrażenia obliczane są od lewej do prawej.

Jest on np. stosowany w pętli for do równoległego sterowania dwoma indeksami np.:

Przykład 10

void reverse(char s[])

{
int c,i,j;

for(i = 0, j = strlen(s)-1; i<J;I++,J--)

{
c = s[i];

s[i] = s[j];

s[j] = c;

}
Funkcja ta odwraca kolejność znaków argumentu s.

Przecinki oddzielające argumenty funkcji nie są operatorami i nie gwarantują obliczeń od lewej do prawej strony.

Wyrażenia przecinkowe byłyby odpowiednie dla powyższej funkcji, którą można przepisać jako:
for(i = 0, j = strlen(s)-1; i<J;I++,J--)

{
c = s[i], s[i] = s[j], s[j] = c;

Poszczególne wartości wyrażeń będą obliczane od lewej do prawej, a wynikiem wyrażenia jest wyrażenie s[j] = c; o co nam przecież chodziło.


Priorytet i łączność operatorów

Podsumujemy teraz wszystkie operatory pod kątem ich priorytetów (ważności kolejności obliczeń).
Priorytety(od największego) podaje zestawienie:

15. () [] -> L

14. ! ~ ++ -- + - * & sizeof p // adres i wskaźnik

13. * / % L

12 + - L

11. << >> L

10. < <= > >= L

9. == != L

8. & L

7. ^ L

  1. | L

5. && L

4. || L

3. ?: P

2. = += -= *= /= %= ^= |= <<= >>= P

1. , L

Pamiętanie priorytetów (przynajmniej wszystkich) nie jest konieczne. Wystarczy tam, gdzie mamy wątpliwości wstawić nawiasy nie tylko dlatego, że mają one bardzo wysoki priorytet, ale również dla czytelności wyrażenia.

Litery L i P określają w jaki sposób grupowane jest wykonywanie wyrażenia i tak np.:

dla wyrażenia z operatorem + (L)

a + b + c + d + e

lewostronna łączność oznacza:

((((a + b) + c) + d) + e)

natomiast dla wyrażenia z operatorem = (P)

a = b = c = d = e

oznacza:

(a = ( b = (c = (d = e))))

Zakończyliśmy już omawianie operatorów języka C/C++, a teraz zgodnie z obietnicą pomówimy o typach pochodnych. Zanim zaczniemy, uzupełnimy swoją wiedzę o dodatkowy typ zarezerwowany dla liczb całkowitych.


Typy wyliczeniowe enum

Jak wspomnieliśmy, jest to typ zarezerwowany dla liczb całkowitych i przydać się może w wielu sytuacjach.
Często zdarza się tak, że w obiekcie typu całkowitego chcemy przechowywać nie tyle liczbę, co pewien rodzaj informacji. Wtedy korzystamy z typu wyliczeniowego. Wyjaśnijmy to na przykładzie. Za pomocą liczb określimy działanie pewnego urządzenia (pralki). Określamy sobie pewną zmienną np. działanie, do której będziemy wstawiali liczbę określającą daną akcje. Niech akcje te będą miały następujące nazwy i kody:

0 - start

1 - wciąganie wody

2 - podgrzewanie wody

3 - płukanie

4 - wirowanie

5 - wyłączenie

typ wyliczeniowy definiujemy następująco:

enum nazwa_typu {lista wyliczeniowa}

Dla naszego przypadku definicja będzie wyglądała następująco:

enum pralka {

W ten sposób zdefiniowaliśmy nowy typ o nazwie pralka. A teraz zdefiniujemy zmienną tego typu:
pralka działanie;

W zmiennej działanie może znajdować się tylko wartość określona w liście wyliczeniowej pralka. Sposób użycia tych zmiennych jest następujący:

działanie = start;

działanie = płukanie;

błędem jest natomiast podstawienie:

działanie = 0;

działanie = 3;

Na liście wyliczeniowej znajdują się liczby. Jednak mimo, że funkcje pralki reprezentowane są przez liczby, to nie mogliśmy ich wstawić do zmiennej działanie. Jest to bardzo ważna cecha, gdyż nawet przez nieuwagę nie mogliśmy do zmiennej działanie wstawić czegoś innego, nawet gdyby to coś przez przypadek pasowało jako wartość liczbowa.

Brak określeń liczbowych w liście nie jest błędem, ponieważ wstawione by tam były przez domniemanie. Np.:

enum miesiąc {

styczeń,
luty,
marzec = 2,

..................
grudzień = 11

};
Przez domniemanie kompilator zakłada, że wyliczenie ma zacząć się od 0. Poza tym, reprezentacje liczbowe mogą być dowolne, bo nie do nich odwołujemy się, tylko do elementów listy.

2. Typy pochodne zmiennych.

Powstają z typów podstawowych. Oznacza się je stosując nazwę typu od którego pochodzą i operator deklaracji typu pochodnego. Tych operatorów jest cztery, tak jak cztery typy pochodne:
[] - tablica obiektów danego typu. Tablica to inaczej macierz, albo wektor obiektów danego typu.
* - wskaźnik do pokazywania na obiekt danego typu. Wskaźnik to obiekt, w którym można umieścić adres jakiegoś innego obiektu w pamięci

() - funkcja zwracająca wartość danego typu. Jeśli stoi zamiast typu słówko void np. void fun() to oznacza, że funkcja nie zwraca wartości. Funkcja czyli podprogram.

& - referencja obiektu danego typu. To jakby przezwisko jakiegoś obiektu. Dzięki referencji na tę samą zmienną można mówić używając jego drugiej nazwy.

Przykłady typów podstawowych i pochodnych:

int a; int tab[10];

short int b; float *p;

float x; char func();

Mimo bardzo skomplikowanego zapisu użycie tych typów pochodnych nie jest trudne. Teraz po kolei będziemy omawiać poszczególne typy pochodne.


Tablice
Jeśli mamy do czynienia z grupą zmiennych tego samego typu, to można z nich zrobić tablicę. Jest to ciąg obiektów tego samego typu, zajmujących ciągły obszar w pamięci. Taka cecha jest doskonała, bo zamiast odnosić się do każdej zmiennej z osobna, odnosimy się do n-tego elementu tablicy. Ze sposobem deklarowania tablic spotkaliśmy się już wcześniej.
Rozmiar tablicy musi być wartością stałą, znaną już na etapie kompilacji, dlatego na sztywno musimy podawać ilość elementów tablicy, aby wiedzieć ile miejsca zarezerwować na tablicę w pamięci. W związku z tym rozmiar ten nie może być ustalony dopiero w trakcie działania programu. Jeśli jednak zachodzi konieczność definiowania " rozmiaru tablicy w czasie programu" ,to wtedy używamy tzw. dynamicznej alokacji pamięci, a do zmiennych odwołujemy się poprzez adres.
Tablice można tworzyć z:

Numeracja tablic w języku C/C++ zaczyna się od zera.

Jeśli zadeklarowaliśmy tablicę:

float tablica[3];

to jest to zbiór trzech elementów typu float: tablica[0], tablica[1], tablica[2]. Może to być pewnym utrudnieniem dla tych z Państwa, którzy programowali w Pascalu, gdzie numeracja tablic była naturalna od 1. Próba zapisania czegoś do elementu tablica[3] nie będzie sygnalizowane jako błąd, gdyż język C/C++ tego nie sprawdza. Zapis taki powoduje zniszczenie czegoś co jest zapisane bezpośrednio za tablicą i co nam może być jeszcze przydatne. Sposób pracy na tablicach jest bardzo prosty. Zilustrujemy to prostym przykładem.


Przykład 11

/* Program zliczający wszystkie znaki stringu wprowadzonego z klawiatury */

#include
#include
#inclued
main()
{
char string[20];

int licznik = 0;

clrscr();
gets(string);
while(string[licznik] != NULL)

licznik ++;

cout<<"Liczba znaków w stringu: "<<LICZNIK;

while (!kbhit());

return 0;

}
W programie tym wystąpiły dwie nowe funkcje związane z obsługą konsoli: clrscr() - funkcja czyszcząca ekran, kursor ustawia się w lewym górnym rogu ekranu. kbhit() - funkcja czekająca na naciśnięcie klawisza (testująca klawiaturę) - obie z prototypem w zbiorze .

Najczęściej tablice wprowadza lub wyprowadza się przy pomocy instrukcji for:

wprowadzanie:
int tab [10], i;

for( i = 0;i<10;i++) // lub for (i=0; i<=9;i++)

cin>>tab[i];

wypisywanie:
int tab [10], i;

for( i = 0;i<10;i++) // lub for (i=0; i<=9;i++)

cout>>tab[i];
Wprowadziliśmy tu podwójny zapis pętli, różniący się stosowanym typem nierówności, lepiej jest stosować pierwszy sposób z nierównością silną, ponieważ nie trzeba odejmować jedynki od rozmiaru tablicy ( co może doprowadzić do błędu);

Innym sposobem nadawania wartości tablicom jest ich inicjalizacja, czyli nadanie wartości w momencie deklaracji tablicy. Pamiętamy, że dla typów podstawowych odbywało się to w następujący sposób:

int i =123;

float j = 0.123;

W przypadku tablic należy inicjować każdy element z osobna. Dla tablicy int tab[3];
inicjacja tablicy ma postać:


int tab[3] = {1,2,3}; // lub nie podając rozmiaru tablicy: int tab[] = {1,2,3};


Jeślibyśmy w nawiasie klamrowym umieścili więcej liczb niż jest zadeklarowanych w tablicy, kompilator wykryje błąd, ponieważ sprawdza w inicjalizacji, czy nie został przekroczony zakres.
Można też w ten sposób zainicjować tablice:

int tab[4] = {1,2}

Wówczas pozostałe elementy są automatycznie inicjowane zerami.


Tablice wielowymiarowe

Ten typ tablic z jakim spotkaliśmy się dotychczas nazywał się tablicami jednowymiarowymi. Można też tworzyć tablice tablic, inaczej nazywane tablicami wielowymiarowymi. Oto przykład tablicy dwuwymiarowej:

int tab1[3][2];

Jest to trzyelementowa tablica tablic dwuelementowych. Jej elementami są:

tab[0][0],tab[0][1],
tab[1][0],tab[1][1],
tab[2][0], tab[2][1].

Należy sobie przyswoić zapis tablicy: podwójny operator [][]. Pascal przyjmował inną konwencję zapisu: [i,j].

Próba takiego zapisu spowoduje przyjęcie przez kompilator jako elementu tab[j], bo jak wiemy operator przecinka ma tą właściwość, że wartością wyrażenia jest wartość prawego argumentu. Elementy tablicy zapamiętywane są wierszami. Podobnie jak dla tablic jednowymiarowych można je inicjalizować:

tab[3][2] = {0,1, 10,11, 20,21, 30,31};

Można powiedzieć, że tablica tab ma 3 wiersze i dwie kolumny. Przykład zastosowania obu typów tablic przedstawia program obliczania sumy elementów wierszy macierzy 5x5.


Przykład 12

#include
#inclued
main()
{
int A[5][5], B[5];

int i,j;

clrscr();
/* wprowadzenie elementów tablicy A */

for(i = 0; i<5;i++)

for(j = 0; j<5;j++)

cin>>A[i][j];
/* sumowanie wierszy */

for(i = 0; i<5;i++)

for(j = 0; j<5;j++)

b[i] += A[i][j];

/* wyświetlanie wyników zapisanych w tablicy jednowymiarowej */

for(i = 0; i<5; i++)

cout << "b[" << i << "] = " << b[i];

return 0;

}

Tablice znakowe

Specjalnym rodzajem tablic są tablice znakowe, stąd przyglądniemy się im bliżej. Deklaracja takiej tablicy:
char zdanie[50];

W tablicach takich można przechowywać tekst, dzięki temu, że każdy jej element może przechowywać kod znaków ( dla komputerów IBM - kod ASCII);

Teksty w tablicach przechowywane są tak, że po ciągu znaków następuje znak o kodzie 0 - tzw. NULL. Taki ciąg znaków zakończony NULL'em nazywa się stringiem.

Początek stringu w pamięci podaje nazwa stringu (nazwa jest adresem pierwszego elementu stringu w pamięci);

Podobnie jak inne tablice, tablica znakowa może być w trakcie deklaracji inicjalizowana:
char zdanie [50] = {"ala ma kota"};

Pamiętajmy o używaniu cudzysłowia.


Znak NULL został zapisany automatycznie, ponieważ przy inicjalizacji tablicy ciąg znaków ograniczyliśmy znakami cudzysłowia.

Można też inaczej inicjować tablicę:

char zdanie[50] = {'a','l','a',' ','m','a',' ','k','o','t','a'};

W tym przypadku jednak, nie dołączony zostanie znak NULL. Jest to ryzykowne. Chociaż przypomnijmy sobie, że w przypadku gdy ilość znaków tablicy jest mniejsza niż jej długość to pozostałe elementy inicjowane są bajtami o wartości zero.

Zakres tematyczny :

  1. Obsługa plików - ujęcie obiektowe (powtórka i uzupełnienia).

  2. Formatowanie wewnętrzne

  3. Grafika w implementacji Borland C++.

  1. Obsługa plików - powtórka i uzupełnienie.

W ujęciu języka C++, jeśli chcemy zapisać coś do pliku, lub z niego czytać, mamy do dyspozycji klasy, które takie operacje umożliwiają:

ofstream - zapis do pliku

ifstream - odczyt z pliku

fstream - oba powyższe

Podczas pracy ze strumieniami niepredefiniowanymi, sami musimy te strumienie zdefiniować- to oczywiste, ponieważ musimy wyraźnie określić do lub od jakiego pliku dyskowego strumień ma płynąć. Zatem aby czytać lub pisać do pliku, należy:

  1. Zdefiniować strumień, czyli wykreować strumień odpowiedniej klasy.

  2. Określić strumieniowi z jakim konkretnie plikiem ma się komunikować i otworzyć ten plik. Otwarcie strumienia można wykonać albo funkcją open albo za pomocą konstruktora

  3. Wykonać odpowiednie operacje we/wy

  4. Zlikwidować strumień, gdy uznamy, że praca z plikiem jest zakończona.

Możliwe jest także wielokrotne otwierania strumienia. Strumień po zakończeniu pracy z jakimś plikiem może posłużyć do pracy z innym:

np.:
ofstream stream("dane.txt");

stream<<"teks do zapisu";

stream.close();


stream.open("dane1.txt");
stream<<"teks do zapisu do drugiego zbioru";

stream.close();
Oczywiście strumienia można użyć tylko do celów do jakich był wykreowany, jeśli jest to więc strumień wyjściowy to tylko takimi operacjami może się dalej zajmować. Innymi słowy strumień jest nadal tej samej klasy, ale zmienia się tylko plik z jakim się komunikuje.

Błędy w trakcie pracy strumienia. Nie zawsze otwarcie pliku się udaje. Nie zawsze wymagana operacja odczytu lub zapisu może zostać zrealizowana. W programach przykładowych można się bez kontroli poprawności obejść , jednak przy pisaniu większych aplikacji, należy pamiętać o kontroli poprawności operacji na strumieniach. W dalszej części wykładu pomówimy o narzędziach do tego celu przeznaczonych.

Zebrane zostały w klasie ios.

W każdym obiekcie klasy strumień znajduje się słowo odpowiadające za stan błędu strumienia. W przypadku wystąpienia błędu w pracy strumienia, ustawiany jest bit odpowiadający za daną kategorie błędu. Kategorie te określane są typem wyliczeniowym zdefiniowanym w klasie ios. :
enum io_state{

goodbit = 0,

eofbit = 1,

failbit = 2,

badbit = 4 };

goodbit - nie jest to właściwie flaga błędu. Stan goodbit jest wtedy, gdy wszystkie bity stanu błędów są wyzerowane.

eofbit - flaga ustawiana jest wtedy, gdy przy czytaniu napotkany został koniec pliku EOF
failbit - ustawienie tej flagi oznacza, że jakaś operacja we/wy nie powiodła się. Strumień tkwi w stanie błędu, ale po wyzerowaniu tej flagi nadaje się do pracy.

badbit- ustawienie tej flagi oznacza, ze powstał jakiś poważniejszy błąd. Dalsza praca z tym strumieniem jest nie możliwa.

Flagami tymi nie zajmujemy się zwykle bezpośrednio. Do tego celu służą funkcję zdefiniowane w klasie ios.

int good()

zwraca wartość niezerową, jeśli wszystko jest w porządku, czyli żaden z bitów błędów nie jest ustawiony.
char c;

do {

cin>>c;
cout<<C;
while( cin.good() );

pętla wykonuje się dopóki nie wystąpi błąd strumienia cin.

int eof()

zwraca wartość niezerową, jeśli jest ustawiona flafa eofbit, czyli przy operacji odczytu napotkany został koniec pliku.

Użycie tej funkcji mieliście państwo na poprzednim wykładzie przy okazji omawiania funkcji odczytu z pliku

int fail()

zwraca wartość niezerowa gdy flagi failbit lub badbit są ustawione.

int num;

cin>>num;
if(cin.fail())
cout<<"Błędna dana";

zamiast funkcji fail możemy użyć zdefiniowanego w klasie ios operatora !. Robi to samo co funkcja fail:
cin >>num;

if(cin.fail()) cout <<"Błąd";

if(!cin ) cout <<"Błąd";

Dwa powyższe zapisy są równoważne.


flaga failbit ustawiona zostanie wówczas, gdy zamiast wartości liczbowej z klawiatury strumieniem przypłynie znak tekstowy.

int bad()

zwraca wartość niezerową gdy flaga badbit jest ustawiona. Dzieje się tak, gdy np. chcemy aby strumień czytał z pliku, który nie istnieje.

ifstream list("dane", ios::in);

if(list.bad())
cout<<"Błąd otwarcia pliku";


W klasie ios zdefiniowane są jeszcze dwie funkcje pracujące na flagach błędów. Ponieważ nie są one powszechnie używane nie będziemy się nimi zajmować szczegółowo. Powiem tylko, że są to:
rdstate - zwraca jako rezultat słowo int, w którym odpowiednie bity odpowiadają flagom błędów
clear - umożliwia ustawianie "ręczne" flag błędów.

Na zakończenie rozważań na temat obsługi plików dyskowych, należy wspomnieć o możliwości zmiany koryta strumienia. Czasami podejmujemy decyzję, że np. od tej chwili zamykamy plik a ujściem strumienia jest np. ekran. Do realizacji takiego zadania służy funkcja:

void attach(int)

argumentem tej funkcji jest liczba int oznaczająca tzw. deskryptor pliku, czyli numer oznaczający plik lub urządzenie wyjściowe (klawiatura ekran).

0 - st. wejście (klawiatura - używa go strumień cin)

1 - st. wyjście (ekran - używa go strumień cout)

2 - st.wyjście dla komunikatu o błędach(- używa go strumień cerr)

char c;

ofstream s("dane");

s<<"proba zapisu do pliku";

cout<<"zamknac plik i pisac dalej ? t/n";

cin.get(c);

if (c=='t') {

s.close();
s.attach(1);
}
s<<"proba zapisu na ekran";

2. Formatowanie wewnętrzne.

Dotychczas jeśli mówiliśmy o operacjach we/wy, mieliśmy na myśli komunikowanie się programu z urządzeniami zewnętrznymi - klawiaturą i ekranem, bądź plikami dyskowymi. Tymczasem nie są to jedyne operacje we/wy. Strumienie o których teraz krótko porozmawiamy, płynąć będą nie do plików , ale do wybranych obszarów pamięci, tak jakby tam właśnie znajdował się plik. Zwykle tym miejscem w pamięci jest tablica znakowa.

Jak łatwo się domyślać inaczej będzie się realizować to zadanie w klasycznym języku C, a inaczej w C++. Zacznijmy od tego drugiego:

Załóżmy, że mamy instrukcje wypisującą na ekranie pewien komunikat:

cout << "Pierwsza dana" << dana1 <<"Druga dana" << dana2

<< "Trzecia dana" << dana3;

Z jakiś powodów chodzi nam o skierowanie strumienia nie na ekran, ale do tablicy np.:

char dane[50];

W bibliotece klas istnieje klasa która umożliwia posługiwanie na tego typu strumieniami. Musimy

o programu włączyć plik nagłówkowy strstream.h, zawierający odpowiednie deklaracje.

Wpisanie stringu do tablicy obsługuje strumień klasy ostrstream (output string stream - wyjściowy strumień do tablicy znakowej). Można powiedzieć wpisywanie stringu do tablicy to nic takiego - można tego dokonać np. funkcją strcpy. Tak, ale nie będziemy mogli wykorzystywać formatowania oferowanego przez strumienie we/wy - możemy określić precyzję, typ notacji, szerokość. Podamy teraz przykład:

#include
#include
#include
main()
{
int dane1 = 2, dane2 = 7;

float dane3 = 123.123;

char dane[50];


/* tworzymy strumień klasy osttrstream */


ostrstream tablica(dane, sizeof(dane)); ( !!! )

tablica << "Pierwsza dana" << dana1 <<"Druga dana" << dana2

<< "Trzecia dana" << dana3 <<ENDS;

cout << "Odczyt zawartości tablicy

" <<DANE;


/* zmieniamy zawartość tablicy tak jak byłby to plik */


tablica.seekp(3, ios::beg); (2)

tablica<<"AAA";
cout<<DANA;
}

(1) Wpisanie do tablicy nie powoduje automatycznego dostawiania znaku NULL. Jeśli chcielibyśmy jednak go tam umieścić musimy użyć manipulatora ends (end string). Jeśli w trakcie operacji zapełniania tablicy przekroczylibyśmy liczbę 49 znaków, wówczas wpisywanie zostaje przerwane, a na pozycji 50 dopisywany jest znak NULL. Ustawiana jest flaga badbit.
(2) praca strumienia płynącego do tablicy przypomina prace strumienia płynącego do pliku. Można pozycjonować wskaźnik pisania.

( !!! ) Przy tworzeniu strumienia wykorzystywany został konstruktor strumienia klasy ostrstream:

ostrstream::ostrstream(char *tab, int rozmiar,int tryb);


char *tab - adres miejsca ujścia strumienia (tablicy)

int rozmiar - ile bajtów w pamięci zajmuje ta tablica

int tryb - tryb pracy strumienia. Przez domniemani dla tej klasy przyjęty jest tryb out - do pisania w tablicy. Można użyć trybu ios::app - dopisanie.

Jeśli zastosujemy tryb app strumień uznaje, że w tablicy jest już coś zapisane, odnajduje znak NULL kończący poprzednią wartość, a od tego miejsca zapisuje nową treść:


char napis[30] = {"Pamiętaj!!!"};

ostrstream pisz(napis , sizeof(napis), ios::app);

pisz <<"To nie takie trudne "<<ENDS;


Takie podejście do zapisu nie jest wygodne, nie wiemy bowiem zawczasu jaka długa jest tablica, która chcemy zapisać. Klasa ostrstream zapewnia możliwość zrezygnowania z określania rozmiaru tablicy:
ostrstream pisz;

korzystamy z konstruktora, któremu przy tworzeniu strumienia nie podajemy tablicy z jaka chcemy pracować. Używając operatora new rezerwuje sobie pewien obszar pamięci. Jednak w czasie pisania obszar ten może okazać się za mały. Wtedy rezerwowany jest większy obszar zawartość starego przepisywana jest do nowego, a stary usuwany operatorem delete. Jeśli i on nie wystarcza, wtedy sytuacja powtarza się.

#include
#include
#include
main()
{
ostrstream pisz;

for(int i = 0; i<10; i++)

{
pisz<<"
petla "<< i ;

if(!pisz)
{
cout<<"Błąd strumienia";

return (1);

}
pisz<<ENDS;

char *wsk;

wsk = pisz.str(); !!!

cout<<"Adrees tablicy do której pisalismy:" << wsk;

delete wsk; !!!

}
Po utworzeniu strumienia i wpisaniu so niego potrzebnych informacji, wywołujemy na rzecz tego strumienie funkcje składowa klasy ostrstream : str(). Rezultatem działania tej funkcji jest adres tablicy na której pracował strumień. Od tego momentu strumień nie wykona żadnej operacji zapisu do tablicy. Zwolnienie obszaru rezerwowanego przez strumień należy do nas.
Gdybyśmy nie wywołali tej funkcji strumień byłby w stałej gotowości do wykonywania na tej tablicy dalszych operacji. Przy zakończeniu zakresu ważności obiektu destruktor skasowałby ta tablicę. Za pomocą strumieni można także odczytać informacje znajdujące się w jakiejś tablicy:
Do tego celu służą strumienie klasy istrstream (input stream string - strumień odczytujący z tablicy.
#include
#include
#include
main()
{
char tablica[30] = {"3.14 to jest pi"};

istrstream napis(tablica, sizeof(tablica));


float lpi,modyf = 1.1;

char text[30];

napis>>lpi >>text;

pli +=modyf;


cout<<"Odczyt z tablicy" <<LPI<<TEXT;

}
Na strumieniu tym można wykonywać tez operacje typu seekg(wskaźnik odczytu) , get(c) itp.
Konstruktor użyty w przykładzie może mieć inną postać:

istrstream napis(tablica);

stosujemy go gdy strumień pracuje z tablica w której string zakończony jest znakiem NULL. Określenie rozmiaru stringu nie jest wówczas konieczne. Strumień sam jest w stanie odczytać znak NULL. Zapis i odczyt realizowany jest za pomocą strumienia klasy strstream pochodnej klas ostrstream i istrstream. Jest to strumień który zarówno pisze jak i czyta z tablicy. Konstruktor ma takie same parametry jak konstruktor klasy ostrstream. Dostępna jest funkcja str(), pozwalające gromadzić znaki w tablicy której rozmiaru nie określamy.

#include
#include
main()
{
char tablica[30];

strstream zapis_odczyt(tablica, sizeof(tablica),ios::in|ios::out);


zapis_odczyt<<"123 10 11.22";

int a,b;

float c;

zapis_odczyt >>a;

zapis_odczyt>>b;
zapis_odczyt>>c;
cout << "razem" << (a+b+c);


zapis_odczyt.seekg(2,ios::beg);
char t[10];

zapis_odczyt.getline(t,4);
t[4] = NULL';

cout<<"znaki wyjęte"<<T<<ENDL;

}

Zakres tematyczny:

  1. Typy pochodne zmiennych:

  1. Wskaźniki.

Najogólniej mówiąc, wskaźnik jest zmienną, która przechowuje adres innej zmiennej. Język C jest językiem tak zorganizowanym, że często posługuje się wskaźnikami. Są nimi argumenty wielu funkcji bibliotecznych, czy też wartości przez nie zwracane. Co prawda, istnienie wskaźników nie jest konieczne (można by się było bez nich obejść), ale właśnie dzięki nim język C tworzy tak zwarte i efektywne kody. Ponieważ wskaźniki i tablice są blisko ze sobą spokrewnione, przy okazji omawiania wskaźników uzupełnimy nieco informacje o tablicach.

Wskaźnikami możemy posłużyć się wobec dowolnej zmiennej podstawowej, tablicy, funkcji, innego wskaźnika, struktury itp. Wskaźniki deklarujemy używając następującej składni:
typ_zmiennej * nazwa_zmiennej

np.:

char * wsk1;

int *wsk2;

float *wsk3;

Wskaźniki są 16 - to lub 32 bitową liczbą, która przechowuje adres zmiennej danego typu: i tak czytając powyższe zapisy:

wsk1 jest wskaźnikiem do wskazywania (wskazujący) na zmienną typu char

wsk2 jest zmienną wskazującą na zmienną typy integer

wsk3 wskazuje na zmienną typu flaot


Przypomnijmy sobie na wstępie jak zorganizowana jest pamięć. Jest to tablica kolejno numerowanych lub adresowanych komórek pamięci: można nimi manipulować pojedynczo lub posługując się grupami sąsiednich komórek. Każda zmienna ma unikalny adres wskazujący początkowy obszar pamięci zajmowany przez tą zmienną. Ilość pamięci zajmowanej przez zmienną zależy jak pamiętamy od typu zmiennej. Zamiast odwoływać się do wartości zmiennej, można mieć do niej dostęp poprzez manipulowanie zmiennymi, które zawierają ich adres, czyli wskaźniki. Przydaje się to szczególnie przy manipulowaniu na tablicach, czy strukturach.

Treścią wskaźnika jest informacja, gdzie wskazany obiekt się znajduje, a nie to co się w nim znajduje.
Operator * informuje nas, że mamy do czynienia ze wskaźnikiem. Podobnie jak nawiasy [] informowały nas o tablicach. Sam wskaźnik nazywa się tak jak nazwa_zmiennej. Typami pochodnymi są też referencje i tzw. pola bitowe. Referencja nie jest obiektem (jak np. tablica) i do niej nie ma wskaźników. Do poszczególnych pól bitowych też nie (ponieważ zwiększenie adresu o jeden powoduje przesunięcie się w pamięci o 8 bitów(bajt) a nie o jeden bit).

Należy też pamiętać, że wskaźnik stworzony do wskazywania na obiekty jednego typu nie może

skazywać na inne typy:

int *wsk;

float a;

wsk = &a; //nieprawidłowo.


Operator & jest operatorem adresu i może być stosowany tylko do obiektów zajmujących pamięć: zmienne, elementy tablic. Nie można go stosować do wyrażeń, stałych i tzw. zmiennych register( o tym później). Pomaga on nadać zadeklarowanemu wskaźnikowi wartość początkową, czyli przypisać go do konkretnego obiektu. Kiedy wskaźnik pokazuje już na konkretnie miejsce możemy odnieść się do tego obiektu na który wskazuje( odczytać jego wartość, lub zapisać coś do niego- obiektu nie wskaźnika).

Operator * jest jak mówiłam operatorem adresowania pośredniego, ale także odwołania pośredniego: zastosowany do wskaźnika daje zawartość obiektu wskazanego przez ten wskaźnik np.:
int *wsk1 ;

float wsk2;

int zmienna1 = 10;

float zmienna2 = 1.2;

wsk1 = &zmienna1;

cout<<"Wskaźnik wsk1 wskazuje na zmienną o adresie"<

<<"zawierającą zmienną = "<<*wsk1

wsk2 = &zmienna2;

cout<<"Wskaźnik wsk2 wskazuje na zmienną o adresie"<

<<"zawierającą zmienną = "<<*wsk2

Przypisanie adresu konkretnej zmiennej można skrócić. Zapis:

int *wsk;

int zmienna;

wsk = &zmienna;

jest równoważny:

int zmienna = 100;

int *wsk = &zmienna;

Teraz do zmiennej chcemy wpisać nową wartość:

*wsk = 200;

cout<<ZMIENNA;
Czyli:Do obiektu można coś wpisać albo używając nazwy albo wskaźnika pokazującego na ten obiekt.
Jeśli wsk wskazuje na zmienną całkowitą, to *wsk może wystąpić wszędzie tam gdzie może wystąpić zmienna, a więc np.:

int *wsk;

int zmienna = 10;

wsk = &zmienna;

*wsk = *wsk + 2; // zmienna = 12;

Ponieważ operatory * oraz & są silniejsze niż operatory arytmetyczne, to dla wyrażenia:
x = *wsk + 1;

nie trzeba nawiasu: najpierw pobierana jest wartość z adresu wsk i zwiększona o 1 zostaje zapisana do x.

*wsk +=1 ++*wsk (*wsk)++ są równoważne.

W ostatnim przypadku nawiasy są niezbędne bo jednoargumentowe operatory * i ++ wykonywane są od prawej do lewej. Czyli operacja wykonała by zwiększenie wskaźnika o jeden, a nie obiektu.
Ponieważ wskaźniki są zwykłymi zmiennymi można ich używać bez adresowania pośredniego:
wsk = wsk1;

Zapis ten mówi, że teraz wsk będzie wskazywał na to samo co wsk1.


Wskaźnik void

Przy okazji powiemy sobie o wskaźniku void. Jak pamiętamy, deklaracja wskaźnika niosła w sobie dwie informacje: adres jakiegoś miejsca w pamięci, oraz na jakiego typu zmienną pokazuje:int, char float itp.

Możemy jednak zdefiniować wskaźnik, który informacji o typie nie przenosi - jest to tzw. wskaźnik void. Definiujemy go następująco:

void *w;

Służy on najogólniej mówiąc do reprezentowania bloku pamięci np.

void *wsk_blok = malloc(100);

O funkcji malloc mówić będziemy przy okazji omawiania dynamicznej alokacji pamięci. Teraz mawiając zapis powyższy mówimy, że zmienna wsk_blok wskazuje adres 100 bajtowego bloku pamięci.
W związku z tym, że wskaźnik void nie określa typu zmiennej na jaki wskazuje, jest rzeczą oczywistą, że nie można nim posłużyć się do odczytania pamięci. Nie można się nim poruszać po sąsiednich adresach: nie wiemy co ile bajtów poruszać się po pamięci.

Aby móc teraz traktować ten obszar pamięci jako np. tablicę znaków czy innych zmiennych, musimy dokonać odpowiednich konwersji do żądanych typów, przy pomocy operatora rzutowania, o którym mówiliśmy na poprzednich wykładach , i tak np.:

void wsk_blok = malloc(100);

char *wsk;

// int *wsk1;

.................

wsk_blok = wsk;

// wsk_blok = wsk1;

Zapisy te oznaczają, że teraz wskaźnik typu void wskazuje na to samo, na co wskazuje wskaźnik ypu char (int).

Wskaźnik każdego typu można przypisać wskaźnikowi typu void, bez konieczności rzutowania.
Przy okazji możemy napomknąć, że operator rzutowania może być stosowany również do wskaźników np.:

int *wsk1, *wsk2;

float *wsk;

...............
wsk1 = wsk2; //poprawny zapis

wsk = wsk1; //bład kompilatora, bo chcemy wskaźnikiem do float pokazywać //na int
wsk = (float *)wsk1; //świadomie każemy kompilatorowi to zrobi, kompilacja bez //błędu
Operatora rzutowania musimy użyć, gdy chcemy przypisać wskaźnikowi rzeczywistymi wskaźnik typu void:

float *wsk;

voiod *wsk1;

....................
wsk = wsk1; //błąd

wsk = (float *)wsk1; //poprawnie z operatorem rzutowania

Jest różnica między ANSII C a C++. W języku C niezależnie od tego po której stronie operatora przypisania stał wskaźnik void nie trzeba było używać rzutowania.


1a. Wskaźniki a tablice

Wskaźniki generalnie stosuje się w 4 przypadkach:

Najpowszechniej na początku stosuje się wskaźniki do pracy z tablicami. Zadeklarujmy wskaźnik i tablicę:
int *wsk;

int tab_a[10];

  1. instrukcja:

wsk = &tab_a[n];

ustawia wskaźnik na n-tym elemencie tablicy. Operator & jest operatorem adresu. Typ wskaźnika zgadzać się musi z typem tablicy.

  1. instrukcja:

wsk = &tab_a[0];

jest równoważna instrukcji:

wsk = tab_a;

i oznacza ustawienie wskaźnika na pierwszy element tablicy(na początek). Zapisy są równoważne, ponieważ jak już wspomnieliśmy, nazwa tablicy stanowi adres jej zerowego (pierwszego) elementu.
Po ustawieniu wskaźnika na n-ty element tablicy:

wsk = tab_a[7];

to przejście do następnego elementu tablicy umożliwia

instrukcja 3 :

wsk = wsk + 1; lub wsk ++;

aby przesunąć się o n elementów w tablicy napiszemy

instrukcję 4 :

wsk + = n;

Prostota zapisu jest zaskakująca. Można zapytać skąd wskaźnik wie, gdzie się przesunąć gdy np.: zwiększamy go o 1. Otwórz w definicji wskaźnika powiedzieliśmy, że np. wsk = jest wskaźnikiem na int. Stąd kompilator wnioskuje, że aby odnaleźć następny element typu int należy przesunąć się o 2 bajty. (typ int - 2 bajty)


Powyższe rysunki ilustrują mechanizm poruszania się wskaźnika do tablicy dla różnych typów zmiennych. Przepiszemy teraz program z poprzedniego wykładu tak, aby posługiwał się wskaźnikami, a nie klasycznym odwoływaniem się do n-tego elementu tablicy:
Przykład 1.

#include
#include
#include
main()
{
int *wsk_a, *wsk_b,i, j,pom;

int a[4][4];

int b[4]={0,0,0,0};

clrscr();
wsk_a = a; //inicjowanie wskaźników

wsk_b = b;


for(i = 0;i<4;i++)

for(j = 0;j<4;j++)

{
printf("a[%i][%i] = ",i,j); //wprowadzenie danych do tablicy

scanf("%i",wsk_a++);
/* *wsk_a++ = i+1; */

}
printf("Tablica a:

");
for(i = 0,wsk_a = a;i<4;i++)

for(j = 0;j<4;j++) //Wydruk tablicy wprowadzonej

printf("
%i",*wsk_a++);
for(wsk_a = a,i = 0;i<4;i++)

{
pom = 0;

for(j = 0;j<4;j++)

pom += *wsk_a++; //sumowanie elementów wiersza

*wsk_b++ = pom;

}
for(j = 0,wsk_b = b;j<4;j++) //wydruk wyników

printf("%i ",*wsk_b++);

while(!kbhit());
return 0;

}
Jak widać, posłużenie się wskaźnikami nic szczególnego nam nie dało. Pozornie tylko. W efekcie otrzymujemy szybszy od poprzedniego program. Zapis np. a[5] powoduje żmudne liczenie adresu czego przy wskaźnikach nie ma.

Pamiętajmy !!!

Mimo, że możemy zapisać:

wsk = tab_a;

to o ile możemy postąpić:

wsk++;
to nie możemy napisać:

tab_a++;
Różnicą między wskaźnikiem a nazwą tablicy jest to, że wskaźnik jest obiektem w pamięci, w związku z tym można np. ustalić jego adres, natomiast nazwa tablicy nie jest obiektem i na niej nie można przeprowadzać żadnych operacji.

Dla tak zadeklarowanego wskaźnika:

int *wsk;

adresem tego wskaźnika jest wartość wyrażenia:

&wsk;

1b.Arytmetyka wskaźników

  1. Możemy dodawać i odejmować liczby całkowite do wskaźników tak, aby w potrzebny sposób przesuwać je po tablicy. Operacje te nie są sprawdzane przez kompilator, i wtedy możemy przesunąć wskaźnik poza zadeklarowaną tablicę i np. zniszczyć istniejące tam potrzebne dane. Błędy takie są najtrudniejsze do wykrycia, dlatego przy przesuwaniu wskaźników należy zachować daleko idącą ostrożność.

Możemy odjąć dwa wskaźniki od siebie:

wsk_a - wsk_b Gdy pokazują one na różne elementy tej samej tablicy to wynikiem takiej operacji jest liczba dzielących je elementów. Liczba może być ze znakiem - lub +.

Wskaźniki można ze sobą porównać. Do tego celu służą nam operatory:

== != < > <= >=

Dla dwóch wskaźników:

int *wsk1, *wsk2;

wyrażenie : wsk1 = wsk2 oznacza, że wskazują one na ten sam obiekt.

if(wsk1 == wsk2)

cout<<"Oba wskaźniki pokazują na ten sam obiekt";

Jeśli wskaźniki wskazują na jakieś elementy tej samej tablicy, to wyrażenie wsk1 < wsk2 oznacza,

e wsk1 wskazuje na element tablicy o mniejszym indeksie.

  1. Każdy wskaźnik można porównać z adresem 0 zwanym NULL. Takie ustawienie wskaźnika:
    wsk = 0; //lub wsk = NULL

informuje, że wskaźnik nie pokazuje na nic konkretnego (czasami niektóre funkcje biblioteczne zwracają wskaźnik NULL (null pointer) np. funkcja gets(string)). Potem możemy łatwo sprawdzać:
if(wsk == 0) // if(wsk ==NULL) if(!wsk)

............. .............. .......


1c. Inicjowanie wskaźników

Przed rozpoczęciem pracy ze wskaźnikami należy pamiętać o tym , że przed ich pierwszym użyciem muszą być one ustawione.

W tym punkcie zbierzemy wszystkie sposoby ustawiania wskaźników i te o których mówiliśmy oraz te o których nie wspominaliśmy.

  1. tak aby wskazywał na konkretny obiekt:

wsk = &obiekt;

  1. można ustawić go na to samo na co wskazuje inny wskaźnik:

wsk = wsk1;

  1. ustawić wskaźnik na początek jakiejś tablicy:

wsk = tab_a; // wsk = &tab_a[0];

  1. ustawić wskaźnik na jakąś funkcję.

  2. ustawić wskaźnik na zarezerwowany dynamicznie obszar

  3. ustawić wskaźnik na konkretny adres którego np. zawartość chcemy sprawdzić

  4. ustawienie wskaźnika na string

wsk = "to jest string";


1.d Tablice wskaźników

Pamiętamy, że tablica to ciągły obszar w pamięci, w którym przechowywany jest ciąg zmiennych tego samego typu. Skoro mogą być tablice int , float , char , to dlaczego nie miałoby być tablic wskaźników. W końcu adres to też liczba. Np.:

float *wsk1[3];

int *wsk2[10];

powyższe zapisy czytamy:


1.e Wskaźniki a stringi

Wielokrotnie mówiliśmy, że string to tablica znaków zakończona znakiem NULL.
Jeśli jakiś wskaźnik ma pokazywać na ciąg znaków, to można go zadeklarować jako:
char *text;

i zainicjować:

text = "To jest próbny tekst";

lub razem w jednej linii:

char *text = "to jest próbny tekst";

Zapis 1 nie oznacza kopiowania, jest to przypisanie wskaźnika konkretnemu stringowi. W języku C

ie ma instrukcji do obsługi ciągu znaków jako całości.

Porównajmy zapis:

char tab_text [] = "To jest próbny tekst";

char *text = "to jest próbny tekst";

Pierwsze jest tablicą, poszczególne znaki tablicy można zmieniać, ale nazwa tab_text zawsze będzie

odwołaniem do tego samego miejsca w pamięci. Z drugiej strony text jest wskaźnikiem zainicjowanym tak, aby wskazywał na string. Wskaźnik można później zmieniać, tak aby wskazywał na cokolwiek, ale zmiana w stałej napisowej ma skutek nieokreślony

W jaki sposób można wykorzystać wskaźniki do stringu, zademonstrujemy na przykładzie funkcji append_string, dodającej dwa stringi do siebie:


Przykład 2.

append_string(char *string1, char *string2)

{
//znajdywanie końca stringu2 do którego dołączymy string1

while(*string2 != NULL)

string2++;
//dołączanie stringów

while((*string2 = *string1) != NULL)

{
string1++;
string2++;
}
}

W praktyce funkcję pisze się nieco inaczej. Można to zrobić tak:


append_string(char *string1, char *string2)

{
//znajdywanie końca stringu2 do którego dołączymy string1

while(*string2++)
;
//dołączanie stringów

while(*string2++ = *string1++)

;
}
Zwiększanie wskaźników string1 i string2 przeniesiono do części warunkowej. Jak mówiliśmy wcześniej, wartością operacji: *string1++ jest znak na który wskazywał wskaźnik przed zwiększeniem. Znak ze stringu 1 wstawiany jest na końcu stringu2 i na koniec dołączany jest znak NULL. Porównanie ze znakiem '' jest zbyteczne, bo dla string1 = NULL wartość wyrażenia = 0 co jest warunkiem zakończenia pętli. Po instrukcji while wykonywana jest instrukcja pusta, ponieważ wszystkie operacje wykonywane są w momencie sprawdzania warunku.
Podobnie jak w przypadku tablic wskaźników na int czy float, można również używać wskaźników do stringów. Jej elementami są wskaźniki mogące pokazywać na stringi:
char *wsk_tab_char[4];

Wskaźniki te można ustawiać podobnie jak do zwykłych stringów:

char *text[4] = {"pon", "wto", "sro", "czw"};

Elementami tej tablicy nie są oczywiście stringi - nazwy dni tygodnia, ale adresy tych miejsc pamięci, gdzie umieszczone są te stringi


1.f Dostęp do konkretnych komórek pamięci:

Czasami zachodzi konieczność uzyskania bezpośredniego dostępu do konkretnej komórki pamięci bez podania jej nazwy. Wtedy odnosimy się do niej poprzez wskaźnik:

wsk = 0x1111;

podajemy po prostu do wskaźnika adres komórki i odtąd możemy posługiwać się tym wskaźnikiem
cout<<"wartość przechowywana pod adresem"<

<Nie zawsze jednak ustawienie adresu jest takie proste: w Borlandzie C++ istnieje makro o nazwie: MK_FP (make far pointer) - zainteresowani muszą się sami z nim zapoznać (podaje się segment i offset).

1.g Dynamiczna alokacja pamięci.

Wcześniej czy później, programista staje przed problemem dostępności pamięci. Jak intuicyjnie wiadomo, jest ona ograniczona i nie możemy do niej ładować dowolnej ilości dowolnie dużych obiektów. Stajemy więc przed problemem, jak tworzyć tymczasowe obiekty tak, aby gdy nie będą potrzebne łatwo pozbyć się ich z pamięci. Odpowiedź jest prosta - alokować pamięć.
W językach C i C++ podejście do problemu alokacji pamięci jest całkowicie inne.
W języku C pamięć dostępna dla programu w czasie jego uruchomienia nazywa się HEAP'em, w C++ - FREE STORE- pamięć wolna. Różnica między nimi dwoma leży tylko w funkcjach używanych do dostępu do tej pamięci.

W języku C, do alokacji pamięci służy grupa funkcji malloc. Omówimy pokrótce te funkcje.
Pierwsza to malloc

void *malloc(size_t n);

funkcja zwraca wskaźnik do n bajtów niezainicjowanej pamięci, lub NULL jeśli żądanie nie może być spełnione.

Druga funkcja to calloc

void *calloc(size_t n, size_t size);

zwraca wskaźnik do obszaru mogącego pomieścić tablicę n elementową, każdy element ma rozmiar size. Zwraca NULL, gdy pamięć nie może być przydzielona. Pamięć inicjowana jest zerami.

Po wykorzystaniu pamięci można ją zwolnić. Do tego celu służy funkcja free

void free(p);

zwalnia pamięć wskazaną przez p, przy czym p musi być wynikiem wcześniejszego wywołania funkcji malloc lub calloc. Nie ma ograniczeń na kolejność zwalniania pamięci, natomiast poważnym błędem jest zwalnianie czegoś, co nie było poprzednio przydzielone w/w funkcjami.
Zadaniem naszym będzie dynamiczne alokowanie struktury o nazwie "data":
struct date *dateptr;

dateptr = (struct date *) malloc(sizeof(struct date));

W przykładzie tym, funkcja malloc alokuje blok pamięci wystarczający do przechowania struktury i zwraca wskaźnik. Funkcja zwraca void pointer i dlatego jak pamiętamy musimy go rzutować na odpowiedni typ kiedy chcemy przypisać go do zmiennej dateptr. Po tej operacji blok pamięci możemy traktować jako strukturę date.

W języku C możemy korzystać z jednego z 6 standardowych modeli pamięci:tiny, small, medium,compact, large, huge które różnią się min. ilością pamięci przeznaczonej na dane. Dla modelu compact, large i hudge, gdzie pamięć na dane jest ponad 64 kB, funkcja malloc zamienia na jest na funkcję farmalloc, farfree operujące na pamięci o długości ponad 1 segment.
Alternatywą do tych funkcji w języku C++ jest operator new i delete. Operator new tworzy obiekt, a operator delete usuwa obiekt z pamięci. Jeśli zdefiniujemy wskaźnik:

char *wsk;

to instrukcja

wsk = new char;

powoduje utworzenie nowego obiektu typu char. Nie ma on nazwy, ale możemy się do niego odwoływać poprzez wskaźnik zawierający adres tego obiektu.

Natomiast:
delete wsk;

powoduje usunięcie obiektu wskazanego przez wsk z pamięci.

Jeśli chcemy utworzyć tablice w pamięci to postępujemy następująco:

int *wsk_tab;

wsk_tab = new int[10];

operator new utworzył 10-elementowa tablicę int. Kasowanie tablicy zarezerwowanej dynamicznie:
delete [] wsk_tab;

Zwróćmy uwagę na nawiasy kwadratowe.


Cechy obiektów utworzonych operatorem new

  1. Obiekty żyją od momentu utworzenia operatorem new aż do momentu usunięcia operatorem delete.

  2. Obiekty nie mają nazwy. Operujemy na nich tylko przy pomocy wskaźników.

  3. Obiekty utworzone operatorem new nie są automatycznie inicjowane (są w nich śmieci)>

Przykład 3;

#include
main()
{
int dl_tab,i;

cout<<"podaj rozmiar tablicy: "

cin>>dl_tab;
int *wsk_tab = new int[dl_tab];

for(i = 0;i<DL_TAB;I++)

*wsk_tab++ = i;

//.......instrukcje wykorzystujące tablice

delete [] wsk_tab;

return 0;

}
Za pomocą operatora delete kasuje się tylko obiekty utworzone przy pomocy operatora new, przy czym nie należy kasować wcześniej skasowanego obiektu. Można kasować natomiast wskaźnik ustawiony na NULL:

wsk = NULL;

delete wsk;

W trakcie alokowania pamięci może zdarzyć się tak, że operator new zwróci NULL. Oznacza to, że wyczerpaliśmy pamięć dostępną na dane. W związku z tym w programach tworzących dużą liczbę dużych obiektów należy kontrolować poprawność operacji alokacji. Można tego dokonać albo poprzez fragment programu:

int *wsk;

wsk = new int[10000];

if(!wsk)
error("pamięć się wyczerpała");

lub przy wykorzystaniu funkcji set_new-handler:


Przykład 4.

#include
#include // exit

#include //set_new_handler

void alarm();

long k;

main()
{
set_new_handler(alarm);
for(k = 0; ; k++)

new int;

}
void alarm()

{
cout<<"Brak pamięci przy k = "<<K;

exit(1);
}
W funkcji main wykonuje się nieskończona pętla tworząca dynamicznie obiekty. Jeśli w którymś momencie zabraknie pamięci, sterowanie przejmuje automatycznie funkcja set_new_handler uruchamiająca funkcję alarmową napisaną przez użytkownika. Argumentem tej funkcji jest wskaźnik do funkcji alarm (dalej dowiemy się, że nazwa funkcji jest jej adresem w pamięci).

Porównanie strych i nowych metod

O szczegółach mówić będziemy przy okazji omawiania w przyszłym semestrze dynamicznej alokacji obiektów zdefiniowanej przez programistę klasy.


1.h Stale wskaźniki i wskaźniki do stałych.

Zadeklarujmy wskaźnik:

char *const text = "to jest próba tekstu";

Jest to tzw. stały wskaźnik. Musi być od razu ustawiony w momencie deklarowania. Będzie on wskazywał zawsze na to samo miejsce w pamięci, ale zmienna przez niego wskazywana może być zmieniana:
*text = 'a'; // OK

char str[10];

text = str; // nie poprawne wskaźnik nie może być zmieniany, nie można się

//nim poruszać.


Deklaracja :

const int *wsk_int

jest to deklaracja wskaźnika do stałego obiektu. Nie musi być od razu ustawiany. Wskaźnik uznaje pokazany obiekt za stały, nie może więc go modyfikować:

int x[4] = {0,1,2,3};

int tmp, *w;

const int *wsk_do_st;

w = x;

wsk_do_st = x; //ustawienie wskaźnika na początek tablicy

tmp = *wsk_do_st; //odczytujemy 0 element tablicy

wsk_do_st ++; //poruszamy wskaźnik

*wsk_do_st = 0; //błąd -za pomocą tego wskaźnika obiektu modyfikować //nie wolno

Oba typy wskaźników można połączyć. Otrzymujemy wtedy stały wskaźnik do stałego obiektu:
const int *const wsk;

int m = 6,n = 4,tmp;

const int *const wsk = &m; //musimy go od razu ustawić bo jest to stały wskaźnik

tmp = *wsk;

*wsk = 15; //błąd nie można zapisać - wskaźnik traktuje swój obiekt jako //stały

w = &n; //błąd, nieruchomy wskaźnik, nie można nim pokazywać na inny //obiekt

Pozostałe informacje na temat wskaźników podamy przy okazji omawiania funkcji oraz innych typów pochodnych.

Zakres tematyczny:

  1. Łączenie procedur w różnych językach.

1. Łączenie procedur w różnych językach.

Czasami przychodzi moment, kiedy programy napisane w języku C/C++ muszą wywołać programy napisane w innych językach, lub kiedy program napisany w innym języku musi wywołać funkcję napisaną w języku C/C++. Taki proces nazywany jest mixed-language programing- programowanie w mieszanych językach. Np. kiedy jakiś szczególny podprogram dostępny jest w języku innym niż C/C++, lub kiedy algorytm opisany jest w sposób bardziej naturalny w innym języku, występuje potrzeba użycia w programie, więcej niż jednego języka.

Na dzisiejszym wykładzie omówimy najważniejsze zasady mixed language programing.

Tworzenie wywołań w językach mieszanych

Programowanie w językach mieszanych MLP, sprowadza się do odwołań do funkcji, procedur lub podprogramów, w zależności od konwencji przyjętej dla danego języka. Np.:, BASIC-kowski moduł główny, może wykonywać specyficzne zadanie, które chciałby programista zaprogramować oddzielnie. Zamiast wywoływać jednak podprogram BASIC-kowski, decyduje się wywołać funkcję C.
Wywołanie funkcji napisanych w różnych językach polega na wywołaniu funkcji umieszczonych w różnych modułach. Zamiast kompilowania wszystkich modułów jednym kompilatorem, używa się różnych kompilatorów (różnych języków). Na marginesie należy tu zaznaczyć, iż uwaga ta dotyczy kompilatorów języków tego samego producenta. np. Microsoft czy też Borland, co związane jest z różnicami w formatach modułów obj.

Dla powyższego przykładu oznacza to, że moduł główny kompilowany jest w kompilatorze BASIC, inny zbiór zawierający funkcję C kompilatorem C, a dopiero potem oba zbiory objektowe linkowane są w jeden kod wynikowy.


Na rysunku powyżej widzimy wywołanie z poziomu języka BASIC funkcji prn() języka C, podobnej do podprogramu w języku BASIC. Są dwie istotne różnice pomiędzy tym wywołaniem mieszanym, z wywołanie pomiędzy dwoma BASIC-kowskimi modułami:

Można wykonywać wywołania mieszane nie zważając na to, czy funkcje wywoływane z innych modułów zwracają wartości.


Językowe odpowiedniki dla wywołań funkcji (procedur, podprogramów)


Język Zwracająca wartość Nie zwracająca wartości

Asemblery procedure procedure

BASIC FUNCTION procedure Subprogram

C/C++ function void function

FORTRAN FUNCTION SUBROUTINE

PASCAL Function Procedure


Jak widać z tabelki moduł C może wykonać wołanie fortranowskiego SUBROUTINE, będącego odpowiednikiem void function z języka C, itp.


Wymagania dotyczące konwencji językowych

W mieszanym programowaniu, wywołujący program musi przestrzegać tych samych konwencji, jak program wywoływany. Konwencje te można pogrupować w następujące grupy:


Konwencja nazewnictwa

Zarówno program wywołujący jak i wywoływany podprogram, musza zgadzać się na płaszczyźnie nazw identyfikatorów. Identyfikatory mogą odnosić się do podprogramów (funkcji, procedur) lub zmiennych mających publiczny lub globalny zakres. Każdy język zmienia nazwy identyfikatorów. Termin "konwencja nazewnictwa" odnosi się do sposobu w jaki procesor zmienia nazwy podprogramów zanim umieści je w zbiorze obiektowym. Każdy język zmienia te nazwy na swój sposób. Programista może wybierać pomiędzy różnymi konwencjami nazewnictwa, aby upewnić się że nazwy w wywołującym programie zgadzają się z tymi w programie wywoływanym. Jeśli nazwy wywoływanego podprogramu są przechowywany w każdym zbiorze obiektowym inaczej, wtedy linker nie byłby w stanie znaleźć powiązań. Będzie wtedy raportował o nierozerwalnym zewnętrznych powiązaniach.

Większość kompilatorów języka ładuje kod maszynowy do zbioru obiektowego. Przechowywane tam też są nazwy publicznych podprogramów i zmiennych. Linker może wtedy porównać nazwę podprogramu wywoływanego w jednym module z nazwą podprogramów zdefiniowanych w innym module i rozpoznaje powiązania. Nazwy tworzone są zgodnie ze standardem znaków ASCII

BASIC, FORTRAN i PASCAL używają podobnej konwencji nazewnictwa. Tłumaczą każdą literę na duże znaki.

Każdy język rozpoznaje różną liczbę znaków. I tak np. FORTRAN rozpoznaje pierwszych 31 znaków w nazwie (chyba że nazwy identyfikatorów są obcinane - ustawieniem opcji $TRUNCATE), Pascal pierwsze 8, Basic pierwszych 40 znaków. Jeśli nazwa zawiera więcej znaków niż dopuszcza dany kompilator, pozostałe znaki nie są po prostu umieszczane w zbiorze obiektowym.
Kompilatory języka C nie zamieniają każdej litery na duże. Nazwa każdego podprogramu uzupełniana jest od przodu znakiem podkreślenia. Kompilator tego języka rozpoznaje 31 znaków nazwy identyfikatora (32 włączając znak podkreślenia). Używając odpowiednich opcji kompilatora (np./H) można zmienić liczbę rozpoznawanych znaków.

Kompilator języka C++..................................................... Rozpoznaje pierwszych 247 znaków nazwy.
Słowa kluczowe języków programowania mieszanego zajmują się automatycznie różnicami w konwencji nazewnictwa tak długo jak przestrzegane są dwie zasady:

  1. Ograniczać nazewnictwo identyfikatorów do 6 znaków, jeśli:

  1. włączane procedury kompilowane były kompilatorem Fortran <5.0

  2. włączane procedury kompilowane były kompilatorem z opcją /4Yt lub w metakomendą $TRUNCATE .

  1. Nie używać opcji linkera /NOIGNORECASE. Z modułami C/C++ oznacza to, że w czasie programowania nie można liczyć na rozpoznawanie dużych i małych liter.


Jeśli używamy opcji /Gc (generate Pascal-style function call) podczas kompilacji , lub deklarujemy funkcję lub zmienną ze słowem kluczowym __pascal, kompilator przekształca nazwy identyfikatorów na duże litery.


Zwróćmy uwagę, że np. kompilator Basci'u wprowadza poprzedzający nazwę funkcji znak podkreślenia do zbioru obiektowego, ponieważ słowo CDECL każe kompilatorowi Basic'u używać konwencji nazewnictwa charakterystycznej dla języka C. Powoduje również zamianę wszystkich znaków nazw na małe litery (co prawda nie jest to konwencja przyjęta dla języka C, ale uznawana jest powszechnie przy pisania programów w tym języku).


Konwencja wywołań

Termin ten odnosi się do sposobu w jaki kompilator realizuje wywołania. Wybór konwencji polega na zrealizowaniu odpowiedniej wygenerowanej przez kompilator instrukcji maszynowej wykonującej (i powracającej z ) wywołanie funkcji, procedury lub subroutine.

................
Wybór konwencji wywołań oddziaływuje na program w trzech aspektach:

  1. Określenia kolejności w jakiej zostaną przesłane parametry do innego podprogramu. Można to kreślić przy pomocy specjalnych deklaracji lub używając instrukcji interfejsowych, dotyczących programowania mieszanego. Konwencje te dotyczą podprogramu wywołującego.

  2. Dla podprogramu wywoływanego będzie to określenie kolejności odbierania parametrów do nich przesyłanych. W większości programów sposób odbierania parametrów określany jest w nagłówkach podprogramów, jednak np. Basic zawsze używa swojego własnego sposobu odbierania parametrów.

  3. Oba podprogramy: wywołujący i wywoływany muszą zgadzać się jeśli chodzi o odpowiedzialność za uporządkowanie stosu w czasie usuwania parametrów.

Innymi słowy: każde wywołanie podprogramu używa pewnej konwencji wywołań. Każdy nagłówek podprogramu zawiera określenie lub ustawienie innej konwencji. Musza być one kompatybilne dla obu podprogramów. W każdym języku z wyjątkiem Basica jest możliwość zmiany tej konwencji, w momencie wywołania lub przy pomocy deklaracji w wywoływanym podprogramie. Zwykle jednak łatwiej jest dostosować konwencje wywoływanego programu.
Języki C++, Basic, Fortran i Pascal używają tego samego standardu wywołań, natomiast język C ma odmienną.

Jeśli chodzi o uporządkowanie stosu, to języki :C++, Pascal, Fortran i Basic przesyłają parametry inaczej jak język C. Składają one parametry na stos w kolejności icj pojawienia się w kodzie źródłowym np. Basic-kowe wywołania:

CALL Calc(A,B)

składuje na stosie parametr A, przed parametrem B. W tej konwencji przyjęte jest też, że za uporządkowanie stosu odpowiada procedura wywołana bezpośrednio przed przejęciem sterowania przez wywołujący moduł.

W konwencji C parametry przesyłane są w odwrotnej kolejności. Np. wywołanie funkcji C:
calc(a,b)
składuje na stosie najpierw parametr b, a potem dopiero a. W przeciwieństwie od innych języków wysokiego poziomu, konwencja języka C określa, że podprogram wywołujący porządkuje stos natychmiast po zwrócenia sterowania przez wywoływany podprogram.

Konwencja wywołań Basic'u, Pascal'a i Fotran'u sprawia, że kod obiektowy jest mniejszy niż C. Jednak Z kolei jak państwo wiecie konwencja języka C umożliwia wywołanie funkcji ze zmienną liczba parametrów. Jeśli w języku C++ używamy wywołania funkcji ze zmienna liczbą argumentów, funkcja używa automatycznie konwencji wywołań dla języka C.
Należy unikać używania słowa kluczowego __fastcall lub opcji /Gr przesyłającej parametry do rejestrów, Tworzy to niekompatybilności z programami napisanymi w innych językach.

Konwencja przesyłania parametrów

Oprócz zgodności konwencji nazewnictwa i wywołań, programy muszą zgadzać się co do sposobu w jakim przesyłane są parametry. Zapewnia to poprawną transmisję danych i prawidłowe efekty pracy programu. Jak państwo wiecie w języku C były dwa sposoby przesyłania parametrów: poprzez wartość i adres, dla C++ doszło jeszcze przesyłania przez referencje. Z wyjątkiem języka Basic który przyjął jedynie przesyłanie przez adres (typu near - w obrębie jednego segmentu), pozostałe języki mają możliwość wykorzystania takich samych sposobów przesyłania parametrów.
Kiedy programujemy w językach mieszanych należy:

Język przez adres Near przez adres Far przez wartość

BASIC wszystkie - -

C/C++ małe tablice duże tablice pozostałe

FORTRAN wszystkie(medium model) wszystkie (large model) z atrybutami

PASCAL var, const vars,consts inne parametry



Kompilacja i linkowanie

Po odpowiednim napisaniu programu, z zastosowaniem odpowiednich konwencji, pora na kompilowanie i linkowanie poszczególnych modułów.


Kompilacja z odpowiednim modelem pamięci

W językach Pascal, Basic, Fortran nie ma specjalnych opcji wymaganych do kompilacji zbiorów źródłowych, będących częścią programu napisanego w językach mieszanych.
Ponieważ języki te używają tylko adresów kodu typu FAR należy wybrać jedną z dwu technik dla programu w języku C/C++ wywołującego podprogramy w jednym z tych języków:
- kompilować program w modelu: medium, huge lub large (używających również kodu adresu typu FAR)
- używanie słowa kluczowego __far do definicji funkcji publicznych języka C/C++.
Wybranie modelu pamięci w C,C++ lub Fortranie powoduje ustawienie rozmiaru wskaźnika danych "by default", chociaż mogą być one zmieniane słowami kluczowymi: __near, __far. Określa on również czy obiekt umieszczany jest w bieżącym segmencie danych. Jeśli nie, nie może być przesyłany przez adres typu __near.


Linkowanie z bibliotekami

W większości przypadków, można w prosty sposób łączyć moduły kompilowane różnymi językami. Aby zagwarantować, że wszystkie wymagane biblioteki linkowane będą w odpowiedniej kolejności, należy wykonać jedną z następujących czynności:

  1. umieścić wszystkie biblioteki w tej samej kartotece co źródła

  2. w zmiennej LIB określić katalogi zawierające wszystkie potrzebne biblioteki
    3. pozwolić, aby linker promptował (prosił o określenie z ręki) biblioteki

W każdym powyższym przypadku, linker znajdzie biblioteki w kolejności jaka jest wymagana dla niego.
Link/NOD mod1 mod2,,,GRAFIX+LLIBCE+LLIBFOR

mod1,mod2 - dwa moduły napisane w językach mieszanych

GRAFIX- biblioteka użytkownika

LLIBCE - biblioteka C

LIBFORE - biblioteka Fortranowska


Wywoływanie podprogramów języków wyższego poziomu z poziomu C

Interfejsem do innych języków programowania w języku C jest użycie słów kluczowych __fortran/__pascal. Ich użycie powoduje, że podprogramy będą wywoływane używając Fortran/Pascal-owych konwencji nazewnictwa i wywołania. Konwencje te działają również dla Basic'a.
Aby poprawnie zrealizować wywołanie mieszane należy wykonać następujące działania:

  1. Napisać prototypy każdej wywoływanej mieszanej procedury. Prototyp powinien zawierać deklaracje extern, chociaż nie jest to wymogiem. Zamiast użycia w/w słów kluczowych, można stosować opcję /Gc. Powoduje ona, że wszystkie funkcje używają fortranowo/pascalowej konwencji z wyjątkiem tych które stosują słowo kluczowe __cdecl .

  1. Przesłać zmienne lub adresy zmiennych, w sposób odpowiadający danej konwencji.

  2. wykonać wywołanie funkcji w programie tak jak gdyby była to funkcja C

  3. 4. kompilować moduły C w medium huge lub large model, lub użyć __far w prototypie funkcji. Zapewni to wykonanie wywołania typu far.


Użycie słów kluczowych __pascal lub __fortran

Dwie zasady kierują użyciem tych słów:

  1. modyfikują identyfikatory znajdujące się bezpośrednio po ich prawej stronie

  2. razem z nimi mogą być użyte słowa __near, __far. Sekwencje:

__fortran__far
__far__fortran
są ekwiwalentne

Słowo kluczowe:

__pascal - deklaruje podprogram Pascal-owy

__fortran - deklaruje podprogram Fortranowski

obie - deklarują podprogram BASIC-owski


Użycie tych słów daje ten sam efekt. Użycie jednego lub drugiego nie powoduje żadnych różnic, z wyjątkiem wewnętrznej dokumentacji programu.

Przykłady:
short __pascal func(short sarg1,short sarg2);

deklaracja funkcji func Basic-owa, Pascal-owa lub Fortran-owską pobierającą 2 argumenty short i zwracającą wartość short.

void (__fortran *func)(long larg);

deklaracja funkcji func Basic-owa, Pascal-owa lub Fortran-owską pobierającej zmienną long i nie zwracającej żadnej wartości. Void odpowiednie jest do użycia dla podprogramu w BASIC, procedury w PASCAL lub subroutine w FORTRAN, które nie zwracają wartości.

short __near__pascal func(__near double *darg);

równoważne:

short __pascal__near func(__near double *darg);

deklaracja funkcji func Basic-owa, Pascal-owa lub Fortran-owską typu near pobierającą argument przez referencjeadres) i zwracającą wartość short.


Przy wywoływaniu podprogramu w Basicu musimy używać w/w słów. Dla podprogramów Fortranowskich lub Pascalowych mamy wybór: albo adaptować C do konwencji F/P, albo adaptować Fortran lub Pascal do konwencji C. Ustawia się wówczas atrybut C w nagłówku definicji podprogramu. Następujące przykłady ilustrują sposób postępowania:

dla podprogramu Fortranowskiego:

SUBROUTINE FFROMC [C] (N)

INTEGER *2 N

dla podprogramu w Pascalu:

PROCEDURE Pfromc(n:integer) [C];

aby zaadaptować procedure C do konwencji P/F deklarujemy funkcje jako__pascal lu __fortran np.:
void __pascal CfromP(int n);


Wywołanie BASIC z poziomu C

Żaden podprogram napisany w Basic'u nie zostanie wykonany, jeśli program główny nie będzie napisany również w tym języku . Jest to spowodowane, wymaganiami co do środowiska, które musi być inicjowane w sposób unikalny dla tego języka. Żaden inny język nie wymaga takiej szczególnej inicjalizacji.
Jednakże, program może startować z poziomu Basic, wywoływać funkcję C wykonującą większość programu i wtedy wywołać podprogram w Basicu. Rysunek poniżej ilustruje jak to zrobić:

Przy wywoływaniu Basic z poziomu C należy postępować zgodnie z następującymi zasadami:

  1. Napisać moduł główny w języku BASIC. Potrzebna jest instrukcja DECLARE określająca nterfejs z C.

  2. W module C napisać prototyp dla podprogramu Basic i dołączyć informacje o typie parametrów.

  3. Żyć słów kluczowych __pascal lub __fortran

  4. Upewnić się, czy wszystkie dane przesyłane są jako wskaźniki near. Basic może przesyłać parametry na różne sposoby, ale nie może otrzymać ich w inny sposób niż wskaźnik near. Jeśli chcemy przesłać dane nie mieszczące się w bieżącym segmencie, należy je przekopiować do zmiennej w segmencie bieżącym

  5. Skompilować moduł C w modelu medium lub large


Poniższy program demonstruje program Basicowski wywołujący funkcję C, która z kolei wywołuje funkcję Basicowska, zwracającą podwojoną liczbę przesłaną do niej. Drukuje te dwie liczby:

'BASIC program

'The main program is in BASIC becouse of BASIC's startup

'requirements. The BASIC main program calls the C function Cprog

'Cprog calls the BASIC subroutine Db1

'
DEFINT A-Z

DECLARE SUB Cprog CDECL()

ALL Cprog

END
'
FUNCTION Db1(N) STATIC

Db1 = N*2

END FUNCTION

'
SUB Printnum(A,B) STATIC

PRINT "The first number is";A

PRINT "The second number is";B

END SUB


/* C source; compile in medium or large model

The parameters are declared as near pointer becouse of Basic requirements*/


int __fortran db1(int __near *N);

void __fortran printnum(int __near *A, int __near *B);


void cprog()

{
int a=5,b=6;

printf("%D times 2 is %d

,a,db1(&a) );

printnum(&a,&b);
}

Konwencje nazewnictwa i wywołań określane są przez słowo kluczowe CDECL w deklaracji Basicowej i __fortran w deklaracji funkcji db1 i printnum w module C.

Wywołanie podprogramu Fortranowskiego z poziomu C


W języku Fortran egzystują dwa rodzaje podprogramów: subroutine oraz function. Function zwraca wartość , subroutine nie. Poniżej przedstawione zostaną dwa przykłady ilustrujące różnice w użyciu function i subroutine.


Wywołanie subroutine z poziomu C

/* C source file - calls FORTRAN subroutine

compile in medium or large model */


extern void __fortran maxparam(int __near *I, int __near *J);


/*Declared as void becouse there is no return value;

Fortran keword causes C to use FORTRAN/PASCAL

calling and naming convenctios

two integer parameters, passed by near adresse */


main()
{
int a=5, b=7;

printf("a = %d, b = %d", a,b);

maxparam(&a,&b);
printf(" a=%d, b=%d", a,b);

}

C FORTRAN source file, subroutine MAXPARAM

C
$NOTRUNCATE
SUBROUTINE MAXPARAM(I,J)

INTEGER*2 I [NEAR]

INTEGER*2 J [NEAR]

C
C I and J received by near adress becouse of NEAR attribute

C
IF (I.GT.J) THEN

J=I
ELSE
I=J
ENDIF
END
Procedura maxparam przypisuje większemu parametrowi wartość mniejszego.
W tym przykładzie C program adoptuje konwencje Fortranowskie przy pomocy użycia w prototypie funkcji słowa kluczowego __fortran.

Ponieważ z treści wynika, iż parametry mogą zmieniać swoją wartość musimy je przesłać przez adres(referencje). W tym przypadku wybrano typ near. W związku z tym w programie Fortranowskim należy je również zadeklarować jako NEAR.

Gdybyśmy kompilowali program fortranowski w modelu medium, a parametry w C zadeklarowane byłyby jako FAR podobną operacje należałoby wykonać w programie fortranowskim.

Wywołanie function z poziomu C

Poniższy przykład demonstruje wywołanie z poziomu C Fortranowskiej funkcji liczącej silnię:fact:

/* C source file - calls FORTRAN function

compile in medium or large memory model */


int __fortran fact(int N);


/* Fortran keyword causes C to use FORTRAN calling and naming convenction.

Integer parameter passed by value */


main()
{
int x=3,y=4;

printf("The factorial of x is %4d",fact(x) );

printf("The factorial of y is %4d",fact(y) );

printf("The factorial of x+y is %4d",fact(x+y) );

}

C FORTRAN source file - factorial function

C
C N is received by value, because of VALUE attribute

C NOTRUNCATE - no truncate identifier name to 6 characters

C
$NOTRUNCATE
INTEGER *2 FUNCTION FACT (N)

INTEGER*2 N[VALUE]


INTEGER *2 I

FACT = 1

O 100 I=1,N

FACT=FACT*I
100 CONTINUE

RETURN
END
Ponieważ w przeciwieństwie do poprzedniego przykładu parametry nie ulegały zmianie zdecydowano przesłać je przez wartość (sposób default dla języka C). Wymagało to określenia atrybutu VALUE dla przesyłanych parametrów w definicji funkcji Fortranowskiej.

Wywoływanie podprogramów Pascalowskich z poziomu C.

W Pascalu mamy do czynienia z dwoma rodzajami podprogramów: procedurą i funkcją. Procedura nie zwraca wartości, funkcja zwraca.


Procedura Pascalowa wywoływana z poziomu C

Procedura Pascalowska maxparam jest odpowiednikiem takiej samej w języku Fortran:

/* C source file - calls Pascal procedure. Compile in medium or large memory model */

void __pascal maxparam(int __near *a, int __near*b);


/* Declare as void becouse there is no return value.The __pascval keyword causes C to use PASCAL calling and naming convenction. Two integer parameters passed by near adress */

main()
{
int a=5,b=7;


printf("a=%d, b=%d",a,b);

maxparam(&a,&b);
printf("a=%d,b=%d",a,b);
}

{ Pascal source code = maxparam procedure }


MODULE Psub;

PROCEDURE Maxparam(var a:integer;var:b:integer);

begin
if a>b then

b:=a
else
a:=b;
end;
end.

W powyższym przykładzie ponieważ parametry przesyłane były przez adress near można było użyć w procedurze słowa VAR. Dla adresu far konieczne byłoby użycie VARS. Konwencja pascalowa określana jest przez słowo kluczowe __pascal


Wywołanie funkcji Pascalowskiej z poziomu C

Funkcja jest odpowiednikiem funkcji z Fortranu:

/* C source file - calls PASCAL function

compile in medium or large memory model */


int __pascal fact(int N);


/* Pascal keyword causes C to use PASCAL calling and naming convenction.

Integer parameter passed by value */


main()
{
int x=3,y=4;

printf("The factorial of x is %4d",fact(x));

printf("The factorial of y is %4d",fact(y));

printf("The factorial of x+y is %4d",fact(x+y));

}

{ Pascal source code - factorial function }


MODULE Pfun;

FUNCTION Fact (n:integer):integer;

begin
fact:=1;
while n>0 do

begin
fact :=fact*n;

n:=n-1;
end;
end;
end.

Dla języka C++ interfejs pomiędzy językiem C++ i innym językiem wyższego poziomu odbywa się poprzez odniesienie do C. Mimo, że język C jest podzbiorem języka C++, ze względu na różnice między sposobem pracy ze stosem w momencie wywoływania funkcji (kolejność porządkowania, kolejność składowania parametrów na stosie) należy używając na poziomie C++ funkcji napisanych w C, dołączyć interfejs: extern "C"

{
void prin; // odwołanie do funkcji w języku C

}

extern "C" { int __pascal fact(int n); } //deklaruje funkcję napisaną w konwencji Pascala


Odwołanie C do języków asemblerowych

Najczęstszym sposobem pisania "wstawek" asemblerowych jest użycie tzw. inline asemblera. Można również tworzyć samodzielne moduły w dostępnych asemblerach zewnętrznych. Jednak asembler "inline" jest bardziej efektywny od samodzielnych asemblerów. Oto kilka zalet takiego podejścia:

  1. Kod asemblera inline włączony jest do kodu języka C. Kod napisany w asemblerach wewnętrznych musi być umieszczony w oddzielnych plikach.

  2. Krótkie wstawki asemblerowe mogą optymalizować program.

  3. Nie wymagają też wykonywania wywołania funkcji tak jak by to było a asemblerach zewnętrznych. Są to po prostu linie kodu tyle tylko, że w asemblerze. Nie będziemy mówili o asemblerach zewnętrznych. Powiemy tylko parę słów o asemblerze "inline";
    Zakres użycia tej techniki jest bardzo szeroki jednak najczęściej stosowany jest do:

Asembler inline umożliwia umieszczenie kodu asemblerowego bezpośrednio w kodzie C. Jest wbudowany w kompilator i nie wymaga zewnętrznych asemblerów. Kod asemblerowy poprzedzony musi zostać słowem kluczowym asm:

__asm
{
mov ah,2

mov dl,7

int 21h

}
kod ten równoważny jest zapisowi:

__asm mov ah,2

__asm mov dl,7

__asm int 21h

Jeśli słowo kluczowe asm używane jest przed każdą instrukcja wtedy mogą być one umieszczone w jednej lini:

__asm mov ah,2 __asm mov dl,7 __asm int 21h


Funkcja asemblera inline w kodzie C

#include
int power2(int num,int power);

void main(void)

{
printf("3 times 2 to the powerr of 5 is %d

",power2(3,5) );

}
int power2(int num,int power)

{
__asm
{
mov ax,num;

mov cx,power;

shl ax,cl;

}
/* return with result in ax */

}

Wywoływanie funkcji C w bloku asemblerowym

W bloku __asm możemy wywoływać funkcje biblioteczne języka C: wykonuje to instrukcja CALL:

#include
char format[]="%s %s";

char hello[] = "Hello";

char world[] = "world";


void main(void)

{
__asm
{
mov ax, offset world

push ax

mov ax, offset hello

push ax

mov ax, offset

push ax

call printf

}
}
funkcja printf zbiera swoje argumenty, które umieszczane są na stosie.

Powyższy kod emuluje zapis C:

printf(format,hello,world);

Poniższy przykład demonstruje wywołanie z poziomu C Fortranowskiej funkcji liczącej silnię: fact:
/* C source file - calls FORTRAN function

compile in medium or large memory model */


int __fortran fact(int N);

/* Fortran keyword caises C to use FORTRAN calling and naming convenction.

Integer parameter passed by value */


main()
{
int x=3,y=4;

printf("The factorial of x is %4d",fact(x));

printf("The factorial of y is %4d",fact(y));

printf("The factorial of x+y is %4d",fact(x+y));

}

C FORTRAN source file - factorial function

C
C N is received by value, becausse of VALUE attribute

C NOTRUNCATE - no truncate identifier name to 6 characters

$NOTRUNCATE
INTEGER *2 FUNCTION FACT (N)

INTEGER*2 N[VALUE]

C
INTEGER *2 I

FACT = 1

DO 100 I=1,N

FACT=FACT*I
100 CONTINUE

RETURN
END
Ponieważ w przeciwieństwie do poprzedniego przykładu parametry nie ulegały zmianie zdecydowano przesłać je przez wartość (sposób default dla języka C). Wymagało to określenia atrybutu VALUE dla przesyłanych parametrów w definicji funkcji Fortranowskiej.

Wywoływanie podprogramów Pascalowskich z poziomu C

W Pascalu mamy do czynienia z dwoma rodzajami podprogramów: procedurą i funkcją. Procedura nie zwraca wartości, funkcja zwraca.

Procedura Pascalowa wywoływana z poziomu C

Procedura Pascalowska maxparam jest odpowiednikiem takiej samej w języku Fortran:
/* C source file - calls Pascal procedure. Compile in medium or large memory model */
void __pascal maxparam(int __near *a, int __near*b);

/* Declare as void becouse there is no return value.The __pascval keyword causes C to use PASCAL calling and naming convenction. Two integer parameters passed by near adress */
main()
{
int a=5,b=7;

printf("a=%d, b=%d",a,b);

maxparam(&a,&b);
printf("a=%d,b=%d",a,b);
}
{Pascal source code = maxparam procedure }

MODULE Psub;

PROCEDURE Maxparam(var a:integer;var:b:integer);

begin
if a>b then

b:=a
else
a:=b;
end;
end.
W powyższym przykładzie ponieważ parametry przesyłane były przez adress near można było użyć w procedurze słowa VAR. Dla adresu far konieczne byłoby użycie VARS. Konwencja pascalowa określana jest przez słowo kluczowe __pascal


Wywołanie funkcji Pascalowskiej z poziomu C

Funkcja jest odpowiednikiem funkcji z Fortranu:

/* C source file - calls PASCAL function

compile in medium or large memory model */


int __pascal fact(int N);


/* Pascal keyword caises C to use PASCAL calling and naming convenction.

Integer parameter passed by value */


main()
{
int x=3,y=4;

printf("The factorial of x is %4d",fact(x));

printf("The factorial of y is %4d",fact(y));

printf("The factorial of x+y is %4d",fact(x+y));

}
{ Pascal source code - factorial function }

MODULE Pfun;

FUNCTION Fact (n:integer):integer;

begin
fact:=1;
while n>0 do

begin
fact :=fact*n;

n:=n-1;
end;
end;
end.

Dla języka C++ interfejs pomiędzy językiem C++ i innym językiem wyższego poziomu odbywa się poprzez odniesienie do C: extern "C"

{
void prin; // odwołanie do funkcji w języku C

}

extern "C" { int __pascal fact(int n); } //deklaruje funkcję napisaną w konwencji Pascala


Odwołanie C do języków asemblerowych

Najczęstszym sposobem pisania "wstawek" asemblerowych jest użycie tzw. inline asemblera. Można również tworzyć samodzielne moduły w dostępnych asemblerach. Jednak asembler "inline" jest bardziej efektywny od samodzielnych asemblerów. Oto kilka zalet takiego podejścia:

  1. kod asemblera inline włączony jest do kodu języka C. Kod napisany w asemblerach zewnętrznych musi być umieszczony w oddzielnych plikach.

  2. krótkie wstawki asemblerowe mogą optymalizować program

  3. nie wymagają też wykonywania wywołania funkcji tak jak by to było a asemblerach zewnętrznych. Są to po prostu linie kodu tyle tylko, że w asemblerze

Nie będziemy mówili o asemblerach zewnętrznych. Powiemy tylko paręŽ słów o asemblerze "inline";
Zakres użycia tej techniki jest bardzo szeroki jednak najczęściej stosowany jest do:
- poprawiania szybkości programu

Umożliwia umieszczenie kodu asemblerowego bezpośrednio w kodzie C. Jest wbudowany w kompilator i nie wymaga zewnętrznych asemblerów kod asemblerowy poprzedzony musi zostać słowem kluczowym asm:

__asm
{
mov ah,2

mov dl,7

int 21h

}
kod ten równoważny jest zapisowi:

__asm mov ah,2

__asm mov dl,7

__asm int 21h

Jeśli słowo kluczowe asm używane jest przed każdą instrukcja wtedy mogą być one umieszczone w jednej lini:

__asm mov ah,2 __asm mov dl,7 __asm int 21h

Poniższy przykład ilustruje użycie funkcji napisanej w asemblerze inline wywołanej w kodzie języka C:


#include
int power2(int num,int power);

void main(void)

{
printf("3 times 2 to the powerr of 5 is %d

",power2(3,5));
}
int power2(int num,int power)

{
__asm
{
mov ax,num;

mov cx,power;

shl ax,cl;

}
/* return with result in ax */

}

W bloku __asm możemy wywoływać funkcje biblioteczne języka C: wykonuje to instrukcja CALL:

#include
char format[]="%s %s";

char hello[] = "Hello";

char world[] = "world";


void main(void)

{
__asm
{
mov ax, offset world

push ax

mov ax, offset hello

push ax

mov ax, offset

push ax

call printf

}
}
funkcja printf zbiera swoje argumenty, które umieszczane są na stosie

powyższy kod emuluje zapis C:

printf(format,hello,world);

Zakres tematyczny:

  1. Organizacja pamięci.

  2. Funkcje.

  1. Organizacja pamięci.

Pamięć została podzielona na wiele różniących się części.

Mamy więc obszar kodu w którym znajdują się instrukcje programu, i jest to obszar nie zmieniany

trakcie pracy programu. Znajdują się tam instrukcje składające się na kod, np. program:

int x, y = 1;

main()
{
int z;

char *string;

x = 5;

x++;
y -=10;

string = malloc(5);

}
po przetłumaczeniu na język rozkazów procesora wygląda tak:

MOV x,5

INC x

SUB y,10

Instrukcje te pojawią się w obszarze kodu. Jak można się domyślić, obszar ten nie powinien ulegać

mianom po uruchomieniu programu. Jednak nie można tego powiedzieć o używanych danych, zajmijmy się więc obszarem danych.

Obszar danych w rzeczywistości składa się z dwóch części: dla danych zainicjowanych i dla danych niezainicjowanych.
W naszym programie zmienna x nie jest zainicjowana, a y jest. Oznacza to, że powinny być przechowywane w różnych obszarach.

Obszar danych niezainicjowanych nazywany jest też obszarem BSS (block started segment - segment bloku początkowego.) Powodem wspólnego przechowywania danych niezainicjowanych jest możliwość jednoczesnego przypisania wszystkim danym wartości zero.
Jednak nie wszystkie dane zainicjowane lub niezainicjowane przechowywane są w obszarze danych. W rzeczywistości w obszarze danych przechowywane są trzy rodzaje zmiennych:


Zasięg zmiennych

Omówimy teraz rodzaje obiektów w zależności od zasięgu i czasu ich życia. Mamy następujące typy zmiennych:


Zmienne automatyczne: Wszystkie zmienne deklarowane wewnątrz funkcji -czy to będzie main, czy też funkcja zdefiniowana przez użytkownika są prywatne, czyli lokalne dla danej funkcji. Żadna inna funkcja nie ma do nich bezpośredniego dostępu. Każda zmienna lokalna funkcji, zaczyna dopiero żyć w chwili wywołania funkcji, a zanika po jej zakończeniu. Dlatego zmienne te nazywane są zwykle automatycznymi

Jeśli po raz drugi wywołamy funkcję, to zmienne te zostaną ponownie powołane do życia, ale nie ma pewności, że na pewno znajdą się w tym samym miejscu w pamięci co poprzednio. Co więcej, nie zachowają się jej poprzednie wartości (z poprzedniego wywołania) i muszą być jawnie określane od nowa przy każdym wejściu do funkcji. Ponieważ zmienne te nie są zerowane w chwili definicji, to początkowo tkwią w nich śmieci - trzeba pamiętać, aby nie odczytywać tych zmiennych zanim w nich czegoś sensownego nie zapiszemy. Zmienne te przechowywane są na stosie - przedziela się tam tylko obszar dla tych zmiennych i nic więcej - nie inicjuje się.
Z tego typu obiektami wiąże się słowo kluczowe auto :

auto int x;

Jest ono rzadko używane, ponieważ obiekty są definiowane jako automatyczne przez domniemanie. Jeśli więc w bloku funkcji wystąpi taka deklaracja jest ona równoważna :

int x;


Zmienne globalne: Przeciwieństwem zmiennych lokalnych są zmienne zewnętrzne w stosunku do wszystkich funkcji, zwane globalnymi. Oznacza to, że są one dostępne poprzez nazwę we wszystkich funkcjach pliku. Deklaracje takich zmiennych umieszcza się na początku programu (pliku). W naszym programie zmienne x i y zostały zadeklarowane jako globalne.
Ze względu na to, że zmienne globalne są ogólnie dostępne, mogą być stosowane zamiast listy argumentów przy przekazywaniu danych między funkcjami. Co więcej, ponieważ istnieją stale, a nie pojawiają się i znikają razem z wywołaniem funkcji zachowują więc swoje wartości nawet po zakończeniu działania tych funkcji, które nadały im wartość. Zmienna globalna może być zdefiniowana tylko jeden raz (tylko jeden raz można przydzielić jej pamięć) na zewnątrz wszystkich funkcji. Taka zmienna musi być także deklarowana (określony charakter zmiennej bez przydzielania pamięci) we wszystkich funkcjach które chcą mieć do niej dostęp - można to zrobić poprzez deklaracje extern:

extern int x;

W pewnych przypadkach słówko to można pominąć: jeśli definicja zmiennej globalnej pojawia się w pliku źródłowym przed użyciem zmiennej w konkretnej funkcji. W rzeczywistości definicje wszystkich zmiennych globalnych umieszcza się na początku pliku źródłowego, a następnie pomija w całym pliku deklaracje extern.

Jeśli jednak np. program składa się z 3 plików i zmienne globalne definiujemy np. w pliku 1, to w plikach korzystających z tych zmiennych nie można pominąć deklaracji extern.
Zmienne globalne przechowywane są w normalnym obszarze pamięci i są wstępnie inicjowane zerami

Zmienne statyczne: Są to zmienne deklarowane przy pomocy słowa kluczowego static. Można ją stosować zarówno do zmiennych zewnętrznych jak i wewnętrznych.

W odniesieniu do zmiennych globalnych przydomek static oznacza, że nie życzymy sobie, aby nazwa ta była znana w innych plikach składających się na dany program. Nazwa jest nadal globalna, ale tylko dla danego pliku. Jest to więc sposób na ukrycie nazw zmiennych. np.:
static char x[10];

static int y=0;

................
int fun1(void) {...}

void fun2(int c) {...}

W danym pliku zdefiniowane są zmienne x, y jako zmienne globalne i chcemy aby tylko funkcje umieszczone w tym pliku: fun1 i fun2 miały dostęp do zmiennych x i y. Wówczas żadne inne funkcje nie będą miął do nich dostępu, a nazwy nie będą kolidować z takimi samymi nazwami w innych plikach tego samego programu. Deklaracje takie stosuje się także do nazw funkcji.
Lokalne zmienne statyczne są tak samo lokalne dla funkcji, jak zmienne automatyczne. Jednak w przeciwieństwie do automatycznych nie pojawiają się i nie znikają razem z wywołaniem funkcji, lecz istnieją między jej wywołaniami. Czyli przy powtórnym wejściu do funkcji ma wartość taką jaka miała przy opuszczaniu funkcji.

Zmienne te są zakładane w tym samym obszarze co globalne i są wstępnie inicjowane zerami.

Zmienne register: zmienne rejestrowe informują kompilator, że zmienna będzie często używana i trzeba umieszczać ją jeśli się da w rejestrach przez co znacznie zwiększa się szybkość programu. Kompilator może tą deklarację zignorować jeśli np. liczba zmiennych zadeklarowanych jako register jest większa niż liczba rejestrów lub nieprawidłowy typ zmiennych deklarowany jest jako register (można deklarować tylko zmienne całkowite automatyczne) lub gdy nie ma takiej możliwości kompilator. Deklaracje tej zmiennej ma postać:

register int x;

Nie można uzyskać adresu takiej zmiennej, ponieważ rejestr to nie kawałek pamięci, a więc nie adresuje się go w zwykły sposób.


Stos tworzony jest w górnej części obszaru pamięci i w miarę zapełniania rozrasta się w kierunku dolnej części pamięci - to tu właśnie C++ umieszcza zmienne lokalne, wykorzystuje stos do przesyłania parametrów do funkcji, przechowuje adresy powrotne. W naszym programie miejsce na stosie dla zmiennej z zostanie utworzone w momencie rozpoczęcia programu. O tym jak funkcje wykorzystują stos pomówimy przy okazji omawiania funkcji.

Sterta ostatni obszar pamięci znajduje się powyżej obszaru kodu i danych. Rozmiar sterty podobnie jak stosu zwiększa się, ale wzrost następuje w górę (do wyższej pamięci, nie jak w przypadku stosu). Obszar ten przydziela programista dla zmiennych dynamicznych.


Rozmiar stosu i sterty może się zmieniać. Obszar kodu i danych po utworzeniu pliku EXE ma ustaloną wielkość.

  1. Funkcje.

Jedną z najlepszych cech nowoczesnych języków programowania jest możliwość posługiwania się funkcjami. W przeciwieństwie do Pascala, gdzie były procedury i funkcje, w języku C/C++ mamy do czynienia tylko z funkcjami. Język ten opracowano tak, aby posługiwanie się funkcjami było łatwe, wygodne i skuteczne. Tam gdzie tylko jest możliwe, powinno się tworzyć funkcje, ponieważ zwiększa to czytelność programu. Zacznijmy od najprostszego programu wykorzystującego uncje:
#include
#include
int dodaj(int a, int b);

main()
{
int wynik;

wynik = dodaj(2,5);

cout<<"wynik obliczeń funkcji = "<< wynik;

}
int dodaj(int a, int b)

{
int w;

w = a + b;

return w;

}
Funkcję wywołuje się przez podanie jej nazwy i umieszczonych w nawiasie argumentów. Funkcja ma swoją nazwę która ją identyfikuje. Jak pamiętamy, aby można było użyć jakąś nazwę, musi ona być przed pierwszym użyciem zadeklarowana. Trzecia linijka programu to właśnie deklaracja funkcji zwana inaczej prototypem funkcji. Deklaracja funkcji informuje kompilator jaką wartość funkcja będzie zwracała i jakiego typu są jej argumenty. Definicja funkcji, czyli po prostu jej treść jest napisana na końcu, choć można napisać ją na początku, wtedy prototyp funkcji nie jest konieczny. Zatrzymajmy się teraz przy deklaracjach:

float fun1( int a); float fun1(int)

void fun2(int a, char b); void fun2(int, char)

int fun3(void);

char fun4();

void fun5(...);

Na końcu deklaracji znajduje się średnik.

  1. Funkcja może zwracać wartość wtedy przed jej nazwą umieszczamy typ zmiennej zwracanej przez funkcję;

  2. Jeśli funkcja nie zwraca wartości jej nazwę poprzedzamy słowem void.

  3. Funkcja może być wywoływana z argumentami, wtedy lista argumentów łącznie z typami umieszczana jest po nazwie funkcji w nawiasach.

  4. Jeśli funkcja wywoływana jest bez argumentu wtedy nawias jest pusty lub słowo void w nawiasie.

  5. Możemy deklarować funkcję z bliżej nie znaną liczba argumentów, wówczas w nawiasie umieszczamy ... .

W języku C++ zaszła zmiana w stosunku do C:

void fun(a,b)

int a,b;

{...}
Nowy styl programowania wymaga tego:

void fun(int a,int b)

{...}

Nazwy argumentów ( nie typy) są nieistotne dla kompilatora i można je pominąć. Istotne to jest dopiero przy definicji zmiennej.

Przesyłanie argumentów do funkcji

Weźmy np. funkcję:

void func(int a)

{
cout<<"W funkcji parametr formalny modyfikowany ma wartość: a = "<<A;

a += 5;

}
Nazwa a to tzw. argument (parametr) formalny funkcji.

W funkcji main wywołujemy ją w następujący sposób:

int a1=5;

cout<<"Parametr aktualny przed wywolaniem: a1 = "<<A1;

func(a1); // a = 10;

cout<<"Parametr aktualny po wywołaniu: a1 = "<<A1;

To co pojawia się w wywołaniu funkcji: a1 to jest parametr aktualny funkcji, czyli taki na którym ma pracować dana funkcja. Należy w tym miejscu zapytać się w jaki sposób przesyłane są argumenty do funkcji. Otóż w języku C++ mamy trzy takie sposoby:

W standardzie języka dostępne były dwa pierwsze sposoby, C++ dodało trzeci.

Przesyłanie przez wartość polega na tym, że do funkcji przesyłamy wartość argumentu aktualnego, ale funkcja nie pracuje na tym argumencie, nie ma możliwości modyfikowania go. Parametr aktualny służy jedynie do modyfikacji parametru formalnego przechowywanego tymczasowo na stosie. Jak widać, w programie parametr aktualny nie został zmieniony. Dodanie 5 nie nastąpiło do parametru aktualnego, ale jedynie do zmiennej lokalnej na stosie, gdzie mieści się kopia ( o nazwie a).
Można spowodować jeśli to konieczne, aby funkcja zmieniała wartość parametru aktualnego. W tym celu :

  1. funkcja wywołująca musi przekazać adres zmiennej, a funkcja wywoływana musi zadeklarować odpowiedni parametr jako wskaźnik - przekazywanie przez adres

#include
struct bigone


{
int serno;

char text[100];

} bo = {123,"This is a big structure"}

void(ptrfunc(const bigone *p1);

oid main()


{
ptrfunc(&bo);
}
void ptrfunc(const bigone *p1)


{
cout<<'
'<serno;
cout<<'
'<text;
}
b) do funkcji należy przekazać referencje. To jest tzw. przesyłanie przez referencje.

Na początek powiedzmy sobie co to jest referencja. Jest to nowy typ zmiennej wprowadzony przez C++. Referencja jest to 16 lub 32 bitowa liczba przechowująca adress zmiennej, ale zachowująca się syntaktycznie jak zmienna. Możemy myśleć o referencji jako o przezwisku zmiennej. Jest ona alternatywną nazwą zmiennej. W czasie jej inicjacji referencje przypisujemy do danej zmiennej. Referencje tworzymy przy pomocy unarnego operatora &:

int actualint;

int &otherint = actualint; // deklaracja referencji

Powyższe instrukcje deklarują zmienną całkowitą actualint i informują kompilator, że inną nazwa actualint jest otherint. Od tej pory wszystkie operacje na każdej zmiennej mają ten sam rezultat:
// przykład użycia referencji

#include
void main()

{
int actualint = 123;

int &otherint = actualint;

cout<< '

'<< actualint; //123

cout<< '

'<< otherint; //123

otherint++;

cout<< '

'<< actualint; //124;

cout<< '

'<< otherint; //124

actualint++;

cout<< '

'<< actualint; //125

cout<< '

'<< otherint; //125

}
Należy pamiętać, że referencja nie jest kopią zmiennej, ale tą samą zmienną pod inną nazwą. Poniższy program umożliwia wyświetlenie adresu zmiennej i referencji:

#include
void main()

{

int actualint = 123;

int &otherint = actualint;

cout<< &actualint << ' ' <<&otherint

}

Po uruchomieniu programu zostaną wydrukowane te same adresy dla obu identyfikatorów, zależne

d konfiguracji systemu. Referencje nie mogą istnieć bez zmiennej do której są przypisane i nie można na nich przeprowadzać żadnych operacji jako na niezależnych obiektach. W związku z tym przy deklarowaniu jednocześnie inicjujemy referencje.


Rozpatrzmy przykład funkcji :

void func(int a,int &b)

{

cout<<"W funkcji parametr formalny przed modyfikacją ma wartość: a = "<<A;

out<<"W funkcji parametr formalny przed modyfikacją ma wartość: b = "<<B;

a += 5; b += 6;

cout<<"W funkcji parametr formalny modyfikowany ma wartość: a = "<<A;

cout<<"W funkcji parametr formalny modyfikowany ma wartość: b = "<<B;

}

W funkcji main wywołujemy ją w następujący sposób: int a1=5, b1=5; cout<<"Parametr aktualny przed wywolaniem: a1 = "<<A1;

cout<<"Parametr aktualny przed wywolaniem: b1 = "<<B1;

func(a1,b1); // a = b = 5;

// a = 10 , b = 11;

cout<<"Parametr aktualny po wywołaniu: a1 = "<<A1;

cout<<"Parametr aktualny po wywołaniu: b1 = "<<B1;

Jak widać, funkcja wywoływana jest z dwoma argumentami.

Jeden klasycznie przekazywany jest przez wartość, w związku z tym zmienna aktualna a1 po wykonaniu funkcji nie zmieniła się, ale zmienna b1 została zmieniona, ponieważ przekazana została przez referencję.


Przy przekazywaniu parametru przez referencje do funkcji przesyłany jest adres przesyłanej zmiennej, a na stosie tworzona jest referencja. W naszym programie została zmieniona poprzez dodanie 6. Ponieważ wszystkie operacje wykonywane na referencji są operacjami wykonywanymi na zmiennej (referencja to adress zmiennej) do której przypisana jest referencja, w związku z tym zmodyfikowana została wartość argumentu aktualnego b1.

Po zakończeniu funkcji kopia zmiennej a1 przechowywana na stosie ( a po modyfikacji = 10) jest ze stosu usuwana - zmienna a1 - nie jest zmieniona.

Drugi argument przesyłany przez referencje, więc na stosie przechowywany był adres zmiennej, która w funkcji nazwaliśmy b. Po jego skasowaniu (ze stosu) pozostaje nadal zmienna b1, ale już zmieniona.
Przyjrzyjmy się dokładnie wywołaniu funkcji. Oba sposoby przekazywania parametrów w momencie wywołania nie różnią się od siebie. Może to prowadzić do wielu nieporozumień szczególnie gdy wywołujemy dużo funkcji, których opisy znajdują się w innym module. Możemy zapomnieć, które parametry przekazywane są przez referencje i nie będziemy mogli zidentyfikować błędu w przypadku gdy powstał on na skutek modyfikowania parametru wywołania jakiejś funkcji.
Przesyłanie przez referencję stosuje się w przypadku, gdy argumentami funkcji są duże obiekty, do przesyłania których przez wartość konieczne było rezerwowanie na stosie setek bajtów.

Zastosowanie wskaźników w argumentach funkcji

Omawialiśmy już sposób przesyłania argumentów przez adres - na stosie przechowywany jest adres, a nie wartość zmiennej. Ten sposób przesyłania argumentów można z powodzeniem zastosować do przypadku gdy zmiennymi są tablice. Jeśli do funkcji przesyłamy całą tablicę to nie przesyłamy kolejno wszystkich elementów, ale jedynie jej adres, czyli jeśli mamy funkcje korzystającą z tablicy:

void fun_tab(int tab[]);

int tab[10];

to wywołanie takiej funkcji jest następujące:

fun_tab(tab);
pamiętamy, ze nazwa tablicy jest jej adresem. Ponieważ do funkcji przesłany został adres tablicy to wszystkie operacje w niej wykonywane będą się odbywać na oryginale, a nie na kopii. Po wysłaniu tablicy do funkcji może ona ją odebrać na dwa sposoby:

#include
void f1(*wsk1,n);

void f2(tab[],n);

main()
{
int tablica[4] = {1,2,3,4};

.............// inicjacja tablicy lub wypełnienie

f1(tablica,4);
f2(tablica,4);
}
void f1(int *wsk1,li)

{
for(int i = 0;i<N;I++)

cout<<*(wsk++);
}
void f2(int tab[],n)

{

for(int i = 0;i<N;I++)

cout<<TAB[I];
}

Wskaźniki do funkcji

Jak dotychczas wspominaliśmy, wskaźnikami możemy pokazywać na różne obiekty. nic nie stoi więc na przeszkodzie, aby wskazywać na funkcje. Ponieważ wskaźnik zawiera adres, wskaźnik do funkcji wskazuje więc na obszar gdzie zaczyna się kod będący instrukcjami żądanej funkcji. Zapis tego wskaźnika miałby postać:


int (*wsk_fun)()

co oznaczałoby, że wsk_fun jest wskaźnikiem do funkcji bezargumentowej zwracającej wartość typu int. Istotne są tu nawiasy bo zapis:


int *wsk_fun()


oznaczałby deklarację funkcji (a nie wskaźnika do funkcji) bezargumentowej zwracającej wskaźnik do int.

void test(char *p) //deklaracja funkcji

{
cout <<P;

}
void (*tun_test)(char *) //wskaźnik do funkcji


main()
{
fun_test = test; // fun_test wskazuje na funkcje test

(*fun_test)("to jest test");

}
Do zmiennej fun_test wstawiamy adres funkcji test. Ponieważ nazwa funkcji jest jej adresem jak to było w przypadku tablic, operowanie wskaźnikami do funkcji jest stosunkowo proste.:
wskaźnik = nazwa_funkcji;

Nie ma tu nawiasów bo oznaczałoby to : wywołaj funkcje o takiej nazwie

Istnieją dwa sposoby zrealizowania funkcji:

(*fun_test)("to jest test");

lub
fun_test("to jest test"):

Zapisy te są równoważne. Należy pamiętać, że typ wskaźnika do funkcji musi się zgadzać z typem funkcji na która wskazuje:

char fun(int,int);

imt (*wsk)(); wsk = fun; //niezgodność typów wskaźnika i funkcji

Na wskaźnikach do funkcji nie wolno wykonywać żadnych operacji arytmetycznych.
Kiedy więc przydają się wskaźniki do funkcji:

  1. przy przekazywaniu funkcji jako argumentu innej funkcji

int f1(int a,void (*wsk_f2)());

void f2();

main()

{

int i;

............
i = f1(1,f2);

.......
}
int f1(int a, void (*wsk_f2)())

{
........
(*wsk_f2)();
..............
}
void f2() {...}

2)do tworzenia tablic wskaźników do funkcji

void f1(){...}

void f2(){...}

void f3(){...}

main()
{
int wsk;

void (*tab_fun[3])() = {f1,f2,f3}; //tab_fun jest 3-elementową tablica wskaźników do //funkcji bezargumentowej void.

cout<<"menu";
cout<<"1 - akcja 1";

cout<<"2 - akcja 2";

cout<<"3 - akcja 3";

cout<<"4 - koniec";

cout<<"wybierz opcje";


cin>>wsk;

switch(wsk)
{
case 1:

case 2:

case 3:

(*tab_fun[wsk])();break;
case 4: exit(1);

default:break;
}
}
Na zakończenie omawiania funkcji wspomnieć należy o funkcji main z argumentami. Wcześniej mówiliśmy, że main jest taką samą funkcja jak inne, w związku z tym może występować z argumentami , nie tak jak to było dotychczas.

W środowisku języka C, do uruchamianego programu można przekazać parametry, czyli argumenty wywołania, w wierszu polecenia wywołującego program. Działanie programu rozpoczyna się wtedy wywolaniem funkcji main z dwoma argumentami. Pierwszy umownie nazywany argc(argument counter) jest liczbą argumentów z jakimi program został wywołany. Drugi argument argv(argument vector) jest wskaźnikiem do tablicy zawierającej argumenty, każdy jako odrębny tekst.
Przykład:
#include
#include
#include
int main(int argc, char *argv[])

{
char źródło[13],cel[13];

if(argc !=3)

{
cout<<"niepoprawna liczba argumentów";

exit(1);
}
strcpy(zrodlo,argv[1]);
strcpy(cel,argv[2]);
if(strcmp(zrodlo,cel) == 0)

{
cout<<"Ta sama nazwa zbioru wejściowego i wyjściowego";

exit(1);
}
// wejście

ifstream pl_zrodlo(argv[1]);

if(!pl_zrodlo)
{
cout<<"Blad otwarcia zbioru wejściowego";

exit(1);
}
// wyjście

ofstream pl_cel(argv[2]);

if(!pl_cel)

{
cout<<"Blad otwarcia zbioru wyjściowego";

exit(1);
}
int c;

while((c = pl_zrodlo.get()) != EOF)

{
if(!pl_cel.put(c))
{
cout<<"blad zapisu do pliku";

break;
}
}
if(pl_zrodlo.eof)
cout<<"EOF pliku zródłowego - KONIEC ODCZYTU";

return 0;

}
W tablicy argumentów są dwa elementy specjalne:

argv[0] - nazwa programu

argv[argc] - 0 tablica kończy się zerem.

Zakres tematyczny:

  1. Instrukcja typedef

  2. Struktury, unie, pola bitowe.

  3. Jeszcze raz o funkcjach:

  1. Instrukcja typedef.

W języku C wprowadzono mechanizm zwany typedef, do tworzenia nowych nazw typów danych. Np. deklaracja :

typedef int length;

tworzy dla typu int synonim lenght. Z tego typu można korzystać w deklaracjach, rzutowaniu itp., tak samo jak z int:

lenght dl, maxdl;

lenght *dl[];

Deklaracja typedef w żadnym wypadku nie tworzy nowego typu, nadaje po prostu inną nazwę dla istniejącego już typu. Za używaniem typedef przemawiają dwa względy:


Typ który określamy w deklaracji typedef nie musi być podstawowy, może to być dowolny typ pochodny:
typedef int *wsk_int

Deklaracja typedef nie redefiniuje nazw już istniejących.

  1. Inne pochodne typy zmiennych.

Należą do nich :

struktury
unie
pola bitowe

Struktury
W klasycznym C struktura była traktowana jako obiekt złożony z kilku lub jednej zmiennej, dla wygody zgrupowanych pod jedną nazwą. Struktury ułatwiają zorganizowanie skomplikowanych danych, ponieważ grupę związanych ze sobą zmiennych (nawet różnych typów) pozwalają traktować jako jeden obiekt, a nie zestaw oddzielnych danych. Typowym przykładem takiej struktury może być np. lista płac. Pracownik opisany jest przez kilka zmiennych: imię, nazwisko, adres, wynagrodzenie itp.

Język C++ poszedł dalej w definiowaniu struktur, i wzbogacił je o możliwość przechowywania tzw. funkcji składowych tzn. funkcji operujących na składowych struktury. Stąd już tylko krok do definiowania klas. Można nawet powiedzieć, że w pojęciu języka C++ zarówno struktury jak i unie są typami "klasowymi", czyli typu class. Ale o tym pomówimy w przyszłym semestrze. Na tym wykładzie omówimy struktury w starym stylu, natomiast po omówieniu klas powrócimy do struktur w nowym ujęciu.

Deklarację struktury rozpoczyna słowo kluczowe struct. Podajmy przykład struktury odpowiedniej dla grafiki. Punkt opisują dwie współrzędne, zadeklarowane jako składowe struktury:
struct point

{
int x;

int x;

}
Po słowie struct występuje nazwa struktury (point) zwana etykietą struktury. Nazwy zmiennych występujących w strukturze nazywamy składowymi struktury.

Obiekt typu struktura wymaga tyle pamięci do zapisu, ile wynosi suma pamięci koniecznej do zapisu poszczególnych składowych struktury.

Składowa struktury, normalna zmienna, etykieta mogą mieć te same nazwy bez obawy o konflikt. Deklaracja struct jest definicją nowego typu złożonego.

Po prawej klamrze zamykającej listę zmiennych może występować lista zmiennych, tak jak po każdym podstawowym typie. Zatem definicja:

struct {...} x,y,z

odpowiada składniowo definicji:

int x,y,z;

Deklaracja struktury, która nie zawiera listy zmiennych nie rezerwuje pamięci, jedynie opisuje wzorzec struktury. Mając deklarację struktury można używać jej do deklaracji zmiennych typu struktura np.:

struct point punkt; //równoważne point punkt;

deklaruje zmienną punkt będącą strukturą typu point.

Strukturę można zainicjować dopisując na końcu jej definicji listę wartości początkowych jej składowych np.:

struct point maxpoint = {320,200};

W wyrażeniach dostęp do konkretnej składowej struktury umożliwia konstrukcja:
nazwa_struktury.składowa
dla zmiennej strukturalnej punkt odwołania są następujące:

a = punkt.x;

b = punkt.y;

Język C umożliwia konstruowanie struktur zagnieżdżonych:

struct rect{

struct point p1;

struct point p2;

}
W takich strukturach dostęp do składowych podstawowych wykonuje się przy pomocy operacji:
struct rect ekran;

a1 = ekran.p1.x

b1 = ekran.p1.y;

Podobnie jak w przypadku zmiennych podstawowych, możemy posługiwać się tablicami struktur. Jest to często spotykane przy tworzeniu rekordów, które mają być zapisane do pliku. Można więc stworzyć tablicę struktur rect:

struct rect{

struct point p1;

struct point p2;

} okno[5];

Każdy element tablicy jest strukturą. Powyższy zapis można zapisać inaczej:
struct rect okno[5];

Inicjowanie tablicy struktur przeprowadza się podobnie jak dla pojedynczych struktur, po definicji podaje się ujętą w nawiasy klamrowe listę wartości początkowych:

struct point {

int x;

int y;

} punkt[ ] = {

0,0,

1,1,
1.5,
16,8 }

Precyzyjniej byłoby ująć wszystkie elementy wierszy tablicy w nawias {}np.:

{1,2},
{1.4},
{1,5},
........
ale gdy podano wszystkie wartości i gdy wartościami są proste stałe lub napisy, wówczas wewnętrzne nawiasy można pominąć. Jak w przypadku normalnych tablic, gdy pominięto wymiar i podano listę wartości początkowych, to liczba elementów tablic struktur zostaje wyliczona automatycznie.
Podczas omawiania wskaźników powiedzieliśmy, że można je tworzyć do większości typów podstawowych i pochodnych (złożonych). Pora więc teraz na omówienie wskaźników do struktur. Są one tak powszechne, że do języka włączono specjalny operator (strzałkę), mający postać: ->. Użycie tego operatora przedstawimy na przykładzie struktury dane:

struct dane{

int klucz;

int tabllica[200];

}dane_dos

Deklarujemy wskaźnik wsk_str do zmiennej dane_dos:

struct dane *dane_dos;

inicjujemy go zmienna dane_dos:

wsk_str = &dane_dos;

Od tego czasu do pól struktury można odwoływać się poprzez wskaźnik w następujący sposób:

wsk_str->klucz = 1; // ekwiwalent dane_dos .klucz = 1;

Zwróćmy uwagę, że nie stosujemy * do odwoływania się do wartości elementu składowego struktury wskazanej przez wskaźnik wsk_str. Powodem wprowadzenia operatora -> do obsługi stuk tur jest fakt, że przez operatory * i & nie uzyskamy dostępu do poszczególnych elementów struktury. Nie ma więc czegoś takiego jak:

*wsk_str.klucz;
Unie
Unia jest zmienną, która w różnych momentach może przechowywać w tym samym miejscu pamięci obiekty różnych typów. Obiekty typu unia wymagają do zapisu tyle bajtów pamięci ile wynosi liczba bajtów potrzebna na przechowanie najdłuższego elementu uni. Deklaracja unii podobna jest do deklaracji struktury, tyle tylko, że dokonuje się jej przy pomocy słowa kluczowego union. Operacje dozwolone na strukturach są dozwolone na uniach : kopiowanie i przypisywanie unii traktowanych jako całość, pobieranie adresu, dostęp do ich składowych. Sposób deklaracji i operowania uniami przedstawimy na przykładzie:

#include
#include
#include
union NumericType //deklaracj auni mogącej przechowywać:

{
int iValue; //wartość typu int

long lValue; //wartość typu long

double dValues; //wartość typu double

};

int main(int argc, char *argv[])

{
NumericType *Values = new NumericType[argc-1];

for(int i =1; i<ARGC;++I)

if(strchr(argv[i],'.') != 0) //typ float. użyj składowej dValue do przypisania.

Values[i].dValues = atof(argv[i]);

else //typ nie float

{
if(atol(argv[i]) > INT_MAX) //jeśli dana jest większa niż największy

Values[i].lValues = atol(argv[i]); //int to zapisz ją w lValues

else jeśli nie to w iValues

Values[i].ivalue = atoi(argv[i]);

}
return 0;

}
Przykładowa unia NumericType jest rozmieszczona w pamięci jak przedstawiono na rysunku

Należy pamiętać o tym, aby do przypisywania składników unii zadeklarować taką zmienną, która będzie mogła przechować najdłuższą składowa unii.

Do składowej uni można się odwoływać także poprzez wskaźnik podobnie jak i w strukturze:
wskaźnik_do_unii -> składowa.

Unię można zainicjować podobnie jak innej zmienne, należy jednak pamiętać , że można to zrobić jedynie wartością o typie jej pierwszej składowej np.:

NumericType values = 5; //Ok

NumericType values = 14578.9 //Bład - pierwszym elementem unii jest int

Można definiować tzw. unie anomimowe tj. takie które same nie mają nazwy, jak też nie ma nazwy jedyny egzemplarz tej unii. Do składników tej unii odwołujemy się po prostu poprzez nazwę składnika bez operatora '.' np.:

union {

int a;

float b;

char c;

};

int a; //Bład redefinicja zmiennej a co wynika z anonimowości unii

a = 4; cout<<A;

b = 1.2;

Podobnie jak w przypadku struktur unia może mieć funkcje składowe, wtedy traktuje się ją jak zmienną typu klasa. O tym, później.

Pola bitowe

Struktury i klasy mogą przechowywać składowe zajmujące mniej miejsca niż typ całkowity. Takie składowe nazywane są polami bitowymi. Jest to zbiór przylegających do siebie bitów, znajdujących się w jednej jednostce pamięci zwanej jak państwo wiecie słowem. Składnia deklaracji pola bitowego jest następująca:

deklaratoropt : wyrażenie_stałe

deklarator jest nazwą poprzez którą odwołujemy się do konkretnego pola bitowego w programie (musi to być typ całkowity), wyrażenie stałe określa liczbę bitów, które zajmuje pole bitowe w strukturze np.:

struct Date{

unsigned nWeekDay : 3; //0...7 (3bits)

unsigned nMonthDay : 6; //0...31(6bits)

unsigned NMonth : 5; //0...12(5 bits)

unsigned NYear : 8; //0...100(8 bits)

Możliwe rozmieszczenie bitów w pamięci (zależy to od implementacji) przedstawia rysunek:

Ponieważ pole mYear przekroczyłoby typ unsigned int, dlatego zostało wpisane do następnego słowa typu unsigned int.

Pole bitowe bez nazwy, służy jako separator (wypełniacz), a jeśli ma dodatkowo 0 bitów to sugeruje, aby następne pole bitowe znalazło się w następnym słowie np.

struct Date{

unsigned nWeekDay : 3; //0...7 (3bits)

unsigned nMonthDay : 6; //0...31(6bits)

unsigned : 0; //przenosi pozostałe pola do innego słowa

unsigned NMonth : 5; //0...12(5 bits)

unsigned NYear : 8; //0...100(8 bits)

Możliwe rozmieszczenie bitów w pamięci (zależy to od implementacji) przedstawia rysunek:

Istnieją ograniczenia w używaniu pól bitowych:

  1. Nie można pobrać adresu do pola bitowego.

  2. Nie można deklarować wskaźników do pól bitowych.

  3. Deklarować referencji do pól bitowych.

3. Jeszcze raz o funkcjach.

Funkcje o zmiennej liczbie argumentów

Jedną z zalet języka C/C++ jest możliwość definiowania funkcji o zmiennej liczbie parametrów. Spotkaliśmy się już wcześniej z takimi funkcjami: np. funkcja standardowa printf ma zmienną liczbę argumentów, gdyż zależy ona od liczby znaków formatujących znajdujących się w łańcuchu formatującym. Deklaracja takiej funkcji zwykle wygląda następująco:

nazwa_funkcji(parametry ustalone,...);

Informacje o zmiennej liczbie argumentów niosą trzy kropki, występujące na końcu listy parametrów funkcji. Sposób tworzenia i posługiwania się takimi funkcjami podamy na podstawie funkcji wielomian, obliczającej wartość wielomianu danego stopnia st, w danym punkcie x:
#include
#include
#include
double wielomian(double x, int st,...)

{
double wart = 0,wsp;

va_list ap;


va_start(ap,st);
for(;st;--st)
wart += va_arg(ap,double) * pow(x,st);

wart += va_arg(ap,double);

va_end(ap);
return wart;

}
int main(void)

{
printf("
%lf",wielomian(2.0,3.0,1.0,2.0,-3.0,5.0));
return 0;

}
W celu dostępu do parametrów nieustalonych posługujemy się makrodefinicjaami: va_start, va_arg, va_end oraz typem va_list, zdefiniowanym w zbiorze nagłówkowym . Schemat wykorzystania parametrów nieustalonych na podstawie przykładu przedstawia się następująco:

  1. Deklarujemy zmienną ap typu va_list.

  2. Przed rozpoczęciem dostępu do parametrów nieustalonych, wywołujemy makro: va_start z parametrami: zmienna ap i identyfikatorem ostatniego parametru ustalonego: u nas st:
    va_start(ap,st).

  3. W celu dostępu do parametrów nieustalonych, wywołujemy makro va_arg z parametrami ap i nazwą typu pobieranego parametru:

va_arg(ap,double)
spowoduje to pobranie nieustalonego parametru i potraktowanie go jako liczby double. Kolejne wywołania makra powodują pobieranie kolejnych parametrów funkcji. Ilość wywołań określa się na podstawie stopnia wielomianu (w przykładowym programie).

  1. Po zakończeniu operacji na parametrach nieustalonych należy wywołać makro va_end umożliwiające normalne zakończenie wykonania funkcji o zmiennej liczbie argumentów.


Przeładowanie nazw funkcji

W języku angielskim przeładowanie (overloading) jakiegoś słowa oznacza, że ma ono wiele znaczeń. We wcześniejszych wersjach języka, oraz innych językach programowania z nazwami funkcji związane było jedno poważne zastrzeżenie:

w programie mogła być jedna funkcja o danej nazwie. Używając nazwy tej funkcji mówiliśmy kompilatorowi o którą funkcję nam chodzi. Kompilator C++ jest o wiele bardziej inteligentny i dopuszcza użycie tej samej nazwy dla kilku funkcji np.:

void screen(int);

void screen(char, float, int);

Obie te funkcje mimo tej samej nazwy różnią się jednak od siebie: mają inne parametry wywołania. W związku z tym należy przypuszczać, iż kompilator rozpoznaje funkcje nie tylko po nazwie, ale także po liście argumentów tej funkcji. Zjawisko to nazywamy właśnie przeładowaniem nazw funkcji. Funkcje przeładowane, mają tę samą nazwę, ale różnią się liczbą lub typem zmiennych , bądź dla tej samej liczby argumentów kolejnością typów ich występowania. To, która funkcja zostanie wywołana zależy od kontekstu, czyli od towarzyszących jej argumentów wywołania.
Błędem jest próba definicji dwóch funkcji o identycznej nazwie i tej samej liście argumentów. Przy przeładowaniu istotna jest tylko lista argumentów, natomiast typ zwracany przez funkcje nie jest brany pod uwagę.

O szczegółach dotyczących funkcji przeładowanych musicie państwo przeczytać samodzielnie w podręcznikach. Rekurencja
Funkcje języka C mogą być wywoływane rekurencyjnie, tzn. funkcja może wywoływać sama siebie zarówno bezpośrednio jak i pośrednio. Najprostszym przykładem funkcji rekurencyjnej może być funkcja obliczająca wartość silni dowolnej liczby naturalnej. Porównajmy wersję silnia - nierekurencyjną z wersją silnia_rec - rekurencyjna:

Przykład
#include
#include
long silnia_rec(int);

long silnia(int);

main()
{
int n;

long res;

clrscr();
cin >> n;

res = silnia(n);

cout<<"Silnia = "<<RES;

while(!kbhit());
}
long silnia_rec(int n)

{
long sil;

if(n == 1)

sil = 1;

else
sil = n * silnia_rec(n-1);

return sil;

}
long silnia(int n)

{
long sil=1;

int i;

for(i = 1;i<=n;i++)

sil = i * sil;

return sil;

}
Na ćwiczeniach zapoznamy się z jeszcze innymi algorytmami rekurencyjnymi. Rekurencja nie musi przynosić oszczędności pamięci, ponieważ trzeba gdzieś podziać stos używanych wartości. Nie zawsze przyspiesz też działania programu. Dlaczego się więc ja stosuje? Postać rekurencyjna dla wielu algorytmów jest bardziej zwarta i często łatwiejsza do napisania i zrozumienia niż jej wersja nierekurencyjna.

Zakres tematyczny:

  1. Klasy.

  1. Klasy.

Na dzisiejszym wykładzie wprowadzimy pojęcie klas. Klasy, które przechowują dane i funkcje wprowadzają do programu typy zdefiniowane przez użytkownika (user-defined types). Typy zdefiniowane przez użytkownika, w tradycyjnym języku programowania, przechowują dane, które zebrane razem opisują atrybuty i stan obiektu. Typ klasowy w języku C++ pozwala opisać atrybuty i stan obiektu, ale także pozwala na zdefiniowanie zachowania obiektu.

Odpowiednikiem klasy w tradycyjnym programowaniu jest typ zmiennej, a obiekt danej klasy jest odpowiednikiem zmiennej tego typu.

Typy klasowe definiowane są przy pomocy słów kluczowych class, struct, union. Zmienne i funkcje zdefiniowane wewnątrz klasy nazywane są składowymi klasy. Podczas definiowania klasy w praktyce, składnikami klasy (chociaż opcjonalnymi) są:

Definiowanie typów klasowych

Do typów klasowych język C++ zalicza: struktury, klasy i unie. Jak definiujemy struktury i unie w pojęciu klasycznym mówiliśmy na wcześniejszych wykładach, teraz podamy prosty przykład deklaracji klasy.

Przypuśćmy, że piszecie państwo program, który często operuje na datach. Można w tym celu stworzyć nowy typ reprezentujący datę, używając następującej struktury:

struct Date

{
int month;

int day;

int year;

};
Składowymi tej struktury są zmienne: month, day, year.

Aby przechować konkretną datę można ustawić składowe struktury np.:

stryct Date my_date;

my_date.month = 1;

my_date.year = 1990;

my_date.day = 12;

Aby wydrukować datę nie można przesłać jej bezpośrednio do funkcji printf . Programista musi albo drukować każde elementy struktury osobno, albo napisać własną funkcje drukującą strukturę w całości jak np.:

void display_date(struct date *dt)

{
static char *name[] = {"zero","Jan","Feb",Mar","Apr","May","Jun","Jul","Aug","Sep",

"Oct","Nov","Dec"
};
printf("%s %d %d",name[dt->month],dt->day, dt->year);

}
Aby wykonać inne operacje na datach, takie jak np. porównanie, należy porównywać składowe struktury oddzielnie lub podobnie jak to było w przypadku drukowania napisać funkcję., która przyjmuje jako parametr strukturę datę i wykonuje porównanie.

Kiedy definiujemy strukturę w C definiujemy nowy typ zmiennej. Kiedy piszemy funkcje operujące na tej strukturze, definiujemy operacje wykonywane na tym typie zmiennych. Taka technika dla implementacji daty ma złe strony:

  1. Nie daje gwarancji, że struktura Date zawiera prawidłowe dane. Każda funkcja ślepo używająca takich danych np.: 56.45.1000 będzie generowała nonsensowne efekty.

  2. Załóżmy, że w pewnym momencie chodzi nam o ograniczenie pamięci przeznaczonej na zapisanie daty np.: można zdecydować, że obie dane: day i month mogą być przechowywane na zmiennej single lub przy użyciu pola bitowego lub przez zapisanie tylko numeru dnia w roku (jako liczba od 1 do 356). Aby dokonać tych zmian każdy program, który wykorzystuje typ Date musi być przepisany. Każde wyrażenie, mające dostęp do zmienionych składowych musi być przepisane.


Można uniknąć tych problemów , jednak nie bez problemów. Np., zamiast ustawiać składowe struktury można napisać funkcję która będzie jednocześnie testowała poprawność danych. Niestety niewielu programistów ma ten nawyk we krwi, w rezultacie programy tak napisane (przy bezpośrednim dostępie do składowych struktury) są trudne do poprawiania. Na szczęście, język C/C++ dostarcza nam takich narzędzi, które ułatwiają prace na typach zmiennych zdefiniowanych przez użytkownika.


W C++ definiujemy jak już wspomnieliśmy zarówno dane jak i operacje jednocześnie poprzez deklarowanie klas. Klasa zawiera dane i funkcje na nich operujące. Deklaracja klasy wygląda podobnie do deklaracji struktury, z wyjątkiem tego, że oprócz danych zawiera jeszcze funkcje. Podam teraz przykład klasy, która jest wersją klasowa struktury Date:


#include
// --- klasa Date

class Date

{
public:
Date(int mn,int dy, int yr); //Konstruktor

void display(); // Funkcja do drukowania daty

~Date(); //Destruktor

private:
int month, day, year; // prywatne dane składowe

};
Jak widać, rzeczywiście deklaracja klasy jest połączeniem deklaracji struktury i zestawu funkcji. Zawiera ona:

  1. Zmienne przechowujące datę: month, day, year.

  2. Prototypy funkcji z którymi klasa może być użyta

  3. Definicje funkcji umieszcza się po deklaracji klasy. Poniżej przedstawimy definicję funkcji składowych w/w klasy:

  4. inline int max(int a, int b)

{
if(a>b) return a;

return b;

}
inline int min(int a, int b)

{
if(a
return b;

}
// ---- Konstruktor

Date:: Dte(int mn, int dy, int yr)

{
static int lernght[]={0,31,28,31,30,31,30,

31,31,30,31,30,31 };

// zignorowanie roku przestępnego - dla uproszczenia

month = max(,mn);

month = min(month,12);

day = max(1,dy);

day = min(day, lenght[month]);


year = max(1,year);

}

// --- Funkcja do drukowania daty

void Date::display()

{
static char *name[] = {"zero","Jan","Feb",Mar","Apr","May","Jun","Jul","Aug","Sep",

"Oct","Nov","Dec"
};
cout<
<<<day<<<year;}

// --- Destruktor

Date::~Date()
{
// brak akcji

}

Funkcja display wygląda podobnie, jednak dwie funkcje są nowe: Date i ~Date, czyli konstruktor i destruktor odpowiednio. Są one używane do tworzenia i likwidowania obiektu. Później o nich powiemy bardziej szczegółowo. Oczywiście nie są to wszystkie funkcje które można napisać dla tej klasy. Poniższy program demonstruje użycie klasy Date:

void main()

{
Date myDate(3,12,1985);

Date yourDate(23,128,1966);


myDate.display();
cout<<'
';
yourDate.display();
cout<<'
';
}
Używanie klas

Po zdefiniowaniu klasy można deklarować jeden lub więcej przykładów tego typu, tak jak to robiliśmy w przypadku typów wbudowanych jak np. integer. Przykład klasy jak wspomniano wcześniej, jest nazywany obiektem, a nie zmienną.

W poprzednim przykładzie, w funkcji main deklarowane są dwa obiekty: myDate i yourDate, które zawierają 3 wartości całkowite jako inicjalizatory. Są one przesyłane do konstruktora. Zwróćmy uwagę na wyświetlanie obiektu Date. W C trzeba było przesyłać strukturę jako argument funkcji display:
display_date(&myDate);
W C++ , wywołujemy funkcję składową używając składni podobnej do tej poznanej przy dostępie do składowych struktury:

myDate.display();
Taka składnia kładzie nacisk na ścisły związek pomiędzy danymi i funkcjami które na nich pracują. Można więc pomyśleć, że operacja display jest częścią klasy.

Jednak to połączenie funkcji i danych pojawia się tylko w składni. Każdy obiekt Date nie zawiera swojej własnej kopii funkcji display. Każdy obiekt zawiera jedynie dane składowe.

Składowe klasy

Teraz zastanówmy się czym struktura różni się od klasy. Podobnie jak w deklaracji struktury klasa deklaruje trzy zmienne, ale różni się od niej w następujących miejscach:

*posiada słowa kluczowe: public, private

*deklaruje funkcje

*posiada konstruktor i destruktor.

Rozpatrzmy te różnice.


Dostęp do składowych klasy

Dostęp do składowych klasy określają etykiety public i private:

Jeśli jakaś funkcja inna niż składowa klasy chce użyć składowej pivate kompilator generuje błąd np.:
void main()

{
int i;

Date myDate(3,12,1985);


i = myDate.month; //Błąd nie można czytać prywatnych danych

myDate.day = 1; //Błąd nie można modyfikować prywatnych danych

}
Przez konstrans funkcja display jest publiczna, co czyni ją widoczną na zewnątrz klasy. Przez domniemanie przyjmuje się, że dopóki w definicji klasy nie wystąpi jakakolwiek etykieta, to składniki są prywatne.


Funkcje składowe

Funkcja display zdefiniowana dla klasy do drukowania daty jest podobna do zdefiniowanej wcześniej funkcji display_date dla struktury. Są jednak pewne zasadnicze różnice:
Po pierwsze, prototyp funkcji pojawia się wewnątrz klasy, a kiedy funkcja jest definiowana jest nazywana: Date::display. To wskazuje, że jest to funkcja składowa klasy a, jej nazwa posiada "zakres klasy". W związku z tym można definiować funkcję o tej samej nazwie na zewnątrz klasy lub wewnątrz innej bez obawy o konflikt. W przykładowym programie mieliśmy:
myDate.display();
yourDate.display();
Funkcja automatycznie używa dane składowe bieżącego obiektu.

Można także wywoływać funkcje składowe poprzez wskaźnik, używając podobnie jak to było dla struktur operatora ->:

Date myDate(3,12,1985);

Date *datePtr = &myDate;


datePtr->display();

lub poprzez referencję:

Date myDate(3,12,1985);

Date &otherDate = myDate;


otherDate.display();
Powyżej opisane techniki wywoływania funkcji składowych pracują tylko z funkcjami publicznymi. Jeśli funkcja składowa jest prywatna tylko inna funkcja składowa może z niej korzystać. Np.:

class Date

{
public:
void display();

//....
private:
int daysSoFar();

// ....

};
void Date::display()

{
cout<<DAYSSOFAR()<<" <<year;

Destruktor
Jest uzupełnieniem konstruktora. Jest to funkcja która jest automatycznie wywoływana kiedy mamy zlikwidować obiekt. Nazwa destruktora musi być taka jak nazwa klasy, ale poprzedzona jest znakiem ~. Nie wszystkie klasy muszą mieć destruktory. Są one wymagane dla bardziej skomplikowanych klas, gdzie no. wykorzystuje się dynamiczna alokację pamięci. Destruktor wykonuje wówczas odblokowanie zarezerwowanej pamięci przed zlikwidowaniem obiektu.
Jest tylko jeden destruktor dla danej klasy( nie ma listy parametrów), w związku z tym nie może on być przeładowany.

Tworzenie i kasowanie obiektu

Podamy na przykładzie definicję konstruktora i destruktora które drukują wiadomości, tak że możemy prześledzić dokładnie kiedy te funkcje są wywoływane:


#include
#inclyde
class Demo

{
public:
Demo(const char *nm);

~Demo();
private:
char name[20];

};
Demo::Demo(const char *nm)

{
strncpy(name,nm,20);
cout<<"Konstruktor wywołany dla obiektu"<<NAME<<'

';
}
Demo::~Demo()
{
cout<<"Destruktor wywołany dla obiektu"<<NAME<<'

';
}
void func()

{
Demo localFuncObject("localFuncObject");

static Demo staticObject("staticObject");

cout<<"Wewnątrz funkcji func

";
}
Demo globalObject("globalObject");

void main()

{
Demo localMainObject("local MainObject");

cout<<"W mainie przed wywołaniem funkcji func

";
cout<<"W mainie, po wywołaniu funkcji func

";
}
Program drukuje komunikaty:

Konstruktor wywołany dla obiektu globalObject

Konstruktor wywołany dla obiektu localMainObject

W mainie przed wywołaniem funkcji func

Konstruktor wywołany dla obiektu localFuncObject

Konstruktor wywołany dla obiektu staticObject

Wewnątrz funkcji func

Destruktor wywołany dla obiektu localFuncObject

W mainie, po wywołaniu funkcji func

Destruktor wywołany dla obiektu localMainObject

Destruktor wywołany dla obiektu staticObject

Destruktor wywołany dla obiektu globalObject

Dla lokalnego obiektu, konstruktor wywoływany jest przy deklaracji obiektu, a destruktor kiedy obiekt wychodzi z bloku w którym był deklarowany.

Dla obiektów globalnych, konstruktor wywoływany jest kiedy program rozpoczyna się, a destruktor przed zakończeniu programu.

Dla statycznych obiektów konstruktor wywoływany jest przed pierwszym wejściem do funkcji w którym jest deklarowany, a destruktor przed zakończeniem programu.

Klasa zdefiniowana jak wyżej nie umożliwia dostępu do składowych danych. Nie można zmieniać, ani czytać danych. Podamy teraz przykładową w miarę pełną deklarację klasy:

class Date

{
public:
Date(int mn, int dy, int yr);

int getMonth();

int getDay();

int getYear();

void setMonth(int mn);

void setDay(int dy);

void setYear(int yr);

void display();

~Date();
private:
int month, day, year;

};
Ta wersja klasy zawiera funkcje do czytania i modyfikowania daty. Ich definicja ma postać:
inline int Date::getMonth()

{ return month; }


inline int Date::getDay()

{ return day; }


inline int Date::getYear()

{ return year; }


void Date ::setMonth(int mn)

{
month = max(1,mn);

month = min(month,12);

}

void Date::setDay(int dy)

{
static int length[] = {0,31,28,31,30,31,30,

31,31,30,31,30,31};
day = max(1,dy);

day = min(day,lenght[month]);

}

void Date::setYear(int yr)

{
year = max(1,yr);

}

void main()

{
int i;

Date deadline(3,10,1980);

i = deadline.getMonth();

deadline.setMonth(4);
deadline.setMonth(deadline.getmonth() + 1);

}
Zwróćmy uwagę, że funkcje get ze względu na to, że są krótkie zostały zadeklarowane jako inline. Funkcje składowe mogą być deklarowane jako inline bez użycia słowa kluczowego inline. Wtedy ciało funkcji musi zostać zawarte wewnątrz definicji klasy np.:

clsss Date

{
public:
// ............

int getMonth() { return month; }

//.........
};

Oba style sa dopuszczalne i do wyboru programisty.

Kilka słów teraz o konstruktorach. W poniższym przykładzie zdefiniujemy dwie wersje konstruktora: bez parametrów i z parametrami:

class Date

{
public:
Date(); //konstruktor bez parametrów

Date(int mn,int yr,int dy);

//.............
}
Date::Date()
{
month = day = year = 1;

}
Date::DAte(int mn, int yr, int dy)

{
setMonth(mn);
setDay(dy);
setYear(yr);
}

void main()

{
Date myDate; //deklaracja bez inicjacji

Date yourDate(12,12,1990); //deklaracja z inicjacja obiektu

myDate,setMonth(3);
myDate.setYear(1994);
myDate.setDay(12);
}
Deklaracja myDate nie niesie za sobą inicjacji obiektu. W rezultacie pierwszy konstruktor jest używany do tworzenia obiektu i inicjowanie go wartością "January 1,1". Wartości obiektu ustawiane są później przy pomocy funkcji set.

W drugim przypadku konstruktor używany jest do tworzenia obiektu yourDate i inicjowania jego danych składowych wyspecyfikowanymi wartościami. Jest dopuszczalne, aby konstruktor używał funkcji składowych do momentu dopóki nie usiłują one czytać inicjowanych danych.

34

Mateusz Wiczyński

3



Wyszukiwarka

Podobne podstrony:
Programowanie Neurolingwistyczne 1, rozwój osobisty NLP, NLS itp
Programowanie neurolingwistyczne 4, rozwój osobisty NLP, NLS itp
Pojęcie motywu, 02.ROZWÓJ OSOBISTY +.....), 01.Psychologia ; Rozwój osob.;NLP..itp, Psychologia w Pi
Próba zdań niedokończonych Bonneta, 02.ROZWÓJ OSOBISTY +.....), 01.Psychologia ; Rozwój osob.;NLP..i
Funkcje intrapersonalne, 02.ROZWÓJ OSOBISTY +.....), 01.Psychologia ; Rozwój osob.;NLP..itp, Psychol
Grupa - realizacja zadań, 02.ROZWÓJ OSOBISTY +.....), 01.Psychologia ; Rozwój osob.;NLP..itp, Psycho
dziesięciościan, 02.ROZWÓJ OSOBISTY +.....), 01.Psychologia ; Rozwój osob.;NLP..itp, Psychologia w P
Emocje i nastroje, 02.ROZWÓJ OSOBISTY +.....), 01.Psychologia ; Rozwój osob.;NLP..itp, Psychologia w
Mord Psychologia I Psychiatria Zaburzenia Życia Uczuciow, 02.ROZWÓJ OSOBISTY +.....), 01.Psychologia
psych prenatalna, 02.ROZWÓJ OSOBISTY +.....), 01.Psychologia ; Rozwój osob.;NLP..itp, Psychologia w
Emocje - narządy wewnętrzne, 02.ROZWÓJ OSOBISTY +.....), 01.Psychologia ; Rozwój osob.;NLP..itp, Psy
Ankieta - Dlaczego oglądam telewizję, 02.ROZWÓJ OSOBISTY +.....), 01.Psychologia ; Rozwój osob.;NLP.
Typy motywacji, 02.ROZWÓJ OSOBISTY +.....), 01.Psychologia ; Rozwój osob.;NLP..itp, Psychologia w Pi
Socjalizacja a rozwój, 02.ROZWÓJ OSOBISTY +.....), 01.Psychologia ; Rozwój osob.;NLP..itp, Psycholog
Psychologiczna koncepcja humanistyczna, 02.ROZWÓJ OSOBISTY +.....), 01.Psychologia ; Rozwój osob.;NL
neuropsych5II, 02.ROZWÓJ OSOBISTY +.....), 01.Psychologia ; Rozwój osob.;NLP..itp, Psychologia w Pig

więcej podobnych podstron