Wstęp do Programowania
Tomasz Gorazd
Spis treści
2
Czego potrzebujemy do kursu . . . . . . . . . . . . . . . . . .
2
Co robią kompilatory . . . . . . . . . . . . . . . . . . . . . . .
3
Pierwszy program . . . . . . . . . . . . . . . . . . . . . . . . .
3
Jak wygląda program? . . . . . . . . . . . . . . . . . . . . . .
4
Komentarze . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5
Dowolność formatowania tekstu programu
. . . . . . . . . . .
5
7
Zmienne . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7
Słowa kluczowe . . . . . . . . . . . . . . . . . . . . . . . . . .
8
Stałe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9
Nadawanie wartości zmiennym . . . . . . . . . . . . . . . . . .
10
Wejście - Wyjście . . . . . . . . . . . . . . . . . . . . . . . . .
11
Operatory arytmetyczne . . . . . . . . . . . . . . . . . . . . .
13
. . . . . . . . . . . . . . . . .
14
1
17
. . . . . . . . . . . . . . . . . . . . . . . . . .
17
Instrukcja if . . . . . . . . . . . . . . . . . . . . . . . . . . . .
17
. . . . . . . . . . . . . . . . . . . . . . .
18
Blok instrukcji . . . . . . . . . . . . . . . . . . . . . . . . . . .
20
If - wielowariantowe . . . . . . . . . . . . . . . . . . . . . . . .
21
Trochę inne if. Wyrażenie warunkowe . . . . . . . . . . . . . .
22
Pętla for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
24
. . . . . . . . . . . . . . . . . . . . . . . .
27
29
Tablice liczbowe . . . . . . . . . . . . . . . . . . . . . . . . . .
29
. . . . . . . . . . . . . . . . . . . . .
32
Tablice znakowe . . . . . . . . . . . . . . . . . . . . . . . . . .
33
Operatory logiczne . . . . . . . . . . . . . . . . . . . . . . . .
34
Operatory przypisania cd. . . . . . . . . . . . . . . . . . . . .
36
Połączenie operatora przypisania z operatorami arytmetycznymi 37
Pętla while . . . . . . . . . . . . . . . . . . . . . . . . . . . .
38
41
Pętla do . . . while . . . . . . . . . . . . . . . . . . . . . . . .
41
Instrukcja break . . . . . . . . . . . . . . . . . . . . . . . . .
43
Instrukcja switch . . . . . . . . . . . . . . . . . . . . . . . . .
45
Instrukcja goto . . . . . . . . . . . . . . . . . . . . . . . . . .
49
. . . . . . . . . . . . . . . . . . . . . . .
51
2
53
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
53
Definiowanie typów . . . . . . . . . . . . . . . . . . . . . . . .
58
Funkcje - wprowadzenie . . . . . . . . . . . . . . . . . . . . .
61
Funkcje - definicje i deklaracje funkcji . . . . . . . . . . . . .
63
Funkcje - zwracanie wartości . . . . . . . . . . . . . . . . . . .
66
69
. . . . . . . . . . . . . . . . . . . . . . . .
69
Inicjalizacja zmiennych . . . . . . . . . . . . . . . . . . . . . .
72
Funkcje - przekazywanie argumentów przez wartość . . . . . .
75
Funkcje - przesyłanie argumentów przez referencje . . . . . . .
78
Funkcje -argumenty domniemane . . . . . . . . . . . . . .
80
Wywoływanie funkcji przez samą siebie - rekurencja . . . . . .
82
Operator sizeof . . . . . . . . . . . . . . . . . . . . . . . . . .
83
86
Funkcje - różne funkcje o tych samych nazwach
. . . . . . . .
86
. . . . . . . . . . . . . . . . . . . .
87
Obiekty globalne . . . . . . . . . . . . . . . . . . . . . . . . .
89
Zmienne automatyczne i statyczne . . . . . . . . . . . . . . . .
92
Programy składające się z kilku plików . . . . . . . . . . . . .
94
Modyfikatory const, register, volatile
. . . . . . . . . . . . 100
102
Wskaźniki - definiowanie . . . . . . . . . . . . . . . . . . . . . 102
Wskaźniki - zastosowanie . . . . . . . . . . . . . . . . . . . . . 103
Wskaźniki do typu void . . . . . . . . . . . . . . . . . . . . . 108
Wskaźniki a tablice . . . . . . . . . . . . . . . . . . . . . . . . 111
Operacje arytmetyczne na wskaźnikach . . . . . . . . . . . . . 114
3
116
10.1 Wszystkie pliki nagłówkowe po kolei
. . . . . . . . . . . . . . 116
10.2 conio.h - Funkcje dotyczą trybu tekstowego . . . . . . . . . . . 117
10.3 alloc.h - Operacje na pamięci
. . . . . . . . . . . . . . . . . . 118
10.4 mem.h - Funkcje operujące na pamięci . . . . . . . . . . . . . 119
10.5 string.h - Wszelkie operacje na stringach . . . . . . . . . . . . 119
10.6 dos.h - Tylko kiedy piszemy aplikacje pod DOS
. . . . . . . . 120
10.7 dir.h - Funkcje obsługujące pliki i katalogi . . . . . . . . . . . 120
10.8 ctype.h - Funkcje informujące o znakach
. . . . . . . . . . . . 121
10.9 math.h - Funkcje matematyczne . . . . . . . . . . . . . . . . . 121
10.10stdlib.h - Przydatne funkcje, często używane . . . . . . . . . . 121
10.11time.h - Funkcje dotyczące czasu
. . . . . . . . . . . . . . . . 122
10.12stdio.h - Standardowe wejście i wyjście . . . . . . . . . . . . . 122
10.13graphics.h - Tryb graficzny . . . . . . . . . . . . . . . . . . . . 122
126
11.1 Animacja - co ma robić komputer . . . . . . . . . . . . . . . . 126
11.2 Skąd komputer wie, czego chce użytkownik . . . . . . . . . . . 128
138
12.1 Dynamiczna rezerwacja i zwracanie pamięci
. . . . . . . . . . 138
12.2 Jak to się robi w C . . . . . . . . . . . . . . . . . . . . . . . . 139
12.3 Alokacja miejsca dla tablic . . . . . . . . . . . . . . . . . . . . 141
12.4 Listy struktur . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
12.5 Wskaźniki w argumentach funkcji . . . . . . . . . . . . . . . . 144
12.6 Jeszcze raz tablice w argumentach funkcji
. . . . . . . . . . . 145
12.7 Wskaźniki do obiektów typu const oraz wskaźniki typu const . 146
12.8 Wskaźniki do funkcji . . . . . . . . . . . . . . . . . . . . . . . 147
12.9 Tablice wskaźników do funkcji . . . . . . . . . . . . . . . . . . 149
12.10Wskaźnik do funkcji jako jeden z argumentów innej funkcji . . 151
4
154
13.1 Otwieranie i zamykanie pliku . . . . . . . . . . . . . . . . . . . 154
13.2 Podstawowe operacje na otwartych plikach . . . . . . . . . . . 156
13.3 Pisanie formatowane do pliku . . . . . . . . . . . . . . . . . . 160
13.4 Poruszanie się po pliku . . . . . . . . . . . . . . . . . . . . . . 164
13.5 Kasowanie plików . . . . . . . . . . . . . . . . . . . . . . . . . 166
13.6 Biblioteka io . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
167
14.1 Parametry funkcji main . . . . . . . . . . . . . . . . . . . . . 167
14.2 Funkcje ze zmienną ilością argumentów . . . . . . . . . . . . . 167
14.3 Kilka standardowych funkcji o zmiennej ilości argumentów . . 170
14.4 Operatory bitowe . . . . . . . . . . . . . . . . . . . . . . . . . 172
175
15.1 Typ enum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
15.2 Unie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
15.3 Zmienne typu extern . . . . . . . . . . . . . . . . . . . . . . . 177
. . . . . . . . . . . . . . . . . . . . . . 177
. . . . . . . . . . . . . . . . . . . . . . 178
15.6 Preprocesor . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
15.6.1 #include . . . . . . . . . . . . . . . . . . . . . . . . . . 178
. . . . . . . . . . . . . . . . . . . . . . . . . . 179
. . . . . . . . . . . . . . . . . . . . . . 180
15.6.4 Kompilacja warunkowa . . . . . . . . . . . . . . . . . . 181
. . . . . . . . . . . . . . . . . . 185
5
1
Wykład 1
1.1
Czego potrzebujemy do kursu
Kurs ”Wstęp do Programowania” ma na celu nauczenie programowania w
języku C, z niewielkimi dodatkami języka C++. Myślę, że niniejszy kurs
powinien być wystarczający by to zrealizować. Dla chętnych podaję kilka
pozycji literatury, z których można korzystać podczas nauki.
Język C, B.W.Kernighan, D.M.Ritchie
Język C, Conor Sexton
Język C : Szkoła programowania, Stephen Prata
Język C : Dla wszystkich, Marek Lont
Przydatne mogą być wszelkie pozycje mające w tytule Język C lub
C/C++. Aby móc w pełni programować, prócz wiedzy na temat programo-
wania potrzebujemy jeszcze specyficznego oprogramowania, czyli programu
który zamieni treść napisanego przez nas programu na coś, co jest zrozu-
miałe dla procesora naszego komputera. Takie programy będziemy nazywać
kompilatorami.
Jeśli mamy na naszym komputerze zainstalowany system Linux (Unix),
możemy skorzystać z dostarczanego razem z systemem programu g++.
W systemach pochodzących od DOS - czyli wszystkich odmianach Win-
dows - możemy skorzystać z programu Turbo C++, który można pobrać
bezpłatnie, po uprzednim zarejestrowaniu się, ze stron muzeum firmy Bor-
land - bdn.borland.com/museum.
Przy nauce do tego wykładu można jednak korzystać z dowolnego kompi-
latora języka C++. Większość programów omawianych na wykładzie bę-
dzie można przy ich pomocy kompilować. Jednak, im program jest starszy
(szczególnie pod Windows), tym będzie on bardziej przydatny od strony
dydaktycznej.
Aby dokładnie dowiedzieć się, jak dany kompilator działa, należy zapoznać
się z jego dokumentacją.
6
1.2
Co robią kompilatory
Tak naprawdę to kompilator jest jedynie jedną z części składowych wcze-
śniej wymienionych programów. Jeśli chodzi o Turbo C++, to można wy-
mienić trzy podstawowe jego części:
Edytor tekstu, przy pomocy którego będziemy pisać nasze programy.
Kompilator, tworzy z naszego programu wersję programu w języku maszy-
ny. Nie jest ona jednak pełna - brakuje tam wywoływanych przez nas
funkcji bibliotecznych.
Linker, łączy kod po kompilacji z bibliotekami.
Należy zaznaczyć, iż program nie koniecznie musimy pisać w edytorze tekstu
dostarczonym z kompilatorem. Może być on pisany w dowolnym edytorze
tekstowym (nie formatującym dodatkowo wpisanego tekstu) - np. Notatnik
w Windows.
1.3
Pierwszy program
Napiszmy więc nasz pierwszy program. Uruchommy program tc.exe. Następ-
nie otwórzmy nowy dokument. Wpiszmy poniższy tekst w okienku edytora.
Zapamiętajmy go pod nazwą pr1.cpp.
#include <stdio.h>
main()
{
printf("To program pierwszy");
}
Notatki:
Aby nasz program uruchomić wystarczy wejść do menu Run i wybrać opcję
Run. Można to zrobić szybciej naciskając kombinację klawiszy Ctrl-F9.
Aby zobaczyć wynik działania programu przyciśnijmy klawisze Alt-F5. Aby
znów powrócić do okienka edycji przyciskamy również Alt-F5.
7
Aby uruchomić program pod systemem Linux korzystając z kom-
pilatora g++, należy:
1. Napisać tekst programu w dowolnym edytorze tekstowym i
zapamiętać jako pr1.cpp
2. Będąc w kartotece, gdzie znajduje się plik pr1.cpp, wywo-
łać g++ -o pr1 pr1.cpp
3. Program możemy teraz uruchomić przez ./pr1
Notatki:
Pierwszy nasz program napisany był w ”czystym” języku C. Z powodów
czysto dydaktycznych będziemy jednak na tym wykładzie stosować pewne
dodatki pochodzące z języka C++. Proszę zwrócić uwagę na sposób wypi-
sywania tekstu na ekranie. Linijka postaci:
cout << "To program pierwszy";
jest właśnie typowa dla C++. Jej odpowiednikiem w ”czystym” C jest:
printf("To program pierwszy");
Dlaczego stosujemy wersję z C++, wyjaśni się w dalszej części wykładu.
A oto nowa wersja pierwszego programu:
#include <iostream.h>
main()
{
cout << "To program pierwszy";
}
Notatki:
1.4
Jak wygląda program?
W każdym programie musi być funkcja main. Wszystkie instrukcje tej funk-
cji są zawarte pomiędzy znakami { oraz }. Każda instrukcja kończy się zna-
kiem ; (średnik). Oczywiście programy, które nauczymy się tu pisać będą
bardziej rozbudowane, wszystkie jednak podlegają powyższej zasadzie.
8
1.5
Komentarze
Jeżeli piszemy dłuższe programy, szczególnie jeśli wracamy do nich po pew-
nym czasie, można nie pamiętać, w jakim celu napisaliśmy jakąś ich cześć.
Dotychczasowe nasze programy były krótkie, ale programy, które pisze się
profesjonalnie, mają często np. kilkadziesiąt tysięcy linii. W treści progra-
mów możemy napisać takie uwagi, które są ignorowane przez kompilator, nie
mają żadnego wpływu na sam program - to waśnie komentarze.
Komentarzem jest wszystko, co jest zawarte między znakami /* a znakami
*/. Takie komentarze mogą obejmować nawet kilka linii tekstu.
Drugim rodzajem komentarza jest tekst zaczynający się od znaków //. Koń-
cem takiego komentarza jest koniec linii.
/* A oto nasz kolejny program
napisany na pierwszym wykładzie
z komentarzem w wielu linijkach
*/
/*----------------------------------------------------------------------*/
#include <iostream.h>
// Tu mówię kompilatorowi, z jakich dodatkowych
// funkcji będę ewentualnie korzystał
main()
//Ta funkcja musi się znajdować w każdym programie
{
//Ciało funkcji zaczyna się znakiem {
cout << "To kolejny program";
//Tu właśnie wypisuje tekst na
//standardowe urządzenie wyjściowe
//(tu ekran)
}
//Ciało funkcji kończy się znakiem }
/*----------------------------------------------------------------------*/
Notatki:
1.6
Dowolność formatowania tekstu programu
Tak jak komentarze, przez kompilator ignorowane są w treści programu
wszystkie białe znaki, tzn. znaki spacji, tabulacji, nowych linii (między
kolejnymi instrukcjami). Tak więc nasz program może wyglądać tak:
9
/*Przykład programu bez zbednych białych znaków*/
#include <iostream.h>
main() {cout << "To program pierwszy";}
Notatki:
lub też tak:
/*Przykład programu z białymi znakami, które są ignorowane*/
#include <iostream.h>
main() {
cout
<<
"To program pierwszy"
;
}
Notatki:
Tylko po co? Oczywiście taka dowolność jest potrzebna, by uczynić program
bardziej czytelnym. Należy stosować wcięcia w treści programu i czynić go
jak najbardziej czytelnym. Powinno się wybrać swój ulubiony sposób forma-
towania programu i tego sposobu trzymać się konsekwentnie.
Tekst programu powinien być przede wszystkim czytelny.
10
2
Wykład 2
2.1
Zmienne
Zmienne to obiekty do przechowywania wartości określonego typu (rodza-
ju). To, co przechowuje taki obiekt, możemy zmieniać w trakcie działania
programu.
Podstawowe typy zmiennych:
Oznaczenie
Opis
IBM PC
int
liczby całkowite (...-2,-1,0,1,2 ...)
2 bajty
unsigned int
liczby całkowite dodatnie (0,1,2,3,4 ....)
2 bajty
short
tak jak int lecz mniejszy zakres liczb
2 bajty
long
tak jak int lecz większy zakres liczb
4 bajty
float
liczby rzeczywiste (np. 2.23, 3.14, -89,6)
4 bajty
double
tak jak float lecz większy zakres liczb
8 bajtów
long double
tak jak double lecz większy zakres liczb
8 bajtów
char
znaki alfanumeryczne ( A d G 3 5 - tak jak widać)
1 bajt
Każdy z tych typów całkowitoliczbowych możemy poprzedzić napisem unsi-
gned. Spowoduje to, że będziemy mieli do dyspozycji tylko liczby nieujemne,
ale za to największa dostępna dla nas liczba będzie dwa razy większa.
Jeśli chodzi o typ
char
, to może on być traktowany jako typ liczbowy. W
obiektach tego typu będą przechowywane kody ASCII znaków.
Aby ze zmiennych (tych obiektów w pamięci) móc korzystać, powinniśmy
mieć sposób, aby do nich się dostać. Robimy to poprzez nazwanie, czyli de-
finicję zmiennej.
Definicja zmiennej - powiedzenie kompilatorowi, że dana nazwa reprezen-
tuje obiekt danego typu. Przez definicję zajmujemy automatycznie miejsce
w pamięci dla danej (tak właśnie nazwanej) zmiennej.
Jako nazw zmiennych możemy używać ciągów znaków złożonych z cyfr,
liter (alfabet angielski),
(znaku podkreślenia), zaczynających się od
litery lub znaku . Uwaga, małe i duże litery są dla C różne. Wiec np. nazwy
Cena
i
cena
uważane są za różne.
11
Przykład definiowania zmiennych
int liczba;
// zdefiniowaliśmy zmienną o nazwie liczba do
//przechowywania liczby całkowitej
float z;
// zdefiniowaliśmy zmienną o nazwie z do
//przechowywania liczby rzeczywistej
long a, dluga;
// w jednej linii definicja dwóch zmiennych a
//i dluga typu long
Dalej przykład działającego programu z definiowaniem zmiennych.
/* Program pr2_1.cpp
demonstracja definiowania zmiennych
*/
#include<iostream.h>
main()
{
int liczba;
float a;
char znak;
cout << "Ten program nic nie robi, ale ma zmienne";
}
Notatki:
2.2
Słowa kluczowe
Nie wszystkie jednak ciągi znaków, o których mówiono w poprzednim roz-
dziale mogą być nazwami zmiennych. Niektóre z nich są zarezerwowane jako
pewne słowa z samego języka. Oto lista słów zarezerwowanych (podano rów-
nież te zarezerwowane z C++).
12
asm
auto
break
case
catch
char
class
const
continue
default
delete
do
double
else
enum
extern
float
for
friend
goto
if
inline
int
long
new
operator
private
protected
public
register
return
short
signed
sizeof
static
struct
switch
template
this
throw
try
typedef
union
unsigned
virtual
void
volatile
while
2.3
Stałe
Mając zmienne chcielibyśmy, coś w nich przechowywać. To, co tam możemy
wstawić, to np. stałe.
Stałe liczbowe, przykłady:
23 45 -90 . . . stałe będące liczbami całkowitymi
Możemy stosować również zapis liczb całkowitych w systemie szesnastko-
wym lub ósemkowym.
Zapisz szesnastkowy zaczyna się zawsze od symboli 0x.
0x12 - to zapisz szesnastkowy. Dziesiętnie jest to 18.
0xA2 - to jest dziesiętnie 162.
W zapisie szesnastkowym używamy cyfr oraz symboli A B C D E F
Zapis ósemkowy zaczyna się zawsze od 0 (zera) - używamy cyfr od 0 do 7
012 - to dziesiętnie 10, a
073 - to dziesiętnie 59
Stałe reprezentujące liczby rzeczywiste (zmiennoprzecinkowe)
41.9 3.14 -98.09 88876. 43.8
Uwaga, w zapisie stosujemy kropkę (.) a nie przecinek (,).
Stałe znakowe
’a’ - litera a
’7’- cyfra 7 traktowana jako znak a nie liczba
’P’ - litera P Znaki sterujące
13
’\b’ - cofnięcie
’\f’ - nowa strona
’\n’ - nowa linijka
’\r’ - powrót karetki
’\a’ - sygnał dźwiękowy
’\t’ - znak tabulacji
Inne szczególne znaki
’\\’ znak \
’\”’ znak ”
’\’ ’ apostrof
’\0’ znak o kodzie 0
’\?’ pytajnik
Stałe tekstowe - stringi: ciągi znaków zawarte między cudzysłowami.
”To program pierwszy”
”A to jakiś napis”
”To napis \n ze znakiem specjalnym”
2.4
Nadawanie wartości zmiennym
Aby do zdefiniowanej wcześniej zmiennej wstawić jakąś wartość, używamy
operatora =. Uwaga, znak ten nie ma nic wspólnego ze znanym z matema-
tyki operatorem porównania. Mówi on, że do miejsca pamięci reprezentowa-
nego przez zmienną z lewej strony znaku = ma być wstawiona wartość, która
znajduje się po jego prawej stronie. Wartość podstawiana może być np. stałą
lub inna zmienną.
Jeśli typ wyrażenia po prawej stronie jest inny niż typ zmiennej, możemy
napotkać na komplikacje. Np. jeśli zmienna jest typu całkowitoliczbowego a
wartość podstawiana jest rzeczywista, może dojść do obcięcia wielkość po
kropce dziesiętnej.
Uwaga, jeśli chcemy używać jakiejś zmiennej w programie, musi ona być
wcześniej w tym programie zdefiniowana.
14
Oto przykład z nadawaniem wartości zmiennym:
/* Program pr2_2.cpp
Podstawianie wartości za zmienne
*/
main()
{
int liczba, bzyk;
float pi, pier_z_2, a;
char znak;
liczba = 3;
//Podstawienie pod zmienną liczba wartości 3.
bzyk = liczba;
//Zmienna bzyk otrzymuje tę samą wartość co
//zmienna liczba.
pi = 3.14;
pier_z_2 = 1.4142;
a = pi;
//Tu pod zmienną a podstawiamy wartość
//zmiennej pi,
a = 35;
//a tu za zmienną a podstawiamy liczbę 35
pi = 3.14115;
//Tu chcemy mieć dokładniejszą wartość pod pi
znak = ’z’;
//W zmiennej znak przechowujemy znak z
znak = ’\0’;
//a teraz znak o kodzie 0
}
Notatki:
2.5
Wejście - Wyjście
Wcześniej już zostało pokazane, jak na ekranie możemy wypisywać napisy. W
ten sam sposób możemy wypisać dowolną stałą jak również zawartość pozna-
nych przez nas zmiennych (poznamy później inne typy zmiennych, których
w ten sposób nie można wypisać).
Gdy chcemy wypisać zmienną
a
(typu liczbowego lub znakowego), piszemy:
cout << a;
Aby wypisać wartość
5
, napiszemy:
cout << 5;
Możemy też łączyć wypisywanie kilku wartości, również ze stringami:
15
cout << "Zmienna a ma wartość " << a;
Oto działający przykład:
/* Program pr2_3.cpp
Wypisywanie wartości
*/
#include<iostream.h>
main()
{
int liczba;
float pi;
cout << "Ala ma kota";
//Wypisanie stringu.
cout << "Linia 1\nLinia 2\n";
//Piszemy dalej w tej samej linijce a
//następnie przechodzimy do nowej i
//piszemy Linia 2.
liczba = 5;
cout << liczba;
//Wypisanie zawartości zmiennej liczba - czyli 5.
pi = 3.14;
cout << pi;
//Jak wyżej, tylko ze zmienną zmiennoprzecinkową.
// A teraz ładniej. Tak też można.
cout << "\nZmienna liczba ma wartość " << liczba <<" a zmienna pi ";
cout << "ma wartość " << pi;
}
Notatki:
Oczywiście programowanie nie miałoby sensu, gdyby występujące w progra-
mach wartości były tam od zawsze zapisane. Aby program mógł mieć jakieś
użyteczne zastosowanie, powinien również czerpać wiadomości z otoczenia.
Program liczący np. pierwiastki równania kwadratowego powinien od użyt-
kownika pobierać to równanie (liczby występujące przy poszczególnych po-
tęgach).
Aby wczytać wartość i umieścić pod zmienną (liczbową)
a
, możemy napisać:
cin >> a;
Po tej instrukcji to, co poda użytkownik (umówmy się, że będzie tak dobry
i wpisze odpowiednią liczbę), zostanie umieszczone pod zmienną
a
.
16
Popatrzmy na taki program:
/* Program pr2_4.cpp
Wczytywanie wartości liczbowych
*/
#include<iostream.h>
main()
{
int wiek;
float kurs;
//Wpierw wczytam liczbę całkowitą
cout << "Podaj swój wiek ";
//Linijka zachęty, bardzo ważna.
cin >> wiek;
//Tu wczytuję.
cout << "O masz " << wiek << " lat\n"; //Czy się zgadza ?
//Teraz wczytywanie liczby rzeczywistej.
cout << "Jaki dziś kurs dolara ? ";
cin >> kurs;
cout << "Widzę że za 1$ zapłacę " << kurs << " złotych\n";
}
Notatki:
2.6
Operatory arytmetyczne
Oczywiście, gdy mamy stałe i zmienne, chcemy na nich wykonywać jakieś
operacje arytmetyczne - dodawanie, odejmowanie, mnożenie itp. W języku
C mamy wszystkie operacje znane ze szkoły. Dodatkowo, symbole dla nich
są wszystkim znane. Możemy z nich tworzyć bardziej złożone wyrażenia przy
pomocy nawiasów okrągłych. Jeśli nie zastosujemy nawiasów, to najpierw są
wykonywane operacje o najwyższym priorytecie. Spośród dwóch operatorów
o tym samym priorytecie wykonywany jest najpierw ten z lewej strony.
Priorytet
Operator
Uwagi
3
-
jednoargumentowy -
3
+
jednoargumentowy +
2
*
mnożenie
2
/
dzielenie
2
%
reszta z dzielenia
1
-
odejmowanie
1
+
dodawanie
17
Wyrażenie arytmetyczne możemy postawić zawsze po prawej stronie znaku
podstawienia.
/* Program pr2_5.cpp
Wyrażenia arytmetyczne
*/
main()
{
int liczba, n;
float pi, pier_z_2, a;
liczba = 3;
n = liczba + liczba;
n = (n - liczba) * 8;
//Pod n mamy wartość otrzymaną poprzez odjęcie
//od poprzedniej wartości zmiennej n wartości
//zmiennej liczba i pomnożenie wyniku przez 8.
liczba = 345 % n;
n = (n * 2) / 45;
pi = 3.14;
pier_z_2 = 1.4142 * 2 * pi;
a = -pi;
a = 35 + a * pi ;
//Najpierw wykona się mnożenie a potem dodawanie.
}
Notatki:
2.7
Typ wyrażenia arytmetycznego
Jeśli po prawej stronie podstawienia znajduje się wyrażenie arytmetyczne,
wyrażenie to jest wykonywane niezależnie od tego, za co będzie podstawiona
jego wartość. Sprawą bardzo ważną jest typ wynikowy wyrażenia arytme-
tycznego. Popatrzmy na przykład. Jeśli w programie znajduje się wyrażenie
int a, b;
float c;
a = 3;
b = 2;
c = a / b;
to prawie każdy powie, że jego wartością c jest teraz
1.5
. Jest to NIE-
PRAWDA i to niezależnie od tego, że zmienna
c
jest typu
float
. Najpierw
18
zostanie obliczone
a/b
, a jego wartością jest
1
. Dlaczego? Jeżeli mamy wy-
rażenie arytmetyczne (na chwile tylko) z jednym operatorem, to typ wyniku
jest najmniejszym typem, w którym zmieszczą się obydwa argumenty wystę-
pujące w wyrażeniu. W tym wypadku tym typem jest
int
, i dlatego wartość
tego wyrażenia to
1
.
Jeśli mamy bardziej złożone wyrażenie arytmetyczne, to powyższa zasada
stosuje się do kolejno wykonywanych operacji. Następny przykład obrazuje,
jak liczą się wyrażenia arytmetyczne:
/* Program pr2_6.cpp
Typy wyrażeń arytmetycznych
*/
#include <iostream.h>
main()
{
int a;
float f;
f = 40 / 3;
//Obliczone wyrażenie ma typ int; następnie
//jest zamieniane na typ float, przed podstawieniem.
cout << "\nwartość f po f = 40/ 3 jest równa " << f;
f = 40.0 / 3;
//Obliczone wyrażenie ma typ double, następnie
//jest zamieniane na typ float, przed podstawieniem.
cout << "\nwartość f po f = 40.0/ 3 jest równa " << f;
a = 40.0 / 3;
cout << "\nwartość a po a = 40.0/ 3 jest równa " << a;
a = 40.0;
f = a / 3;
cout << "\nwartość f po f = a / 3 jest równa " << f;
a = 40.0;
f = a / 3.0;
cout << "\nwartość f po f = a / 3.0 jest równa " << f;
a = 40;
f = a / (3 + 0.0);
cout << "\nwartość f po f = a / (3 + 0.0) jest równa " << f;
}
Notatki:
Aby lepiej zrozumieć to zagadnienie, proszę napisać kilka podobnych przy-
kładów. Niezrozumienie powyższego zagadnienia jest, szczególnie dla począt-
19
kujących programistów, źródłem wielu błędów.
20
3
Wykład 3
3.1
Prawda i Fałsz
W języku C nie ma stałych ani zmiennych reprezentujących wartości logiczne
prawdy i fałszu. Każdy jednak zdaje sobie sprawę, że w programowaniu takie
pojęcia są niezbędne. W języku C przyjęto następującą konwencję:
Wartość zero - reprezentuje fałsz
Wartość różna od zera - reprezentuje prawdę
Nie musi być to wartość liczbowa. Może to być wartość jakiegoś wyrażenia,
zmiennej - np. typu char.
Od tego miejsca będziemy używać pojęć prawda i fałsz zgodnie z powyższa
definicją.
Mamy również możliwość porównywania wartości liczbowych lub znakowych
Jeśli a, b - to zmienne, stałe lub wyrażenia o wartościach liczbowych lub
znakowych to:
a == b
Prawda, jeśli a równe b, w przeciwnym przypadku fałsz
a != b
Prawda, jeśli a różne b, w przeciwnym przypadku fałsz
a < b
Prawda, jeśli a mniejsze niż b, w przeciwnym przypadku fałsz
a > b
Prawda, jeśli a większe od b, w przeciwnym przypadku fałsz
a <= b
Prawda, jeśli a mniejsze lub równe b, w przeciwnym przypadku fałsz
a >= b
Prawda, jeśli a większe lub równe b, w przeciwnym przypadku fałsz
!a
Prawda, jeśli a fałszem, fałsz gdy a jest prawda (przeczenie)
3.2
Instrukcja if
Instrukcja ta służy do sterowania przebiegiem programu. Ma ona postać
if (wyrażenie)
instrukcja1
Jeśli
wyrażenie
przyjmuje wartość niezerową (prawdy), to wykonuje się in-
strukcja
instrukcja1
. W przypadku przeciwnym nie wykona się żadna in-
strukcja.
21
/* Program pr3_1.cpp
Instrukcja sterująca if
*/
#include <iostream.h>
main()
{
int a;
cout << "Podaj liczbę całkowitą-> ";
cin >> a;
//Wczytuję liczbę.
if (a == 3) cout << "O podałeś trójkę\n";
//Powyższe wykona się, gdy wczytaliśmy liczbę 3.
cout << "Koniec programu\n"; //To wykona się zawsze.
}
Notatki:
3.3
Instrukcja if . . . else
Instrukcja ta jest rozszerzeniem poprzedniej instrukcji.
if (wyrażenie)
instrukcja1
else
instrukcja2
Powoduje ona, że jeśli
wyrażenie
jest niezerowe, to wykonuje się
instrukcja1
,
w przeciwnym wypadku
instrukcja2
.
22
/* Program pr3_2.cpp
Instrukcja sterująca if ... else
*/
#include <iostream.h>
main()
{
int a;
cout << "Podaj liczbę -> ";
cin >> a;
if ( a == 3)
//Sprawdzamy, czy a jest równe 3.
cout << "Teraz podałeś trójkę\n";
else
//Poniższe wykona sie, gdy a nie jest równe 3.
cout << "Nie podałeś trójki\n";
cout << "Koniec programu\n";
}
Notatki:
Następny przykład robi to samo co poprzedni, lecz proszę zwrócić uwagę na
warunek występujący po
if
.
/* Program pr3_3.cpp
Instrukcja sterująca if ... else
Przykład z liczbą w warunku pod if
*/
#include <iostream.h>
main()
{
int a;
cout << "Podaj liczbę -> ";
cin >> a;
if ( a - 3) //Sprawdzamy, czy a-3 jest równe zeru, czy nie.
cout << "Nie podałeś trójki\n";
else
cout << "Teraz podałeś trójkę\n";
cout << "Koniec programu\n";
}
Notatki:
23
3.4
Blok instrukcji
Jeśli w instrukcji warunkowej chcemy wykonać więcej niż jedną instrukcję,
możemy zapisać je jako blok instrukcji i wtedy są one traktowane jako jedna
instrukcja. Blok tworzymy, umieszczając instrukcje pomiędzy znakami { i }.
Blok instrukcji
{
instrukcja1
instrukcja2
...
...
instrukcja ostatnia
}
/* Program pr3_4.cpp
Blok instrukcji
*/
#include <iostream.h>
main()
{
int a, licznik;
cout << "Podaj liczbę -> ";
cin >> a;
if ( a == 3)
{
//Jeśli warunek jest prawdziwy,
//wykonają się dwie poniższe instrukcje.
cout << "Teraz podałeś trójkę\n";
licznik = 1;
}
else
{
cout << "Nie podałeś trójki\n";
licznik = 0;
}
if(licznik) cout << "Widzę, że lubisz trójkę\n";
else
cout << "Oj, nie lubisz trójki\n";
}
Notatki:
24
3.5
If - wielowariantowe
Często, sprawdzając coś w programie przy pomocy instrukcji if, po uzyskaniu
odpowiedzi jesteśmy zmuszeni zadać następne pytanie. Pomocna może tu być
poniższa konstrukcja:
if (wyrażenie1)
instrukcja1
else if (wyrażenie2)
instrukcja2
else if (wyrażenie3)
instrukcja3
else instrukcja4
lub (tak jest chyba czytelniej) tak jak pokazano poniżej:
if (wyrażenie1)
instrukcja1
else if (wyrażenie2)
instrukcja2
else if (wyrażenie3)
instrukcja3
else instrukcja4
Jeśli prawdą jest
wyrażenie1
, to wykonana będzie
instrukcja1
, jeśli nie, to
jeśli prawdą jest
wyrażenie2
, to wykonana będzie
instrukcja2
, jeśli nie, to
jeśli prawdą jest
wyrażenie3
, to wykonana będzie
instrukcja3
, jeśli nie, to
wykonana będzie
instrukcja4
Oczywiście ilość instrukcji if jest dowolna.
Aby taki zapis był czytelniejszy, należy stosować nawiasy klamrowe. Jeśli
mamy kilka if i else, to else należy zawsze do najbliższej z poprzednich
instrukcji if. Oczywiście, nawiasy klamrowe mają pierwszeństwo przed tą
zasadą.
25
/* Program pr3_5.cpp
If - wielowariantowe
*/
#include <iostream.h>
main()
{
int a;
cout << "podaj liczbę -> ";
cin >> a;
if (a == 1) //Jeśli wpisano 1, to wykona się poniższa instrukcja
//i żadna z dalszych instrukcji.
cout << "Teraz podałeś jedynkę\n";
else if (a == 2)
cout << "Teraz podałeś dwójkę\n";
else if (a == 3)
cout << "Teraz podałeś trójkę\n";
else
cout << "Nie podałeś liczb 1, 2 ani 3\n";
}
Notatki:
3.6
Trochę inne if. Wyrażenie warunkowe
Wyobraźmy sobie, że mamy nadać wartość zmiennej
wynik
w zależności od
zmiennej
a
. Jak inaczej zapisać poniższy fragment programu:
if (a == 5)
wynik = 1;
else wynik = 2;
Właśnie tak:
wynik = (a == 5) ? 1 : 2;
Jest to przykład wyrażenia warunkowego. Jego postać jest następująca:
(warunek) ? wartość1 : wartość2;
Jeśli warunek jest spełniony, to wyrażenie przyjmuje wartość -
wartość1
,
w przeciwnym razie
wartość2
. Dodatkowo
wartość1
i
wartość2
mogą być
dowolnymi stałymi lub wartościami wyrażeń.
26
Słowa warunek, tak jak powyżej, będziemy używać w dalszej części wykładu
dla dowolnych wyrażeń, gdzie prawda i fałsz są interpretowane tak, jak to
zdefiniowano poprzednio.
/* Program pr3_6.cpp
Wyrażenie warunkowe 1
*/
#include <iostream.h>
main()
{
int wynik, a;
cout << "Podaj liczbę całkowitą -> ";
cin >> a;
//Zmienna wynik przyjmie wartość zależną od zmiennej a.
wynik = (a == 3) ? 1 : 0;
if (wynik)
cout << "Wpisałeś 3";
else cout << "Nie wpisałeś 3";
}
Notatki:
/* Program pr3_7.cpp
Wyrażenie warunkowe 2
*/
#include <iostream.h>
main()
{
int a;
cout << "Podaj liczbę całkowitą -> ";
cin >> a;
//Poniżej wyrażenie warunkowe zwraca wartość będącą stringiem.
cout << ( (a == 3) ?
"Napisałeś trojkę\n" : "Nie lubisz trójki\n" );
}
Notatki:
27
3.7
Pętla for
Pętle służą do wykonywania kilka razy tych samych instrukcji. Pierwsza z
pętli, które tu poznamy, to pętla for.
for (instr-początkowa; warunek; instr-kroku) instrukcja-pętli
Słowo
instrukcja-pętli
oznacza tu jak zwykle jedną instrukcje lub ich blok.
Wykonanie powyższej pętli obejmuje następujące kroki:
1. Wykonanie rozpoczyna się od wykonania instrukcji
instr-początkowa
.
2. Następuje sprawdzenie wyrażenia
warunek
. Jeśli jego wartość jest 0, to
pętla jest przerywana. W przeciwnym wypadku:
3. Wykonuje się
instrukcja-pętli
.
4. Wykonuje się
instr-kroku
i skaczemy do punktu 2.
Poniżej kilka przykładów zastosowania pętli for.
28
/* Program pr3_8.cpp
Pętla for. Program wypisujący kolejne liczby naturalne
*/
#include <iostream.h>
main()
{
int i, ilosc;
cout << "Do ilu mam liczyć -> ";
cin >> ilosc;
cout << "Więc liczę od 1 do " << ilosc << "\n";
/*Poniższa pętla spowoduje wypisanie liczb od 1 do wartości
podanej przez użytkownika i zapisanej do zmiennej ilość.*/
for( i = 1; i <= ilosc; i = i+1)
{
cout << i;
//W każdym przebiegu pętli zmienna i ma inna wartość
//ze względu na instrukcje kroku.
cout << "\n";
}
cout << "No - jestem zmęczony... Cześć\n";
}
Notatki:
29
/* Program pr3_9.cpp
Pętla for. Zliczający śpiących studentów ;)
*/
#include <iostream.h>
main()
{
int i, ilosc, licznik;
char znak;
cout << "Ilu jest dziś studentów na wykładzie -> ";
cin >> ilosc;
cout << "Sprawdzę czy uważają \n";
licznik = 0;
//Ponizsza pętla przebiega ilosc razy.
for( i = 1; i <= ilosc; i = i+1)
{
cout << "Czy student nr " << i << " śpi
(t/n) -> ";
cin >> znak;
//W zależności od użytkownika zwiększamy lub nie wartość
//w zmiennej licznik. Wykozystano tu instrukcję warunkową.
//Jest to o wiele zgrabniejsze niż stosowanie zwykłego if
//(choć może jeszcze nie dla wszystkich czytelne).
licznik = licznik + ( (znak == ’t’) ? 1 : 0 );
}
cout << "Tak więc liczba śpiących studentów wynosi " << licznik;
cout << "\n\n... Przynajmniej do tej pory\n";
}
Notatki:
30
/* Program pr3_10.cpp
Pętla for. Program liczący silnię
Proszę zwrócić uwagę na wartości liczone przez program.
Dla dużych danych mogą być inne od oczekiwanych - dlaczego?
*/
#include <iostream.h>
main()
{
int n, i;
long silnia;
cout << "Podaj liczbę, której silnię mam obliczyć ";
cin >> n;
silnia = 1;
//Nadanie wartości początkowej zmiennej, pod którą
//będziemy przechowywać wartość silni.
for( i = 1; i <= n; i = i+1)
silnia = silnia * i;
//W tej pętli jest tylko jedna instrukcja.
cout << "A oto i wynik " << n <<"! = " << silnia;
}
Notatki:
3.8
Uwagi do pętli for
W schemacie:
for (instr-początkowa; warunek; instr-kroku) instrukcja-pętli
instr-początkowa
nie musi być tylko jedną instrukcją. Jeśli chcemy umieścić
tam więcej instrukcji, to oddzielamy je przecinkami.
Elementy
instr-początkowa
,
warunek
oraz
instr-kroku
mogą również zo-
stać pominięte. ZAWSZE jednak TRZEBA pisać średniki (;). Jeśli opuścimy
warunek
, to pętla będzie się wykonywać w nieskończoność - program się za-
pętli.
31
/* Program pr3_11.cpp
Pętla for. Program liczący silnię na dwa sposoby
Przykłady opuszczania pewnych części w schemacie pętli
*/
#include <iostream.h>
main()
{
int n, i;
long silnia;
cout << "Podaj liczbę, której silnię mam obliczyć ";
cin >> n;
for( i = 1, silnia = 1; i <= n; i = i+1) // Dwie instrukcje początkowe.
silnia = silnia * i;
cout << "A oto i wynik " << n <<"! = " << silnia;
// Jeszcze raz obliczymy silnie dla n
for( i = 1, silnia = 1; i <= n; ) // Dwie instrukcje początkowe.
// Brak instrukcji kroku.
{
silnia = silnia * i;
i = i + 1;
//Tu zadbamy o instrukcję kroku.
}
cout << "\n\nA innym sposobem " << n <<"! = " << silnia;
cout << "\n\nZgadza się ? .....";
}
Notatki:
Pętla for jest typowa pętlą, którą stosuje się w przypadkach, gdy już na po-
czątku jej działania wiemy, ile razy ma się ona wykonać. W powyższych przy-
kładach wiedzieliśmy przed wykonaniem pętli, do ilu mamy liczyć, czy też
jaka jest wartość argumentu przy liczeniu silni. Oczywiście w programowaniu
spotkamy sytuacje, w których nie mamy takiej wiedzy - wtedy przydadzą się
nam inne pętle.
32
4
Wykład 4
4.1
Tablice liczbowe
Często w programie potrzebujemy kilku zmiennych tego samego typu, prze-
chowujących wartości dotyczące podobnych zdarzeń. Przypuśćmy, że pisze-
my program, który zbiera informacje o ocenach studentów i liczy ich średnią.
Zmiennymi, o których była mowa, będą oceny studentów. Program ten mógł-
by wyglądać następująco:
/* Program pr4_1.cpp
Liczenie średniej ocen z kolokwium dla 5-ciu studentów.
Przykład z pojedynczymi zmiennymi.
*/
#include <iostream.h>
main()
{
int o1, o2, o3, o4, o5;
//5 zmiennych, bo 5-ciu studentów.
float srednia;
//Oceny całkowite, średnia może wyjść rzeczywista.
cout << "\nPodaj ocenę studenta nr 1 -> ";
cin >> o1;
cout << "\nPodaj ocenę studenta nr 2 -> ";
cin >> o2;
cout << "\nPodaj ocenę studenta nr 3 -> ";
cin >> o3;
cout << "\nPodaj ocenę studenta nr 4 -> ";
cin >> o4;
cout << "\nPodaj ocenę studenta nr 5 -> ";
cin >> o5;
srednia = (o1 + o2 + o3 + o4 +o5) / 5.0;
//UWAGA, napisaliśmy 5.0 nie 5
cout << "\n\n Średnia ocen wynosi " << srednia << "\n\a";
}
Notatki:
Gdybyśmy teraz chcieli zmienić ten program na taki, który liczy średnią dla
10-ciu studentów, musielibyśmy dopisać dodatkowo 5 zmiennych i 10 linijek.
A jak by to wyglądało przy 100 studentach?
33
Rozwiązaniem tego problemu są tablice. Tablicę, która pomieści w sobie 5
ocen studentów definiujemy następująco:
int ocena[5];
Jest to tablica 5-cio elementowa o nazwie
ocena
. Każdy z elementów tej
tablicy jest typu
int
. Poszczególne elementy tej tablicy to:
ocena[0] ocena[1] ocena[2] ocena[3] ocena[4]
UWAGA, numeracja elementów tablicy zaczyna się od 0 (zero). Najważ-
niejszą chyba cechą tablic jest to, że indeksy (wartości w nawiasach kwadra-
towych) nie muszą być stałymi. Możemy tam wstawić zmienną typu całko-
witego. Tak więc, jeśli np. zmienna
i
ma wartości 3, to
ocena[i]
oznacza dokładnie
ocena[3]
Napiszmy jeszcze raz poprzedni program przy użyciu tablic.
34
/* Program pr4_2.cpp
Liczenie średniej ocen z kolokwium dla 5-ciu studentów.
Przykład z tablicą.
*/
#include <iostream.h>
main()
{
int oceny[5];
//Tablica pięcioelementowa.
int suma, i, ilosc;
float srednia;
//Oceny całkowite, średnia może wyjść rzeczywista.
ilosc = 5;
//Zmienna potrzebna, by łatwiej zmieniać program.
for (i = 0; i < ilosc; i = i + 1)
{
cout << "\nPodaj ocenę studenta nr " << (i + 1) << " -> ";
cin >> oceny[i];
//W każdym przebiegu pętli wczytujemy
//do komórki o numerze i.
}
for (i = 0, suma = 0; i < ilosc; i = i + 1)
suma = suma + oceny[i];
srednia = suma / (ilosc + 0.0);
//Ta dziwna konstrukcja jest, by uzyskać poprawną średnią
cout << "\n\nŚrednia ocen wynosi " << srednia << "\n\a";
}
Notatki:
Jeśli teraz zechcemy, aby ten program liczył średnią dla 100 studentów, wy-
starczy zmienić jedynie dwie linijki. Musimy zmienić:
int oceny[5];
na
int oceny[100];
oraz
ilosc = 5;
na
ilosc = 100;
Przykłady definicji tablic:
float zaorbki[34];
//tablica o nazwie zarobki złożona z 34
// elementów typu float
long odleglosc[12];
//tablica o nazwie odleglosc złożona z 12
//elementów typu long
double ABC[28];
//tablica ABC złożona z 28 elementów typu double
35
Ważną cechą tablic w C/C++ jest to, że zawsze w pamięci komputera zaj-
mują spójny obszar. Kolejne elementy tablicy następują w pamięci komputera
bezpośrednio jeden za drugim. UWAGA, tablice są definiowane statycznie.
Już przed kompilacją trzeba podać ich rozmiar. Nie jest poprawny program
typu:
int rozmiar;
cout << "Podaj rozmiar tablicy ";
cin >> rozmiar;
int tablica[rozmiar];
//BŁĄD rozmiar tablicy musi być znany przed
//kompilacją a tu zmienna rozmiar jest
//ustawiana w czasie działania programu
4.2
Tablice wielowymiarowe
Tak jak elementami tablic mogą być liczby, tak też elementami tablic mogą
być tablice. Takie tablice nazywamy tablicami wielowymiarowymi.
int Tab[5][9]; // Tab jest tablicą 5-cio elementową złożoną z 9-cio
//elementowych tablic o elementach typu int
Taką tablicę można również traktować, co łatwiej sobie wyobrazić, jako tabli-
cę o rozmiarach 5 x 9 złożoną z elementów typu int. Poszczególne elementy
takiej tablicy to:
Tab[0][0] Tab[0][1] Tab[0][2] .... Tab[0][8]
Tab[1][0] .........................Tab[1][8]
...........................................
...........................................
Tab[4][0]
....................... Tab[4][8]
Analogicznie możemy zdefiniować tablice dowolnego wymiaru - byle mieściły
się w pamięci.
Inne przykłady tablic wielowymiarowych:
char Znaki[4][3];
//Tablica 3x4 znakowa
float FT[3][4][6];
//Tu już tablica 3 wymiarowa 3x4x6 typu float
double Tab_D[9][67];//Każdy widzi
36
/* Program pr4_3.cpp
Wyliczanie średnich ocen wyników kolokwium dla
studentów z 3 grup*/
#include<iostream.h>
main()
{
int wyniki[3][5];
//Tablica do przechowywania wyników.
//Mamy 3 grupy po 5-ciu studentów.
int nr_grupy, nr_stud, suma;
float srednia;
//Wczytujemy wyniki kolokwium.
for(nr_grupy = 0; nr_grupy <= 2; nr_grupy = nr_grupy+1)
//Pętla for odpowiedzialna za numer grupy. Indeksacja tablicy od 0.
{
cout << "\nGrupa nr " << (nr_grupy + 1);
for(nr_stud = 0; nr_stud <= 4; nr_stud = nr_stud +1)
//Pętla dotyczy poszczególnych studentów w przeglądanej grupie.
{
cout << "\nIlość punktów studenta nr " << (nr_stud +1) << " -> ";
cin >> wyniki[nr_grupy][nr_stud]; // Wczytujemy wyniki w odpowiednie
// miejsce w tablicy wyniki.
}
}
//Obliczę średnie wyniki dla wszystkich studentów.
for(suma = 0, nr_grupy = 0; nr_grupy <= 2; nr_grupy = nr_grupy+1)
for(nr_stud = 0; nr_stud <= 4; nr_stud = nr_stud +1)
{
suma = suma + wyniki[nr_grupy][nr_stud];
}
srednia = suma / 15.0;
cout << "\n\nŚrednia wyników ze wszystkich grup wynosi " << srednia;
}
Notatki:
4.3
Tablice znakowe
Z tablic typu znakowego możemy korzystać tak, jak z tablic liczbowych. Jed-
ną z ich istotnych cech (inne poznamy później) jest to, że mogą w sobie
przechowywać łańcuchy znakowe wpisywane przez użytkownika programu.
Mogą być również w całości wypisywane. Taka operacja z tablicami liczbo-
wymi nie dawałaby oczekiwanego rezultatu - proszę sprawdzić i spróbować
odpowiedzieć, dlaczego tak jest.
37
#include<iostream.h>
main()
{
char imie[20];
cout << "\nJak masz na imię ? ";
cin >> imie;
//Wczytuje ciąg znaków
//Wypisuję ciąg znaków z tablicy imie.
cout << "\nWitaj " << imie;
}
4.4
Operatory logiczne
Poza poznanymi wcześniej operatorami porównania, możemy w wyrażeniach
logicznych stosować operatory sumy i iloczynu logicznego:
||
- suma logiczna
(lub)
&&
- iloczyn logiczny (i)
Priorytet operatora
&&
jest większy od priorytetu
||
. Z kolei, oba te operatory
mają priorytety mniejsze od operatorów porównywania.
Bardzo istotną cechą wyrażeń złożonych z operatorów
&&
i
||
jest to, że są
one obliczane od lewej do prawej, a liczenie to, jest przerywane, gdy już jest
pewne, czy wyrażenie jest prawdą czy fałszem.
38
/* Program pr4_4.cpp
Operatory logiczne 1
*/
#include<iostream.h>
main() {
char znak;
cout << "\nWpisz dowolną literę -> ";
cin >> znak;
if ( (znak == ’t’) || (znak == ’T’) ) //Czy przyciśnięto T lub <shift>T.
cout << "\nWpisaleś małą lub dużą literę t\n";
else
cout << "\nNie wpisałeś ani małej ani dużej litery t\n";
}
Notatki:
/* Program pr4_5.cpp
Operatory logiczne 2
Kilka operatorów w jednym wyrażeniu
*/
#include<iostream.h>
main()
{
char znak;
int wiek;
cout << "\nCzy jesteś kobietą (t/n) ";
cin >> znak;
cout << "Podaj swój wiek -> ";
cin >> wiek;
//Poniżej, pod if, bardziej złożony warunek logiczny.
if ( ((znak == ’t’) || (znak == ’T’)) && (wiek <= 30) && (wiek >= 18) )
cout << "\nMoże pójdziemy na kawę? \n";
else
cout << "\nMiło cię było poznać. Do zobaczenia.";
}
Notatki:
Innym operatorem logicznym (wcześniej już wymienianym) jest jednoargu-
mentowy operator
!
(wykrzyknik). Jest to operator negacji.
39
/* Program pr4_6.cpp
Operatory logiczne 3 - negacja
*/
#include<iostream.h>
main()
{
int czas, ilosc;
cout << "\nIle jeszcze minut do końca -> ";
cin >> czas;
if ( ! (czas <=10) ) //Negacja zastosowana do warunku.
cout << "\nNie wiem czy to jeszcze wytrzymam.";
else
cout << "\nNo może jeszcze mi się uda dożyć do
końca.";
cout << "\n\nIle złotych masz w portfelu? ";
cin >> ilosc;
if ( ! ilosc )
//Negacja zastosowana do zmiennej.
cout << "\nTo tyle co i ja.";
else
cout << "\nSzczęśliwiec !!!";
}
Notatki:
4.5
Operatory przypisania cd.
Często przydają się operatory zmniejszania i zwiększania wartości zmiennej
o 1. Jeśli
i
jest zmienną typu liczbowego, to:
++i ; // i = i+1 powiększenie przed użyciem zmiennej i
i++ ; // i = i+1 powiększenie po użyciu zmiennej i
--i ; // i = i-1 pomniejszenie przed użyciem zmiennej i
i-- ; // i = i-1 pomniejszenie po użyciu zmiennej i
40
/* Program pr4_7.cpp
Przykład demonstrujący działanie operatorów ++ --
*/
#include<iostream.h>
main() {
int i;
i = 5;
cout << "\nWartość i++ = " << i++;
cout << "\nWartość i po operacji -> " << i;
i = 5;
cout << "\nWartość ++i = " << ++i;
cout << "\nWartość i po operacji -> " << i;
i = 5;
cout << "\nWartość i-- = " << i--;
cout << "\nWartość i po operacji -> " << i;
i = 5;
cout << "\nWartość --i = " << --i;
cout << "\nWartość i po operacji -> " << i;
}
Notatki:
Przy stosowaniu operatorów zwiększania i zmniejszania należy uważać pisząc
wyrażenia, w których następuje wielokrotne stosowanie tych operatorów do
jednej zmiennej. Jakie są wartości zmiennych
i
oraz
k
po wykonaniu poniż-
szego fragmentu?
i = 4;
j = (++i) * (++i);
Miejscem, w którym stosujemy omawiane operatory, jest np. pętla for.
for(i = 0; i <= 5; i++)
4.6
Połączenie operatora przypisania z operatorami
arytmetycznymi
Jeśli
i
jest zmienną typu liczbowego,
u
-zmienną, stałą lub wyrażeniem typu
liczbowego wtedy to, możemy stosować następujące skróty:
41
i += u;
to samo co
i = i + u;
i -= u;
to samo co
i = i - u;
i *= u;
to samo co
i = i * u;
i /= u;
to samo co
i = i / u;
i %= u;
to samo co
i = i % u;
/* Program pr4_8.cpp
Przykład demonstrujący działanie
poszerzonych operatorów przypisania
*/
#include<iostream.h>
main()
{
int i;
i = 5 ;
i += 3;
cout << "\nWartość i po i += 3
-> " << i;
i = 5 ;
i -= 3;
cout << "\nWartość i po i -= 3
-> " << i;
i = 5 ;
i *= 3;
cout << "\nWartość i po i *= 3
-> " << i;
i = 5 ;
i /= 3;
cout << "\nWartość i po i /= 3
-> " << i;
i = 5 ;
i %= 3;
cout << "\nWartość i po i %= 3
-> " << i;
}
Notatki:
4.7
Pętla while
Innym rodzajem pętli w C jest pętla while. Można ją zasymulować pętlą
for. Przydaje się jednak do jaśniejszego zapisywania programów.
while(wyrażenie) instrukcja_pętli
Wykonanie powyższej pętli obejmuje następujące kroki:
42
1. Jeśli
wyrażenie
ma wartość różną od zera (prawda), wykonaj
instrukcja_pętli
.
Jeśli
wyrażenie
ma wartość równą zero (fałsz), przejdź do następnej
instrukcji występującej po pętli. Instrukcja
instrukcja_pętli
nie jest
wykonywana.
2. Po ew. wykonaniu instrukcji
instrukcja_pętli
przejdź do punktu 1.
Pętla for jest używana zazwyczaj wtedy, gdy ma się ona wykonać określoną,
czasami przed jej wykonaniem znaną, ilość razy. Gdy ilość przebiegów pętli
nie jest od razu znana, korzystamy z pętli while.
/* Program pr4_9.cpp
Pętla while
*/
#include<iostream.h>
main()
{
char znak;
znak = ’n’;
//Poniższa pętla będzie się wykonywała do czasu, aż
//nie wpiszemy znaku t lub T. Nie wiemy z góry, jak długo
//ta pętla będzie wykonywana.
while( (znak != ’t’) && (znak != ’T’))
{
cout << "Czy przerwać pracę (t/n) -> ";
cin >> znak;
}
cout << "Koniec programu.\n";
}
Notatki:
Następny przykład byłoby może łatwiej zapisać używając for (to zrobi czy-
telnik), my użyjemy pętli while.
43
/* Program pr4_10.cpp
Pętla while - zamiast pętli for
*/
#include<iostream.h>
main()
{
int ilosc;
cout << "\nIle sekund do końca wykładu? -> ";
cin >> ilosc;
cout << "\nOdliczam ...";
while ( ilosc > 0 )
{
cout << ’\n’;
cout << ilosc <<" - cyk";
ilosc--;
}
cout << "\nNo to koniec";
}
Notatki:
44
5
Wykład 5
5.1
Pętla do . . . while
Zapoznamy się tu z kolejnym (już ostatnim) rodzajem pętli w C. Jej postać
jest następująca:
do
instrukcja
while(warunek);
Wykonanie powyższej pętli przebiega następująco:
1. Wykonaj instrukcję
instrukcja
.
2. Jeśli
warunek
przyjmuje wartość niezerową(prawdy), przejdź do punk-
tu 1. W przeciwnym wypadku przerwij działanie pętli.
Różnica w stosunku do poznanej pętli while jest taka, że
warunek
sprawdzany
jest po wykonaniu instrukcji
instrukcja
a nie przed. Z tego też powodu
instrukcja pętli do . . . while wykona się zawsze przynajmniej jeden raz,
co nie koniecznie miało miejsce w przypadku pętli while. Popatrzmy na
przykład z poprzedniego wykładu dotyczący pętli while a zapisany teraz
przy pomocy do . . . while.
45
/* Program pr5_1.cpp
Pętla do ... while - stosuje się ją tu wygodniej niż
pętlę while
*/
#include<iostream.h>
main()
{
char znak;
//Poniższa pętla będzie się wykonywała do czasu aż
//nie wpiszemy znaku t lub T. Nie wiemy z góry, jak długo
//ta pętla będzie wykonywana.
//Ważne jest to, że instrukcje tej pętli wykonają się
//przynajmniej jeden raz.
do
{
cout << "Czy przerwać pracę (t/n) -> ";
cin >> znak;
}while( (znak != ’t’) && (znak != ’T’) );
cout << "Koniec programu.\n";
}
Notatki:
Następny program pokazuje jak zastosować pętle do . . . while, gdy pytając
o potwierdzenie podczas wykonywania programu, chcemy tylko reagować na
przyciśnięcie klawiszy T,t,N,n i żadnych innych.
46
/* Program pr5_2.cpp
Kody znaków
Przykład działania pętli do
...
while
*/
#include <iostream.h>
void main()
{
char znak, pytanie;
int kod;
cout << "\nProgram podający kody znaków\n";
//Jeśli uruchomiliśmy program, to chcemy, by przynajmniej raz coś
//zrobił. Dlatego zastosujemy pętlę do ... while.
do
{
//Wczytaj znak i wypisz jego kod ASCII
cout << "\nWpisz znak -> ";
cin >> znak;
kod = znak;
cout << "\nZnak - " << znak << "
Kod - " << kod;
//Poniższa pętla bedzie wykonywana do wpisania T,t,N lub n.
do
{
cout << "\nCzy liczymy dalej t/n ?";
cin >> pytanie;
}while( (pytanie != ’t’) && (pytanie != ’T’) &&
(pytanie != ’n’) && (pytanie != ’N’) );
}while( (pytanie == ’t’) || (pytanie == ’T’) );
}
Notatki:
5.2
Instrukcja break
Instrukcja break służy do przerywania działania pętli. Gdy program napotka
na tę instrukcję wewnątrz pętli for, while czy do . . . while, bez sprawdzania
jakichkolwiek warunków przechodzi do wykonania pierwszej instrukcji poza
tą pętlą.
UWAGA - jeśli instrukcja break znajduje się w zagnieżdżonych pętlach,
to przerywa ona działanie tylko pętli najbardziej zagnieżdżonej, w której się
znajduje.
47
/* Program pr5_3.cpp
Instrukcja break
Przykład przerwania pętli
while przy pomocy break
Oczywiście zgrabniejszym rozwiązaniem byłoby zastosowanie
pętli do ... while z odpowiednim warunkiem - ćwiczenie.
*/
#include <iostream.h>
void main()
{
char znak;
//Warunek w poniższej pętli zawsze jest spełniony
//dlatego bez użycia instrukcji break program się zapętli.
while(1)
{
cout << "\nPierwsza instrukcja pętli while";
cout << "\nCzy przerywamy pętlę t/n? -> ";
cin >> znak;
//Jeśli podano t lub T, pętla zostanie przerwana.
if( (znak == ’t’) || (znak == ’T’) )
break;
//Jeśli wykona się break, poniższa linia zostanie pominięta.
cout << "\nOstatnia linia pętli while";
}
cout << "Tu już jesteśmy poza pętlą while";
}
Notatki:
Jak działa break w zagnieżdżonych pętlach, pokazuje następny przykład.
Ze względu na oszczędność miejsca w większych przykładach będę stosował
inne ustawianie nawiasów { i }. Proszę pamiętać, że nie ważne jaki sposób
formatowania wybierzemy - ważne, by w jednym programie stosować go kon-
sekwentnie i by był czytelny.
48
/* Program pr5_4.cpp
Instrukcja break
Przykład przerwania pętli zagnieżdżonych.
*/
#include <iostream.h>
void main() {
char znak;
for ( ; ; ){
//Nieskończona pętla for.
cout << "\nPierwsza instrukcja pętli for";
cout << "\nCzy przerywamy pętlę for t/n? -> ";
cin >> znak;
//Poniższe break przerywa pierwszą pętle for.
if( (znak == ’t’) || (znak == ’T’) )
break;
do{
cout << "\nPierwsza instrukcja pętli do .. while";
cout << "\nCzy przerywamy pętlę do .. while t/n? -> ";
cin >> znak;
if( (znak == ’t’) || (znak == ’T’) )
break;
//Powyższe break przerywa drugą pętle - do .. while.
cout << "\nOstatnia linia pętli do ... while";
}while(1);
//Ten warunek też zawsze jest spełniony.
cout << "\nPierwsza instrukcja po pętli do ... while";
cout << "\nOstatnia linia pętli for";
}
cout << "Tu już jesteśmy poza pętlą for";
}
Notatki:
5.3
Instrukcja switch
Instrukcja switch służy do podejmowania decyzji wielowariantowych. Często
działanie programu, w zależności od np. wartości zmiennych, może przebie-
gać na kilka(kilkanaście) sposobów. Możemy takie sytuacje obsłużyć przy
pomocy if, lecz instrukcja switch będzie tu poręczniejsza.
49
switch(wyrażenie)
{
case wartość_1:
instrukcja_1;
case wartość_2:
instrukcja_2;
.
.
case wartość_N:
instrukcja_N;
default:
instrukcja_default;
}
wyrażenie
- musi być typu całkowitego.
wartość_1 ... wartość_N
to stałe tego samego typu co
wyrażenie
.
Wykonanie:
1. Obliczana jest najpierw wartość wyrażenia -
wyrażenie
.
2. Następnie przeglądane są zgodnie z kolejnością napisaną w programie
wartość_1 ... wartość_N
. Gdy któraś z tych wartości (np.
wartość_K
) jest równa wartości wcześniej wyliczonego wyrażenia
wyrażenie
, pro-
gram zaczyna wykonywać instrukcje po
case wartość_K:
aż do ewen-
tualnego wystąpienia instrukcji break, która powoduje przeskok do
pierwszej instrukcji poza całą instrukcją switch.
3. Jeśli nie udało się znaleźć nic w punkcie 2, To program zaczyna wyko-
nywać instrukcje po
default:
UWAGA, etykieta
default:
nie musi być etykietą ostatnią - może wystę-
pować w dowolnym miejscu. Może być ona również pominięta.
W związku z uwagą w punkcie 2. instrukcja switch wygląda zazwyczaj na-
stępująco:
50
switch(wyrażenie)
{
case wartość_1:
instrukcja_1;
break;
.
.
.
case wartość_N:
instrukcja_N;
break;
default:
instrukcja_default;
break;
}
Poniższe programy pokazują zastosowanie instrukcji switch.
51
/* Program pr5_5.cpp
Instrukcja switch
Przykład stosowania instrukcji switch. Nie używam tu
zbyt wielu instrukcji break.
*/
#include <iostream.h>
void main()
{
int kod;
cout << "\nPodaj kod awarii -> ";
cin >> kod;
switch(kod)
{
case 3:
cout << "\n3 - Sprawdzam silnik";
case 2:
cout << "\n2 - Sprawdzam olej";
case 1:
cout << "\n1 - Sprawdzam światła";
break;
//To break, by nie wykonywać następnych instrukcji.
default:
cout << "\nNie znam takiego kodu";
}
}
Notatki:
52
/* Program pr5_6.cpp
Przykład stosowania instrukcji switch.
Tu używam break przy każdej etykiecie
*/
#include <iostream.h>
void main()
{
int kod;
cout << "\nPodaj, co mam sprawdzić:"
"\n1 - swiatła\n2 - olej\n3 - silnik"
"\nCo wybierasz -> ";
cin >> kod;
switch(kod)
{
default:
//Przy takim stosowani break etykietę default:
//moge wstawić w tym miejscu.
cout << "\nNie znam takiego kodu";
break;
case 3:
//W tym przypadku sprawdzę silnik i nic więcej.
cout << "\n3 - Sprawdzam silnik";
break;
case 2:
cout << "\n2 - Sprawdzam olej";
break;
case 1:
cout << "\n1 - Sprawdzam światła";
break;
}
}
Notatki:
5.4
Instrukcja goto
Instrukcja goto służy do przeskoku z jednego miejsca programu do drugiego.
goto etykieta ;
.
.
.
etykieta:
.
.
Program, napotykając na instrukcje
goto etykieta;
jako następną wykonuje
instrukcję następującą po wierszu
etykieta:
. Etykieta to ciąg znaków (taki
53
jak nazwa zmiennej) zakończony dwukropkiem (:). Etykieta może się również
znajdować przed instrukcją
goto
. Zakres ważności (widoczności) etykiety jest
ograniczony do funkcji, w której ona występuje.
cout << "\nWykonuję tę linię";
goto abc;
cout << "\nDo tej linii nigdy nie przejdę";
abc:
cout << "\n Właśnie tu przeskoczyliśmy";
UWAGA, instrukcja goto nie jest polecana gdyż sprawia, że program staje
się nieczytelny. Mogą wystąpić kłopoty z eleganckim skompilowaniem progra-
mu. Instrukcji goto można zawsze uniknąć, stosując poznane już instrukcje.
UWAGA, po instrukcji goto musi znaleźć się średnik, nie dwukropek (musi
mieć formę
goto etykieta ;
)
UWAGA, jako że instrukcja jest przez początkujących przez programistów
nadużywana, przyjmijmy zasadę, że będzie ona stosowana tam, gdzie jest ona
naprawdę konieczna. Dotyczy to programów oddawanych na ćwiczeniach. Z
każdej instrukcji goto trzeba się będzie wytłumaczyć.
Jedynym chyba miejscem, gdzie wskazane jest używanie instrukcji goto, jest
wychodzenie z zagnieżdżonych pętli. Przypominam, że poznana wcześniej in-
strukcja break powodowała tylko przerwanie działania najbardziej zagnież-
dżonej pętli. Oto przykład zastosowania goto.
54
/* Program pr5_7.cpp
Instrukcja goto
Przykład pokazujący przydatność instrukcji goto przy wychodzeniu
z zagnieżdżonych pętli.
*/
#include <iostream.h>
void main()
{
int i, j, k;
char pytanie;
for(i = 1; i <= 5; i++)
for(j = 1; j <= 5; j++)
for(k = 1; k <= 5; k++)
{
cout << "\ni = " << i << "
j = " << j << "
k = " << k ;
cout << "\n\nCzy masz już dosyć t/n ? -> ";
cin >> pytanie;
//Jeśli wpisano ’T’ lub ’t’ to wyskoczymy do linijki po
//etykiecie koniec: .
if( (pytanie == ’t’) || pytanie == ’T’) goto koniec;
//Instrukcją break nie poszłoby nam tak łatwo
}
koniec: cout << "\nNo i koniec pracy";
}
Notatki:
5.5
Instrukcja continue
Czasem zachodzi konieczność by przerwać w pewnym momencie jeden z prze-
biegów pętli i zacząć następny przebieg. Do tego właśnie służy instrukcja
continue. Ma ona postać
continue;
Może wystąpić wewnątrz dowolnej pętli (for, while, do . . . while). Program
napotykając tę instrukcję wewnątrz pętli przerywa aktualną iterację pętli i
przechodzi do następnej iteracji. Następna iteracja jest wykonywana o ile
prawdziwy jest warunek wejścia do tej pętli.
UWAGA, w pętli for(. . . ) wykonuje się wpierw instrukcja kroku, a następ-
nie sprawdzamy warunek wejścia do pętli.
55
while (i < 10) // Linia 1
{
i++;
cout << "\n" << i;
continue;
// Teraz skaczemy do Linia 1
cout << "\nTa linia nie zostanie nigdy wykonana";
}
W powyższej pętli nigdy nie wykona się ostatnia linia.
/* Program pr5_8.cpp
Instrukcja continue
Wypisuję wszystkie liczby parzyste od 0 do zadanej liczby.
Oczywiście można to zrobić prościej.
*/
#include <iostream.h>
void main()
{
int i, zakres;
cout << "\nPodaj zakres -> ";
cin >> zakres;
for (i = 0; i <= zakres; i++)
{
if( (i % 2) == 1 ) continue;
//Do poniższej linii dojdziemy tylko dla i parzystych.
cout << "\n" << i;
}
}
Notatki:
56
6
Wykład 6
6.1
Struktury
Tablice łączyły w jedno obiekty tego samego typu. Jeśli chcemy połączyć w
całość obiekty różnych typów użyjemy struktur.
Przypuśćmy, że w programie bazodanowym opracowujemy dane o pracowni-
kach. Dane dotyczące imienia, nazwiska, daty urodzenia, zarobków itd. może-
my przechowywać w oddzielnych zmiennych lub związać je ze sobą za pomocą
struktury. Zdefiniujmy zmienną osoba, która będzie przechowywać dane do-
tyczące pojedynczej osoby.
Przykładowa struktura:
struct{
char imie[30], nazwisko[30], data_urodz[11];
int zarobki;
char plec;
}osoba;
W ten sposób zdefiniowaliśmy zmienną o nazwie
osoba
, która zawiera w sobie
5 pól: tablice znakowe
imie
,
nazwisko
,
data_urodz
, pole typu
int
o nazwie
zarobki
, oraz pole znakowe
plec
. Jeśli chcemy mieć kilka zmiennych takich
jak osoba, można to zrobić następująco:
struct{
char imie[30], nazwisko[30], data_urodz[11];
int zarobki;
char plec;
}osoba1, osoba2, osoba3;
Często takie same zmienne strukturalne chcemy definiować w różnych miej-
scach programu. Aby za każdym razem nie wypisywać dokładnie jak nasza
struktura ma wyglądać, możemy określić wpierw nazwę typu zmiennej, a
potem używać tej nazwy do definicji zmiennych. Robimy to w sposób nastę-
pujący:
struct opis_osoby{
char imie[30], nazwisko[30], data_urodz[11];
int zarobki;
char plec;
};
57
W ten sposób zdefiniowaliśmy typ ( pewną nazwę )
opis_osoby
, który bę-
dzie równoważny opisowi całej struktury. Nazwę tę możemy używać teraz tak
samo jak predefiniowane nazwy typów np.
int
,
float char ...
. Teraz jeśli
chcemy zdefiniować nową zmienną opisującą pracowników w naszym bazo-
danowym programie, piszemy:
opis_osoby osoba;
Możemy również łączyć ze sobą definicje typów i zmiennych tych typów.
struct opis_osoby{
// Tu zdefiniowaliśmy typ opis_osoby
char imie[30], nazwisko[30], data_urodz[11];
int zarobki;
char plec;
}osoba1;
//Tu definiujemy zmienną osoba1 która jest typu opis_osoby
opis_osoby kierownik; //Definicja zmiennej kierownik typu opis_osoby
Aby odwołać się do poszczególnych pól tak zdefiniowanych zmiennych, uży-
wamy operatora . (kropka).
struct opis_osoby{
char imie[30], nazwisko[30], data_urodz[11];
int zarobki;
char plec;
}osoba;
//Poniżej wczytujemy do poszczególnych pól zmiennej osoba.
cin >> osoba.nazwisko;
cin >> osoba.zarobki;
//Tu wypiszemy płeć osoby
cout << osoba.plec;
58
/* Program pr6_1.cpp
Struktury.
*/
#include<iostream.h>
main() {
struct{
char imie[30], nazwisko[30], data_urodz[11];
int zarobki;
char plec;
}osoba1;
struct opis_osoby{
char imie[30], nazwisko[30], data_urodz[11];
int zarobki;
char plec;
};
opis_osoby kierownik;
cout << "Podaj dane osoby \n";
cout << "Imię -> ";
cin >> osoba1.imie;
cout << "Nazwisko -> ";
cin >> osoba1.nazwisko;
cout << "Data urodzenia -> ";
cin >> osoba1.data_urodz;
cout << "Podaj dane kierownika \n";
cout << "Nazwisko -> ";
cin >> kierownik.nazwisko;
cout << "Zarobki -> ";
cin >> kierownik.zarobki;
cout << "Płeć -> ";
cin >> kierownik.plec;
}
Notatki:
Możemy również tworzyć tablice struktur.
struct opis_osoby{
//Tu tylko definicja typu
char imie[30], nazwisko[30], data_urodz[11];
int zarobki;
char plec;
};
// Zdefiniuję tablicę o nazwie pracownicy
// składającą się z 30 rekordów opisywanych przez opis_osoby
opis_osoby pracownicy[30];
59
Teraz każdy element tablicy
pracownicy
jest strukturą. W ten sposób np.
nazwisko osoby zapisanej w tej tablicy w polu o numerze 4 to:
pracownicy[4].nazwisko;
/* Program pr6_2.cpp
Tablice struktur. */
#include<iostream.h>
main(){
struct opis_osoby{
char imie[30], nazwisko[30], data_urodz[11];
int zarobki;
char plec;
};
opis_osoby pracownicy[30];
int i;
char z;
i = 0;
do
{
cout << "Podaj dane osoby nr " << (i + 1) << ’\n’;
cout << "Imię -> ";
cin >> pracownicy[i].imie;
cout << "Nazwisko -> ";
cin >> pracownicy[i].nazwisko;
cout << "Data urodzenia -> ";
cin >> pracownicy[i].data_urodz;
cout << "Zarobki -> ";
cin >> pracownicy[i].zarobki;
cout << "Płeć -> ";
cin >> pracownicy[i].plec;
i++;
cout << "Czy chcesz dopisywać dalej (t/n)";
cin >> z;
}while( (z != ’n’) && (z != ’N’) && ( i < 30));
}
Notatki:
Można również używać jednej struktury w opisie innej struktury.
60
struct opis_osoby{
char imie[30], nazwisko[30], data_urodz[11];
int zarobki;
char plec;
};
struct opis_szczegolowy{
//wewnątrz struktury typu opis_szczegołowy mamy
//pole dane_podstawowe typu strukturalnego opis_osoby
opis_osoby dane_podstawowe;
int numer_buta;
//dodajemy jeszcze pewne dane szczegółowe
}osoba;
Aby dostać się do pól takiej struktury, robimy to tak:
osoba.dane_podstawowe.nazwisko;
ale
osoba.numer_buta;
61
/* Program pr6_3.cpp
Struktury jako pola struktur. */
#include<iostream.h>
main() {
struct opis_osoby{
char imie[30], nazwisko[30], data_urodz[11];
int zarobki;
char plec;
};
struct opis_szczegolowy{
opis_osoby dane_podstawowe;
int numer_buta;
}osoba;
cout << "Podaj dane osoby \n";
cout << "Imię -> ";
cin >> osoba.dane_podstawowe.imie;
cout << "Nazwisko -> ";
cin >> osoba.dane_podstawowe.nazwisko;
cout << "Data urodzenia -> ";
cin >> osoba.dane_podstawowe.data_urodz;
cout << "Zarobki -> ";
cin >> osoba.dane_podstawowe.zarobki;
cout << "Płeć -> ";
cin >> osoba.dane_podstawowe.plec;
cout << "\nInformacje dla wywiadu\n\n";
cout << "Numer buta -> ";
cin >> osoba.numer_buta;
}
Notatki:
6.2
Definiowanie typów
Podobnie jak w przypadku struktur, tak i dla innych typów danych nie mu-
simy od razu definiować zmiennej, lecz możemy wpierw opisać pewien (zło-
żony) typ, a potem jego nazwy używać do definicji zmiennych. Robimy to
przy pomocy słowa kluczowego typedef.
62
//Poniżej nie definiujemy żadnej zmiennej. Mówimy tylko, że
//tablica opisuje typ będący tablicą o 30 elementach typu int
typedef int tablica[30];
tablica t1;
//Tu mówimy, że t1 jest typu tablica.
//Czyli t1 jest 30 elementową tablicą o elementach typu int
Od teraz słowa
tablica
możemy używać tak jak innych znanych już nazw
typów
int
,
float char ...
. Definicje typów mogą znaleźć się w dowolnym
miejscu programu, lecz zawsze przed ich użyciem do definiowania konkret-
nych zmiennych. W następnym przykładzie definicje te występują przed funk-
cją
main()
.
/* Program pr6_4.cpp
Definiowanie typów*/
#include<iostream.h>
typedef int tablica[30]; //tablica określa typ.
//W tym miejscu jeszcze
//nie zdefiniowaliśmy żadnej zmiennej.
typedef char napis[30];
main() {
tablica tab; //Tu definiujemy zmienną tab typu tablica, a więc
//30 elementową tablicę.
napis nazwisko, imie;
//Zmienne typu napis
int i;
cout << "Podaj imię -> ";
cin >> imie;
cout << "Podaj nazwisko -> ";
cin >> nazwisko;
for(i = 0; i <5; i++)
{
cout << "Podaj liczbę nr " << i << " -> ";
cin >> tab[i];
}
}
Notatki:
UWAGA - możemy też definiować, przy pomocy typedef typy będące
strukturami. Robi się to trochę inaczej niż poprzednio robiliśmy to ze struk-
turami. Przepiszmy przykład z opisem osoby, ale przy pomocy typedef.
63
typedef struct{
//Słowo typedef mówi, że definiujemy nowy typ
char imie[30], nazwisko[30], data_urodz[11];
int zarobki;
char plec;
}opis_osoby;
//UWAGA, teraz nazwa typu występuje na końcu opisu
typedef struct {
//Definiujemy następny typ
opis_osoby dane_podstawowe; // Wykorzystujemy zdefiniowany wyżej typ
int numer_buta;
//Dodajemy jeszcze pewne dane szczegółowe
}opis_szczegolowy; //Dopiero tu nazwa typu
opis_szczegolowy osoba;
//Dopiero tu możemy zdefiniować zmienną,
//już poza typedef
64
/* Program pr6_5.cpp
Definiowanie typów strukturalnych przy pomocy typedef. */
#include<iostream.h>
typedef struct{
char imie[30], nazwisko[30], data_urodz[11];
int zarobki;
char plec;
}opis_osoby; // Teraz tu definiujemy nazwę typu.
typedef opis_osoby tablica_pracownikow[30];
main() {
//W samym programie nic się nie zmienia.
tablica_pracownikow pracownicy;
int i;
char z;
i = 0;
do
{
cout << "Podaj dane osoby nr " << (i + 1) << ’\n’;
cout << "Imię -> ";
cin >> pracownicy[i].imie;
cout << "Nazwisko -> ";
cin >> pracownicy[i].nazwisko;
cout << "Data urodzenia -> ";
cin >> pracownicy[i].data_urodz;
cout << "Zarobki -> ";
cin >> pracownicy[i].zarobki;
cout << "Płeć -> ";
cin >> pracownicy[i].plec;
i++;
cout << "Czy chcesz dopisywać dalej (t/n)";
cin >> z;
}while( (z != ’n’) && (z != ’N’) && ( i < 30));
}
Notatki:
6.3
Funkcje - wprowadzenie
Jeśli jakąś część programu chcemy wydzielić z jego całości, możemy ją zapisać
jako funkcję. Wtedy to, zamiast wpisywać tę część w miejscu jej użycia,
mówimy programowi jedynie aby wykonał taką a taką funkcję, której treść
zapisana jest gdzieś indziej. Program w C/C++ składa się z funkcji. Główną
z tych funkcji już znamy, to funkcja
main()
. Od tej funkcji program zawsze
zaczyna swoje działanie. Wewnątrz tej funkcji (jak i w dowolnej innej funkcji)
65
są wywoływane różne inne funkcje. Funkcje wywoływane przez program mogą
być funkcjami napisanymi przez programistę lub też funkcjami dostarczanymi
wraz z kompilatorem czy systemem. Funkcję wywołujemy pisząc jej nazwę z
nawiasami, w których podajemy listę parametrów oddzielonych przecinkami:
nazwa_funkcji(parametr1, parametr2, ...., parametr_ostatni);
Często używaną funkcją biblioteczną jest np. funkcja licząca pierwiastek kwa-
dratowy. Ma tylko jeden parametr. Wywołanie może wyglądać np. tak:
sqrt(9);
Jako że funkcja zwraca wartość, możemy tę wartość np. podstawić pod zmien-
ną :
a = sqrt(9);
Aby wywołać funkcję, w miejscu wywołania funkcja musi być znana. Musi
więc być zdefiniowana przed miejscem wywołania, lub przed jej wywołaniem
musi wystąpić deklaracja tej funkcji. Aby móc wykorzystywać funkcję
sqrt
,
musimy powiedzieć programowi, jak ona wygląda. Funkcja ta jest dostarcza-
na z kompilatorem i dostępna w bibliotece
math
. Jej opis jest podany w pliku
math.h
. Aby użyć funkcji z tej biblioteki, musimy do programu dołączyć linię:
#include<math.h>
66
/* Program pr6_6.cpp
Użycie funkcji sqrt przy liczeniu pierwiastków
równania kwadratowego. Wersja prymitywna :-). */
#include<iostream.h>
#include<math.h>
//Ta linia by móc użyć funkcję sqrt().
main(){
int a, b, c;
float delta, x1, x2;
cout << "\n\nSzukanie pierwiastków równania ax^2 + bx + c = 0\n\n";
cout << "\na = ";
cin >> a;
cout << "\nb = ";
cin >> b;
cout << "\nc = ";
cin >> c;
delta = b*b - 4*a*c;
if(delta < 0) cout << "\nBrak rozwiązań";
else if (delta == 0){
x1 = (-b) / (2*a);
cout << "\x = " << x1;
}
else{
//Poniżej używam funkcji sqrt
x1 = (-b - sqrt(delta)) / (2*a);
x2 = (-b + sqrt(delta)) / (2*a);
cout << "\nx1 = " << x1 << "\nx2 = " << x2;
}
}
Notatki:
6.4
Funkcje - definicje i deklaracje funkcji
W tym rozdziale nauczymy się pisać własne funkcje.
Aby zdefiniować funkcję piszemy:
typ_zwracanej_wartości nazwa_funkcji(typ_arg1 arg1, ...,typ_ost arg_ost)
{
CIAŁO FUNKCJI
}
Zdefiniujmy funkcję wypisującą przekazaną do niej wartość
67
void wypisz( int a)
//Typ funkcji void mówi, że funkcja nie zwraca
//żadnej wartości
//funkcja wypisująca wartość zmiennej a
{
cout << "Kazano mi wypisać wartość -> " << a;
}
Po takiej definicji można już używać tej funkcji pisząc np.:
wypisz(5);
/* Program pr6_7.cpp
Pisanie własnych funkcji.
*/
#include<iostream.h>
void dodaj( int a, int b)
//Typ funkcji void mówi, że funkcja nie
//zwraca żadnej wartości.
//funkcja wypisująca sumę przekazanych do niej wartości
{
cout << "\n\nSuma liczb " << a << " i " << b << " wynosi " << (a+b);
}
main()
{
int v = 5;
dodaj(v, 4);
// Wywołanie funkcji dodaj().
}
Notatki:
Do tej pory każda używana przez nas funkcja była zdefiniowana (w cało-
ści napisana) przed jakimkolwiek jej wywołaniem. Czasem takie definiowa-
nie funkcji może prowadzić do powstawania nieczytelnych programów. Jeśli
nie chcemy pisać całej funkcji przed jej wywołaniem, możemy tylko, przed
wywołaniem, poinformować kompilator o funkcji, a samą funkcje zdefinio-
wać potem. Takie poinformowanie kompilatora nazywa się deklaracją funkcji.
Wygląda ona następująco:
typ_zwracanej_wartości nazwa_funkcji(typ_arg1 arg1, ...,typ_ost arg_ost);
lub też
typ_zwracanej_wartości nazwa_funkcji(typ_arg1, ...,typ_ost);
podajemy tylko typy argumentów
UWAGA NA ŚREDNIK!!!
68
void wypisz( int a);
//Deklaracja funkcji wypisz.
// void wypisz( int ); //Można też tak, opuszczając nazwę argumentu.
//Dla kompilatora jest tylko istotne, jak
//funkcja wygląda
main()
{
.
.
wypisz(9); //Wywołanie funkcji
}
void wypisz( int a)
//Definicja funkcji dopiero tutaj
{
cout << "Kazano mi wypisać wartość -> " << a;
}
Aby móc używać funkcji, przed ich wywołaniem MUSI zawsze znaleźć się
jej definicja lub deklaracja. Wprawdzie w czystym C są od tego pewne od-
stępstwa, ale w praktyce będziemy stosować C++, i tam ta zasada zawsze
obowiązuje. Zapominamy więc o tych odstępstwach - dotyczy to też pytań
na egzaminie.
Opisane wcześniej użycie
#include<math.h>
jest właśnie deklaracją dostęp-
nych w bibliotece funkcji.
/* Program pr6_8.cpp
Pisanie własnych funkcji
Przed wywołaniem funkcji tylko deklaracja. */
#include<iostream.h>
void dodaj( int a, int b);
//Deklaracja funkcji dodaj().
main()
{
int v = 5;
dodaj(v, 4);
//Tu wywołujemy funkcję, chociaż jeszcze jej nie
//zdefiniowaliśmy. Kompilator wie jednak, jak ona wygląda.
}
void dodaj( int a, int b)
//Tu dopiero definiujemy funkcję.
//funkcja wypisująca wartość zmiennej a
{
cout << "\n\nSuma liczb " << a << " i " << b << " wynosi " << (a+b);
}
Notatki:
69
6.5
Funkcje - zwracanie wartości
Jak już widzieliśmy na przykładzie funkcji bibliotecznej sqrt(), funkcja może
zwracać wartość. Funkcję, która zwraca wartość, należy tak zadeklarować,
by:
1.
typ_zwracanej_wartości
był różny od void. Z poznanych dotych-
czas typów, może to być dowolny typ liczbowy, znakowy oraz struk-
tury (UWAGA, przy pewnych typach struktur może dojść do
”błędów” - ale o tym w wykładzie o wskaźnikach). UWAGA -
typ_zwracanej_wartości
NIE może być tablicą (choć może być struk-
turą, której polami są tablice).
2. W ciele funkcji musi znaleźć się instrukcja
return (wartość);
lub
return (wyrażenie);
lub
return wartość;
lub
return wyrażenie;
gdzie
wartość
czy też
wyrażenie
są typu
typ_zwracanej_wartości
.
Deklaracja i wywołanie funkcji zwracającej wartość może wyglądać następu-
jąco:
70
int dodaj(int a, int b) //Funkcja dodająca dwie liczby
// UWAGA NIE można napisać int dodaj(int a, b)
{
int c;
c = a + b;
return (c); // można było też prościej return a + b;
}
//Tę funkcję możemy wywołać np.:
w = dodaj(3, 7);
//Zmienna w ma teraz wartość 10
//lub tak
c = 5;
e = 7;
w = dodaj(c, e); //Pod w mamy teraz 12
71
/* Program pr6_9.cpp
Pisanie własnych funkcji.
Funkcja zwracająca wartość. */
#include<iostream.h>
int dodaj( int, int);
//Deklaracja funkcji dodaj().
main()
{
int v, wynik;
v = 5;
wynik = dodaj(v, 4); //Za wynik podstawi się to,
//co zwróci funkcja dodaj().
cout << "\n\nSuma liczb " << v << " i 4 wynosi " << wynik;
}
//Definiujemy funkcje dodaj
int dodaj( int a, int b)
//Typ funkcji int mówi, że funkcja
//zwraca wartość typu int.
{
int c;
c = a + b;
return (c);
//Tu funkcja zwraca wartość
}
Notatki:
72
7
Wykład 7
7.1
Tablice znakowe
Ten rodzaj tablic nie różni się od innych tablic liczbowych. W końcu zmien-
ne typu
char
przechowują kody ASCII znaków. Przyjęto jednak pewne kon-
wencje dotyczące tego rodzaju tablic. Jeśli tablica przechowuje tekst, to po
tekście w tablicy znajduje się znak o kodzie 0 ( ’\0’). Znak ten mówi róż-
nym funkcjom działającym na tekście o jego długości. W pewnych sytuacjach
wstawienie znaku ’\0’ dzieje się automatycznie - szczególnie tam gdzie wia-
domo, że z tekstem (a nie ze zbiorem niezależnych od siebie kodów) mamy
do czynienia. Jeśli np wykonamy
char tab[20];
cin >> tab;
i użytkownik poda z klawiatury tekst, np. ala, to w tablicy tab będą się
znajdowały następujące wartości:
tab[0] - ’a’
tab[1] - ’l’
tab[2] - ’a’
tab[3] - ’\0’
To, co jest w komórkach 4 -19, jest (zazwyczaj) nieistotne,
a wiemy o tym dlatego, że w komórce 3 jest wartość ’\0’.
W poniższym programie można przy pomocy debugger’a podejrzeć zawartość
odpowiedniej tablicy. Uwaga, jeśli wczytujemy coś z klawiatury przy pomocy
cin >> tab;
jako koniec tekstu uważany jest dowolny wprowadzony biały
znak lub <Enter>.
73
/* Program pr7_1.cpp
Tablice znakowe.
*/
#include<iostream.h>
main() {
char tab[20];
cout << "Podaj swoje imię -> ";
//Ponizej wczytamy tekst do tablicy. Te pola
//w tab, które nie zostaną zapisane, będą miały stare wartości.
cin >> tab;
//Przy wypisywaniu tablicy tab - nawet jeśli imię było
//krótkie nie zostaną wypisane śmieci z tablicy
cout << "Witaj " << tab;
}
Notatki:
Na tablicach znakowych można działać tak, jak na zwykłych tablicach. Mamy
również kilka funkcji przeznaczonych szczególnie do tego typu tablic. Znaj-
dują się one w bibliotece
string
, więc korzystając z nich należy na początku
programu umieścić linijkę
#include <string.h>
Przykładowo fragment programu mógłby wyglądać następująco:
74
#include <string.h>
.
.
char tab1[10], tab2[10], tab3[5];
cin >> tab1;
//Tu właśnie kopiuje string zawarty w tablicy tab1 do tablicy tab2
//przy pomocy funkcji strcpy.
strcpy( tab2, tab1);
strcpy( tab3, tab1); //UWAGA, tu może zdarzyć się coś niemiłego:
//w tab3 może być za mało miejsca.
//Program, gdyby w tab1 był za długi tekst,
//będzie pisał poza tablicą, NIE ostrzeże nas.
strncpy( tab3, tab1, 5) //Działa jak strcpy, ale kopiuje nie więcej
//niż liczba znaków podana przez
//ostatni parametr, tu 5. Nie będziemy więc
//mieli poprzedniego problemu.
tab3[4] = ’\0’; //Na wszelki wypadek, bo gdyby tekst w tab1 był za
//długi, strncpy mogłoby nie dopisać końcowego znaku ’\0’
strcat(tab1, tab2); //Po tym co, było w tab1, zapisujemy zawartość
//tab2. Zapisujemy od pozycji, w której był
//’\0’ w tab1, kasując go.
Aby poznać inne funkcje działające na ciągach znakowych, zobacz opis
string.h w pomocy twego kompilatora.
75
/* Program pr7_2.cpp
Operacje na tablicach znakowych.
*/
#include<iostream.h>
#include<string.h>
main()
{
char tab1[10], tab2[10], tab3[5];
cout << "Podaj Twoje imię";
cin >> tab1;
strcpy(tab2, tab1);
cout << "Twoje imię to " << tab2 << ’\n’;
// strcpy(tab3, tab1);
//Tu mógł być błąd.
strncpy(tab3, tab1, 5);
//Na wszelki wypadek robimy to tak:
tab3[4] = ’\0’;
cout << "Cztery początkowe litery imienia to " << tab3 << ’\n’;
//Można też tak postępować z tablicą.
cout << "Pierwsza litera Twojego imienia to " << tab1[0] << ’\n’;
}
Notatki:
7.2
Inicjalizacja zmiennych
Dotychczas wartości zmiennym nadawaliśmy wewnątrz programu, przy po-
mocy operacji przypisania. Można to jednak robić już przy definicji zmiennej.
76
//Definiujemy tu zmienną liczba typu int nadając jej od razu wartość 1
int liczba = 1;
//Poniżej nadając wartość tablicy nie musimy podawać wartości
//wszystkich komórek. Tu pola wartości pól 0 1 2 są odpowiednio
//równe 1 2 3, a pozostałe pola są ustawiane na 0.
int tablica[30] = {1, 2 ,3};
//A teraz nadajemy wartość zmiennej typu opis_szczegolowy (struktura)
opis_szczegolowy osoba = {{"Jan","Kowalski","10-07-1974",750,’m’}, 9};
//A teraz jak inicjalizować tablice znakowe
char tab[10] = {"ala"};
// do tablicy wstawiliśmy cztery znaki UWAGA
//wartość tab[3] została ustawiona na ’\0’
char tablica[10] = "ala"; //Można skrótowo i tak
//Można też tak
chat tab[10] = {’a’, ’l’, ’a’};
//Tu tylko przez "przypadek" tab[3]
//jest ustawione na ’\0’
Jeżeli przy inicjalizacji zmiennych nie podamy wszystkich wartości a tylko
początkowe, to pozostałe zostaną wyzerowane - tak jak pokazano powyżej
dla tablic.
77
/* Program pr7_3.cpp
Nadawanie wartości początkowych zmiennym. */
#include<iostream.h>
typedef struct{
char imie[30], nazwisko[30], data_urodz[11];
int zarobki;
char plec;
}opis_osoby;
typedef struct{
opis_osoby dane_podstawowe;
int numer_buta;
}opis_szczegolowy;
main() {
opis_osoby osoba = {"Jan","Kowalski", "01-02-1987",342,’m’};
opis_osoby nieznany = {""}; //Wszystkie pola wyzerowane.
opis_szczegolowy pan_henio =
{{"Henryk", "Mały", "02-03-1876", 25,’m’}, 25};
int licznik = 9;
float pi = 3.14;
char tab[10] = "ala";
char ttt[10] = {’a’, ’l’, ’a’};
cout << "\nTeraz możesz tych wartości używać.";
cout << "\nPan Henio ma na imię " << pan_henio.dane_podstawowe.imie;
cout << "\n\na z kolei w tablicy tab jest napis " << tab;
}
Notatki:
Można jeszcze tak definiować i inicjalizować tablice.
char tab[] = "ala";
//Nie, nie ma tu błędu, tak zdefiniowaliśmy tablicę
//znakową o czterech polach. W tab[3] jest ’\0’
char ttt[] = {’a’, ’l’, ’a’} //A tu tylko tablica 3-elementowa.
//Z taką tablicą nie możemy postępować
//jak z napisem (stringiem)
78
/* Program pr7_4.cpp
Definiowanie i inicjalizowanie tablic. */
#include<iostream.h>
main()
{
char tab[] = "ala";
char ttt[] = {’a’, ’l’, ’a’};
cout << "W tab jest napis " << tab;
cout << "\n\nW ttt jest napis " << ttt;
}
Notatki:
7.3
Funkcje - przekazywanie argumentów przez war-
tość
Weźmy naszą funkcję
dodaj()
z poprzedniego rozdziału.
int dodaj(int a, int b) //Funkcja dodająca dwie liczby
{
int c;
c = a + b;
return (c); // można było też prościej return a + b;
}
Nazwy a i b, które występują w definicji funkcji, nazywamy argumenta-
mi formalnymi funkcji. Gdy zaś wywołujemy funkcje
dodaj(3, 7)
czy też
dodaj(c, e)
, wartości występujące w nawiasach nazywamy argumentami wy-
wołania funkcji.
Wewnątrz funkcji argumenty formalne są traktowane jako normalne zmienne.
Moglibyśmy tak napisać funkcje
dodaj()
:
int dodaj(int a, int b) //Funkcja dodająca dwie liczby
{
a = a + b; //a jest zmienną typu int, możemy za nią coś podstawić
return (a);
}
Co się dzieje, gdy wywołujemy funkcję
dodaj(3,7);
79
Czy nie ma tu podstawienia czegoś za stałą? Przecież 3 odpowiada zmiennej
a
, a za nią coś podstawiamy w funkcji
dodaj()
? Nie, po wywołaniu funkcji
tworzone są dwie zmienne
a
i
b
, a ich wartość jest inicjalizowana odpowiednio
na 3 i na 7.
A co przy wywołaniu
c = 5;
e = 7;
dodaj(c, e);
Jaka jest teraz wartość zmiennej c? Jest ona równa 5. I tu program postę-
puje w identyczny sposób jak poprzednio. Tworzy dwie nowe zmienne
a
i
b
,
inicjalizuje je odpowiednio na takie wartości, jakie miały zmienne c i e i dalej
operacje przeprowadza na zmiennych a i b. Tak więc nie zmienia się wartość
zmiennej
c
.
UWAGA tak nie jest zawsze. Wyjątek stanowią tablice (o tym, że tak na-
prawdę nie jest to żaden wyjątek, dowiemy się na wykładzie o wskaźnikach).
Jeśli argumentem formalnym funkcji jest tablica, to argument aktualny może
być zmieniany.
void wstaw(char tab[20]) //Można było też napisać void wstaw(char tab[]).
//W wypadku tablic "wielowymiarowych" dotyczy
//to jedynie pierwszego wymiaru
{
tab[0] = ’A’;
}
// Po wywołaniu
char ttt[] = "ala";
wstaw(ttt);
// w ttt mamy Ala a nie ala
80
/* Program pr7_5.cpp
Przekazywanie argumentów przez wartość,
parametry formalne.
*/
#include<iostream.h>
int dodaj( int, int);
main()
{
int v = 5, u = 3;
cout << "\n\nSuma liczb 6 i 43
wynosi "
<< dodaj( 6, 43);
cout << "\n\nSuma liczb " << v << " i " << u << " wynosi "
<< dodaj(v, u);
cout << "\n\nZmienna v ma po wywołaniu dodaj(v, u) wartość " << v;
}
int dodaj( int a, int b)
{
a = a + b;
return
a;
}
Notatki:
81
/* Program pr7_6.cpp
Tablica jako parametr funkcji.
*/
#include<iostream.h>
void wstaw(char tab[20])
{
tab[0] = ’A’; //Tablica będąca argumentem wywołania
//zostanie w tym miejscu zmieniona.
}
main()
{
char ttt[] = "ala";
cout << "\n\nPrzed wywołaniem wstaw mam w ttt " << ttt;
wstaw(ttt);
cout << "\n\nPo wywołaniu wstaw mam w ttt " << ttt;
}
Notatki:
7.4
Funkcje - przesyłanie argumentów przez referencje
Rozdział ten prezentuje rozszerzenie języka C występujące dopiero w C++.
Czy z poprzedniego rozdziału wynika, że funkcja nie może (poza tablicami)
zmieniać wartości zmiennych występujących poza ciałem tej funkcji? Tak źle
nie jest. Jeden ze sposobów robienia tego poznamy w rozdziale o wskaźnikach.
Drugi poznamy już teraz, i jest to sposób banalnie prosty.
Jeśli chcemy, aby funkcja nie tworzyła kopii argumentu aktualnego, lecz dzia-
łała wprost na nim, wystarczy w definicji (i też deklaracji) funkcji przed
nazwą odpowiedniego argumentu formalnego postawić znaczek &.
82
int dodaj(int &a, int b) //Przy wywoływaniu tej funkcji nie będzie
//tworzona nowa zmienna int a, lecz funkcja działa
//na zmiennej, z którą wywołujemy funkcję.
//Dla int b będzie tworzona nowa zmienna.
{
a = a + b; //W tej sytuacji podstawiamy za zmienną,
//z którą wywoływaliśmy funkcję dodaj
return (a);
}
c = 5;
e = 7;
w = dodaj(c, e); // pod w mamy teraz 12
// Zmienna c ma teraz wartość 12
//Można też tak jak poniżej - co może wydawać się dziwne,
//w tym przypadku w ciele funkcji utworzona będzie zmienna
w = dodaj(3, 7);
83
/* Program pr7_7.cpp
Przekazywanie wartości do funkcji przez referencje
*/
#include<iostream.h>
int dodaj( int &, int);
main()
{
int v = 5, u = 3;
cout << "\n\nZmienna v ma przed wywołaniem dodaj(v, u) wartość " << v;
cout << "\n\nSuma liczb " << v << " i " << u << " wynosi ";
cout
<< dodaj(v, u);
//UWAGA, sprawdź co się stanie, jeśli w poprzedniej linii nie użyjemy
//powtórnie cout ??? i DLACZEGO ???
cout << "\n\nZmienna v ma po wywołaniu dodaj(v, u) wartość " << v;
//Próba bez zmiennych.
cout << "\n\nSuma liczb 6 i 43
wynosi " << dodaj( 6, 43);
}
int dodaj( int &a, int b)
{
a = a + b;
return
a;
}
Notatki:
7.5
Funkcje -argumenty domniemane
Opisywane tu rozszerzenie definicji funkcji występuje również dopiero w
C++.
Wyobraźmy sobie sytuację, że w naszym programie często obliczamy pole
kwadratu i prostokąta. Moglibyśmy do tego celu napisać dwie różne funk-
cje. Chcielibyśmy jednak mieć tylko jedną funkcje o nazwie
pole
, która, gdy
dostanie jeden parametr, wie, że ma liczyć pole kwadratu, a gdy dwa, pole
prostokąta. Człowiek w takiej sytuacji łatwo by się domyślił - a komputer -
dlaczego nie.
Najpierw jednak napiszmy funkcję o 2 parametrach liczącą powyższe pola,
wg. znanych nam zasad. W dodatku (to pewne udziwnienie, lecz się przyda),
jeśli liczymy pole kwadratu jako pierwszy parametr, podajemy długość jego
84
boku a drugi ustawiamy na 0. Przy prostokącie podajemy jako parametr
długości jego boków.
//Funkcja licząca pole kwadratu, jeśli szerokosc == 0
//jeśli szerokosc != 0, liczymy pole prostokąta
int pole(int dlugosc, int szerokosc)
{
if(szerokosc == 0)
return (dlugosc * dlugosc);
else
return (dlugosc * szerokosc);
}
Teraz zmienimy tę funkcję tak, by spełniała nasze powyższe wymagania.
Chcemy, aby komputer postępował następująco: jeśli nie zostanie podany
drugi parametr, przyjmuje, że jest on ustawiony na 0.
//Funkcja licząca pole kwadratu jeśli szerokosc == 0
//jeśli szerokosc != 0 liczymy pole prostokąta
int pole(int dlugosc, int szerokosc = 0)
{
if(szerokosc == 0)
return (dlugosc * dlugosc);
else
return (dlugosc * szerokosc);
}
Teraz wywołanie:
pole(3);
jest rozumiane jako
pole(3 ,0);
Możemy też wywoływać
pole(3, 0);
pole(34, 12);
85
/* Program pr7_8.cpp
Parametry domyślne w funkcji
*/
#include<iostream.h>
//Funkcja licząca pole kwadratu jeśli szerokosc == 0
//jeśli szerokosc != 0, liczymy pole prostokąta.
int pole(int dlugosc, int szerokosc = 0)
{
if(szerokosc == 0)
return (dlugosc * dlugosc);
else
return (dlugosc * szerokosc);
}
main()
{
cout << "\nPole kwadratu o boku 5 wynosi "
<< pole(5);
//UWAGA, tylko jeden argument.
cout << "\nPole prostokąta o wymiarach 5 x 4 wynosi "
<< pole(5, 4);
//Można też tak.
cout << "\nPole kwadratu o boku 6 wynosi "
<< pole(6, 0);
//UWAGA, dwa argumenty.
}
Notatki:
7.6
Wywoływanie funkcji przez samą siebie - rekuren-
cja
W języku C przy definiowaniu funkcji możemy użyć jej samej. Takie wywoła-
nie nazywamy wywołaniem rekurencyjnym. Typowym przykładem może być
definicja funkcji obliczającej silnię.
Ogólnie obliczanie silni dla argumentu
n
możemy opisać następująco:
n! = (n - 1)! * n
czyli najpierw oblicz silnię dla
n - 1
a następnie wynik pomnóż przez
n
.
Należy pamiętać o jednej, ważnej rzeczy. Tak opisana procedura nigdy się
nie skończy. Należy dodać do niej tzw. warunek stopu. Nasza funkcja może
więc wyglądać następująco:
86
long silnia(unsigned int n)
{
if(n <= 1) return 1; //Tu między innymi dbam, aby funkcja się kiedyś
//zatrzymała, dla n = 1 lub 0
else
return n * silnia(n - 1); //W funkcji silnia wywołuje samą siebie
}
A tak działa to w programie:
/* Program pr7_9.cpp
Funkcja która wywołuje samą siebie.
Przykład klasyczny - liczenie silni.
*/
#include<iostream.h>
long silnia(unsigned int n);
main()
{
unsigned int n;
cout << "\nObliczę Ci silnię, tylko powiedz, dla jakiej wartości -> ";
cin >> n;
cout << "\nLiczę...\n";
cout << "\nA oto i wynik (o ile nie zaszalałeś z argumentem) "
<< n << "! = " << silnia(n);
}
long silnia(unsigned int n)
{
if(n <= 1) return 1;
//Tu między innymi dbam, aby funkcja się kiedyś
//zatrzymała, dla n = 1 lub 0.
else
return n * silnia(n - 1); //W funkcji silnia() wywołuję ją samą.
}
Notatki:
7.7
Operator sizeof
Operator zwracający wielkość (w bajtach) zmiennej lub typu. Można go uży-
wać, gdy np. nie wiemy, jakiego rozmiaru są podstawowe dane na danym
komputerze (w różnych systemach mogą być różne), ile komputer na zmien-
ną rezerwuje miejsca. Możemy też określić, ile miejsca zajmuje obiekt typu
87
definiowanego przez użytkownika (ważne, gdy ten obiekt będzie zapisywany
na lub czytany z dysku). Postać tego operatora:
sizeof(nazwa_typu)
sizeof (nazwa obiektu)
Do typów definiowanych przez programistę możemy użyć tego operatora na-
stępująco:
typedef struct {
char imie[30], nazwisko[30], data_urodz[11];
int zarobki;
char plec;
}opis_osoby;
//UWAGA teraz nazwa typu występuje na końcu opisu
opis_osoby kierownik;
cout << "Wielkość typu opis_osoby" << "sizeof(opis_osoby);
cout << "Rozmiar zmiennej kierownik"
<< sizeof(kierownik);
A teraz zobaczmy, jak to działa w praktyce.
88
/* Program pr7_10.cpp
Zastosowanie operatora sizeof. */
#include<iostream.h>
typedef struct{
char imie[30], nazwisko[30], data_urodz[11];
int zarobki;
char plec;
}opis_osoby; //Teraz tu definiujemy nazwę typu.
typedef struct{
opis_osoby dane_podstawowe;
int numer_buta;
}opis_szczegolowy;
main() {
opis_osoby osoba = {"Jan","Kowalski", "01-02-1987",342,’m’};
opis_szczegolowy nieznany = {""};
char tab[] = {"ala"};
char ttt[] = {’a’, ’l’, ’a’};
cout << "Wielkość typu opis_szczegolowy = " << sizeof(opis_szczegolowy);
cout << "\n\nWielkość zmiennej nieznany typu opis_szczegolowy = "
<< sizeof(nieznany);
cout << "\n\nWielkość zmiennej osoba typu opis_osoby = "
<< sizeof(osoba);
cout << "\n\nWielkość tablicy tab = " << sizeof(tab);
cout << "\n\nWielkość tablicy ttt = " << sizeof(ttt);
cout << "\n\nWielkość typu long = " << sizeof(long);
cout << "\n\nWielkość typu float = " << sizeof(float);
cout << "\n\nWielkość typu double = " << sizeof(double);
}
Notatki:
89
8
Wykład 8
8.1
Funkcje - różne funkcje o tych samych nazwach
W języku C++ (w C - nie) mogą wystąpić w tym samym zakresie ważności
różne funkcje mające te same nazwy. Taką sytuację nazywamy przeładowa-
niem funkcji.
Aby możliwe było zdefiniowanie dwu (lub więcej) różnych funkcji o tych
samych nazwach, muszą być one jakoś rozróżnialne. Tym, czym one się różnią,
są różne typy argumentów wywołania tych funkcji.
void dzwiek(int ile_razy);
//Ta funkcja powtarza ustalony dźwięk
//ile_razy razy
void dzwiek(float czestotliwosc, int dlugosc);
//Powyższa zaś funkcja gra dźwięk o zadanej
//częstotliwości przez okres dlugosc
//A teraz wywołanie tych funkcji
dzwiek(5);
//Tak jak i człowiek kompilator łatwo zauważy,
dzwiek(23.34, 45); //z którą funkcją ma do czynienia
90
/* Program pr8_1.cpp
Funkcje o tych samych nazwach.
*/
#include<iostream.h>
void napisz(int a)
//Funkcja wypisuje wartość zmiennej a.
{
cout << "\nFunkcję wywołano z parametrem a = " << a;
}
void napisz(char tab[])
//Funkcja wypisuje zawartość tablicy tab.
{
cout << "\nW tablicy tab mam -> " << tab;
}
main()
{
char tablica[] = "Ala ma kota";
//Poniżej wywołanie dwóch różnych funkcji o tych samych
//nazwach. O tym, która będzie wywołana, decyduje typ parametru.
napisz(123);
napisz(tablica);
}
Notatki:
8.2
Zakres ważności obiektów
Nazwy deklarowane w programie nie zawsze są widoczne (nie zawsze mo-
gą być używane) w całym programie. Zasada widoczności nazw jest prosta.
Nazwa jest widoczna od miejsca definicji w bloku, w którym została zde-
finiowana. Jest również widoczna w blokach wewnątrz powyższego bloku.
Przypomnijmy, że blok to część programu zawarta w nawiasach klamrowych.
Jako blok uznajemy też cały program. Najczęściej spotykanym blokiem jest
funkcja. Tak więc np. w dwóch różnych funkcjach możemy używać zmien-
nych o tych samych nazwach. Zmienne te prócz nazwy nie mają ze sobą nic
wspólnego.
91
void fun1()
{
int liczba;
char znak;
.
.
}
int fun2(int a)
{
int liczba;
.
//Nie możemy tu użyć zmiennej znak zdefiniowanej,
//powyżej jest ona niewidoczna (nie istnieje tu)
.
}
92
/* Program pr8_2.cpp
Zakres zmienności zmiennych.*/
#include<iostream.h>
void fun1() {
int liczba;
int cyfra = 67;
liczba = 5;
}
int fun2(int a) {
int liczba; //Ta zmienna nie ma nic wspólnego ze zmienną
//liczba zdefiniowaną w fun1().
liczba = a;
a = 5;
cout << "\n\nNa koniec fun2() a = " << a << ’\n’;
return 0;
}
void main() {
int cyfra = 2;
//
liczba = 8;
//Tu byłby błąd, bo w tym miejscu
//nie jest znana zmienna liczba.
cout << "\nNa początku cyfra = " << cyfra;
//Zobaczmy, czy zmienna cyfra zmienia się w funkcjach.
fun1();
cout << "\nPo fun1() cyfra = " << cyfra;
fun2(cyfra);
cout << "\nPo fun2() cyfra = " << cyfra;
}
Notatki:
8.3
Obiekty globalne
Jak wynika z poprzedniego rozdziału, zmienna zadeklarowana na zewnątrz
bloku, ale przed tym blokiem jest w nim widoczna. Dotyczy to oczywi-
ście zmiennych zdefiniowanych poza wszystkimi funkcjami (też poza funkcją
main
), będą one widoczne we wszystkich funkcjach programu znajdujących
się poniżej definicji tych zmiennych.
93
int kod;
.
.
int fun()
{
.
.
zmienna = kod;
// tu już mamy dostęp do zmiennej kod,
kod = 23;
//możemy ją również zmieniać
}
Zmienne globalne mogą być alternatywą dla używania argumentów funkcji.
Nie należy jednak tego nadużywać, gdyż program staje się nieczytelny.
/* Program pr8_3.cpp
Zmienne globalne.
*/
#include<iostream.h>
int globalna; //Zmienna widoczna we wszystkich
//poniższych funkcjach.
void fun1();
void main()
{
int cyfra = 2;
globalna = 3;
cout << "\nNa początku globalna = " << globalna;
fun1();
cout << "\nPo fun1() globalna = " << globalna;
}
void fun1()
{
int liczba;
globalna = 8;
liczba = 5;
//
cyfra = 4;
//Tak nie można, cyfra nie jest tu znana.
}
Notatki:
A co, jeśli przy istnieniu zmiennej globalnej zdefiniujemy w funkcji zmienną o
94
takiej nazwie jak ta zmienna globalna? Wtedy po wejściu do funkcji powstaje
nowa zmienna, przysłaniająca zmienną globalną. Dotyczy to nie tylko funkcji
ale również dowolnych bloków programu. Zmienna zdefiniowana wewnątrz
bloku przysłania zmienne zdefiniowane na zewnątrz tego bloku.
int kod;
//Zmienna globalna
.
.
int fun()
{
int kod;
//Przysłaniamy zmienną globalną.
//Tworzy się zmienna lokalna w funkcji.
kod = 23; //To podstawienia zmienia zmienną lokalną.
}
95
/* Program pr8_4.cpp
Przysłanianie zmiennych. */
#include<iostream.h>
void fun1();
int kod;
//Zmienna widoczna we wszystkich poniższych
//funkcjach, o ile nie jest przysłonięta.
void main() {
float liczba = 5.5;
kod = 3;
cout << "\nNa początku kod = " << kod;
fun1();
cout << "\nPo fun1() kod = " << kod;
{ //Otwórzmy sztuczny blok. To mógłby np. być blok pętli.
float liczba; //Przysłaniamy zmienną liczba.
liczba = 8.8;
cout << "\nW sztucznym bloku liczba = " << liczba;
}
cout << "\nPo sztucznym bloku liczba = " << liczba;
}
void fun1() {
int kod;
kod = 12;
cout << "\nNa końcu fun1() kod = " << kod;
}
Notatki:
8.4
Zmienne automatyczne i statyczne
Zmienne, które zdefiniowaliśmy w bloku jakiejś funkcji, przestają istnieć po
zakończeniu działania tej funkcji. Przy powtórnym wejściu do funkcji ich war-
tości mogą być dowolne. Te zmienne nazywamy zmiennymi automatycznymi.
96
void fun()
{
int zmienna; //przy każdym wejściu do tej funkcji
//zmienna
ma nieokreśloną wartość.
.
.
zmienna = 5; //wartość zmiennej zmienna przy ponownym wejściu do
//funkcji fun() nie zależy od tego podstawienia
return;
}
/* Program pr8_5.cpp
Zmienne automatyczne. */
#include<iostream.h>
void licznik()
{
int ilosc = 1; //Definicja zmiennej automatycznej.
cout << "\nWywołanie funkcji licznik nr - " << ilosc;
ilosc++;
//Czy przy każdym ponownym wejściu do funkcji licznik
//wartość zmiennej ilosc będzie większa o 1 ???
}
main()
{
int i;
for(i = 1; i <= 10; i++)
licznik();
}
Notatki:
Istnieje możliwość, aby zmienne definiowane wewnątrz funkcji nie znikały po
zakończeniu działania tej funkcji. To zmienne statyczne. Ich definicje poprze-
dzamy słowem
static
.
97
void licznik()
{
static ilosc = 1; //Definicja zmiennej statycznej
//UWAGA wartość 1 przypisywana jest zmiennej
//tylko raz, przy tworzeniu zmiennej
// static int ilosc = 1; //Powyzsze jest temu równoważne
cout << "Wywołanie funkcji licznik nr - " << ilosc;
ilosc++;
//Przy każdym ponownym wejściu do funkcji licznik
//wartość zmiennej ilosc będzie większa o 1
}
/* Program pr8_6.cpp
Zmienne statyczne. */
#include<iostream.h>
void licznik()
{
static int ilosc = 1; //definicja zmiennej statycznej
cout << "\nWywołanie funkcji licznik nr - " << ilosc;
ilosc++;
//Czy przy każdym ponownym wejściu do funkcji licznik
//wartość zmiennej ilosc będzie większa o 1 ???
}
main()
{
int i;
for(i = 1; i <= 10; i++)
licznik();
}
Notatki:
8.5
Programy składające się z kilku plików
Pisane dotychczas przez nas programy były niewielkie. Nie trudno wyobrazić
sobie jednak programy, które będą się składać z kilkudziesięciu tysięcy linii.
Jeśli taki program zapiszemy w jednym pliku, będzie on nieczytelny, trudno
będzie nam znaleźć miejsca, gdzie wystąpił błąd itd. Aby temu zapobiec,
możemy program podzielić na logiczne części i zapisywać je w oddzielnych
plikach, a następnie powiedzieć kompilatorowi i linkerowi, by uważał je za
98
jeden program. Jak to się robi, zależy od konkretnego kompilatora i trzeba
to przeczytać w dokumentacji. W kompilatorze firmy Borland możemy np.
tworzyć tak zwane projekty, w których między innymi zapisujemy informacje
na temat kompilowanych i linkowanych plików. Tu dowiemy się, jak to robić
od strony programistycznej.
Jak już wspomniano na poprzednim wykładzie, aby konkretną funkcję uży-
wać, musi być ona znana w momencie jej wywoływania. Aby to zrobić, można
ją zdefiniować przed wszystkimi wywołaniami. Można ją także wcześniej za-
deklarować, a dopiero gdzieś dalej w pliku zdefiniować.
void fun(); //Tu deklaracja funkcji fun()
main()
{
.
.
fun();
//Tu wywołujemy funkcję fun()
}
void fun() //Dopiero tu definiujemy funkcję fun()
{
//Ciało funkcji fun()
}
A co zrobić, gdy program składa się z kilku plików i chcielibyśmy, aby jakaś
funkcja zdefiniowana w jednym z plików była widoczna i mogła być używana
w innym pliku?
Jeśli w jakimś pliku
plik_1
chcemy używać funkcji zdefiniowanej w pliku
plik_2
, to przed wywołaniem funkcji musi się w pliku
plik_1
pojawić jej
deklaracja. Kompilator będzie wiedział, z funkcją jakiego typu ma do czy-
nienia, a linker będzie się martwił, aby w miejscu wywołania wstawić dobrą
funkcję.
99
=================================================
//plik_1
int fun(); //Tu deklaracja funkcji fun()
.
.
.
a = fun(); //Wywołanie funkcji fun()
=================================================
//plik_2
int fun()
//Dopiero tu definiujemy funkcję fun()
{
//ciało funkcji fun()
}
=================================================
/* Program pr8_7_1.cpp
Program składający się z kilku plików.
Plik nr 1.
*/
#include<iostream.h>
int fun(int); //Tu deklaracja funkcji fun().
//Jej definicja jest w innym pliku.
//Kompilatorowi wystarczy deklaracja.
void main()
{
int a;
a = fun(5); //Wywołanie funkcji fun().
cout << "\nFunkcja fun() zwróciła wartość " << a;
}
Notatki:
100
/* Program pr8_7_2.cpp
Program składający się z kilku plików.
Plik nr 2.
*/
//W tym pliku definiujemy funkcję fun().
//Nie jest ona nigdzie w tym pliku używana.
int fun( int a)
{
return (2 * a +3);
}
Notatki:
Oczywiście, w prawdziwym programie, będziemy mieli kilka plików, w każ-
dym po kilkadziesiąt funkcji. Wpisywanie deklaracji funkcji do każdego pliku
powodowałoby pewne zamieszanie, program stawałby się nieczytelny. Aby,
w tej sytuacji, ułatwić sobie życie tworzymy tak zwane pliki nagłówkowe.
Te pliki poznaliśmy już przy okazji wywoływania funkcji z dodatkowych bi-
bliotek (np. string.h). Mają one zwyczajowo rozszerzenie .h. W takim pliku
piszemy jedynie deklaracje funkcji, a następnie tam gdzie te deklaracje są
potrzebne piszemy linię:
#include "nazwa_pliku.h"
zamiast wpisywania po kolei wszystkich deklaracji.
Powyższą linijkę należy rozumieć - w to miejsce wstaw plik
nazwa_pliku.h
.
W przykładach z poprzednich wykładów nazwa wstawianego pliku była pi-
sana w nawiasach
<>
. Tu zaś użyliśmy zwykłych cudzysłowów. Zasada jest
następująca:
Jeśli wstawiamy plik nagłówkowy dostarczony przez twórcę kompilatora, pi-
szemy
<>
, jeśli ten plik napisaliśmy sami, używamy
""
, pisząc w nich ścieżkę
dostępu do pliku, bezwzględną lub względem aktualnej kartoteki.
101
=================================================
//plik_1
#include "plik_2.h"
//W plik_2.h znajduje się deklaracja
.
//funkcji fun()
.
a = fun(); // Tu wywołanie funkcji fun()
=================================================
//plik_2
int fun()
//Tu definiujemy funkcję fun
{
//ciało funkcji fun()
}
=================================================
//plik_2.h
int fun(); //Tu deklaracja funkcji fun()
=================================================
102
/* Program pr8_8_1.cpp
Program składający się z kilku plików.
Zastosowanie plików nagłówkowych.
Plik nr 1 - wykorzystujący niezdefiniowane w nim funkcje.
*/
#include<iostream.h>
#include "pr8_8_2.h"
//Tutaj wstawiam plik z deklaracją funkcji
//fun1() i fun2().
void main()
{
int a;
float b;
a = fun1(5); //Wywołanie funkcji fun1().
cout << "\nFunkcja fun1() zwróciła wartość " << a;
b = fun2(3); //Wywołanie funkcji fun2().
cout << "\nFunkcja fun2() zwróciła wartość " << b;
}
Notatki:
/* Program pr8_8_2.cpp
Program składający się z kilku plików.
Zastosowanie plików nagłówkowych.
Plik nr 2 - definicja funkcji wykorzystanych
w pliku pr8_8_1.cpp
*/
int fun1( int a)
{
return (2 * a +3);
}
float fun2( float a)
{
return ( (a / 4) + 24 );
}
Notatki:
103
/* Program pr8_8_2.h
Program składający się z kilku plików.
Zastosowanie plików nagłówkowych.
Plik nagłówkowy z deklaracją funkcji
z pliku pr8_8_2.cpp
*/
int fun1( int );
float fun2( float );
Notatki:
8.6
Modyfikatory const, register, volatile
Jeśli używamy w programie pewnej wartości stałej, to zamiast za każdym
razem ją wprost wpisywać, możemy zdefiniować zmienną, która będzie miała
stałą wartość. Za zmienną tą nie możemy nic podstawić, jej wartość będzie
zawsze taka sama. Używamy w tym celu modyfikatora
const
.
const float pi = 3.14;
//pi jest zmienną która już zawsze
//w programie będzie miała wartość 3.14.
//Jej wartość MUSI być zadana podczas definicji.
//Operacja
pi = 3.141; //Jest niepoprawna. Wartość zmiennej definiowanej jako
//const można ustawiać jedynie przy definicji.
Jeżeli zależy nam, aby dostęp do zmiennej był szybki, definiujemy ją z mo-
dyfikatorem
register
. Takich zmiennych nie możemy mieć zbyt wiele.
register int i; // Zmienna i będzie przechowywany w rejestrze, czyli
//w komórce, do której dostęp jest szybki.
Chcąc powiedzieć, że program ma być ostrożny z pewną zmienną definiujemy
ją z modyfikatorem volatile. Są to takie zmienne, które mogą się zmieniać
niezauważalnie dla programu, np. przez obcy program lub gdy zmienna od-
powiada stanowi urządzenia peryferyjnego.
volatile int napiecie; //Nie upraszczaj sobie sprawy w kontaktach
//z tą zmienną
104
/* Program pr8_9.cpp
Zastosowanie modyfikatorów const, register, volatile.
*/
#include<iostream.h>
main()
{
const float pi = 3.14;
register i;
volatile int napiecie;
// pi = 3.141;
//Tak nie można, wystąpi błąd. Wartość
//takiej zmiennej ustala się przy definicji.
cout << "Pi = " << pi;
for(i = 0; i < 10; i++) //Może będzie szybciej.
cout << "\n" << i;
}
Notatki:
105
9
Wykład 9
9.1
Wskaźniki - definiowanie
Wskaźniki to obiekty o tej własności, że wskazują na obiekty innego (dowol-
nego) typu. Aby definiować wskaźnik do typu TYP, piszemy:
TYP *nazwa_wskaźnika;
//O tym, że nie jest to zmienna typu TYP
//mówi nam gwiazdka
W praktyce wygląda to następująco:
int *wsk;
//wsk jest wskaźnikiem do obiektu typu int
char * znak_wsk; //Między * a nazwą może być spacja
moja_struktura *ws; //ws jest wskaźnikiem do (wcześniej zdefiniowanego)
//typu strukturalnego moja_struktura
/* Program pr9_1.cpp
Wskaźniki, przykład 1.
*/
#include<iostream.h>
#include<conio.h>
//By móc korzystać z getch().
typedef struct{
int nr_buta;
double zuzycie_na_kilometr;
}moja_struktura;
void main()
{
int *wsk; //wsk jest wskaźnikiem do obiektu typu int.
char *znak_wsk; //Dlaczego jest tak dziwnie wypisywany
//dowiemy się w następnych przykładach.
moja_struktura *ws; //ws jest wskaźnikiem do
//typu strukturalnego moja_struktura.
//Mimo, że nie wiemy, co to jest, wypiszemy te zmienne.
cout << "\nNie wiem, co to jest, ale sobie wypiszę \nws = " << ws
<< "\nznak_wsk = " << znak_wsk << "\nwsk = " << wsk;
getch(); //Taka ciekawa funkcja czekająca na przyciśnięcie klawisza.
}
Notatki:
106
Wskaźniki przechowują w sobie adres, gdzie dany obiekt się znajduje, oraz
informacje o typie wskazywanego obiektu. Uwaga, wskaźnik do obiektów
jednego typu nie nadaje się do wskazywania na obiekty innych typów.
9.2
Wskaźniki - zastosowanie
Aby używać wskaźników, musimy wpierw wiedzieć, jak otrzymać informację,
gdzie nasze obiekty się znajdują, jakie są ich adresy. Robimy to przy pomocy
symbolu
&
Adres obiektu
obiekt
to
&obiekt
Tak więc, mając wskaźnik do typu TYP oraz zmienną typu TYP, możemy
za wskaźnik podstawić adres obiektu.
int *wsk;
//wsk to wskaźnik do obiektów typu int
int zmienna;
//zmienna jest typu int
wsk = &zmienna;
//Teraz wsk wskazuje na zmienną zmienna
Nasuwa się pytanie, jak możemy działać z obiektem, mając jego adres?
Jeśli
wsk
jest wskaźnikiem do obiektu, to
*wsk
jest właśnie tym obiektem.
Można się tego było domyśleć, patrząc na sposób definicji wskaźników!
int *wsk;
int zmienna;
wsk = &zmienna;
zmienna = 3;
//Wartość zmiennej zmienna to 3
//Można teraz tak
*wsk = 5;
//Wartość zmiennej zmienna to teraz 5
107
/* Program pr9_2.cpp
Używanie wskaźników do zmiany zmiennych,
na które wskazują.
*/
#include<iostream.h>
#include<conio.h>
void main()
{
int *wsk; //wsk jest wskaźnikiem do obiektu typu int.
char *znak_wsk;
int liczba = 10;
char znak = ’c’;
wsk = &liczba;
//wsk wskazuje na liczba.
znak_wsk = &znak;
//znak_wsk wskazuje na znak.
cout << "\nznak = " << znak
<< "\na pod wskaźnikiem mamy -> " << (*znak_wsk)
<< "\nliczba = " << liczba
<< "\na wskaźnik pokazuje -> " << *wsk;
*wsk = 50;
//Zmieniamy liczba używając wskaźnika do niej.
znak = ’a’;
cout << "\nA po podstawieniu: \nznak = " << znak
<< "\na pod wskaźnikiem mamy -> " << (*znak_wsk)
<< "\nliczba = " << liczba
<< "\na wskaźnik pokazuje -> " << *wsk;
getch();
}
Notatki:
A co ze wskaźnikami do struktur?
108
typedf struct{
int liczba;
char znak;
}moja_struktura;
moja_struktura obiekt = {123, ’z’};
moja_struktura *wsk;
wsk = &obiekt;
(*wsk).liczba = 4; //Tak dostajemy się do struktury wskazywanej
//przez wskaźnik.
//A jeśli ktoś woli, można też tak (przy pomocy strzałki).
//Prawie wszyscy używają tej formy.
wsk->liczba = 44;
109
/* Program pr9_3.cpp
Używanie wskaźników do struktur. */
#include<iostream.h>
#include<conio.h>
typedef struct{
int nr_buta;
double zuzycie_na_kilometr;
char typ;
}moja_struktura;
void main()
{
moja_struktura ms = {12, 9.5, ’c’};
moja_struktura *ws;
ws = &ms;
cout << "\nnumer buta = " << (*ws).nr_buta
<< "\nzuzycie na 100 km = " << (*ws).zuzycie_na_kilometr
<< "\ntyp = " << (*ws).typ;
cout << "\n\nZmiana parametrów" ;
//Trzy sposoby zmiany pól struktury.
ms.nr_buta = 9;
(*ws).zuzycie_na_kilometr = 11.3;
ws->typ = ’d’;
cout << "\nnumer buta = " << (*ws).nr_buta
<< "\nzuzycie na 100 km = " << ws->zuzycie_na_kilometr
<< "\ntyp = " << ms.typ;
getch();
}
Notatki:
Inicjalizację wskaźników możemy też robić w następujący sposób:
moja_struktura obiekt = {123, ’z’};
moja_struktura * wsk
= &obiekt;
110
/* Program pr9_4.cpp
Inicjalizowanie wskaźników.
*/
#include<iostream.h>
#include<conio.h>
typedef struct{
int nr_buta;
double zuzycie_na_kilometr;
char typ;
}moja_struktura;
void main()
{
moja_struktura ms = {12, 9.5, ’c’}; //Musimy mieć obiekt.
moja_struktura * ws = &ms;
//Inicjalizujemy ws.
cout << "\nCo tam w obuwiu\n\n";
cout << "\nnumer buta = " << (*ws).nr_buta
<< "\nzuzycie na 100 km = " << ws->zuzycie_na_kilometr
<< "\ntyp = " << ms.typ;
getch();
}
Notatki:
Wskaźniki w czasie programu mogą wskazywać raz na jeden, raz na drugi
obiekt.
moja_struktura obiekt_pierwszy = {123, ’z’};
moja_struktura obiekt_drugi = {1, ’a’};
moja_struktura *wsk;
wsk = &obiekt_pierwszy;
//Tu wskazujemy na pierwszy obiekt
wsk = &obiekt_drugi;
//A tu już wskazujemy na drugi obiekt
111
/* Program pr9_5.cpp
Wskaźniki - wskazywanie na różne obiekty.
*/
#include<iostream.h>
#include<conio.h>
typedef struct{
int nr_buta;
double zuzycie_na_kilometr;
char typ;
}moja_struktura;
void main()
{
moja_struktura kalosz = {12, 9.5, ’c’};
moja_struktura super_but = {22, 3.5, ’x’};
moja_struktura *ws;
ws = &kalosz; //Wskazujemy na zmienną kalosz.
cout << "\nCo tam w obuwiu\n\n";
cout << "\nnumer buta = " << (*ws).nr_buta
<< "\nzuzycie na 100 km = " << ws -> zuzycie_na_kilometr
<< "\ntyp = " << ws -> typ;
ws = &super_but; //wskazujemy na super_but
cout << "\n\n\nA teraz SUPER BUT\n";
cout << "\nnumer buta = " << (*ws).nr_buta
<< "\nzuzycie na 100 km = " << ws -> zuzycie_na_kilometr
<< "\ntyp = " << ws -> typ;
getch();
}
Notatki:
9.3
Wskaźniki do typu void
Definiując wskaźnik do obiektu danego typu, oprócz adresu obiektu ze wskaź-
nikiem związana jest wiedza o tym, jakiego typu jest wskazywany obiekt. Jeśli
wskaźnik zdefiniujemy następująco:
void *wsk;
to ze wskaźnikiem
wsk
jest związany adres, ale nie mamy żadnej wiadomości
o typie obiektu, na który
wsk
wskazuje.
Podstawową własnością wskaźnika tego typu jest to, że pod taki wskaźnik
możemy podstawić wskaźnik do dowolnego typu. Tracimy wtedy informację
o typie, ale zachowujemy adres.
112
int *wsk_int;
float *wsk_float;
void *wsk;
.
.
//wsk_float = wsk_int;
//TAK NIE MOŻNA.
wsk = wsk_int;
wsk = wsk_float;
//Tak już można.
//Podstawienie odwrotne jest błędne.
//wsk_int = wsk;
//wsk_float = wsk;
//Takie podstawienia są błędne.
//Aby coś takiego dokonać, należy zastosować operator RZUTOWANIA.
wsk_int = (int *) wsk;
//Takie podstawienia są poprawne.
wsk_float = (float *) wsk;
//Mówimy kompilatorowi, by zmienił typ.
//Konwertować możemy też następująco
wsk_float = (float *) wsk_int;
//Jawna zmiana typu
113
/* Program pr9_6.cpp
Wskaźniki - podstawianie i operator rzutowania. */
#include<iostream.h>
#include<conio.h>
typedef struct{
int nr_buta;
double zuzycie_na_kilometr;
char typ;
}moja_struktura;
void main(){
moja_struktura ms = {12, 9.5, ’c’};
moja_struktura *wsk_but = &ms;
int *wsk_int, liczba = 15;
float *wsk_float, rzeczywista = 3.4;
void *wsk;
//wsk_float = wsk_int; //TAK NIE MOŻNA.
wsk_int = &liczba;
wsk_float = &rzeczywista;
wsk = wsk_int;
wsk = wsk_float; //Tak już można
// cout << *wsk; //tak nie można
cout << "\nPod wsk jako float mamy -> "<<(*((float *)wsk));
wsk = wsk_float;
cout << "\nPod wsk jako int mamy -> "<<*((int *)wsk)
<< " gdy wsk wskazuje na zmienna zmiennoprzecinkową";
// wsk_float = wsk_int; //To błąd.
wsk_float = (float *)wsk_int; //Tak można tylko po co?
wsk_int = (int *)wsk_but;
//Można też tak - ale po co?
cout << "\nPod wsk_int jako int mamy -> " << (*wsk_int)
<< " gdy wsk_int wskazuje na zmienną \nmoja_struktura ms";
getch();
}
Notatki:
Dygresja - odnośnie zmiany typów. Operator rzutowania możemy też
stosować ze wszystkimi typami podstawowymi, nie tylko ze wskaźnikami.
114
int a;
float f = 3.0;
char c;
//Za każdym razem mówimy kompilatorowi, że godzimy się na
//ew. utratę pewnych informacji (gdy np wartość f jest za duża)
a = (int) (f + 2.3);
c = (char) f;
/* Program pr9_7.cpp
Operator rzutowania dla typów podstawowych.
*/
#include<iostream.h>
#include<conio.h>
void main()
{
int liczba = 5;
float rzeczywista = 3.4;
char c;
liczba = (int) rzeczywista; //Tu kompilator wie, że my wiemy,
//co robimy.
cout << "\nliczba = " << liczba;
c = (char)rzeczywista; //Tu też kompilator mówi "Rób co chcesz".
cout << "\nc = " << c;
getch();
}
Notatki:
9.4
Wskaźniki a tablice
Definiując tablice, ich nazw używaliśmy zawsze z indeksem pewnej komórki.
Czym jest sama nazwa tablicy? Przy definicji tablicy jej nazwa jest równocze-
śnie wskaźnikiem na jej pierwszy element. Od poznanych wcześniej wskaź-
ników różni się jedynie tym, że może wskazywać tylko na jedno miejsce w
pamięci - na początek tablicy. Poza tym może być traktowana jak wskaźnik
do typu takiego jak typ elementu tablicy.
115
float *wsk_f;
float tab[20];
//Poprawne jest podstawienie
wsk_f = tab;
//takie podstawienie to to samo co
wsk_f = &tab[0];
/* Program pr9_8.cpp
Nazwa tablicy jako wskaźnik.
*/
#include<iostream.h>
#include<conio.h>
void main()
{
float *wsk_f, liczba;
float tab[20] = {1.1, 2.2, 3.3, 4.5};
//Poprawne jest podstawienie
cout << "\ntab = " << tab;
wsk_f = tab;
cout << "\nwsk_f = " << wsk_f;
//Takie podstawienie to to samo co:
wsk_f = &tab[0];
cout << "\nwsk_f = " << wsk_f;
//Poniższe podstawienie jest niepoprawne,
//tab może wskazywać tylko na jedno miejsce.
//tab = &liczba;
getch();
}
Notatki:
Jedną z ciekawszych własności wskaźników jest to, że jeśli do wskaźnika wska-
zującego na pewien element tablicy dodamy 1, to będzie on wskazywał na
następny element tablicy. Dzieje się tak, ponieważ wskaźnik oprócz adresu
niesie ze sobą wiedzę na temat typu wskazywanego elementu. Oczywiście,
gdy dodamy 1 do dowolnego wskaźnika, to będzie on wskazywał w pamięci
na miejsce oddalone od poprzedniego o wielkość wskazywanego typu.
116
float *wsk_f;
float tab[20];
.
.
wsk_f = &tab[5]
wsk_f = wsk_f + 1; //można też wsk_f++;
//Powyższa linia to to samo co
wsk_f = &tab[6]
//Jesli teraz chcemy pójść dalej o 4 elementy, to robimy
wsk_f = wsk_f + 4;
//A teraz o trzy elementy do tyłu
wsk_f = wsk_f - 3; //Teraz odejmowanie
//UWAGA (jeszcze raz), nazwa tablicy jest adresem zerowego elementu.
//Nie możemy go zmieniać. Tak więc poprawne jest
wsk_f = tab + 8;
//lecz błędne jest
//tab = tab + 4;
// BŁĄD
117
/* Program pr9_9.cpp
Dodawanie liczb do wskaźnika. */
#include<iostream.h>
#include<conio.h>
void main(){
int i;
float *wsk_f;
float tab[7] = {1.1, 2.2, 3.3, 4.5, 6.3, 4.3, 6.6};
wsk_f = &tab[5];
cout << "\nwsk_f wskazuje na " << (*wsk_f)
<< " a tab[5] = " << tab[5];
wsk_f = wsk_f + 1; //można też wsk_f++;
cout << "\nwsk_f po dodaniu 1 wskazuje na " << (*wsk_f)
<< " a tab[6] = " << tab[6];
//Poniżej wypisujemy tablicę nie używając nawiasów [].
cout << "\nWypiszmy całą tablicę \n" ;
wsk_f = tab;
for(i = 0; i < 7; i++)
{
cout << "\ntab[" << i << "] = " << *(wsk_f++);
}
cout << "\n\nWypiszmy całą tablicę od końca\n" ;
wsk_f = &tab[6];
for(i = 6; i >= 0; i--)
{
cout << "\ntab[" << i << "] = " << *(wsk_f--);
}
getch();
}
Notatki:
9.5
Operacje arytmetyczne na wskaźnikach
Jak już wspomnieliśmy, do wskaźników możemy dodawać liczby całkowite.
Wskaźniki możemy od siebie odejmować. Jeśli dwa wskaźniki wskazują na
elementy jakiejś tablicy, to ich różnicą jest różnica między indeksami ele-
mentów.
118
double *wsk1, *wsk2;
double tab[20];
int a;
.
.
wsk1 = &tab[3];
wsk2 = &tab[8];
a = wsk2 - wsk1;
// Teraz pod a mamy oczywiście 5
Dodatkowo wskaźniki można porównywać przy pomocy operatorów
== != < > <= >=
/* Program pr9_10.cpp
Odejmowanie i porównywanie wskaźników. */
#include<iostream.h>
#include<conio.h>
void main()
{
double *wsk1, *wsk2;
double tab[20];
int a;
wsk1 = &tab[3];
wsk2 = &tab[8];
a = wsk2 - wsk1;
cout << "\nRóżnica w indeksach pomiędzy wskazywanymi elementami\n"
<< "wynosi " << a;
if( wsk1 < wsk2) cout << "\nwsk1 wskazuje na coś wcześniejszego niż wsk2";
else cout << "\nwsk2 wskazuje na coś wcześniejszego niż wsk1";
if(wsk1 != wsk2) cout << "\nwsk1 i wsk2 wskazują na różne elementy";
getch();
}
Notatki:
119
10
Wykład 10
Oprócz funkcji, które możemy sami napisać, istnieją jeszcze bardzo użytecz-
ne funkcje dostarczone razem z kompilatorem. W tym wykładzie poznamy
niektóre z nich i nauczymy się, gdzie ich szukać. Przedstawione będą funkcje
dostarczone z kompilatorem Turbo C++. Niektóre z nich mogą nie wy-
stępować z innymi kompilatorami. Dotyczy to w dużej mierze kompilatorów
działających np. pod Linuxem.
To, co musimy wiedzieć, to nazwa funkcji, jakie parametry przyjmuje i w
jakiej jest bibliotece.
10.1
Wszystkie pliki nagłówkowe po kolei
Aby używać funkcji z danej biblioteki, musimy do programu przy pomocy
dyrektywy
#include
włączyć odpowiedni plik nagłówkowy.
alloc.h
assert.h
bcd.h
bios.h
complex.h
conio.h
ctype.h
dir.h
dos.h
errno.h
fcntl.h
float.h
fstream.h
generic.h
graphics.h
io.h
iomanip.h
iostream.h
limits.h
locale.h
math.h
mem.h
process.h
setjmp.h
share.h
signal.h
stdarg.h
stddef.h
stdio.h
stdiostr.h
stdlib.h
stream.h
string.h
strstrea.h
sys\stat.h
sys\timeb.h
sys\types.h
time.h
values.h
W jaki sposób dowiedzieć się, jakie w naszym systemie mamy biblioteki (i
odpowiadające im pliki nagłówkowe), zależy od systemu. Najlepiej poszukać
w dokumentacji. W środowisku Turbo C++ możemy zrobić to następująco:
1. Wchodzimy do menu Help
2. Wybieramy Contents
3. Jeśli chcemy mieć spis dostępnych funkcji, wybieramy Functions
4. Aby mieć spis wszystkich bibliotek. wybieramy Header files
W systemie pomocy Turbo C++ przyciskając kombinację klawiszy
<Alt> + F1
cofamy się o jeden ekran do tyłu.
120
Aby lepiej zrozumieć działanie poszczególnych funkcji najlepiej uruchomić
przykładowe programy znajdujące się przy opisie poszczególnych funkcji w
systemie Help.
10.2
conio.h - Funkcje dotyczą trybu tekstowego
Funkcje z tej biblioteki służą pisanie po ekranie w trybie tekstowym. Przyda
się przy pisaniu aplikacji z systemem tekstowego menu, do tekstowych ani-
macji (zadanie - napisz Tetris).
char *cgets(char *str);
- czyta string z
konsoli do zadanej długości
void clreol(void);
- czyści do końca linii w okienku tekstowym
void clrscr(void);
- czyści okienko tekstowe
int cprintf(const char *format[, argument, ...]);
- wypisuje sformato-
wane wyjście na okienko tekstowe
int cputs(const char *str);
- wypisuje tekst na ekran - korzysta z BIOSu
int cscanf(char *format[, address, ...]);
- czyta sformatowane wejście
void delline(void);
- kasuje linię w oknie tekstowym
int getch(void);
- wczytuje znak bez echa - nie potrzeba wciskać Enter
int getche(void);
- jak wyżej, ale z echem
char *getpass(const char *prompt);
- czyta hasło
int gettext(int left, int top, int right, int bottom, void *destin);
- kopiuje tekst z okienka tekstowego do pamięci
void gettextinfo(struct text_info *r);
- daje informację o trybie teksto-
wym
void gotoxy(int x, int y);
- ustawianie kursora
void highvideo(void);
- ustawia wysoką intensywność znaków
int kbhit(void);
- sprawdza czy dostępny jest jakiś znak z klawiatury
void lowvideo(void);
- ustawia niską intensywność znaków
int movetext(int left, int top, int right,
int bottom, int destleft, int desttop);
- kopiuje tekst na
ekranie z jednego miejsca w drugie
121
void normvideo(void);
- przywraca standardowe opcje tekstu
int putch(int c);
- wypisuje znak na ekran
int puttext(int left, int top, int right,
int bottom, void *source);
- kopiuje tekst z pamięci na ekran
void _setcursortype(int cur_t);
- ustawia typ kursora
void textattr(int newattr);
- ustawia atrybuty tekstu
void textbackground(int newcolor);
- ustawia kolor tła
void textcolor(int newcolor);
ustawia kolor tekstu
int ungetch(int ch);
- wkłada znak do bufora klawiatury - można używać
tylko raz, a potem musi być wczytanie znaku!!
int wherex(void);
int wherey(void);
- podają współrzędne x i y kursora
void window(int left, int top, int right, int bottom);
- definiuje ak-
tywne okno tekstowe
10.3
alloc.h - Operacje na pamięci
Wprawdzie na następnym wykładzie poznamy sposób zarządzania pamięcią
pochodzący z C++, ale funkcje tu zawarte są pomocne np. przy pisaniu
programów graficznych.
void *calloc(size_t nitems, size_t size);
- przydzielenie pamięci
void far *farcalloc(unsigned long nunits, unsigned long unitsz);
-
jak wyżej, tylko na dalekiej stercie
void *malloc(size_t size);
- przydzielenie pamięci
void far *farmalloc(unsigned long nbytes);
- przydzielenie pamięci dale-
kiej
void farfree(void far * block);
- zwolnienie pamięci (przy zajmowaniu
przy pomocy far....)
void free(void far * block);
- jak wyżej
unsigned long coreleft(void);
- sprawdza ilość dostępnej pamięci
unsigned long farcoreleft(void);
- jak wyżej
122
10.4
mem.h - Funkcje operujące na pamięci
void *memccpy(void *dest, const void *src, int c, size_t n);
- kopio-
wanie obszaru pamięci aż do wystąpienia znaku c
void *memcpy(void *dest, const void *src, size_t n);
- kopiowanie n
znaków
void *memset(void *s, int c, size_t n);
- wypełnienie obszaru pamięci
void *memmove(void *dest, const void *src, size_t n);
- kopiuje obsza-
ry pamięci - działa dobrze, nawet gdy te obszary się pokrywają
10.5
string.h - Wszelkie operacje na stringach
Tu tylko wybrane funkcje, jest ich wiele. Ta biblioteka zawiera też bibliote-
kę mem.
char *strcat(char *dest, const char *src);
dolepianie do
dest
stringu z
src
char *strchr(const char *s, int c);
- szukanie znaku
c
w stringu
s
int strcmp(const char *s1, const char *s2);
porównanie stringów
char *strcpy(char *dest, const char *src);
- kopiowanie stringów
size_t strlen(const char *s);
- długość stringu
char *strlwr(char *s);
- zamiana dużych liter na małe w stringu
char *strncat(char *dest, const char *src, size_t maxlen);
- dokleja
część jednego stringu do drugiego z ograniczeniem długości dolepianego strin-
gu
int strncmp(const char *s1, const char *s2, size_t
maxlen);
- porów-
nywanie do pewnego miejsca
char *strncpy(char *dest, const char *src, size_t maxlen);
- kopiowa-
nie nie więcej niż maxlen znaków
char *strstr(const char *s1, const char *s2);
- szuka, czy w stringu
s1
występuje podstring
s2
123
10.6
dos.h - Tylko kiedy piszemy aplikacje pod DOS
int getcbrk(void);
- daje status
<Ctrl><Break>
int setcbrk(int cbrkvalue);
- ustawia aktywność
<Ctrl><Break>
void ctrlbrk(int (*handler)(void));
- ustawia reakcję na
<Ctrl><Break>
na jakąś funkcję
void delay(unsigned milliseconds);
- zatrzymanie programu na ”parę” mi-
lisekund
void _dos_getdate(struct dosdate_t *datep);
unsigned _dos_setdate(struct dosdate_t *datep);
void getdate(struct date *datep);
void setdate(struct date *datep);
- powyższe podają ew. ustawiają datę
systemową
void gettime(struct time *timep);
void settime(struct time *timep);
jw., tylko z czasem
void sleep(unsigned seconds)
- zatrzymanie programu na ”parę” sekund
10.7
dir.h - Funkcje obsługujące pliki i katalogi
int chdir(const char *path);
- zmiana kartoteki
int findfirst(const char *pathname, struct ffblk *ffblk, int attrib);
- szukanie pliku wg. wzorca
int findnext(struct ffblk *ffblk);
- jak wyżej, tylko następny plik
int getcurdir(int drive, char *directory);
- zwraca aktualna kartotekę
int getdisk(void);
int setdisk(int drive);
- zwracają ew. ustalają numer aktualnego dysku
int mkdir(const char *path);
- twórz kartotekę
int rmdir(const char *path);
- kasuj kartotekę
char *searchpath(const char *file);
- szukaj pliku lub kartoteki
124
10.8
ctype.h - Funkcje informujące o znakach
int isalnum(int c);
- czy znak jest alfanumeryczny
int isalpha(int c);
- czy litera?
int isascii(int c);
- czy znak ASCII
int isdigit(int c);
- czy cyfra
int islower(int c);
- czy mała litera
int isupper(int c);
- czy duża litera
int tolower(int ch);
- zamienia literę na małą
int toupper(int ch);
- zmienia literę na dużą
10.9
math.h - Funkcje matematyczne
Tu
jedynie
podamy
nazwy
funkcji
z
tej
biblioteki.
Większość
powinna
być
bez
problemu
rozpoznana.
W
przypadku
wąt-
pliwości
proszę
popatrzeć
do
systemu
pomocy
Turbo
C++.
abs
acos
asin
atan
atan2
atof
cabs
ceil
cos
cosh
exp
fabs
floor
fmod
frexp
hypot
labs
ldexp
log
log10
matherr
modf
poly
pow
pow10
sin
sinh
sqrt
tan
tanh
10.10
stdlib.h - Przydatne funkcje, często używane
W tej bibliotece zebrano kilka często używanych funkcji, które też często
znajdują się w innych bibliotekach.
double atof(const char *s);
- konwer-
tuje string do liczby rzeczywistej
int atoi(const char *s);
- jw., tylko do int
long atol(const char *s);
- jw., do long
void exit(int status);
- kończy działanie programu
char *getenv(const char *name);
- zwraca wartość zmiennej środowiskowej
int putenv(const char *name);
- ustawia zmienną środowiskową
125
char *itoa(int value, char *string, int radix);
-konwertuje liczbę ty-
pu int do stringu, w zadanym systemie
char *ltoa(long value, char *string, int radix);
- jw., tylko liczba typu
long
int random(int num);
zwraca wartość losową
void randomize(void);
- inicjalizuje generator liczb losowych
int system(const char *command);
- wykonuje komendę systemową
10.11
time.h - Funkcje dotyczące czasu
asctime
clock
ctime
difftime
gmtime
localtime
stime
time
tzset
10.12
stdio.h - Standardowe wejście i wyjście
O tej bardzo ważnej bibliotece będziemy mówić na wykładzie o pli-
kach. Zawiera ona jednak nie tylko funkcje działające na plikach ale
też na standardowym wejściu i wyjściu. Poniżej jedynie te funkcje.
char *gets(char *string);
int printf(const char *format [, argument,.]);
int putchar(int c);
int puts(const char *s);
int scanf(const char *format [, ...]);
int sprintf(char *buffer, const char *format [, argument, ...]);
int sscanf(const char *buffer, const char*format [, address,...]);
10.13
graphics.h - Tryb graficzny
Do tej pory wszystkie pisane przez nas programy działały w trybie teksto-
wym. Biblioteka tu opisywana pozwoli na wprowadzenie grafiki. Programy te,
od tych w trybie tekstowym, będą się różniły pisaniem po ekranie. Takiej bi-
blioteki możemy nie spotkać w kompilatorach innych firm czy pod Linuxem.
126
Mimo to zapewne są tam inne biblioteki graficzne, a idea pisania programów
będzie podobna.
Aby móc pisać na ekranie w trybie graficznym, trzeba wpierw ten tryb za-
inicjalizować. Gdy już z niego nie będziemy korzystać, trzeba go zamknąć.
void far initgraph(int far *graphdriver, int far *graphmode,
char far *pathtodriver);
- inicjalizuje tryb graficzny.
Wygląda skomplikowanie, ale jak zobaczymy na przykładzie łatwo ją używać
void far closegraph(void);
- zamyka tryb graficzny
Między powyższymi komendami mogą znajdować się wywołania funkcji gra-
ficznych. Ważną rzeczą jest, aby razem z programem graficznym dostarczyć
odpowiedni sterownik. Sterowniki znajdują się w podkatalogu Bgi katalogu,
gdzie został zainstalowany Turbo C++. Jedynym sterownikiem, który nas
interesuje jest,
Egavga.bgi
. Jego przed uruchomieniem musimy umieścić w
katalogu z programem. Oto przykładowy program z grafiką.
127
/* Program pr10_1.cpp
Prosty program w grafice.
*/
#include <graphics.h>
#include <conio.h>
int main(void)
{
//Program sam rozpozna kartę graficzną.
int gdriver = DETECT, gmode;
//Tak inicjalizuję tryb graficzny.
initgraph(&gdriver, &gmode, "");
//Tu mogę już używać funkcji z trybu graficznego
//Więc teraz coś narysuję i to na zielono
setcolor(GREEN);
circle(50,50,50);
circle(300,50,50);
rectangle(150,50,200,150);
arc(175,50,225,318,150);
getch();
//Zamykam tryb graficzny.
closegraph();
return 0;
}
Notatki:
Poniżej dostępne funkcje z tej biblioteki. Po opis parametrów odsyłam do
pomocy w Turbo C++.
128
arc
getgraphmode
graphresult
setactivepage
bar
getimage
imagesize
setallpalette
bar3d
getlinesettings
initgraph
setaspectratio
circle
getmaxcolor
installuserdriver
setbkcolor
cleardevice
getmaxmode
installuserfont
setcolor
clearviewport
getmaxx
line
setfillpattern
closegraph
getmaxy
linerel
setfillstyle
detectgraph
getmodename
lineto
setgraphbufsize
drawpoly
getmoderange
moverel
setgraphmode
ellipse
getpalette
moveto
setlinestyle
fillellipse
getpalettesize
outtext
setpalette
fillpoly
getpixel
outtextxy
setrgbpalette
floodfill
gettextsettings
pieslice
settextjustify
getarccoords
getviewsettings
putimage
settextstyle
getaspectratio
getx
putpixel
setusercharsize
getbkcolor
gety
rectangle
setviewport
getcolor
graphdefaults
registerbgidriver
setvisualpage
getdefaultpalette
grapherrormsg
registerbgifont
setwritemode
getdrivername
graphfreemem
restorecrtmode
textheight
getfillpattern
graphgetmem
sector
textwidth
getfillsettings
129
11
Wykład 11
Na tym wykładzie zobaczymy, jak napisać prostą animację - prostą grę .
Dla uproszczenia będziemy pisać animację w trybie tekstowym dla DOS’u.
Wszystkie uwagi można zastosować do pisania animacji w grafice. Dodatkowo
można zapamiętać, że aby uniknąć efektu migotania ekranu, można używać
kilku (w tym kompilatorze dwóch) stron graficznych.
Używane pliki nagłówkowe:
conio.h
,
dos.h
,
stdlib.h
.
11.1
Animacja - co ma robić komputer
Program powinien na ekranie rysować poruszające obiekty. Naszym obiektem
będzie pojedynczy znak (w naszym wypadku będzie to
*
, ale można wybrać
dowolny znak). Potrzebujemy funkcji rysującej ten znaczek na ekranie, w
punkcie o współrzędnych podanych w parametrach wywołania funkcji.
//Funkcja rysująca na ekranie znaczek w punkcie
//o współrzędnych x,y przy pomocy znaku znak.
//Gdy za znak wstawię spację, to będę mazał obiekt.
//
void rysuj_znaczek(int x, int y, char znak)
{
gotoxy(x,y);
putch(znak);
}
Aby uzyskać efekt ruchu, będziemy rysować nowy znaczek, a zarazem ka-
sować poprzednio narysowany. To wszystko będzie wykonywane w (na razie
nieskończonej) pętli do . . . while. Do kasowania użyjemy również funkcji
rysuj_znaczek
, podając jako znak do rysowania spację. Uwaga, nie możemy
rysować znaczka zbyt szybko, bo go nie będzie widać, i będzie spadał zbyt
szybko. Popatrz do poniższego przykładu, że opóźniam nie tylko przez funk-
cje
sleep
, czy
delay
- dlaczego wyjaśni się później. Będziemy potrzebowali
dwu globalnych zmiennych:
int x_znaczka, y_znaczka;
do przechowywania aktualnej pozycji znaczka. Znaczek będzie spadał z góry
ekranu na dół zaczynając spadanie od wylosowanego miejsca na górze ekranu.
Wysokość i szerokość ekranu zapamiętamy jako stałe,
130
const int wysokosc = 24;
const int szerokosc = 48;
Aby przygotować ekran dla działania programu, potrzebne nam będą dwie
linijki:
clrscr();
//Czyszczenie ekranu
_setcursortype(_NOCURSOR);
//Wyłączenie kursora
Nasz pierwszy programik będzie wyglądał następująco:
/* Program pr11_1.cpp
Prosta animacja - nr 1
*/
#include <conio.h>
#include <dos.h>
#include <stdlib.h>
const int wysokosc = 24;
const int szerokosc = 48;
//*********************************************
//Funkcja rysująca na ekranie znaczek w punkcie
//o współrzędnych x,y przy pomocy znaku znak.
//Gdy za znak wstawię spację, to będę mazał obiekt.
//
void rysuj_znaczek(int x, int y, char znak)
{
gotoxy(x,y);
putch(znak);
}
Notatki:
131
void main()
{
int x_znaczka, y_znaczka;
int licznik = 0 ;
clrscr(); //Czyszczenie ekranu
_setcursortype(_NOCURSOR); //Wyłączenie kursora.
x_znaczka = 15; //Początkowe ustawienie współrzędnych znaczka.
y_znaczka = 1;
rysuj_znaczek(x_znaczka,y_znaczka, ’*’);
do
{
//Tu rysuje animacje
if ( (licznik++) % 10 == 0 ) //Rysuje raz na dziesięć przebiegów
//pętli.
{
rysuj_znaczek(x_znaczka,y_znaczka, ’ ’);
y_znaczka = y_znaczka % wysokosc + 1 ;
if(y_znaczka == 1)
x_znaczka = random(szerokosc) + 1;
rysuj_znaczek(x_znaczka,y_znaczka, ’*’);
}
delay(10);
}while( 1 );
_setcursortype(_NORMALCURSOR);
}
Notatki:
11.2
Skąd komputer wie, czego chce użytkownik
Drugim obiektem na ekranie będzie duży prostokąt
2x3
, złożony ze
znaczków
#
. Będzie on reagował na działanie użytkownika, będzie się on
poruszał po ekranie zgodnie z przyciskanymi klawiszami strzałek. Do tego
chcemy jeszcze dodać obsługę klawisza
<ESC>
, który będzie kończył działanie
programu.
Gdy program wykryje, że został przyciśnięty jakiś klawisz, powinien zacho-
wać się zgodnie z tym, jaki to klawisz, a następnie dalej animować spadanie
małego obiektu. Jeśli nic nie zostało wciśnięte, powinien kontynuować ani-
mację.
Do sprawdzenia, czy coś zostało przyciśnięte, wywołamy funkcję:
132
kbhit();
Jak rozpoznać, co zostało przyciśnięte? Nie byłoby problemu, gdyby były to
zwykłe klawisze. Po prostu byśmy je przeczytali. Z naszych klawiszy takim
jest tylko ESC, którego kod to
27
. Gdy wciskamy klawisze specjalne (np.
strzałki,
<F1>, <F2>
, ....), do bufora klawiatury zawsze wchodzą dwa kody -
pierwszy to
0
, a drugi to właściwy kod klawisza. Jakie to kody, będzie widać
z następnego przykładu. Jako ćwiczenie można napisać program wczytujący
klawisze, mówiący, czy są one specjalnymi i podający ich kody. Napiszemy
funkcję, która będzie nam mówić, co zostało przyciśnięte - jeśli nic nie zostało
przyciśnięte, będzie zwracała -1. Aby nie było problemów z różnicami między
przyciskami specjalnymi i zwykłymi zdefiniujmy wartości:
//Definiuję nazwy klawiszy
const int GORA = 1;
const int DOL = 2;
const int LEWO = 3;
const int PRAWO = 4;
const int ESC = 0;
Te właśnie wartości będą zwracane po przyciśnięciu odpowiednich klawiszy.
A teraz nasza funkcja:
133
//Funkcja czytająca strzałki i ESC
//
int daj_klawisz(){
int c;
int zwrot;
if (!kbhit()) return -1;
c = getch();
if (c != 0)
zwrot = (c == 27 ? ESC : -1);
else{
c = getch();
//Tu czytamy klawisz, bez wypisywania na ekranie
switch (c){
case 72:
zwrot = GORA;
break;
case 80:
zwrot = DOL;
break;
case 77:
zwrot = PRAWO;
break;
case 75:
zwrot = LEWO;
break;
default:
//To robimy gdy przyciśnięto klawisz
//który nas nie interesuje
zwrot = -1;
}
}
return zwrot;
};
Notatki:
Reakcją na strzałki będzie ruch obiektu na ekranie - potrzebujemy funkcji
rysującej taki obiekt :
134
//Funkcja rysująca na ekranie obiekt - prostokąt (np z #) w punkcie
//o współrzędnych x,y przy pomocy znaku znak.
//Gdy za znak wstawię spację, to będę mazał obiekt
//
void rysuj_obiekt(int x, int y, char znak)
{
for(int i = 0; i < 3; i++)
{
gotoxy(x + i,y);
putch(znak);
gotoxy(x + i,y + 1);
putch(znak);
}
}
Zadbamy również, by obiekt nie szedł zbyt daleko w prawo, ani w lewo. Teraz
możemy to wszystko połączyć:
/* Program pr11_2.cpp
Prosta animacja - nr 2 */
#include <conio.h>
#include <dos.h>
#include <stdlib.h>
//Definiuję nazwy klawiszy
const int GORA = 1;
const int DOL = 2;
const int LEWO = 3;
const int PRAWO = 4;
const int ESC = 0;
//Współrzędne ekranu
const int wysokosc = 24;
const int szerokosc = 48;
Notatki:
135
//********************************
//Funkcja czytająca strzałki i ESC
//
int daj_klawisz(){
int c;
int zwrot;
if (!kbhit()) return -1;
c = getch();
if (c != 0)
zwrot = (c == 27 ? ESC : -1);
else{
c = getch();
switch (c){
case 72: zwrot = GORA;
break;
case 80: zwrot = DOL;
break;
case 77: zwrot = PRAWO;
break;
case 75: zwrot = LEWO;
break;
default: zwrot = -1;
}
}
return zwrot;
};
Notatki:
//*********************************************
//Funkcja rysująca na ekranie obiekt (np prostokąt z #) w punkcie
//o współrzędnych x,y przy pomocy znaku znak.
//Gdy za znak wstawię spację, to będę mazał obiekt
//
void rysuj_obiekt(int x, int y, char znak){
for(int i = 0; i < 3; i++)
{
gotoxy(x + i,y);
putch(znak);
gotoxy(x + i,y + 1);
putch(znak);
}
}
//*********************************************
//Funkcja rysująca na ekranie znaczek w punkcie
//o współrzędnych x,y przy pomocy znaku znak.
//Gdy za znak wstawię spację, to będę mazał obiekt
//
void rysuj_znaczek(int x, int y, char znak){
gotoxy(x,y);
putch(znak);
}
Notatki:
136
void main(){
int klawisz;
int x, y; //to współrzędne obiektu
int x_znaczka, y_znaczka;
int licznik = 0 ;
clrscr();
_setcursortype(_NOCURSOR);
x = 10;
y = 10;
x_znaczka = 15;
y_znaczka = 1;
rysuj_obiekt(x, y, ’#’);
rysuj_znaczek(x_znaczka,y_znaczka, ’*’);
Notatki:
137
do{
//Tu rysuję animację
//Poniższe if, by obiekt spadał raz na 10 przebiegów pętli.
//To po to, by dać szansę użytkownikowi przycisnąć strzałkę.
if ( (licznik++) % 10 == 0 ){
rysuj_znaczek(x_znaczka,y_znaczka, ’ ’);
y_znaczka = y_znaczka % wysokosc + 1 ;
if(y_znaczka == 1)
x_znaczka = random(szerokosc) + 1;
rysuj_znaczek(x_znaczka,y_znaczka, ’*’);
}
delay(10);
//Nastepne opóźnienie. Tu program faktycznie stoi
klawisz = daj_klawisz();
if(klawisz != -1){
switch(klawisz){
case GORA:
rysuj_obiekt(x, y, ’ ’);
if (y >=2) y--;
rysuj_obiekt(x, y, ’#’);
break;
case DOL:
rysuj_obiekt(x, y, ’ ’);
if (y <= wysokosc - 4) y++;
rysuj_obiekt(x, y, ’#’);
break;
case PRAWO:
rysuj_obiekt(x, y, ’ ’);
if (x <= szerokosc - 2) x++;
rysuj_obiekt(x, y, ’#’);
break;
case LEWO:
rysuj_obiekt(x, y, ’ ’);
if (x >= 2) x--;
rysuj_obiekt(x, y, ’#’);
break;
}
}
}while(klawisz != ESC);
_setcursortype(_NORMALCURSOR);
}
Notatki:
Teraz potrzeba nam interakcji między obiektami na ekranie - jak w każdej
grze. Zrobimy to, badając współrzędne obiektów w głównej pętli.
Przy okazji będziemy liczyć punkty. Do ich wypisywania przyda się
nam prawa cześć ekranu, która była niewykorzystywana. Robi to funkcja
wypisz_punkty()
.
138
//*********************************
//Funkcja wypisująca punkty
//
void wypisz_punkty(int ilosc)
{
int x, y;
x = wherex();
//Patrzeę gdzie jest kursor,
y = wherey();
//by móc tam powrócić.
gotoxy(szerokosc + 4 ,10);
//Skaczę do miejsca wypisywania.
cprintf("Punkty: %i", ilosc);
gotoxy(x, y);
//Wracam na poprzednie miejsce.
}
Po dopisaniu tej funkcji, funkcja
main()
nowego programu może wyglądać
następująco:
139
void main(){
int klawisz;
int x, y; //to współrzędne obiektu
int x_znaczka, y_znaczka;
int licznik = 0 ;
int punkty = 0;
clrscr();
_setcursortype(_NOCURSOR);
x = 10;
y = 10;
x_znaczka = 15;
y_znaczka = 1;
wypisz_punkty(0);
rysuj_obiekt(x, y, ’#’);
rysuj_znaczek(x_znaczka,y_znaczka, ’*’);
do{
//Tu rysuję animację
//Poniższe if, by obiekt spadał raz na 10 przebiegów pętli.
//To po to, by dać szanse użytkownikowi przycisnąć strzałkę.
if ( (licznik++) % 10 == 0 ){
rysuj_znaczek(x_znaczka,y_znaczka, ’ ’);
y_znaczka = y_znaczka % wysokosc + 1 ;
if(y_znaczka == 1)
x_znaczka = random(szerokosc) + 1;
rysuj_znaczek(x_znaczka,y_znaczka, ’*’);
//Badam czy znaczek trafił w obiekt
if ((x <= x_znaczka) && (x + 3 > x_znaczka) && (y == y_znaczka) ){
sound(1500);
wypisz_punkty(++punkty);
delay(10); //Pogramy chwilkę
nosound();
}
}
Notatki:
140
delay(10);
klawisz = daj_klawisz();
if(klawisz != -1){
switch(klawisz){
case GORA:
rysuj_obiekt(x, y, ’ ’);
if (y >=2) y--;
rysuj_obiekt(x, y, ’#’);
break;
case DOL:
rysuj_obiekt(x, y, ’ ’);
if (y <= wysokosc - 4) y++;
rysuj_obiekt(x, y, ’#’);
break;
case PRAWO:
rysuj_obiekt(x, y, ’ ’);
if (x <= szerokosc - 2) x++;
rysuj_obiekt(x, y, ’#’);
break;
case LEWO:
rysuj_obiekt(x, y, ’ ’);
if (x >= 2) x--;
rysuj_obiekt(x, y, ’#’);
break;
}
}
}while(klawisz != ESC);
_setcursortype(_NORMALCURSOR);
}
Notatki:
Napisany program ma wiele niedociągnięć, ale na jego podstawie można napi-
sać prawie dowolną animację. Bez problemu można z niego zrobić program w
grafice. Jako zadanie domowe proszę wzbogacić ten programik o ”strzelanie”
po naciśnięciu spacji.
141
12
Wykład 12
12.1
Dynamiczna rezerwacja i zwracanie pamięci
Jedną z głównych dziedzin zastosowania wskaźników jest dynamiczna rezer-
wacja obszarów pamięci. Jeśli np. mamy tablicę złożoną z 50 elementów typu
strukturalnego
osoba
zawierającego dane o pracownikach, to nasz program
jest ograniczony do obsługiwania 50-ciu osób. Z kolei, jeśli za każdym razem,
gdy dopisujemy nową osobę, możemy zająć obszar pamięci do przechowy-
wania danych o tej osobie, nasz program jest ograniczony jedynie wielkością
dostępnej pamięci. A oto jak to można robić przy pomocy operatora new.
Uwaga - operator new jest specyficzny dla języka C++. W następnym
podrozdziale dowiemy się, jak to robić w czystym C.
Aby zobaczyć, jak działa operator new, popatrzmy na poniższy przykład:
struct osoba{
char imie[30], nazwisko[30];
int wiek, zarobki;
};
.
.
osoba *wsk_osoba; //wsk_osoba teraz wskazuje na jakieś nieokreślone
//miejsce pamięci. Miejsca tego NIE powinniśmy używać.
.
.
wsk_osoba = new osoba; //Rezerwujemy w pamięci tyle miejsca ile jest
//potrzebne na zmienną typu osoba. I na to
//zarezerwowane miejsce wskazuje wsk_osoba
wsk_osoba -> wiek = 43;
// Możemy tej pamięci już używać
wsk_osoba -> zarobki = 4233;
.
.
//Jeśli już nie będziemy tej pamięci potrzebować, możemy ją zwrócić do
//systemu. UWAGA, należy to robić, gdyż zasoby pamięci są ograniczone.
//Oto jak zwracamy pamięć zajętą przez new
delete wsk_osoba;
// I z głowy :)
142
UWAGA, jeśli nie ma odpowiedniej ilości pamięci aby wykonać rezerwacje,
operator
new
zwraca wskaźnik
NULL
.
UWAGA, operatorem
delete
możemy jedynie zwracać to, co zajęliśmy ope-
ratorem
new
. UWAGA, nie wolno dwa razy zwracać tej samej pamięci, MO-
ŻE DOJŚĆ DO KATASTROFY. Jedynie próba zwrócenia pamięci o ad-
resie
NULL
nie powoduje żadnych problemów. Program sam zorientuje się, że
tego nie wolno mu robić.
/* Program pr12_1.cpp
Rezerwacja pamięci przy pomocy new.
*/
#include<iostream.h>
typedef struct{
char imie[30], nazwisko[30];
int wiek, zarobki;
}osoba;
void main()
{
osoba *wsk_osoba;
wsk_osoba = new osoba;
//Rezerwacja miejsca.
//Teraz ten obszar należy do mnie. Mogę go używać.
cout << "Podaj imie -> ";
cin >> (wsk_osoba -> imie);
cout << "Podaj nazwisko -> ";
cin >> (wsk_osoba -> nazwisko);
cout << "Podaj zarobki -> ";
cin >> (wsk_osoba -> zarobki);
cout << "Osoba " << (wsk_osoba -> imie) << " "
<< (wsk_osoba -> nazwisko)
<< " zarabia " << (wsk_osoba -> zarobki) << " zł.";
delete wsk_osoba;
//Oddaję miejsce w pamięci.
}
Notatki:
12.2
Jak to się robi w C
Do rezerwacji pamięci używamy funkcji:
malloc farmalloc, calloc, farcalloc
a zwalniać ją można funkcjami:
143
free, farfree
Jeśli chcemy zarezerwować więcej pamięci, tj. powyżej 64KB, należy używać
funkcji z prefiksem
far
.
Ilość dostępnej pamięci możemy sprawdzić funkcjami:
coreleft, farcoreleft
Wyniki zwracane przez te funkcje mogą być różne dla różnych modeli kom-
pilacji.
struct osoba{
char imie[30], nazwisko[30];
int wiek, zarobki;
};
.
.
osoba *wsk_osoba;
//wsk_osoba teraz wskazuje na jakieś nieokreślone
//miejsce w pamięci. Miejsca tego NIE powinniśmy używać.
wsk_osoba = (osoba *) calloc( 1, sizeof(osoba) ); //Rezerwuję
wsk_osoba->zarobki = 2032;
.
.
free(wsk_osoba); //Zwalnaiam, gdy nie potrzebuję.
144
/* Program pr12_2.cpp
Obsługa pamięci w czystym C. */
#include<iostream.h>
#include<alloc.h>
typedef struct{
char imie[30], nazwisko[30];
int wiek, zarobki;
}osoba;
void main() {
osoba *wsk_osoba;
//Sprawdzę miejsce w pamięci.
cout << "W pamięci zostało " << coreleft() << " miejsca,\n";
cout << "a na dalekiej stercie " << farcoreleft() << "\n";
wsk_osoba = (osoba *) calloc( 1, sizeof(osoba) ); //Rezerwuję
//Teraz ten obszar należy do mnie. Mogę go używać.
cout << "Podaj imię -> ";
cin >> (wsk_osoba -> imie);
cout << "Podaj nazwisko -> ";
cin >> (wsk_osoba -> nazwisko);
cout << "Podaj zarobki -> ";
cin >> (wsk_osoba -> zarobki);
cout << "Osoba " << (wsk_osoba -> imie) << " "
<< (wsk_osoba -> nazwisko)
<< " zarabia " << (wsk_osoba -> zarobki) << " zł.";
free(wsk_osoba);
//Oddaję miejsce w pamięci
}
Notatki:
12.3
Alokacja miejsca dla tablic
Przy użyciu operatora
new
możemy łatwo rezerwować miejsca dla tablic.
Przydaje się np., gdy wolimy w dostępie do pamięci posługiwać się nota-
cją tablicową, a nie wskaźnikową. Można to też wykorzystać, aby oszukać
kompilator, który nie pozwala nam statycznie zdefiniować dostatecznie dużej
tablicy. Proszę zwrócić uwagę na specyficzne zastosowanie
delete
.
145
float *wsk_tab;
.
.
wsk_tab = new float[20];
//Zajęcie miejsca na 20-sto elementową
//tablice o elementach float.
//UWAGA, TERAZ TROCHĘ INACZEJ: zwalnianie tak zajętego miejsca.
delete [] wsk_tab;
//!!!! zwalniamy tablicę
!!!!!!
//UWAGA, NIE WSZYSTKIE KOMPILATORY POTRZEBUJĄ [].
// delete wsk_tab;
//Tak trzeba robić w kompilatorze Borland’a.
/* Program pr12_3.cpp
Alokacja miejsca dla tablic C++. */
#include<iostream.h>
#include<string.h>
typedef struct{
char imie[30], nazwisko[30];
int wiek, zarobki;
}osoba;
void main()
{
osoba *tab; //Wprawdzie tab jest wskaźnikiem do osoba,
//ale może być traktowana jak tablica.
//Teraz zajmę miejsce na tablice.
cout << "\n\nTeraz tablica\n";
tab = new osoba[100];
//Wpiszę jakies dane do tablicy
for (int i = 0;
i < 100; i++)
{
strcpy( tab[i].nazwisko, "Kowalski");
tab[i].zarobki = 2000 + i;
cout << tab[i].zarobki << ", " << tab[i].nazwisko << "\n";
};
delete tab;
//Zwalniam tablicę.
}
Notatki:
146
12.4
Listy struktur
Bardzo przydatną cechą wskaźników jest to, że mogą one występować ja-
ko składowe struktury i wskazywać na obiekt typu struktury, w której się
znajdują.
Uwaga, to co pokazano w następnym przykładzie, nie da się zrobić, gdy
poniższy typ definiujemy przy użyciu typedef. Właśnie po to były aż dwa
sposoby na definiowanie typu strukturalnego.
struct osoba{
char imie[30], nazwisko[30];
int wiek, zarobki;
osoba *nastepna;
//następna jest wskaźnikiem na następną
//osobę
};
.
.
osoba *pierwsza;
pierwsza = new osoba;
//Tu mamy miejsce na pierwsza osobę
pierwsza -> nastepna = new osoba; //Tu druga osoba, która jest
//wskazywana przez pole nastepna
//pierwszej osoby
(pierwsza->nastepna)->nastepna = NULL;
//niech pole nastepna drugiej
//osoby pokazuje na NULL
//Tak można robić, dopóki mamy wolną pamięć.
Przy pomocy takich operacji możemy zaimplementować listy wiązane, przy-
datne np. w sortowaniu danych. List takich użyjemy także tam, gdzie mamy
w programie z góry nieznaną ilość powiązanych elementów.
147
/* Program pr12_4.cpp
Lista struktur. */
#include<iostream.h>
struct osoba{
char imie[30], nazwisko[30];
int wiek, zarobki;
osoba *nastepna; //By wskazywać następny elemeny listy.
};
void main(){
osoba *wsk_osoba, *pierwsza;
wsk_osoba = pierwsza = new osoba;
//Miejsce na pierwszą osobę.
pierwsza->nastepna = NULL;
//Zajmuję miejsce na następne 30 osób. TO BĘDZIE LISTA.
for(int i = 0; i < 30; i++){
wsk_osoba->nastepna
= new osoba;
wsk_osoba = wsk_osoba->nastepna;
wsk_osoba->nastepna = NULL;
//Listę kończę NULL’em.
};
//A teraz zwolnię zajętą pamięć. Poniższy sposób jest wzorcowy
//do zwalniania list, których nie znamy. O długości mówi NULL.
wsk_osoba = pierwsza->nastepna; //By wiedzieć, gdzie zaczyna się
//lista, gdy skasujemy jej początek
while(1)
{
delete pierwsza;
if(wsk_osoba == NULL) break;
pierwsza = wsk_osoba;
wsk_osoba = pierwsza->nastepna;
}
}
Notatki:
12.5
Wskaźniki w argumentach funkcji
Jak już wcześniej powiedziano, gdy chcemy w ciele funkcji zmieniać parame-
try aktualne, możemy je do funkcji przekazywać przez referencje (dostępne
tylko w C++). Przez takie postępowanie może czasem dojść do błędów.
Gdy np. wywołujemy
fun(a)
, nie od razu wiemy, czy wartość
a
może zostać
zmieniona w funkcji
fun
, czy też nie. Aby to stwierdzić, musimy zobaczyć,
jak wygląda definicja funkcji.
Bardziej zalecanym, dostępnym już w C sposobem jest przesyłanie argumen-
tów przez adres. W takiej funkcji nie możemy zmienić przesyłanego adresu,
ale to, co przez ten adres jest wskazywane TAK. Jak można się przeko-
148
nać, przeglądając opisy większości funkcji bibliotecznych, ich parametrami
są wskaźniki.
/* Program pr12_5.cpp
Wskaźniki jako argumenty funkcji. */
#include<iostream.h>
void fun(int *wsk_int){
//Argumentem tej funkcji jest wskaźnik do
//zmiennej typu int.
cout << "W fun wsk_int wskazuje na "
<< (*wsk_int);
//Widzimy to, na co wskazuje
//wskaźnik wsk_int.
//Mogę też zmieniać komórki, na którą wskazywał wsk_int.
*wsk_int = 3;
//Sam wskaźnik wsk_int pozostał bez zmian.
}
void main(){
int a;
a = 5;
fun(&a); //Jako parametr fun() podajemy adres a, czyli &a.
//Przy takim wywołaniu możemy się domyślać,
//że wartość a ulegnie zmianie.
cout << "\nPod a mamy " << a ; //Pod a jest teraz 3.
}
Notatki:
12.6
Jeszcze raz tablice w argumentach funkcji
Teraz stało się jasne, dlaczego mogliśmy zmieniać tablice w ciele funkcji, dla
których te tablice były parametrem. Jako że nazwa tablicy to wskaźnik do
jej zerowego elementu, przesyłając tablice do funkcji nie mogliśmy jedynie
zmieniać tego wskaźnika. Z kolei, już bez problemów, mogliśmy zmieniać to,
na co ten wskaźnik wskazywał.
Skoro więc nazwa tablicy i wskaźnik do pierwszego elementu tablicy to to
samo, następujące deklaracje są równoważne:
void fun(float tab[]);
149
oraz
void fun(float *tab);
We wnętrzach obu tych funkcji możemy stosować zapisy typu:
tab[5];
czy też
*(tab + 5)
//Patrz na pierwszą definicję -
//to już znane z poprzednich wykładów.
/* Program pr12_6.cpp
Tablica jako argumenty funkcji. */
#include<iostream.h>
void fun(int *bla)
{
for(int i = 0; i < 10; i++)
{
bla[i] = i;
//Zmieniam to, co było pod
//adresem bla.
}
}
void main(void)
{
int tab[10];
fun(tab);
for(int i = 0; i < 10; i++)
{
cout << "\ntab[" << i << "] = " << tab[i];
}
}
Notatki:
12.7
Wskaźniki do obiektów typu const oraz wskaźniki
typu const
Jeśli z kolei nie chcemy, aby obiekt wskazywany przez wskaźnik był zmieniany,
możemy do niego wskazać wskaźnikiem do typu
const
.
150
const int *wsk; //Czytamy: wsk jest wskaźnikiem do typu const int
.
.
//*wsk = 5;
//Tu kompilator pokaże błąd.
//*(wsk + 2) = 3;
//Tu też błąd.
//wsk[4] = 10; //Następny błąd.
Możemy również deklarować wskaźniki, które mogą wskazywać tylko na jedno
miejsce. Są to wskaźniki typu
const
.
int tab[10] = {0}
int tablica[10] = {0}
.
.
int * const staly_wsk = tab; //Wskaźnik stały
//staly_wsk = tablica; //Tutaj błąd wsk może tylko pokazywać na tab
//i tego nie wolno zmieniać.
12.8
Wskaźniki do funkcji
Jeśli już na wszystko wskazywaliśmy, to może spróbować wskazać na funkcje.
Czy więc wskaźnik może zawierać adres do miejsca w pamięci, gdzie znajduje
się jakaś funkcja ?
Uwaga, nazwa funkcji jest adresem miejsca w pamięci, gdzie ta funkcja się
znajduje.
151
int (* wsk_fun) ();
//wsk_fun jest wskaźnikiem na funkcję, która
//ma pustą listę argumentów
//oraz zwraca wartość typu int.
float (* wsk_fun_float) (int, double);
//wsk_fun jest wskaźnikiem na funkcję, która ma dwa argumenty,
//odpowiednio typu int i double, oraz zwraca wartość typu float.
//A jak za te wskaźniki podstawić wskaźniki do funkcji?
//Wpierw potrzebujemy tych funkcji
int pierwsza();
//Deklarujemy je.
float druga(int a , double licz);
//A teraz podstawmy
wsk_fun = pierwsza;
//UWAGA NIE wsk_fun = pierwsza(); bo to by
//powodowało wywołanie funkcji pierwsza.
//Też błąd kompilacji.
wsk_fun_float = druga;
//A jak posłużyć się tymi wskaźnikami, aby wywołać te funkcje - prosto
a = ( * wsk_fun)();
//Można to czytać: Skocz do podanego adresu
//i wywołaj funkcję, która się tam znajduje.
b = ( * wsk_fun_float)(23, pi);
//ew. z parametrami.
//Na szczęście można też tak
a =
wsk_fun();
//Można to czytać: Wywołaj spod podanego
//adresu funkcję, która się tam znajduje.
b =
wsk_fun_float(23, pi);
//Tu z parametrami.
152
/* Program pr12_7.cpp
Wskaźniki do funkcji. */
#include<iostream.h>
int pierwsza(){
cout << "\nTo ja Twoja funkcja pierwsza \n" ;
return 5;
}
float druga(int a , double licz){
cout << "\nA to funkcja druga \nPrzesłałeś mi parametry -> "
<< a <<" oraz " << licz;
return 3.14;
}
void main(){
int (* wsk_fun) (); //wsk_fun jest wskaźnikiem na funkcję, z pustą
//listą argumentów, która zwraca wartość typu int.
float (* wsk_fun_float) (int, double); // wsk_fun jest wskaźnikiem na
//funkcję, która ma dwa argumenty odpowiednio typu
//int i double, oraz zwraca wartość typu float.
wsk_fun = pierwsza;
//Podstawiamy pod wskaźniki.
wsk_fun_float = druga;
cout << "\nPierwsze wywołanie\n";
wsk_fun();
wsk_fun_float(8, 4.12);
cout << "\n\n\nDrugie wywołanie\n";
(* wsk_fun)();
(* wsk_fun_float)(5, 23.42);
}
Notatki:
12.9
Tablice wskaźników do funkcji
Możemy oczywiście zdefiniować tablice wskaźników do funkcji. Potem te
funkcje wywoływać nie przez podanie nazwy funkcji, tylko podając indeks
w tablicy.
153
int fun();
//To tylko deklaracja funkcji fun
int (* (tab_fun[10]) )(); //tab_fun to 10-cio elementowa tablica
//zawierająca wskaźniki do funkcji nie mających
//argumentów i zwracających wartość typu int
.
.
tab[4] = fun;
//Pod indeks 4 w tablicy wstawiamy adres funkcji fun
.
.
//Teraz wywołamy funkcję fun
(tab[4])();
Tablice ze wskaźnikami do funkcji możemy też inicjalizować inaczej, już przy
jej definicji.
int fun_1();
//To tylko deklaracja funkcji fun_1
int fun_2();
//Deklaracja funkcji fun_2
int (* (tab_fun[10]) )() = {fun_1, fun_2};
//odpowiednio na miejscu 0 i 1 w tablicy tab_fun są adresy
//funkcji fun_1 i fun_2
154
/* Program pr12_8.cpp
Tablice funkcji. */
#include<iostream.h>
//Definiuję kilka funkcji.
void w_prawo(){
cout << "\nSkręcam w prawo --->>>\n" ;
}
void w_lewo(){
cout << "\n<<<--- Skręcam w lewo\n" ;
}
void gazu(){
cout << "\n!!!Wciskam do dechy!!!\n" ;
}
void stop(){
cout << "\n!!!Ostre hamowanie!!!\n"
<< "A nie mówiłem abyś zapiął pasy\n";
}
void main(){
//Zainicjalizowana tablica funkcji.
void (* tab[4] )() = {w_prawo, w_lewo, gazu, stop};
int wybor;
cout << "\nZapnij pasy - ruszamy\n przyciśnij klawisz gazu\n";
while(1){
cout << "\nTwój wybór\n"
<< "0 - skręć w prawo\n" << "1 - skręć w lewo\n"
<< "2 - więcej gazu\n" << "3 - S T O P\n"
<< "4 - już wysiadam\n";
cin >> wybor;
if( wybor == 4) break;
else (tab[wybor])(); //Wywołanie odpowiedniej funkcji.
}
cout << "No to cześć\n";
}
Notatki:
12.10
Wskaźnik do funkcji jako jeden z argumentów
innej funkcji
Jeśli chcemy, aby pewna funkcja wywoływała inną funkcję, i to taką, która
będzie znana dopiero w trakcie działania programu, możemy używać funk-
cji, której argumentem będzie wskaźnik do funkcji. Gdy podczas działania
programu dowiemy się, która funkcja ma być wykonana, wtedy jej adres po-
dajemy jako argument aktualny.
155
int fun1();
int fun2();
void FUNKCJA( int (*wsk_fun)()){ //w tej funkcji wywołamy fun1 lub fun2
wsk_fun(); //wywołanie tej funkcji,
}
//której adres przyszedł jako parametr
void main(){
FUNKCJA(fun1);
//Można np. tak
FUNKCJA(fun2);
//albo tak
}
156
/* Program pr12_9.cpp
Funkcja jako parametr innej funkcji. */
#include<iostream.h>
#include<conio.h>
void bar(){
cout << "\nCo podać ?\n"
<< "1
- Piwo\n"
<< "2
- Mleko\n\n";
if( getch() == ’1’) cout << "Proszę piwo\n";
else cout << "Proszę mleko\n";
}
void szatnia(){
cout << "\nSłużę \n" << "1
- Oddanie ubrania\n"
<< "2
- Odebranie ubrania\n\n";
if( getch() == ’1’) cout << "Proszę oto bloczek\n";
else cout << "Proszę oto pańskie ubranie\n";
}
//Funkcja wybor ma jako parametr adres funkcji
void wybor(char *komentarz, void (*wsk_fun)()){
cout << komentarz << ’\n’;
wsk_fun();
//Tu wywołanie funkcji będącej
}
//argumentem funkcji wybor
void main(){
cout << "Twój wybór:\n" << "1 - idziesz do baru\n"
<< "2 - idziesz do szatni\n";
cout << "Za pierwszym razem\n";
if (getch() == ’1’) wybor("Wybrany został bar", bar);
else
wybor("Wybrana została szatnia", szatnia);
cout << "Twój wybór:\n"
<< "1 - idziesz do baru\n"
<< "2 - idziesz do szatni\n";
cout << "Za drugim
razem\n";
if (getch() == ’1’) wybor("Wybrany został bar", bar);
else
wybor("Wybrana została szatnia", szatnia);
}
Notatki:
157
13
Wykład 13
Aby zapamiętywać dane nie tylko na czas działania programu, a też na na-
stępne sesje z programem, możemy dane zapisywać na dysku w postaci pli-
ków. Przez pliki można także rozumieć urządzenia, np. porty. Gdy dalej bę-
dziemy mówić o plikach i podawać ich nazwę, pamiętajmy o już przypisanych
nazwach do portów, odpowiednio:
lpt1, lpt2, com1, com2, prn
itp. Te pliki
są ze startem programu otwarte.
Aby obsługiwać pliki, użyjemy funkcji z
<stdio.h>
13.1
Otwieranie i zamykanie pliku
Aby móc pisać do pliku lub z niego czytać musimy ten plik otworzyć Otwie-
ranie pliku odbywa się przy pomocy funkcji
fopen
i ma postać:
wsk_plik = fopen("nazwa_pliku", "tryb");
Od tej chwili
wsk_plik
będzie nam reprezentować ten otwarty plik.
Parametr
"tryb"
to string, zawierający (w zależności od tego, jakiego pliku
potrzebujemy) jeden z następujących zestawów znaków:
r
- otwórz plik tylko do czytania.
w
- stwórz plik do pisania. Jeśli plik istnieje, będzie wpierw skasowany.
a
- otwarcie do dopisywania na końcu pliku. Jeśli plik nie istnieje, będzie
utworzony.
r+
- otwarcie istniejącego pliku do pisania i czytania.
w+
- tworzenie nowego pliku do czytania i pisania. Jeśli plik istnieje, będzie
wpierw skasowany.
a+
- otwarcie do dopisywania i czytania. Otwarcie (lub stworzenie, jeśli nie
istnieje) i ustawienie się na końcu pliku.
Dodatkowo do stringu trybu możemy dopisać:
t
- jeśli plik ma być otwarty w trybie tekstowym.
b
- tryb binarny.
Jeśli
ani
t
ani
b
nie zostało podane plik otwierany jest w zależności od wartości
zmiennej
_fmode
- default’owo ustawiona jest ona na
O_TEXT
, ale można ją
ustawić na
O_BINARY
.
158
Funkcja
fopen
zwraca wskaźnik do predefiniowanej struktury
FILE
, opisującej
plik.
FILE *plik;
plik = fopen("moj.txt", "wb");
//lub tak
plik = fopen("../dane/twoj.txt", "a+t"); //tak gdy lubimy Unix
plik = fopen("..\\dane\\twoj.txt", "a+t"); //tak gdy lubimy Windows
Jeśli nie udało się otworzyć pliku,
fopen
zwraca
NULL
.
Zamykanie pliku dokonujemy za pomocą funkcji
fclose
, której parametrem
jest wartość zwrócona przez
fopen
. Uwaga, zamykanie pliku jest bardzo
ważne, gdyż często operacje na plikach dokonywane są przez bufor w pamięci.
Dopiero zamknięcie pliku faktycznie aktualizuje plik.
FILE *plik;
plik = fopen("moj.txt", "wb");
fclose(plik); /Plik zamknięty
/* Program pr13_1.cpp
Otwieranie i zamykanie pliku.
*/
#include<stdio.h>
void main()
{
FILE *plik;
plik = fopen("lolo.txt", "wt"); //Otwieram plik lolo.txt do pisania.
//Jeśli nie było takiego pliku,
//zostanie utworzony.
fclose(plik);
//Teraz zamykam plik.
//W aktualnej kartotece pojawił się pliku lolo.txt
}
Notatki:
159
13.2
Podstawowe operacje na otwartych plikach
fgetc
- wczytanie kolejnego znaku z pliku
fputc
- zapisanie kolejnego znaku
na pliku
Zapiszmy coś do pliku:
FILE *plik;
plik = fopen("moj.txt", "wb");
fputc(’A’, plik);
//wpisanie do pliku moj.txt znaku A
fclose(plik);
Teraz nawet inny program może z tego czytać:
FILE *plik;
int c;
plik = fopen("moj.txt", "rb");
c = fgetc(plik); //Odczytuję znak z początku pliku
fclose(plik);
160
/* Program pr13_2.cpp
Pliki - pisanie i czytanie. fputc(), fgets().
*/
#include<stdio.h>
#include<iostream.h>
void main()
{
FILE *plik;
char c;
plik = fopen("lolo.txt", "wt"); //Otwieram plik lolo.txt do pisania.
fputc(’A’ ,plik);
fputc(’L’ ,plik);
fputc(’A’ ,plik);
fclose(plik);
//Teraz zamykam plik.
cout << "Czytam z pliku 3 znaki\n";
plik = fopen("lolo.txt", "rt"); //Otwieram plik lolo.txt do czytania.
for(int i = 0; i < 3; i++)
{
c = (char)fgetc(plik);
cout << c;
}
fclose(plik);
//Teraz zamykam plik.
}
Notatki:
Jeśli w pliku chcemy zapisywać lub wczytywać z niego większe porcje niż 1
znak, możemy użyć odpowiednio funkcji
fwrite
i
fread
, których deklaracje
wyglądają następująco:
size_t fwrite(const void *ptr, size_t size, size_t n, FILE *stream);
size_t fread(void *ptr, size_t size, size_t n, FILE *stream);
Gdzie
size
i
n
są odpowiednio wielkością i ilością jednostek, które do pliku
chcemy zapisać, lub z pliku wczytać.
Pierwszym z argumentów jest adres miejsca, skąd (lub dokąd) chcemy zapisać
(odczytać) dane na (z) pliku.
161
FILE *plik;
long tab[10];
.
.
plik = fopen("moj.txt", "wb");
fwrite((void *)tab, sizeof(long), 10, plik);
fclose(plik);
//Teraz wczytamy
plik = fopen("moj.txt", "rb");
fread((void *)tab, sizeof(long), 10, plik);
fclose(plik);
162
/* Program pr13_3.cpp
Pliki - pisanie i czytanie większymi porcjami
fwrite() i fread(). Przykład 1.
*/
#include<stdio.h>
void main()
{
FILE *plik;
long tab[10] = {1,2,3,4,5,6,7,8,9,10};
plik = fopen("liczby.baz", "wb");
//Zapisuję całą tablicę na dysk.
fwrite((void *)tab, sizeof(long), 10, plik);
fclose(plik);
//Zeruję całą tablicę.
for(int i = 0; i < 10; i++) tab[i] = 0;
//Teraz wczytamy tablicę z pliku.
plik = fopen("liczby.baz", "rb");
fread((void *)tab, sizeof(long), 10, plik);
fclose(plik);
}
Notatki:
163
/* Program pr13_4.cpp
Pliki - pisanie i czytanie większymi porcjami
fwrite() i fread(). Przykład 2.
*/
typedef struct{
char imie[30], nazwisko[30], data_urodz[11];
int zarobki;
char plec;
}opis_osoby; // Teraz tu definiujemy nazwę typu
#include<stdio.h>
#include<mem.h>
void main(){
FILE *plik;
opis_osoby osoba = {"Jan","Kowalski", "01-02-1987",342,’m’};
opis_osoby pusta = {""};
plik = fopen("osoby.baz", "wb");
//Zapisuję całą zmienną osoba na dysk.
fwrite((void *)&osoba, sizeof(opis_osoby), 1, plik);
fclose(plik);
//Zeruję zmienną osoba.
memcpy((void *)&osoba, (void *)&pusta, sizeof(opis_osoby));
//Teraz wczytamy do zmiennej osoba z pliku.
plik = fopen("osoby.baz", "rb");
fread((void *)&osoba, sizeof(opis_osoby), 1, plik);
fclose(plik);
}
Notatki:
13.3
Pisanie formatowane do pliku
int fprintf(FILE *plik, const char *format[, argument, ...]);
Powyższe wypisuje do pliku
plik
dane w formie podanej przez string
format
.
Do pliku piszemy dokładnie to, co zapisane w stringu
format
, po modyfikacji.
Do tego stringu, w miejsca zaczynajace się znakiem
%
, zostaną wstawione
odpowiednio zinterpretowane argumenty występujące po formacie. Po znaku
%
mogą występować następujące znaki z podaną interpretacją:
d
- zmienna int dziesiętnie
o
- jw., ósemkowo
164
x
- jw., szesnastkowo
ld, lo, lx
- odpowiednio jak wyżej, tylko long
c
- znak
s
- string
e
- double w postaci z wykładnikiem
f
- double
n
- wskaźnik do int
p
- jw.
Przed każdym z tych znaków może wystąpić określenie pola, w którym wpi-
sujemy argument:
-
- wyrównaj do lewej
+
- wypisz ze znakiem
liczba
- szerokość pola
.liczba
- ilość miejsc po przecinku
FILE *plik;
long l = 100;
char c = ’C’;
float f = 1.2345554;
plik = fopen("moj.txt", "wb+");
fprintf(plik, "Liczba long %ld \nliczba float %5.3f
\nstring %s",
l, f, "ala ma kota");
fclose(plik);
Zasada
formatowania
jest
dokładnie
taka
sama,
jak
dla
funkcji
printf, scanf
, odnoszące się do wypisywania na ekran i czytania z klawia-
tury, które to urządzenia są specyficznymi plikami.
165
/* Program pr13_5.cpp
Pliki - pisanie sformatowane.
*/
#include<stdio.h>
void main()
{
FILE *plik;
int l = -100;
float f = 1.2345554;
plik = fopen("bla.txt", "w+");
//Wypisuje w sposób sformatowany. Przydaje się to przy
//robieniu wydruków, czy też raportów.
fprintf(plik, "Liczba int %+20d \nliczba float %-+15.3f
\nstring %20s",
l, f, "ala ma kota\nbla");
fclose(plik);
}
Notatki:
Odpowiednikiem
fprintf
, gdy chodzi o wczytywanie sformatowanego tekstu,
jest funkcja
fscanf
.
int fscanf(FILE *plik, const char *format[, address, ...]);
Uwaga, inaczej niż w
fprintf
po stringu formatującym nie występują zmien-
ne, tylko ich adresy.
FILE *plik;
int l;
float f;
plik = fopen("moj.txt", "r");
fscanf(plik, " %d, %f", &l, &f); //wczytuję z pliku liczbę int i float,
//które są oddzielone przecinkiem
fclose(plik);
166
/* Program pr13_6.cpp
Pliki - czytanie sformatowane.
Aby ten program działał potrzeba pliku moj.txt
napisanego w dowolny edytorze tekstowym o następującej
pierwszej linijce:
123,343.23,ala ma kota
*/
#include<stdio.h>
#include<conio.h>
void main()
{
FILE *plik;
float f;
long l;
char tab[30];
plik = fopen("moj.txt", "rt");
fscanf(plik, "%ld,%f,%s", &l, &f, tab);
fclose(plik);
//Wypisywanie na ekran przy pomocy printf().
printf("Z pliku wczytałem:\nlong -%ld\nfloat - %5.2f\nstring - %s\n",
l, f, tab);
getch();
}
Notatki:
Formatowane zapisywanie do plików jest przydatne przy tworzeniu wydru-
ków, raportów i plików, które mają być czytane przez ludzi. Czytamy pliki
w sposób sformatowany zazwyczaj, jeśli zostały stworzone przez człowieka w
jakimś tekstowym edytorze. Proszę zauważyć, że liczby, które były zapisywa-
ne w sposób niesformatowany, są nieczytelne przy oglądaniu pliku w edytorze
tekstowym.
Uwaga, przy wczytywaniu stringów spacja jest traktowana jako jego koniec.
Jeśli chcemy wczytać z pliku string ze spacjami, zastosujemy funkcję
fgets
,
która wczytuje linię, albo zadaną ilość znaków. Gdy w linii jest mniej znaków
wczytuje całą linię.
char *fgets(char *s, int n, FILE *stream);
Linia z pliku wczytywana jest do (zazwyczaj tablicy)
s
. Jest wczytywane nie
więcej niż
n
znaków. Uwaga, wczytany string jest kończony znakiem
’\0’
.
Jeśli wczytaliśmy całą linię, to przedostatnim znakiem jest w trybie teksto-
wym,
’\n’
. W trybie binarnym jest to
’\n’
w Unixie i
"\r\n"
w Windows -
167
tak tam jest reprezentowany koniec linii.
/* Program pr13_7.cpp
Pliki - wczytywanie linii.
*/
#include<stdio.h>
#include<iostream.h>
#include<conio.h>
void main()
{
FILE *plik;
char tab[130];
plik = fopen("moj.txt", "r");
//Wczytuję linię z pliku lecz nie więcej niż 50 znaków.
fgets(tab, 50, plik);
fclose(plik);
cout << "Z pliku moj.txt wczytałem linię:\n" << tab;
getch();
}
Notatki:
13.4
Poruszanie się po pliku
Ustawienie wskaźnika na określonym miejscu pliku odbywa się przy pomocy
funkcji
fseek()
.
int fseek(FILE *plik, long offset, int skad);
gdzie
skad
może przybierać wartość:
SEEK_SET
0
początek pliku
SEEK_CUR
1
pozycja aktualna
SEEK_END
2
koniec pliku
Aby dowiedzieć się, gdzie jesteśmy zastosujmy funkcję
ftell()
.
long int ftell(FILE *plik);
Zwraca ona pozycję na pliku, licząc od początku.
Aby dowiedzieć się, czy nie jesteśmy poza końcem pliku, mamy funkcję
feof()
.
168
int feof(FILE *plik);
Zwraca wartość różną od zera (prawdę), gdy próbowaliśmy czytać (UWA-
GA, MUSIELIŚMY PRÓBOWAĆ CZYTAĆ) po końcu pliku.
/* Program pr13_8.cpp
Pliki - poruszanie się po pliku. */
#include<stdio.h>
#include<iostream.h>
#include<conio.h>
void main(){
FILE *plik;
long miejsce;
char c;
char tab[130];
plik = fopen("moj.txt", "rb");
miejsce = ftell(plik);
cout << "\nJestem na miejscu " << miejsce;
c = fgetc(plik);
miejsce = ftell(plik);
cout << "\nA teraz jestem na miejscu " << miejsce;
cout << "\nSkaczę na koniec";
fseek(plik , 0, SEEK_END);
miejsce = ftell(plik);
cout << "\nA teraz jestem na miejscu " << miejsce;
fseek(plik , -6, SEEK_END);
miejsce = ftell(plik);
cout << "\nA teraz jestem na miejscu " << miejsce;
fseek(plik , 0, SEEK_SET);
cout << "\n\nWypiszę zawartość całego pliku:\n";
while(1){
c = fgetc(plik);
if( feof(plik) ) break;
cout << c;
}
fclose(plik);
cout << "\n\nNo to koniec - na dzisiaj :)\n";
getch();
}
Notatki:
169
13.5
Kasowanie plików
Dowiedzieliśmy się jak tworzyć pliki i z nimi pracować. Czasem zachodzi
jednak potrzeba wykasowania pliku z dysku. Aby to zrobić, możemy użyć
funkcji:
int unlink(const char *nazwa_pliku);
Oczywiście, jeśli kasujemy plik, nie może być on w tym czasie otwarty.
13.6
Biblioteka io
W Turbo C++ istnieje jeszcze inna biblioteka obsługująca pliki. Jej plik
nagłówkowy to
io.h
. Istnieją tam prawie wszystkie funkcje odpowiadające
tym z biblioteki
stdio
. Obydwie biblioteki można znaleźć pod Unixem.
Biblioteka
io
nie występuje w standardzie ANSI C. Aby dowiedzieć się
więcej o tej bibliotece, proszę przejrzeć system pomocy.
170
14
Wykład 14
14.1
Parametry funkcji main
Dotychczas funkcja
main()
była deklarowana jako
void main();
czyli jako funkcja bezargumentowa. Tak naprawdę jest to funkcja dwuargu-
mentowa. Argumenty te będziemy nazywać odpowiednio
argc
- jest on typu
int
, i
argv
typu
char *[]
. Parametr
argc
zawiera liczbę ciągów znakowych,
które zostały przekazane z linii komend do programu (jako pierwszy z ar-
gumentów uznajemy nazwę programu). Parametr
argv
to tablica złożona z
poszczególnych parametrów.
Dodatkowo, powinniśmy deklarować funkcję
main()
tak, by zwracała wartość
typu
int
. Przy poprawnym zakończeniu programu, powinna ona zwracać
wartość
0
. Wartość tę otrzymuje system i może stwierdzić jak zakończyło się
działanie programu.
/* Program pr14_1.cpp
Parametry funkcji main()
*/
#include <stdio.h>
#include <iostream.h>
int main(int argc, char *argv[])
{
cout << "Program został wywołany z następującymi parametrami:\n";
for (int i =0 ; i < argc; i++)
{
cout << "Argument " << i << " = " << (argv[i]) << "\n" ;
}
return 0;
}
Notatki:
14.2
Funkcje ze zmienną ilością argumentów
Do tej pory tworzyliśmy funkcje, które miały z góry zadaną liczbę argumen-
tów. Wyjątkiem były jedynie funkcje, które mogły mieć argumenty domyślne,
ale i wtedy znana była ich ilość. Można się zapytać, czy tak zawsze trzeba -
171
odpowiedź brzmi nie. Możemy tworzyć funkcje, które przyjmują różne ilości
argumentów.
Czy takie funkcje będą użyteczne. Wyobraźmy sobie funkcję, która np. mnoży
swoje argumenty lub znajduje ich największy wspólny podzielnik - niewąt-
pliwie mogą się przydać. Robi się to w sposób następujący:
Funkcje tego typu charakteryzują się tym, że na miejscu ostatniego parame-
tru znajduje się symbol
...
(trzy kropki). Przed symbolem
...
musi znaj-
dować się przynajmniej jeden ”nazwany parametr”. Nie wolno wywoływać
tego typu funkcji z mniejszą liczbą argumentów niż liczba argumentów na-
zwanych. Aby ”dostać się” do argumentów oznaczonych
...
używamy trzech
makr:
void va_start(va_list ap, lastfix);
type va_arg(va_list ap, type);
void va_end(va_list ap);
których nagłówki znajdują się w
stdarg.h
.
Przypuśćmy, że mamy funkcję
void fun(int a, ...);
Aby z nich korzystać, potrzebna nam będzie zmienna typu
va_list
- repre-
zentująca listę argumentów, np.
va_list ap;
Teraz inicjalizujemy przeglądanie argumentów przez
va_start(ap, a);
Tutaj
a
jest nazwą ostatniego nazwanego parametru.
Następnie, wywołując kolejno makro
va_arg
, otrzymujemy kolejne, nienazwa-
ne argumenty. Drugim argumentem wywołania
va_arg
jest typ oczekiwanego
argumentu, np. jeśli kolejny argument jest typu
float
, wywołujemy:
float liczba;
liczba = va_arg( ap, float);
Po zakończeniu przeglądania listy argumentów powinniśmy wywołać:
va_end(ap);
Uwaga, niestety nie wszystko jest tak piękne. Wewnątrz funkcji
nie potrafimy dowiedzieć się, z iloma parametrami została wywołana funk-
cja, ani jaki był ich typ. Ta informacja powinna przyjść w jakiś sposób z
argumentami.
Teraz przykład funkcji dodająca pewną ilość liczb całkowitych.
172
/* Program pr14_2.cpp
Funkcje ze zmienną ilością parametrów.
Przykład 1.
*/
#include <stdio.h>
#include <iostream.h>
#include <stdarg.h>
//Funkcja dodaje liczby aż nie napotka na argument 0.
//W ten sposób mówię, z iloma argumentami wywołano funkcję.
int dodaj(int a, ...){
int suma; //Zmienna do zapamiętywania sumy (częściowej).
int arg;
//Tu będę zapisywał poszczególne argumenty.
suma = a;
va_list ap;
va_start(ap, a);
//Wiem jakiego typu są argumenty, więc jako drugi
//parametr w va_arg podam int.
while ((arg = va_arg(ap, int)) != 0)
{
suma += arg;
}
return suma;
}
int main(){
int wynik;
wynik = dodaj(1,2,3,4,5,6,7,8,0);
cout << "\n Wynik pierwszego dodawania -> " << wynik;
return 0;
}
Notatki:
173
/* Program pr14_3.cpp
Funkcje ze zmienną ilością parametrów.
Przykład 2.
*/
#include <stdio.h>
#include <iostream.h>
#include <stdarg.h>
//Funkcja dodaje tyle liczb, jaką wartość ma pierwszy parametr
int dodaj_inne(int a, ...){
int suma; //Zmienna przechowująca sumę częściową.
int arg;
//Tu będę zapisywał poszczególne argumenty.
suma = 0;
va_list ap;
va_start(ap, a);
//Wiem, że wszystkie argumenty aktualne mojej
//funkcjii są typu int.
while (a > 0 ){
arg = va_arg(ap,int);
suma += arg;
a--;
}
return suma;
}
int main(){
int wynik;
wynik = dodaj_inne(8,12,12,13,43,52,62,17,8);
cout << "\n Wynik drugiego dodawania -> " << wynik;
return 0;
}
Notatki:
14.3
Kilka standardowych funkcji o zmiennej ilości ar-
gumentów
W trakcie tego wykładu już kilkakrotnie spotkaliśmy się z funkcjami o zmien-
nej ilości argumentów. Prawie wszystkie funkcje działające na otwartych pli-
kach są takie. Popatrzmy np. na funkcję
printf
- też działającą na pliku,
dość specyficznym, czyli ekranie.
int printf(const char *format[, argument, ...]);
Najistotniejszy jest tu dla nas parametr
format
. Spróbujmy sobie wyobrazić,
jak wewnątrz może wyglądać ta funkcja. Pierwszy parametr wywołania tej
174
funkcji to string. Aby dowiedzieć się ile jest pozostałych argumentów, wystar-
czy ten parametr przeczytać znak po znaku i policzyć ilość wystąpień znaku
%
. To, jakiego typu są poszczególne dodatkowe argumenty, również nie jest
trudne do sprawdzenia. Jeśli znaleźliśmy już znaczek
%
, to ostatni znak po
nim, a przed spacją mówi nam o typie - jeśli to jest np.
f
to jest to zmienna
typu
float
.
Innymi poznanymi funkcjami tego typu są np:
fprintf, scanf, fscanf
i
wiele innych.
Istotnym przykładem funkcji ze zmienną ilością parametrów z biblioteki
stdio
są funkcje:
int sprintf(char *bufor, const char *format [, argument, ...]);
int sscanf(const char *bufor, const char*format [, adres,...]);
Zasada ich działania jest analogiczna do funkcji
fprintf
i
fscanf
, z tą różnicą,
że piszemy/czytamy nie do/z pliku lecz do/z bufora w pamięci (zazwyczaj
tablicy znakowej).
175
/* Program pr14_4.cpp
Formatowane pisanie/ czytanie z/do
bufora pamięci.
*/
#include <stdio.h>
#include <iostream.h>
int main()
{
char bufor[256];
int wiek, wzrost;
float nr_buta;
cout << "\nPodaj wiek(int), numer buta(float)"
<<" i wzrost(int) oddzielone spacjami:\n";
//Wczytam linię wejściową do bufora.
gets(bufor);
//Przy interpretacji spacje z bufora są ignorowane.
//Uwaga, stringi są interpretowane zachłannie
//- do wystąpienia dowolnego białego znaku.
sscanf(bufor, "%d%f%d", &wiek, &nr_buta, &wzrost);
cout << "\nMasz " << wiek << " lat"
<< "\nNosisz buty nr " << nr_buta
<< "\nTwój wzrost to " << wzrost << "\n\n";
//Teraz ze zmiennych utworzę napis w tablicy bufor.
sprintf(bufor, "Wiek %d, But %f, Wzrost %d\n",
wiek, nr_buta, wzrost);
cout << bufor;
return 0;
}
Notatki:
14.4
Operatory bitowe
Operatory bitowe, działają wprost na zapisie binarnym informacji. Operują
na argumentach typu całkowitego.
Przesunięcie w lewo:
zmienna << ilość_miejsc
Operacja ta powoduje przesunięcie w lewo zapisu binarnego zmiennej
zmienna
o
ilość_miejsc
miejsc.
int liczba, wynik;
liczba = 14402;
wynik = liczba << 3;
176
Zapis w systemie binarnym wartości zmiennych
liczba
i
wynik
są następu-
jące:
liczba
0100 0000 0001 0010
wynik
0000 0000 1001 0000
Przesunięcie w prawo:
zmienna >> ilość_miejsc
Operacja ta powoduje przesunięcie w prawo zapisu binarnego zmiennej
zmienna
o
ilość_miejsc
miejsc.
Uwaga, jeśli zmienna jest typu unsigned lub ma wartość nieujemną, to bra-
kujące bity z lewej strony są uzupełniane zerami.
Uwaga, jeśli zmienna zawiera liczbę ujemną, to brakujące bity mogą być
uzupełniane zerami lub jedynkami. TO ZALEŻY OD TYPU KOMPUTERA.
int liczba, wynik;
liczba = 14402;
wynik = liczba >> 3;
Zapis w systemie binarnym wartości zmiennych
liczba
i
wynik
są następu-
jące:
liczba
0100 0000 0001 0010
wynik
0000 1000 0000 0010
Bitowe operatory :
|
- suma bitowa
&
- iloczyn bitowy
~
- negacja bitowa
^
- różnica symetryczna
int l, p, a, b, c, d;
l = 3855;
p = 4080;
a = l | p;
b = l & p;
c = ~l;
d = l ^ p;
177
Poszczególne wartości wyglądają bitowo następująco:
l
0000 1111 0000 1111
p
0000 1111 1111 0000
a
0000 1111 1111 1111
b
0000 1111 0000 0000
c
1111 0000 1111 0000
d
0000 0000 1111 1111
Uwaga, zwróć uwagę na różnice pomiędzy operatorami:
&&
i
&
||
i
|
Mamy również zmodyfikowane operatory przypisania:
<<=
>>=
&=
|=
^=
~=
#include<iostream.h>
#include<math.h>
#include<conio.h>
int main(){
unsigned int liczba, pom;
cout << "Podaj liczbę naturalna: ";
cin >> liczba;
cout << "\nPoszczególne bity w pamięci komputera to:\n";
for(int i = 0; i < 16; i++ )
{
cout << "Bit nr " << i << " jest równy ";
//Obliczę i-ty bit przez bitową koniunkcję z potęgą dwójki.
cout << ( ( (int)pow(2,i) & liczba) == 0 ? 0 : 1) << "\n";
}
cout << "Po ustawieniu 4 pierwszych bitów na 1 liczba ma wartość: ";
pom = liczba | 0xF000; //Bitowa koniunkcja z 1111000000000000.
//Przydał się zapis szesnastkowy.
cout << pom;
getch();
}
Notatki:
178
15
Wykład 15
15.1
Typ enum
Czasami dane występujące w programie łatwiej wyrazić jest nie przy pomocy
liczb, a ich własnych nazw. Takim przykładem są np. dni tygodnia. Czasem
byłoby logiczniej napisać pętlę:
for(dzien = pon; dzien <= czw; dzien++) {...}
Okazuje się, że tak można. Trzeba tylko wpierw zdefiniować typ wyliczeniowy:
enum tydzien {niedz, pon, wt, sr, czw, pt, sob};
//Wlaściwie słowo tydzien nie jest tu istotne i można tak
enum {niedz, pon, wt, sr, czw, pt, sob};
Definicja ta tworzy stałe o nazwach w nawiasie, o wartościach kolejno
0,1,..6
. Możemy wymusić jednak inne wartości dla zmiennych, pisząc np.
enum tydzien {niedz, pon = 8, wt, sr, czw, pt = 24, sob};
Jakie wartości otrzymują stałe - widać. Te, które nie zostały wprost usta-
wione, otrzymują wartości o jeden większe od występujących bezpośrednio
przed nimi. W tej definicji
niedz
otrzymuje wartość
0
;
/* Program pr15_1.cpp
Typ enum.
*/
#include <iostream.h>
int main()
{
enum {niedz, pon, wt, sr, czw, pt, sob};
int dzien;
for(dzien = pon; dzien <= czw; dzien++)
cout << "\nDzien nr " << dzien;
return 0;
}
Notatki:
15.2
Unie
Unia to typ danych pomyślany po to, byśmy w jednym miejscu pamięci mogli
179
zapisywać wartości rożnych typów. Kompilator sam zadba, by te wartości
tam się zmieściły. Tak naprawdę, unie to struktury, których wszystkie pola
są w tym samym miejscu pamięci.
union
int_lub_long {
int
i;
long
l;
} liczba;
//Dostęp do poszczególnych pól tak jak w strukturach
liczba.l = 213876;
liczba.i = 24
/* Program pr15_2.cpp
Unie.
*/
#include <iostream.h>
union
long_inny {
long
l;
//Następne pole to podzielenie pola l na 2 części.
//Najpierw bity młodsze bo tak w pamięci jest zapisywany typ long
struct{
int mlodszy, starszy;
}i2;
};
int main()
{
long_inny liczba;
liczba.l = 2138736;
cout << "liczba.l = 2138736;";
cout << "\nPole l = " << liczba.l
<< "\nPole i2.starszy = " << liczba.i2.starszy
<< "\nPole i2.mlodszy = " << liczba.i2.mlodszy;
cout << "\n\n\nliczba.i2.starszy = 24;";
liczba.i2.starszy = 24;
cout << "\nPole l = " << liczba.l
<< "\nPole i2.starszy = " << liczba.i2.starszy
<< "\nPole i2.mlodszy = " << liczba.i2.mlodszy;
return 0;
}
Notatki:
180
15.3
Zmienne typu extern
Mówiliśmy już o zmiennych globalnych, które są widoczne w całym pliku
programu. Jeśli program składa się z kilku plików, a chcielibyśmy, by jakaś
zmienna globalna z jednego pliku była widoczna w innym, postępujemy na-
stępująco. W jednym z plików następuje definicja zmiennej. W plikach, w
których ta zmienna ma być widoczna, występuje jej deklaracja poprzedzona
słowem
extern
.
/*---------------------
Plik nr 1
*/
//Definicje zmiennych
int liczba = 1;
float tablica[20];
/*---------------------
Plik nr 2
*/
//Tu tylko deklaracje. Nie możemy nadawać wartości przy deklaracji
extern int liczba;
//Przy tablicach nie trzeba podawać rozmiaru
extern float tablica[];
/*---------------------*/
15.4
Operator przecinkowy
Zapewne nie dla wszystkich było jasne, co dzieje się w instrukcji for kiedy
inicjalizowaliśmy więcej niż jedną zmienną i poszczególne operacje oddzie-
laliśmy przecinkami. W języku C/C++ przecinek jest operatorem. Jeśli
dwa wyrażenia (zmienne) połączymy przecinkiem to wynik takiej operacji
ma wartość i typ prawego wyrażenia (zmiennej). Poszczególne wyrażenia są
obliczane od lewej do prawej. Operator ten jest lewostronnie łączny.
//Po podstawieniu
b = 12;
a = 2 * b, b - 4;
//a ma wartość 8
181
15.5
Priorytety operatorów
Jeśli w wyrażeniu znajduje się więcej niż jeden operatorów i brak jest nawia-
sów, mówiących o kolejności działań, wyrażenie to oblicza się przy zastoso-
waniu poniższych priorytetów działań. Operatory znajdujące się wyżej mają
większy priorytet. Jeśli operatory mają ten sam priorytet, stosujemy podaną
łączność. Jako że trudno jest zapamiętać te priorytety, poleca się używać od-
powiedniej ilości nawiasów, nawet nadmiarowo, aby uzyskać lepszą czytelność
zapisu.
Operator
Łączność
() [] -> .
lewostronna
! ~ + - ++ -- & * sizeof new
delete
prawostronna
* / %
lewostronna
+ -
lewostronna
<< >>
lewostronna
< <= > >=
lewostronna
== !=
lewostronna
&
lewostronna
^
lewostronna
|
lewostronna
&&
lewostronna
||
lewostronna
?:
prawostronna
= *= /= %= += -= &= ^= |= <<= >>=
prawostronna
,
lewostronna
15.6
Preprocesor
Preprocesor przegląda i przetwarza plik programu przed przekazaniem go w
ręce kompilatora. Rozkazy dla preprocesora wydajemy za pomocą dyrektyw.
Można je rozpoznać po tym, że pierwszym znakiem (nie licząc spacji i tabu-
latora) w linii zawierającej dyrektywę jest znak
#
. Znamy już dobrze jedną z
takich dyrektyw:
15.6.1
#include
#include "nazwa_pliku"
182
czy też ”prawie” równoważnie
#include<nazwa_pliku>
Oznacza ona, że chcemy, aby w miejscu jej wystąpienia został wstawiony plik
o nazwie
nazwa_pliku
.
15.6.2
#define
#define
nazwa
ciąg_znaków_który_nazwa_będzie_oznaczać
Oznacza to, że w miejscu, gdzie wystąpi w pliku
nazwa
, ma ona być zamie-
niona na
ciąg_znaków_który_nazwa_będzie_oznaczać
. Proszę pamiętać, że
dzieje się to jeszcze przed przekazaniem pliku kompilatorowi.
//Przy takim zapisie
#define rozmiar 8
int tablica[rozmiar];
//linia 1
------------------------------------------
//Do kompilatora zostanie przekazane
int tablica[8];
Preprocesor nie przejmuje się składnią C on po prostu zamienia ciągi znaków
”jak leci”.
Uwaga, jedynym miejscem, gdzie
nazwa
nie zostanie zastąpiona przez
ciąg_znaków_który_nazwa_będzie_oznaczać
, są wnętrza stringów.
Można
też tak:
#define wypisz cout << "bala bla bala"
.
.
wypisz ;
//linia 2
------------------------------------------
//linia 2 wyglądać będzie po obróbce przez preprocesor następująco:
cout << "bala bla bala";
//linia 2
#undef nazwa
Odwołuje definicję nazwy
nazwa
uczynioną przez
#define
.
183
/* Program pr15_3.cpp
Preprocesor - #define.
*/
#include <iostream.h>
#define ROZMIAR 8
#define WYPISZ cout << "\n\nbala bla bala"
int main()
{
int tablica[ROZMIAR];
//UWAGA, ROZMIAR to nie zmienna, dla
//kompilatora to dokładnie 8
cout << "ROZMIAR tablicy tablica wynosi " << ROZMIAR;
//Tylko drugie wystąpienie ROZMIAR zostanie zastąpione
//przez 8. Pierwsze nie, bo jest wewnątrz stringu.
WYPISZ; //To nie wywołanie funkcji. W to miejsce wstawione
//zostanie dokładnie to, co po #define WYPISZ.
return 0;
};
Notatki:
15.6.3
Makrodefinicje
Makrodefinicje to coś na wzór funkcji tworzonych przy pomocy preprocesora.
//Obliczanie kwadratu liczby
#define KWADRAT(a)
( (a) * (a) )
//To jedna z bardziej znanych makrodefinicji
//Obliczanie maksimum
#define MAX(a,b)
(
( (a) >= (b) ) ? (a) : (b) )
Uwaga, w pierwszym wyrazie po
#define
nie może być żadnych białych
znaków!!!
Jeśli teraz np. preprocesor napotka na linie
b = KWADRAT(3.89 +5);
c = MAX(v+5, u);
Zamieni je na
b = ( (3.89 + 5) * (3.89 + 5) );
c = (
( (v+5) >= (u) ) ? (v+5) : (u) );
184
Teraz widać, po co było nam potrzebne tak wiele nawiasów - jako parametry
mogą występować wyrażenia.
Uwaga, stosowanie takich definicji może prowadzić do błędów, np. popatrz,
co się stanie przy wywołaniu:
b = KWADRAT(a++);
Stosowanie makrodefinicji jest czasem przydatne - szczególnie tam, gdzie
chcielibyśmy otrzymać ”funkcje” działające na różnych typach danych. Przy-
kładem jest
MAX
, która dział dobrze dla wszystkich argumentów liczbowych.
Problem w stosowaniu makrodefinicji polega na tym, że przy wystąpieniu
błędów trudno jest je zlokalizować i usunąć.
/* Program pr15_4.cpp
Preprocesor - makrodefinicje.
*/
#include <iostream.h>
#define KWADRAT(a)
( (a) * (a) )
#define MAX(a,b)
(
( (a) >= (b) ) ? (a) : (b) )
int main()
{
cout << "\n5 do kwadratu = " << KWADRAT ( 5 );
cout << "\n\n5.34 do kwadratu = " << KWADRAT ( 5.34 );
//Nie musimy pisać dwu funkcji dla różnych typów.
cout << "\n\nWiększą z liczb 3 i 5.34 jest " << MAX(3, 5.34);
int a = 5;
//NIEBEZPIECZENSTWO !!!
cout << "\n\n6 do kwadratu = " << KWADRAT ( ++a );
cout << "\n\nTeraz pod zmienna a mamy
" << a;
//Otrzymane wyniki są inne niż te, których mogliśmy oczekiwać.
return 0;
}
Notatki:
15.6.4
Kompilacja warunkowa
Jeśli chcemy, by w pewnych warunkach kompilator kompilował część pliku,
możemy zastosować kompilację warunkową.
185
#if wyrażenie
//Instrukcje kompilowane, gdy wyrażenie jest prawdziwe
#endif
lub tak
#if wyrażenie
//Instrukcje kompilowane, gdy wyrażenie jest prawdziwe
#else
//Instrukcje kompilowane, gdy wyrażenie jest fałszywe
#endif
lub tak
#if wyrażenie1
//Instrukcje kompilowane, gdy wyrażenie jest prawdziwe
#elif wyrażenie2
//Instrukcje kompilowane, gdy wyrażenie1 jest fałszywe
//a wyrażęnie2 prawdziwe
#elif wyrażenie3
.
.
#else
.
.
#endif
Uwaga,
wyrażenie
,
wyrażenie1
,
wyrażenie2
muszą być wielkościami stałymi,
których wartość znana jest przed kompilacją.
186
#define WERSJA 1
#if (WERSJA == 1)
#include "stary.h"
rodzaj = 5;
#else
#include "nowy.h"
rodzaj = 9;
#endif
W warunku można też używać operatora
defined(nazwa)
, który sprawdza,
czy dana
nazwa
została już zdefiniowana.
#define WERSJA 1
#if (defined(WERSJA))
#include "stary.h"
rodzaj = 5;
#else
#include "nowy.h"
rodzaj = 9;
#endif
Uwaga, linia:
#if (defined(WERSJA))
jest równoważna linii
#ifdef WERSJA
Linia:
#if (!defined(WERSJA))
jest równoważna linii
#ifndef WERSJA
187
/* Program pr15_5.cpp
Preprocesor - kompilacja warunkowa. */
#include <iostream.h>
#define WERSJA 1
#if (WERSJA == 1)
void fun(int a){
cout << "\nZwiększam parametr o 2 ----- a = " << (a +=2) ;
}
#else
void fun(int a){
cout << "\nZwiększam parametr o 30 ----- a = " << (a +=30) ;
}
#endif
int main(){
#ifdef WERSJA
#if WERSJA == 1
cout << "Wersja 1 programu";
#else
cout << "Wersja 2 programu";
#endif
#else
cout << "BŁĄD KRYTYCZNY - NIE ZDEFINIOWANO WERSJI";
#endif
#ifdef WERSJA
fun(5);
#endif
return 0;
}
Notatki:
Po wstępnym przetworzeniu poprzedniego programu do kompilatora zostanie
przekazany tekst:
188
/* Program pr15_5_1.cpp
Preprocesor - kompilacja warunkowa.
To co zrobił preprocesor z pr15_5.cpp */
//Za poniższą linijkę zostanie wstawiony plik - brak tu
//jednak miejsca, by go wstawić.
#include <iostream.h>
void fun(int a){
cout << "\nZwiększam parametr o 2 ----- a = " << (a +=2) ;
}
int main(){
cout << "Wersja 1 programu";
fun(5);
return 0;
}
Notatki:
15.6.5
Predefiniowane nazwy
Preprocesor dostarcza nam też pewnych predefiniowanych nazw, które mo-
żemy używać w programach. Są to:
__LINE__
- numer linii
__NAME__
- nazwa kompilowanego pliku
__DATE__
- data kompilacji
__TIME__
- Czas w momencie kompilacji
/* Program pr15_6.cpp
Preprocesor - predefiniowane nazwy.
*/
#include<iostream.h>
int main()
{
cout << "\nWłaśnie skompilowałem program " << __FILE__;
cout << "\n\nTa instrukcja znajduje się w linii " << __LINE__;
cout << "\n\nKompilacja zaczęła się dnia " << __DATE__
<< " o godzinie " << __TIME__ ;
return 0;
}
Notatki:
189