Komentarze



Pobieranie 204,83 Kb.
Data25.11.2017
Rozmiar204,83 Kb.

v1.3, 2004 – 2006, Jacek Dziedzic, FTiMS, Politechnika Gdańska. jaca@kdm.task.gda.pl

Wstęp do C++ –

praktyczne wskazówki ułatwiające pracę na zajęciach z przedmiotów


  • Obiektowe języki programowania,

  • Programowanie współbieżne i równoległe,

  • Metody numeryczne,

  • Algorytmy równoległe,

  • Zastosowanie różnych środowisk komputerowych do obliczeń numerycznych.



Uwagi różne
C++ rozróżnia małe i wielkie litery. Każda instrukcja kończy się średnikiem. Dopuszczalne jest pisanie kilku instrukcji w jednym wierszu, jeśli każda kończy się średnikiem. Odstępy (spacje, tabulatory, znaki nowej linii) między instrukcjami, operatorami i wyrażeniami są ignorowane. Słowo "funkcja" ma w języku C++ znaczenie bardziej zbliżone do słów "podprogram" i "procedura" niż do frazy "wyrażenie matematyczne".
Najważniejsze typy danych
Zmiennoprzecinkowe: float i double, całkowite (int, z podziałem na unsigned i signed, z podziałem na long, short i 'zwykłe'), typ znakowy char i typ logiczny bool – omówiłem przy tablicy. Typy łańcuchowy (string) i wektor (vector) omawiamy później.
Komentarze
W C++ występują dwa rodzaje komentarzy. Komentarz rozpoczynający się od podwójnego ukośnika "//" rozciąga się do końca linii. Komentarz rozpoczynający się od "/*" a kończący "*/" może rozciągać się na kilka linii. Zawartość komentarza jest – naturalnie – ignorowana. Komentarzy nie można zagnieżdżać (są wyjątki, ale udajemy że nie ma). Przykłady komentarzy:
const double pi=3.14159; // stala pi

/* ten komentarz zaczyna sie tu

rozciąga się tu

i konczy sie tu */
Przypisanie a porównanie
Należy zwrócić szczególną uwagę na różnicę między operatorami przypisania (=) i porównania (==). Pierwszy z nich służy do przypisywania wartości stojącej po prawej stronie do zmiennej stojącej po lewej stronie (odpowiednik pascalowskiego ":="). Drugi z nich (==), który odpowiada pascalowskiemu "=" służy do porównywania i zwraca wartość typu bool: true, bądź false, w zależności od wyniku porównania. Przykładowo:
a=b; // teraz a rowne jest b przypisanie

c=d+3; // teraz c rowne jest d+3 przypisanie

a=b=c=5; // teraz a, b i c rowne sa 5 przypisanie wielokrotne

if(a==3) ... // zrob cos, jesli a rowne jest 3 porównanie

if(a=3) ... // niedobrze. Zamiast porownac a z 3, przypisalismy 3 do a...
Jeśli omyłkowo zamiast porównania zastosujemy przypisanie, jest szansa że kompilator zwróci nam na to uwagę wypisując ostrzeżenie, ale gwarancji nie ma, lepiej się pilnować.
Zmienne, stałe
Zmienne deklarujemy podając najpierw typ zmiennej, potem jej nazwę. Dopuszczalne jest też deklarowanie kilku zmiennych tego samego typu "za jednym zamachem". Przykład:
int a; // deklaruje zmienną a typu całkowitego

float c; // deklaruje zmienną c typu zmiennoprzecinkowego

unsigned long int p; // deklaruje zmienną p typu całkowitego, długiego,

// bez-znaku

double a,b,c,d; // deklaruje zmienne a, b, c, d, wszystkie typu

// zmiennoprzecinkowego podwójnej precyzji


Deklaracja jest informacją dla kompilatora, informuje go o tym, co mamy na myśli korzystając później z nazwy tej zmiennej. Każda zmienna musi być gdzieś zadeklarowana. Uwaga: Zmienne interesujących nas typów po zadeklarowaniu nie są zerowane, a mają wartości przypadkowe1. Warto o tym pamiętać, inicjalizując (przypisując, ustawiając) zmienne przed ich pierwszym użyciem. Dopuszczalne jest łączenie inicjalizacji zmiennej z jej deklaracją, co zabezpiecza nas przed kłopotami związanymi z niezainicjalizowaniem, o tak:
int a=0; // deklaruje zmienną a typu całkowitego, ustawia ją na 0

double b=7.3; // j.w. typu double, ustawia ją na 7.3.


Język C++ pozwala na deklarowanie zmiennych prawie w każdym miejscu programu. Nie ma konieczności (jak w Pascalu czy Fortranie) deklarowania zmiennych w ściśle określonych miejscach. Zmienne deklarujemy tam, gdzie nam wygodnie.
Każda zmienna ma swój "czas życia" – czyli pewien zasięg w programie, w którym jest widoczna. Na przykład zmienna zadeklarowana wewnątrz funkcji będzie lokalna dla tej funkcji, "nie będzie jej widać" poza tą funkcją. Podobnie zmienna zadeklarowana w pętli jest poza tą pętlą niewidoczna. Umożliwia to oszczędne gospodarowanie nazwami. Zmienna zadeklarowana poza jakąkolwiek funkcją jest zmienną globalną, tj. widzialną w każdym punkcie programu. Dobry styl nakazuje unikanie zmiennych globalnych, pragmatyka nakazuje stosowanie ich bez zahamowań, jeśli są naprawdę konieczne (zwłaszcza w prostych programach). Należy zachować równowagę :).
Stałe różnią się tym od zmiennych, że przed nazwą typu dodajemy modyfikator const. Informuje on kompilator, że deklarujemy nie zmienną, a stałą. Wartości stałych nie można zmienić (przynajmniej nie w sposób bezpośredni). Stałe trzeba inicjalizować przy deklaracji (bo potem już nie można nic do nich przypisać, w końcu są stałymi)2. Stałe w C++ są bardzo podobne do stałych w Pascalu, są też bardziej cywilizowaną formą konstrukcji PARAMETER występującej w Fortranie.

Przykłady:


const float pi=3.1415927; // deklaruje i definiuje stałą pi

const int tysiac=1000; // deklaruje i definiuje stałą tysiac

... // gdzies dalej, w programie...

double pole=pi*r*r; // oblicza pole jakiegoś okręgu, korzystając ze stałej

pi=3.00; // próba zmodyfikowania stałej, to nie przejdzie!


Dzięki temu, że jawnie oznaczyliśmy 'pi' i 'tysiac' jako stałe, kompilator nie pozwoli nam ich zmienić (gdybyśmy przez przypadek chcieli to zrobić), może również wykorzystać fakt, że wartości stałych nie zmieniają się do tego, aby zoptymalizować nasz program.

Wniosek: stałe są dobre.

Do stałych odnoszą się te same reguły "czasu życia", co do zmiennych. Jeśli zadeklarujemy stałą nie w treści jakiejkolwiek funkcji (o funkcjach zaraz), ale poza funkcją (na przykład na samym początku programu), stała ta będzie globalna – widzialna z każdego punktu programu.

O ile globalne zmienne są prawie-z-natury-złe, to globalne stałe są niegłupim pomysłem. Pozwalają w jednym miejscu programu (np. na początku) zebrać ważne stałe, którymi program operuje. Z drugiej strony, stałe, które są nie istotne z punktu widzenia programu, a są jedynie na potrzeby jakiejś funkcji warto ukrywać wewnątrz takiej funkcji.

Przykład (TAK robić):


// początek programu

const double pi=3.141592653589793238462643383; // } <- widziane w

const double obwod_ziemi=40000000; // } calym programie

const unsigned int rozmiar_tablicy=999; // }


// ... cos dalej
// przykladowa funkcja, z malo wazna stala ukryta wewnatrz funkcji

int odliczanie_do_10() {

const unsigned int limit=10;

for(int i=1;i<=limit;++i) rob_cos();

}
// ... cos dalej
W powyższym przykładzie skorzytaliśmy już z funkcji i z pętli for, o których będzie za chwilę, warto potem wrócić do tego przykładu!
Podstawowe operatory
Dwuargumentowe operatory dodawania (+), odejmowania (-), mnożenia (*) i dzielenia (/) działają zgodnie z intuicją. Można stosować je zarówno do liczb zmiennoprzecinkowych, jak i całkowitych, przy czym należy pamiętać o pułapkach związanych z dzieleniem całkowitym:
double a; // 1 jest typu int, b jest typu int, wiec i wynik 1/b bedzie typu

int b=2; // int. Ale typ int nie moze przechowac wartosci 0.5, nawet jesli

a=1/b; // za chwile zostanie zamieniony na double. Dlatego a==0...
Z pozostałych operatorów istotne mogą być minus jednoargumentowy (-) i plus jednoargumentowy (+), ten drugi jest ignorowany.
double a=7.3; double b;

a=-a; // a teraz równe -7.3

b=+a; // + zignorowany, czyli b teraz równe a, czyli -7.3
Operator modulo (reszty z dzielenia) (%), tylko dla liczb całkowitych:
int p=10 % 3; // p rowne jest reszcie z dzielenia 10/3, czyli 1.
C++, tak jak Pascal, nie ma operatora potęgowania w stylu Fortranu. Potęgowanie można realizować za pomocą funkcji pow() zdefiniowanej w pliku nagłówkowym cmath (będzie o tym później), bądź też "ręcznie", jeśli jest nieskomplikowane:
double a=pow(7.2,2.5); // a rowne jest 7.2 do potęgi 2.5

double b=a*a*a; // b rowne jest a do trzeciej, obliczone ręcznie


Obliczanie "ręczne" przestaje się opłacać (więcej pisania, wolniej działa) przy potęgach większych od 4.
Operatory porównania i przypisania omówiliśmy już wcześniej. Z bardziej istotnych operatorów można wspomnieć jednoargumentowe operatory inkrementacji "++" (zwiększania o 1) i dekrementacji "--" (zmniejszania o 1):
int a=7;

a++; // a teraz rowne 8. Dziala szybciej niz zwykle a=a+1

a--; // a znowu rowne 7. Dziala szybciej niz zwykle a=a-1

++a; // a znowu rowne 8. Dziala co najmniej nie wolniej niz "a++"

--a; // a znowu rowne 7. Dziala co najmniej nie wolniej niz "a--"
W przypadku, gdy wynik zastosowania operatora chcemy zapisać w tej samej zmiennej, można posłużyć się notacją skróconą (i efektywniejszą), w której po znaku operatora pisze się znak równości, o tak:
a+=6; // to samo, co a=a+6

a*=3; // to samo, co a=a*3

a/=7; // to samo, co a=a/7

a%=4; // to samo, co a=a%4


Notacja ta może wydawać się nieczytelna początkującemu, ale z czasem wchodzi w nawyk.
Gdy stosujemy więcej operatorów, wyrażenie obliczane jest zgodnie z ich priorytetami, na przykład mnożenie wykonywane jest przed dodawaniem. Jeśli chcemy inaczej, stosujemy nawiasy:
int a=2+2*2; // a teraz rowne 6

int b=(2+2)*2; // b teraz rowne 8


Operatory logiczne (&&, ||, !) i operator nie-równe (!=) (odpowiednik "<>" z niektórych języków) zostaną omówione w części poświęconej konstrukcjom warunkowym.
Działanie operatorów porównania ">", "<", ">=", "<=" jest intuicyjnie oczywiste, przynajmniej dla typów wbudowanych.
Konstrukcje warunkowe
Podstawową konstrukcją warunkową jest konstrukcja if-else, pozwalająca na wykonanie różnych instrukcji w zależności od spełnienia – bądź nie – pewnego warunku. Składnia tej instrukcji jest następująca:
if(warunek) instrukcja1;

else instrukcja2;


W przypadku spełnienia warunku, wykonywana jest instrukcja1. W przeciwnym razie wykonywana jest instrukcja2. Część "else" jest opcjonalna, tj. jeśli zależy nam na wykonaniu czegoś w razie spełnienia warunku i nicnierobieniu w razie niespełnienia warunku, wystarczy napisać
if(warunek) instrukcja1;
Często będzie zdarzać się tak, że instrukcja1 bądź instrukcja2, bądź obydwie będą instrukcjami złożonymi:
if(warunek) {

instrukcja1a;

instrukcja1b;

instrukcja1c;

}

else {


instrukcja2a;

instrukcja2b;

instrukcja2c;

}
Klamry pełnią tu podobną rolę jak konstrukcja "begin"/"end" występująca w wielu językach programowania. Instrukcje ujęte w klamry 'udają' pojedynczą instrukcję. Czas życia zmiennych zadeklarowanych wewnątrz klamr kończy się z opuszczeniem tychże klamr.


Warunek powinien być typu logicznego (bool) bądź całkowitego. W pierwszym wypadku warunek uznajemy za spełniony, gdy ma on wartość true, w drugim przypadku, gdy wartość jest niezerowa. Przykładowo:
int a,b,c;

if(a==b) c=5; else c=0; // jeśli a równe jest b, to a==b jest true i

// wykonuje się c=5. W przeciwnym razie c=0.
if(c) a=3; else a=5; // jeśli c jest niezerowe, to a będzie równe 3
Należy zwrócić uwagę na fakt, że warunek dla instrukcji if musi być umieszczony w nawiasach. W budowaniu bardziej skomplikowanych warunków przydatne są operatory logicznego-i "&&", logicznego-lub "||", negacji-logicznej "!" oraz operator nie-równa-się "!=":
if(a==3 && b==4) ... // wykonaj jeśli a równe jest 3 i b równe jest 4

if(a==3 || b==4) ... // wykonaj, jeśli a równe jest 3 lub b równe jest 4

if(!(a==3 || b==4)) ... // wykonaj, jeśli nieprawdą jest, że (a==3 lub b==4)

if(a!=4) ... // wykonaj, jeśli a jest różne od 4


C++ oferuje jeszcze alternatywną konstrukcję warunkową (trójargumentowy operator ?:), którą nie będziemy się zajmować. Jeszcze inną konstrukcję warunkową (wyboru wielokrotnego) zapewnia instrukcja switch, po opis której odsyłam do literatury (pełni podobną rolę do case w innych językach).
Pętle
Za pomocą pętli możemy wykonywać pewne instrukcje wielokrotnie. C++ oferuje trzy rodzaje pętli: pętlę while, pętlę do-while i pętlę for. Składnia pierwszej z nich jest następująca:
while(warunek) instrukcja1;
Instrukcja1 będzie wykonywana dopóty, dopóki spełniony będzie warunek. Jeśli warunek nie będzie spełniony "na dzień dobry" (przed wejściem do pętli), instrukcja1 nie zostanie wykonana ani razu. Przykłady pętli while:
while(aint p=4;

while(p>0) {

// ponizsze operacje wykonuja sie cztery razy (dla p=4, 3, 2 i 1).

c=2*a+7-b;

p--; // tu "recznie" zmniejszamy p

};

Podobną składnię i działanie ma pętla do-while, z tą różnicą, że warunek sprawdzany jest po wykonaniu instrukcji w pętli, a nie przed ich wykonaniem. W związku z tym instrukcje zawarte w do-while będą wykonane co najmniej raz (a dokładnie raz, kiedy warunek nie jest spełniony). Przykładowo:


int p=0;

do {


// instrukcje tu zostana wykonane raz, mimo ze p nie jest wieksze od 0

c=2*a+b-c;

} while(p>0); // po pierwszym sprawdzeniu warunku pętla jest opuszczana
Jak widać, podobnie jak w instrukcji if, można zamiast instrukcji prostych korzystać tu z instrukcji złożonej {}.
Najbardziej elastyczną a także składniowo nieco skomplikowaną jest pętla for. Korzysta się z niej na ogół w sytuacji, kiedy liczba obiegów pętli jest znana z góry, choć niekoniecznie. Pętla for w języku C++ oferuje znacznie większe możliwości, niż jej odpowiedniki w Pascalu (for) czy Fortranie (do). Jej składnia jest następująca:
for(inicjalizacja; warunek; operacja) instrukcja1;
Przed wejściem do pętli wykonywana jest inicjalizacja. Następnie instrukcja1 jest wykonywana tak długo, jak spełniony jest warunek. Warunek sprawdzany jest przed wykonaniem instrukcji. Po każdym obiegu pętli wykonywana jest operacja. Przykładowo:
for(i=1; i<=10; i++) cout << i; // wypisuje liczby od 1 do 10

for(i=10; i>=1; i--) cout << i; // wypisuje liczby od 10 do 1

for(i=1; i<=10;i=i+2) cout << i; // wypisuje liczby 1, 3, 5, 7, 9

for(i=2; i<30; i=i*2) cout << i; // wypisuje 2, 4, 8, 16

for(i=7;i<5;i++) cout << i; // nic nie wypisuje, bo warunek nie spełniony
O tym, że "cout << i" wypisuje zmienną 'i' na ekran będzie wkrótce.
Jak widać na przykładach 2. i 3., operacja, która jest wykonywana przy każdym obiegu może być bardziej skomplikowana niż zwykłe zwiększanie lub zmniejszanie licznika. Wiekszość innych języków tego nie potrafi.
Wyrażenia w pętli for są opcjonalne i można je pomijać z różnym skutkiem:
for(i=1;i<10;) ... // pominieto operacje, wiec 'i' nie jest automatycznie zwiekszane

for(i=1;;i++) ... // pominieto warunek

for(;;) ... // pominieto wszystko, petla nieskonczona
Również i w pętli for często korzysta się z instrukcji złożonej {} w miejsce instrukcji prostej. Część inicjalizacyjna pętli for może wyjątkowo zawierać również deklarację zmiennej – wówczas zmienna ta przestaje być widziana po opuszczeniu pętli (w poprzednim standardzie języka C++ nie było tak!). Przykładowo:
for(int i=5;i<10;i++) {

cout << i; // wypisuje 5, 6, 7, 8, 9

}

cout << i; // nieprawidłowe, zmienna 'i' nie istnieje poza petlą


Niektóre (stare) kompilatory dopuszczają takie niepoprawne odwoływanie się poza pętlą do zmiennej zadeklarowanej wewnątrz pętli – nie należy na tym polegać.

W odróżnieniu od niektórych innych języków (np. Pascal) C++ zezwala na dowolne manipulacje zmienną kontrolną w pętli for. Oznacza to, że możemy ją ręcznie zwiększać, zmniejszać czy zmieniać jej wartość wewnątrz pętli.


Funkcje
Funkcje w języku C++ pełnią rolę taką samą, jak funkcje i procedury w Pascalu i podobną jak podprogramy w Fortranie. Funkcja przyjmuje określoną liczbę argumentów, określonych typów i zwraca wartość, również określonego typu. Przykładami funkcji (deklaracji funkcji) są na przykład:
double pierwiastek(double a);

int wieksza_z_liczb(int a, int b);

double podnies_do_potegi_calkowitej(double podstawa, int wykladnik);
Pierwsza z tych funkcji przyjmuje jeden argument typu double i zwraca wynik typu double. Można spodziewać się, że służy do obliczania pierwiastka z liczby rzeczywistej. Druga z funkcji przyjmuje dwa argumenty typu całkowitego i zwraca wynik również typu całkowitego. Można spodziewać się, że ta funkcja zwraca większą z dwóch liczb. Trzecia z funkcji przyjmuje jeden argument typu double, jeden typu int i zwraca wynik typu double. Wspomniane wyżej konstrukcje były deklaracjami funkcji, czyli informacjami dla kompilatora, że istnieją funkcje o takich nazwach, odpowiednich argumentach i odpowiednim typie wyniku. Gdy chcemy podać definicję funkcji (pokazać przepis na jej wykonanie), musimy dostarczyć ciało funkcji (instrukcje, które składają się na nią), które umieszczamy między nawiasami klamrowymi {}. Dla przykładu definicja funkcji wieksza_z_liczb() może wyglądać następująco:
int wieksza_z_liczb(int a, int b) {

if(a>b) return a;

else return b; // zakladamy, ze jesli a==b, to wynikiem jest b

}
Wynik zwracamy za pomocą instrukcji return, po której podajemy wartość, którą chcemy zwrócić. Wykonanie return opuszcza funkcję.


Funkcja może nie mieć argumentów:
int aktualna_godzina(); // funkcja bez argumentów
Funkcja może nie zwracać żadnej wartości, zaznaczamy to pisząc, że funkcja zwraca typ void (pusty):
void czekaj(int jak_dlugo);
Taka funkcja jest analogiem pascalowskiej procedury – pozwala ona na wykonanie operacji, które nie zwracają żadnego wyniku.
Poza zadeklarowaniem i zdefiniowaniem funkcji możemy jeszcze funkcje wywoływać (po to w końcu są). Składnia wywołania jest intuicyjnie oczywista:
int p,q,r;

p=3; q=7;

r=wieksza_z_liczb(p,q); // wywolanie funkcji z argumentami p i q

Warto zauważyć, że funkcja korzysta z kopii swoich parametrów, niezależnie od tego, jak zostaną one nazwane, np. w poniższym przykładzie:


void zeruj(double x) {

x=0;


cout << x; // wypisze 0

}
int x=4;

zeruj(x);

cout << x; // wypisze 4


Wartość 'x' na której operuje funkcja jest kopią wartości 'x' zadeklarowanej poniżej (i równej 4). Próby zmiany 'x' wewnątrz funkcji skutkują zmianami kopii, oryginał pozostaje nietknięty. Zachowanie takie może z początku dziwić, gdyby jednak było inaczej co miałaby zrobić funkcja 'zeruj' wywołana jak poniżej, z argumentem, który nie jest zmienną a wartością:
zeruj(3); // jak przypisać 3=0?
Możemy zmienić powyższą konwencję przekazując parametry przez referencję (odsyłam do literatury) – wówczas funkcja operuje bezpośrednio na oryginałach swoich parametrów, nie na kopiach.
Uwaga! Nie można funkcji wywołać przed jej definicją, chyba że została uprzednio zadeklarowana. Na przykład:
// -----------------------------------------

double pierwiastek(double a) {

return sqrt(a);

}
double pierwiastek_z_pi() {

const double pi=3.14159;

return pierwiastek(pi); // w porządku. Funkcja pierwiastek()

// byla zdefiniowana wcześniej.

}
Teraz inny przykład:


// -----------------------------------------

double pierwiastek_z_pi() {

const double pi=3.14159;

return pierwiastek(pi); // Niedobrze. Kompilator nie wie (jeszcze)

// czym jest pierwiastek().

}
double pierwiastek(double a) {

return sqrt(a);

}
Powyższy fragment programu można poprawić na dwa sposoby: albo przenosząc definicję funkcji pierwiastek() przed definicję funkcji pierwiastek_z_pi(), albo dając deklarację funkcji pierwiastek() przed funkcją pierwiastek_z_pi(), tak jak tu:


// -----------------------------------------

double pierwiastek(double a); // deklaracja


double pierwiastek_z_pi() {

const double pi=3.14159;

return pierwiastek(pi); // W porzadku. Dzieki deklaracji kompilator wie

// czym jest pierwiastek().

}
double pierwiastek(double a) {

return sqrt(a);

}
Specjalne znaczenie ma w programach C++ funkcja o nazwie main, która zwraca wartość typu int. Funkcja ta jest główną funkcją programu i jest wywoływana automatycznie przy uruchomieniu programu. Ciałem tej funkcji jest nasz "program główny". Dla przykładu bardzo prosty (ale nasz pierwszy kompletny) program w C++ może wyglądać tak:
int main() {

int a=3;


a=2*a-7;

return 0;

}
Powyższy program oblicza wartość 2*3-7, po czym... ignoruje wynik :), a następnie kończy pracę, opuszczając funkcję main() za pomocą instrukcji return. Wartość zwrócona przez funkcję main() zwracana jest do systemu operacyjnego i generalnie nie będzie nas obchodziła. Obowiązuje umowa, że zwrócenie w funkcji main wartości 0 oznacza pomyślny przebieg programu, a każdej innej wystąpienie błędu3, ale nie musimy się tej umowy trzymać. Funkcja main() jest jedyną funkcją zwracającą wynik, w której możemy pozwolić sobie na ekstrawagancję i wyniku tego nie zwrócić (np. pominąć instrukcję return w powyższym przykładzie). Jeśli tego nie zrobimy, kompilator zwróci za nas wartość 0. Uwaga: taka ekstrawagancja w przypadku innych funkcji zwracających wynik może skończyć się żałośnie:
int brzydka_funkcja() {

// cos tu robimy, ale zapomnielismy o return

}
int a;

a=brzydka_funkcja(); // a ma teraz wartość nieokreśloną


Dawno temu funkcja main() mogła być funkcją, która nie zwracała wyniku, tj. można było pisać:
void main() { // <- bleeeee!

// moj program

}
Obecnie jest to niedopuszczalne, jest przejawem złego stylu i jest niepoprawne z formalnego punktu widzenia. Nowoczesne kompilatory ostrzegą nas przy próbie takiego ubliżania językowi :) albo wręcz uznają taki program za niepoprawny (mając rację).
Funkcja main() może pobierać (ściśle określone) argumenty, które dostarcza system operacyjny na podstawie parametrów, które przekazaliśmy naszemu programowi wywołując go. Wrócimy do tego niedługo.

Strumienie
Wstępne choćby omówienie koncepcji strumieni wykracza poza ramy tego dokumentu, tak że powiemy sobie tylko kilka słów i to w wielkim uproszczeniu. Strumień to takie zwierzę, do którego można wysłać dane (strumień wyjściowy) bądź pobrać z niego dane (strumień wejściowy). Co się dzieje z danymi wysłanymi do strumienia albo skąd pochodzą dane pobrane ze strumienia? To zależy z czym ten strumień jest stowarzyszony. Nas najbardziej będą interesować dwa predefiniowane (czyli takie, które ktoś zrobił za nas) strumienie:
cout – wyjściowy, domyślnie stowarzyszony z konsolą (monitor)

cin – wejściowy, domyślnie stowarzyszony z konsolą (klawiatura)


Wysyłanie danych do strumienia cout powoduje pojawianie się ich na monitorze. Pobieranie danych ze strumienia cin powoduje, ze program oczekuje na wprowadzenie danych z klawiatury. Operatorem wysłania czegoś do strumienia jest "<<", a operatorem pobrania ze strumienia jest ">>". Kilka przykładów:
cout << "Ala ma kota"; // wypisuje tekst na ekran

cout << "Oto siódemka: " << 7; // wypisuje na ekran tekst i liczbę

cout << "Zmienna a ma wartość" << a // wypisuje na ekran tekst, liczbę

<< " a zmienna b " << b; // i jeszcze jeden tekst i liczbę

cout << "Oto pierwiastek z pi" << sqrt(pi);


int a;

double b;

float c;

cin >> a; // wprowadza liczbę całkowitą a z klawiatury

cin >> b; // wprowadza liczbę zmiennoprzecinkową z klawiatury

cin >> a >> b >> c; // wprowadza kolejne liczby z klawiatury


Istotną zaletą strumieni jest to, że jeśli nie chcemy, nie musimy dbać ani o format wyprowadzanych danych, ani o ich typ – martwi się o to za nas język C++. Dla porównania, jeśli korzystamy ze składni typowej dla C:
printf("Oto liczba %d",liczba);
i zdecydujemy się zmienić typ zmiennej liczba z całkowitej na zmiennoprzecinkową, zmiana ta pociągnie za sobą również zmianę formatu "%d" na "%f". W C++ nie musimy martwić się o takie szczegóły (jeśli nie chcemy). Ceną, którą za to płacimy jest nienajlepsza wydajność mechanizmu strumieni. Są jednak sytuacje, w których bardzo zależy nam na tym, żeby wypisywane wartości były odpowiednio sformatowane (szczególnie na laboratorium z Metod Numerycznych). Możemy na przykład życzyć sobie, żeby wszystkie liczby były wypisywane z dokładnością do 4 cyfr po przecinku, żeby przed wartościami dodatnimi pojawiał się znak "+", mimo tego że jest nieistotny, żeby liczby były wyrównywane w prawo czy w lewo, etc., etc. Mechanizm strumieni pozwala również na to (niedługo sobie o tym powiemy), jednak gdy nam nie zależy na formatowaniu, możemy korzystać z bardzo prostej składni, jak powyżej. Takie postawienie sprawy stawia C++ w lepszej sytuacji niż języki w których albo przywiązuje się maniakalną uwagę do formatowania (Fortran), albo nie ma w zasadzie kontroli nad formatowaniem (Pascal), albo formatowanie jest dość niewygodne (C). Niestety strumienie są nienajszybsze.
Gdyby strumienie służyły tylko do pisania na ekran i czytania z klawiatury, nie byłyby szczególnie użyteczne. Jedną z cech przesądzających o ich "potędze" jest to, że można je stowarzyszyć z czymś zupełnie innym, na przykład z plikiem, z łańcuchem znaków czy obszarem pamięci. Na przykład:

ofstream plik; // deklaruje 'plik' jako strumien wyjsciowy

plik.open("mojplik.txt"); // otwiera go

plik << "Wlasnie pisze do pliku ile wynosi pi: " << 3.14159;

plik.close(); // zamyka
Mniejsza w tej chwili o szczegóły implementacyjne – o tym jak otwiera i zamyka się pliki jeszcze będziemy mówić – istotne jest to, że ten sam mechanizm, co na ekran, pozwala na pisanie do pliku. Podobnie będzie z czytaniem. Dzięki "magicznym" własnościom strumieni (dzięki temu, że dbają one o konwersję typów) można z nich korzystać na przykład do zamiany tekstu na liczbę, liczby na tekst, etc., ale o tym kiedy indziej.
Istotna uwaga: Aby móc skorzystać z mechanizmu strumieni, trzeba poinformować o tym kompilator, włączając odpowiedni plik. Jak to działa jest w tej chwili nieistotne, odpowiednie "zaklęcie" ma postać
#include
i powinno pojawić na samym początku programu. Proszę zauważyć, że nie kończy się ono średnikiem (bo z formalnego punktu widzenia nie jest instrukcją, ale to nieważne).
Uwaga dla tych, którzy kiedyś pisali już trochę w (starym) C++:
#include // tak dawniej, już tak nie robimy

#include // tak teraz
Dodanie ".h" spowoduje włączenie starej wersji plików nagłówkowych, co jest niepożądane.
Uwaga: w nowoczesnych kompilatorach4 (nowszych niż ten na laboratorium w s.13/14) strumienie są dodatkowo "schowane" w przestrzeni nazw std i aby się do nich dostać należy po wspomnianym już zaklęciu dodać jeszcze formułę:
using namespace std;
Na tym etapie nieistotne jest dla nas co to robi, jak to robi i po co to robi. :), w ignorancji siła – na razie!

Tablice
Przede wszystkim warto zauważyć, że tablice są mechanizmem nieco przestarzałym – język C++ odziedziczył je w spadku po C. Nowszą, bardziej cywilizowaną formą są wektory, o których pokrótce w następnym rozdziale. Dwie główne cechy, które odróżniają wektory od tablic (in plus dla wektora) to możliwość kontroli zakresu (wyłapywania przypadków wyjścia poza tablicę) oraz elastyczny rozmiar (tablice mają rozmiar ustalony).
Jeśli masz mało czasu, nie czytaj o tablicach a czytaj o wektorach.
Tablice deklaruje się w C++ podając ich rozmiar w nawiasach kwadratowych po nazwie zmiennej (nie po nazwie typu!), na przykład:
int liczby[100]; // stuelementowa tablica liczb całkowitych

double macierz[15][15]; // macierz 15x15 liczb rzeczywistych

char lancuch[256]; // 256-elementowa tablica znaków
Tablice wielowymiarowe deklaruje się powtarzając nawias kwadratowy wielokrotnie (tak jak w przykładzie 2), niedopuszczalne jest pisanie na przykład (podobnie jak w Pascalu) macierz[x,y] zamiast macierz[x][y] – przecinek ma w C++ znaczenie specjalne.
Dostęp do elementów tablicy uzyskujemy za pomocą operatora indeksowania "[]", podając numer elementu wewnątrz nawiasów kwadratowych. Dla przykładu:
p=liczby[5]; // przypisz do 'p' element tablicy o indeksie 5

macierz[3][3]=4.0; // ustaw jeden z elementow macierzy

lancuch[i]='k'; // ustaw i-ty znak lancucha na 'k'
Uwaga: W C++ elementy tablicy indeksowane są od 0, nie od 1 jak na przykład w Fortranie. I tak tablica[0] reprezentuje pierwszy element, tablica[1] drugi, itd. Wobec tego tablica, która ma n elementów zawiera elementy o indeksach 0..n-1, NIE 1..n. Próba skorzystania z nieistniejącego elementu tablicy (o indeksie spoza zakresu) na ogół kończy się żałośnie i będzie przyczyną trudnych do wyśledzenia błędów. Tablice nie zapewniają żadnego mechanizmu kontroli zakresu, dlatego należy się pilnować!
Tablice zadeklarowane we wspomniany sposób muszą mieć rozmiar znany z góry (tj. podczas kompilacji programu). Niedopuszczalne jest zatem deklarowanie tablic, rozmiar których jest zmienną. Dla przykładu poniższa próba stworzenia tablicy o rozmiarze zadanym z klawiatury nie powiedzie się:
int rozmiar;

cin >> rozmiar; // 'rozmiar' nie jest znany podczas kompilacji

int tablica[rozmiar]; // wiec nie da rady
Niektóre kompilatory pozwalają na coś takiego, trzeba jednak pamiętać że jest to rozszerzenie języka oferowane przez kompilator, nie część języka C++.
Tablice o nieznanych z góry rozmiarach można tworzyć dynamicznie za pomocą operatora new. Tablicami takimi raczej nie będziemy się zajmowali.
Wektory
Zanim skorzystasz z wektorów, musisz włączyć plik nagłówkowy "vector":

#include


O wektorach należy myśleć jak o tablicach jednowymiarowych, pomimo swojej nieco mylącej nazwy nie mają one żadnych konotacji fizycznych czy matematycznych. Krótko mówiąc – wektor to n-elementowy ciąg. Elementy wektora mogą być dowolnego typu5 – możemy mieć wektor liczb rzeczywistych, wektor liczb całkowitych, wektor nazwisk, etc. Wektor deklarujemy podając typ elementów przechowywanych w wektorze, nazwę wektora oraz opcjonalnie liczbę elementów wektora, umieszczoną w nawiasach okrągłych (inaczej niż w przypadku tablicy).
vector wek1(100); // wektor 100 elementów int

vector wek2(1000); // wektor 1000 elementów double

vector wek3; // pusty (inicjalnie) wektor elementów bool
Wszystkie elementy wektora są domyślnie zerowane, czego nie można powiedzieć o tablicach.
W każdej chwili możemy sprawdzić rozmiar wektora (liczbę elementów przechowywaną w nim w danym momencie) – aby to zrobić wywołujemy na rzecz wektora metodę size():
cout << wek1.size(); // wypisze 100

int rozm = wek3.size(); // rozm jest teraz rowny 0


Dostęp do elementów wektora jest realizowany tak samo jak dla tablicy – za pomocą operatora indeksowania [], gdzie wewnątrz nawiasów podajemy numer elementu:
p=wek1[5]; // przypisz do 'p' element wektora o indeksie 5

wek2[i]=7; // ustaw i-ty element wektora na 7


Gdy korzystamy z takiej notacji musimy dbać o to, aby nie przekroczyć zakresu wektora, tj. nie próbować dostępu do nieistniejących elementów, np. próba wywołania
wek2[2314]=7; // ale on ma tylko 1000 elementów!
skończy się żałośnie6. Aby uniknąć podobnych kłopotów, wektor umożliwia dostęp kontrolowany za pomocą metody at(), z której można korzystać jak poniżej:
p=wek1.at(5); // przypisz do 'p' element wektora o indeksie 5, z kontrolą

wek2.at(i)=7; // ustaw i-ty element wektora na 7, z kontrolą


W przypadku przekroczenia zakresu przy korzystaniu z metody at() mamy gwarancję natychmiastowego zakończenia programu7, co łatwo pozwala wyśledzić błędy. Ceną, którą płacimy za ten komfort jest pogorszenie wydajności (przy każdym dostępie do wektora program kontroluje zakres). Dobrym nawykiem jest stosowanie at() zawsze, dopóki nie okaże się, że pogorszenie wydajności jest zauważalne. Dopóki nie wykonujemy milionów operacji na wektorach można zakładać, że nie zauważymy różnicy.
Uwaga: Podobnie jak tablice, wektory w C++ indeksowane są od 0, nie od 1 jak na przykład w Fortranie. I tak wektor[0] reprezentuje pierwszy element, wektor[1] drugi, itd. Wobec tego wektor, który ma n elementów zawiera elementy o indeksach 0..n-1, NIE 1..n.
Wektor ma takie same prawa jak "zwykłe" typy – można go przekazywać jako argument funkcji, zwracać jako wynik funkcji, przypisywać, etc. – nie można tego powiedzieć o tablicach. Przykładowo:
// funkcja, która bierze argumenty, które są wektorami i zwraca wektor
vector dodaj_wektory(vector pierwszy, vector drugi) {

vector wynik;

// wykonaj dodawanie element po elemencie

return wynik;

}
vector wekt1(100);

vector wekt2(40);

// ... inne operacje, np. ustawienie elementów wektora 'wekt2'

wekt1=wekt2; // nadpisanie wektora pierwszego drugim

Jedną z istotnych cech odróżniających wektor od tablic jest oferowana przez niego możliwość zmiany rozmiaru, którą możemy osiągnąć korzystając z metody resize(), podając nowy rozmiar wektora:
vector wektor(100); // wektor ma rozmiar 100 elementów

wektor.resize(250); // już ma 250 elementów

wektor.resize(0); // a teraz jest pusty
Przy powiększaniu wektora, na jego końcu zostają dopisane 'puste' elementy. Przy skracaniu wektora ostatnie elementy giną bezpowrotnie.
Wektor umożliwia jeszcze operację dodania elementu na końcu: push_back() – skutkuje ona powiększeniem długości wektora o 1 i ustawieniem ostatniego elementu na zadaną wartość:
vector wektor(3); // wektor jest pusty

wektor[0]=3; wektor[1]=1; wektor[2]=4; // wektor ma 3 elementy: 3, 1, 4

wektor.push_back(5); // wektor ma 4 elementy: 3, 1, 4, 5
Wbrew temu, co możnaby przypuszczać wektory nie oferują żadnych operacji arytmetycznych – gdy chcemy dodać do siebie dwa wektory (w sensie pierwszy element do pierwszego, drugi do drugiego, etc.), musimy "ręcznie" dokonać dodawania element po elemencie. Jest to podyktowane tym, że wektory mogą przechowywać elementy niebędące liczbami (jeszcze raz podkreślam ich "niematematyczną" naturę) – jak miałoby być zrealizowane dodanie dwóch wektorów, których elementami są numery PESEL albo nazwiska? Odpowiednie funkcje łatwo napisać samemu.
Wektory są z natury jednowymiarowe. Gdy potrzebujemy czegoś na kształt tablicy dwuwymiaro-wej (macierzy), możemy stworzyć... wektor wektorów. Jeśli chcemy tablicy trójwymiarowej (tensora), możemy posłużyć się wektorem wektorów wektorów. Przykładowo:
vector< vector< double > > macierz(30,40); // macierz 30x40

vector< vector< vector< bool> > > kostka; // tensor trójwymiarowy


O tak utworzonej macierzy myślimy jak o wektorze, którego każdy element jest wektorem. Wobec tego np. macierz[4] oznacza piąty wiersz (piąty wektor), a macierz[4][7] oznacza ósmy element tego wiersza (pamiętamy o numerowaniu od zera!). Przy tablicach o liczbie wymiarów większej niż dwa nie możemy niestety zadać rozmiaru przy deklaracji – możliwe jest tylko tworzenie tablic pustych, a potem ustawianie rozmiarów za pomocą resize().
Uwaga: tworząc wektory wektorów należy zadbać, aby znaki ">" znajdujące się w jego definicji oddzielić od siebie znakiem spacji – w przeciwnym przypadku kompilator potraktuje dwa sklejone ze sobą znaki ">" jako operator wczytania ze strumienia ">>" i "zgłupieje". Jest to znane niedopracowanie języka.
Tak utworzone macierze nie oferują typowych "matematycznych" operacji (mnożenia, obliczania wyznacznika, diagonalizacji, etc.) – znowu przypomina się "niematematyczna" natura wektora – mogą one przecież przechowywać nie tylko liczby, ale np. litery, numery kont czy nazwiska. Gdy potrzebujemy zaawansowanych operacji matematycznych, korzystamy na ogół z bibliotek stworzonych przez kogoś innego, które udostępniają funkcje w rodzaju pomnóż(), diagonalizuj(), wyznacznik() itp. Przykładem bardzo często wykorzystywanej biblioteki jest LAPACK, która udostępnia mnóstwo operacji przydatnych w algebrze liniowej – nie pracuje ona co prawda bezpośrednio z wektorami (jest napisana w Fortranie), ale można ją do tego przystosować.


Strumienie – c.d.
Wiemy już, że ze strumienia wejściowego możemy czytać dane za pomocą operatora >>. Podczas czytania kolejnych danych ignorowane są automatycznie wszystkie białe znaki
(tj. znaki spacji, końca wiersza, końca strony, tabulacji). Zatem podczas czytania tym sposobem nie możemy wykryć końca wiersza, bo zostanie on pominięty i czytanie przejdzie automatycznie do następnego wiersza.
Alternatywą do czytania za pomocą operatora >> jest czytanie ze strumienia całymi liniami. Możemy tego dokonać za pomocą metody (funkcji) strumienia o nazwie getline(), którą wywołujemy następująco:
cin.getline(bufor,rozmiar,'\n');
Powyższa konstrukcja nakazuje wczytanie jednej linii ze strumienia cin do tablicy znaków o nazwie bufor i rozmiarze rozmiar. Zostanie wczytane wszystkie znaki, aż do napotkania znaku '\n' (końca wiersza), chyba że linia będzie zbyt długa, by zmieścić się w tablicy bufor – wówczas wczytane zostanie tylko (rozmiar-1) znaków i tablica nie zostanie przepełniona, a reszta linii pozostanie w strumieniu do kolejnego odczytu. Tablicę znaków bufor musimy oczywiście zadeklarować wcześniej. Uwaga na marginesie: tablice znaków są niewygodnym sposobem przechowywania łańcuchów znaków i są pozostałością po języku C. Znacznie wygodniejszym rozwiązaniem jest stosowanie zmiennej łańcuchowej (typ string). Niestety parametr dla getline() musi być tablicą znaków, nie może być łańcuchem. Tablice znaków można jednak przypisywać do łańcuchów, zatem możemy pisać tak:
char bufor[256]; // zakladamy, ze linie nie moga miec wiecej niz 255 znakow

string linia;

cin.getline(bufor,256,'\n');

linia=bufor;


Od tego momentu w zmiennej łańcuchowej linia mamy jedną linię pobraną ze strumienia.
Prostszym sposobem poradzenia sobie z czytaniem pliku po linii jest skorzystanie z globalnej8 funkcji getline, która jako pierwszy parametr pobiera nazwę strumienia, a jako drugi zmienną typu string:
string linia;

getline(cin,linia);


Podejście takie zwalnia nas od decydowania jak długa maksymalnie może być linia – rozmiar naszej zmiennej łańcuchowej linia sam dostosuje się do długości linii w pliku.
C++ umożliwia stworzenie strumienia w pamięci, który nie jest związany ani z konsolą, ani z żadnym fizycznym plikiem dyskowym. Strumień taki pozwala nam interpretować uprzednio wczytaną linię jak inny strumień. Przykładowo dopisanie do powyższego kodu:
stringstream linia_jako_strumien; // zadeklaruj strumien w pamieci

linia_jako_strumien << linia; // wyslij linie do strumienia

linia_jako_strumien >> a >> b >> c; // odczytaj a, b i c z tego strumienia
pozwala nam czytać zmienne a, b i c ze strumienia, który zawiera jedną linię (wczytaną uprzednio z innego strumienia). Takie rozwiązanie pozwala łączyć czytanie z pliku po linii z wygodnym czytaniem za pomocą operatora >>.
Strumień w pamięci pozwala nam także dokonywać konwersji między reprezentacjami liczb. Na przykład
string lancuch="1.23456"; // mamy zmienna lancuchowa

double liczba; // i liczbe

stringstream strumien; // deklarujemy strumien w pamieci

strumien << lancuch; // wysylamy lancuch

strumien >> liczba; // a pobieramy liczbe
pozwala na konwersję string  double. Analogicznie możemy przeprowadzić konwersję w drugą stronę.
Uwaga: Aby skorzystać ze strumieni pamięciowych należy do programu dołączyć plik nagłówkowy sstream za pomocą dyrektywy #include na początku programu. Podobnie aby korzystać z typu łańcuchowego string należy zastosować dyrektywę #include.
Wspomnieliśmy już, że jedną z zalet strumieni jest możliwość wyprowadzania za ich pomocą danych bez dbania o format, w jakim zostaną wyprowadzone. Zdarzają się jednak sytuacje (szczególnie podczas obcowania z Metodami Numerycznymi), w których chcemy dokładnie sprecyzować format wyprowadzania danych. Jak można to zrobić? Pewne parametry strumienia (określające m.in. format) można zmieniać wywołując metodę (funkcję) strumienia setf() i przekazując jej odpowiednie argumenty. Można również do strumienia wysyłać tzw. manipulatory, które działają jak sekwencje kontrolne ustawiając pewne parametry strumienia. Druga z tych metod jest elegantsza, jednak z uwagi na nieszczególną aktualność kompilatora g++ stosowanego na zajęciach czasem (często) będziemy posługiwać się metodą pierwszą. Kilka najczęściej zmienianych parametrów, za pomocą metody pierwszej w zastosowaniu do strumienia cout pokazano poniżej:
cout.setf(ios::left,ios::adjustfield); // włącza wyrównywanie do lewej

cout.setf(ios::right,ios::adjustfield); // włącza wyrównywanie do prawej

cout.setf(ios::fixed,ios::floatfield); // przechodzi do trybu fixed

cout.setf(ios::scientific,ios::floatfield); // przechodzi do trybu notacji

// naukowej (scientific)

cout.setf(ios::showpoint); // wymusza pokazywanie kropki dziesiętnej zawsze

cout.setf(ios::showpos); // wymusza znak '+' przed liczbami dodatnimi
Wyrównywanie do lewej bądź do prawej odnosi się do położenia liczby na wydruku względem pola, w którym się ona znajduje. W trybie fixed wszystkie liczby wypisywane są z taką samą, zadaną liczbą cyfr po przecinku. W trybie notacji naukowej (scientific) wszystkie liczby wypisywane są razem z wykładnikiem. W trybie domyślnym (ani fixed, ani scientific) liczby wypisywane są ze zmienną liczbą cyfr po przecinku (np. końcowe zera są obcinane) bądź w notacji naukowej. Tryb domyślny kiepsko pasuje do Metod Numerycznych, na ogół będziemy więc pracować w trybie fixed bądź scientific.
Za pomocą manipulatora setprecision() możemy kontrolować precyzję wyprowadzanych liczb (domyślnie równą 6). Jego interpretacja zależy od trybu, w którym jest strumień. W trybach fixed i scientific ustalamy liczbę cyfr po przecinku jaka jest wyprowadzana. W trybie domyślnym manipulator ten określa maksymalną liczbę cyfr w ogóle (przed i po przecinku), jaka jest wyprowadzana. Z manipulatora korzystamy wysyłając go do strumienia:
cout << setprecision(10) << 1.43; // ustaw precyzje na 10
Działanie powyższej instrukcji wyprodukuje:

w trybie fixed: 1.4300000000

w trybie scientific: 1.4300000000e+00

w trybie domyślnym: 1.43 (bo końcowe zera są obcinane)


Za pomocą manipulatora setw() możemy kontrolować szerokość pola, do którego wyprowadzana jest dana. W tym polu dana zostanie wyrównana do prawej, bądź do lewej. Uwaga: manipulator setw() działa tylko dla danej wyprowadzanej bezpośrednio po jego zastosowaniu, zatem na ogół trzeba go stosować wielokrotnie. Przykładowo:
cout.setf(ios::fixed); // włącz tryb fixed

cout.setf(ios::left,ios::adjustfield); // wyrównuj do lewej

cout << setprecision(4); // 4 cyfry po przecinku

cout << setw(10) << 1.43 << endl; // szerokosc pola 10, wypisz

cout.setf(ios::right,ios::adjustfield); // wyrównuj do prawej

cout << setw(10) << 1.43 << endl; // szerokosc pola 10, wypisz


wyprodukuje ( '_' oznacza spację):

1.4300 _ _ _ _

_ _ _ _ 1.4300
Pole ma w powyższym przykładzie szerokość dziesięciu znaków, a precyzję ustawiliśmy na 4 cyfry po przecinku.
Próba wyprowadzenia zmiennej, która nie mieści się w polu kończy się powiększeniem pola tak, żeby liczba mieściła się w nim (zasada mówiąca, że lepiej wyprowadzić dobre dane w brzydki sposób (psując rozmiar pola), niż złe dane ładne (wciskając liczbę w pole)).
Manipulator setfill() pozwala na zmianę znaku, którym dopełniane jest pole (domyślnie jest nim spacja). Przykładowo dopisanie
cout << setfill('#');
przed poprzednim przykładem zmieniłoby wydruk na:

1.4300####

####1.4300
Uwaga: Aby skorzystać z manipulatorów należy do programu dołączyć plik nagłowkowy iomanip za pomocą dyrektywy #include na początku programu.
Strumienie są 'inteligentne' ponieważ pozwalają wczytywać dane w różnych formatach w zależności od typu wczytywanych zmiennych. Na przykład
int a; double b; string c;

cin >> a >> b >> c;


wczyta zmienną całkowitą a, zmiennoprzecinkową b i łańcuch znaków c. Co się jednak stanie, jeśli użytkownik będzie chciał popsuć nam plany i wpisze z klawiatury na przykład tekst "32ala ma kota"? Pierwsza w kolejności do wczytania jest zmienna całkowita 'a'. Ze strumienia pobierane są znaki '3' i '2' i zmienna 'a' przybiera wartość 32. Następnie podejmowana jest próba wczytania liczby zmiennoprzecinkowej 'b', lecz w strumieniu nie ma już liczby, lecz tekst "ala ma kota".
W sytuacji, w której dane wejściowe nie pasują do oczekiwanego formatu, strumień przechodzi w stan zepsuty. W stanie zepsutym wszystkie próby odczytu ze strumienia są ignorowane (podobnie z zapisem). Zatem w naszym przykładzie po wczytaniu zmiennej 'a' strumień przechodzi do stanu zepsutego i zmienne 'b' i 'c' nie zmieniają swojej wartości, bo strumień zepsuty ignoruje żądania, które do niego wysyłamy. To, czy strumień jest w stanie dobrym (wszystko OK), czy w stanie zepsutym (coś poszło nie tak) możemy sprawdzić wołając metodę (funkcję) good(), np:
if(cin.good()) cout << "strumien cin jest w stanie dobrym";

else cout << "strumien cin jest w stanie zepsutym";


jeśli po każdym odczycie ze strumienia będziemy sprawdzać, czy nie jest on zepsuty będziemy mieć kontrolę nad poprawnością operacji odczytu (podobnie dla zapisu).
if( ! moj_strumien.good()) ... // jeśli coś poszło nie tak, to...
Co dalej ze strumieniem, który jest zepsuty? Strumień możemy naprawić wywołując metodę clear(), np.
cin.clear(); // cin jest teraz w stanie dobrym

Niestety, nawet po naprawieniu strumienia dane, które spowodowały jego przejście do stanu zepsutego nadal pozostają w strumieniu, tak więc próba ponownego ich odczytania znów zakończy się niepowodzeniem (i zepsuciem strumienia), chyba że zmienimy typ wczytywanych danych na odpowiedni. Dane będące przyczyną kłopotu te powinniśmy ze strumienia usunąć, co jednak okazuje się zadaniem nietrywialnym (bo nie wiadomo ile trzeba ich usunąć). Na razie nie będziemy się zajmować tym zagadnieniem, wystarczy nam podejście, w którym przy napotkaniu kłopotów po prostu kończymy pracę awaryjnie, nie przejmując się tym, że strumień pozostał zepsuty.


Podczas czytania możemy natrafić na koniec pliku związanego ze strumieniem. To, czy w strumieniu nie ma już danych możemy sprawdzić za pomocą metody eof():
if(moj_strumien.eof()) ... // nie ma już danych w strumieniu moj_strumien
Należy tu jednak wykazać sporo ostrożności – o tym, że w strumieniu nie ma już danych można się przekonać tylko po próbie wczytania danych. Innymi słowy, sprawdzenie eof() nie mówi nic o tym, czy odczyt kolejnej danej powiedzie się, czy nie (!) – mogłoby się na przykład zdarzyć tak, że jesteśmy na końcu pliku, a w ułamek sekundy później ktoś inny coś do tego pliku dopisze. Wobec powyższego jedyny słuszny sposób korzystania z eof() to taki, w którym po wczytaniu danej sprawdzamy, czy próba się powiodła:
moj_strumien >> dana;

if(moj_strumien.eof()) ... // 'dana' nie ma sensu, bo byliśmy na końcu strumienia

else ... // 'dana' odczytana w porządku
Uwaga: strumień, w którym osiągnęliśmy już koniec przechodzi również w stan zepsuty (może niezgodnie z intuicją), zatem to, że strumień jest zepsuty nie musi jeszcze oznaczać kłopotów – warto też sprawdzić wtedy, czy strumień nie jest czasem eof(). Podsumowując:


good()

eof()

co to oznacza

false

false

stało się coś niedobrego, strumień zepsuty i nie na końcu pliku

false

true

strumień zepsuty, ale dlatego że dotarliśmy do końca pliku

true

false

wszystko dobrze, strumień w porządku, nie jesteśmy na końcu pliku

true

true

sytuacja niemożliwa

Na razie zajmowaliśmy się głównie strumieniami predefiniowanymi cin i cout. Na co dzień będziemy korzystać ze strumieni związanych z plikami. Strumienie takie mogą być wejściowe (czytanie plików), wyjściowe (zapisywanie do plików) bądź wejściowe-i-wyjściowe. Strumienie takie deklarujemy jak w przykładzie:


ifstream plik_we("plik1.txt"); // plik do odczytu

ofstream plik_wy("plik2.txt"); // plik do zapisu

fstream plik_wewy("plik3.txt"); // plik do odcz./zapisu [nie zadziała w g++ 2.95]
W ten sposób zadeklarowaliśmy strumień wejściowy o nazwie plik_we stowarzyszony z plikiem dyskowym "plik1.txt", strumień wyjściowy plik_wy stowarzyszony z plikiem dyskowym "plik2.txt" i strumień we-wy stowarzyszony z plikiem dyskowym "plik3.txt". Ze strumieni takich możemy czytać i pisać do nich tak, jak z/do predefiniowanych strumieni, np:
plik_we >> a >> b; // odczytaj 'a' i 'b' z plik1.txt

plik_wy << "Pi wynosi " << 3.14 << endl; // zapisz do plik2.txt


Pliki związane ze strumieniami są otwierane w momencie utworzenia zmiennej strumienia i zamykane automatycznie w momencie, w którym czas życia zmiennej strumienia kończy się. Gdy chcemy wymusić wcześniejsze zamknięcie strumienia, korzystamy z metody close(). Przykładowo poniższa funkcja zapisująca jedną liczbę do pliku "zapis.txt" otwiera i zamyka ten plik za każdym wywołaniem, więc za każdym razem tworzony jest on na nowo (domyślne zachowanie plików wyjściowych).
void zapis(int liczba) {

ofstream plik("zapis.txt"); // tu plik jest tworzony za każdym razem

if(!plik.good()) return; // czy kłopoty z otwarciem?

plik << liczba << endl; // zapisujemy



} // tu plik jest zamykany, bo zmienna 'plik' kończy życie

Uwaga: Aby skorzystać ze strumieni związanych z plikami należy do programu dołączyć plik nagłówkowy fstream za pomocą dyrektywy #include na początku programu.


1 Ściśle rzecz biorąc, zmienne lokalne typów wbudowanych nie są zerowane, a zmienne globalne są – służy to zwiększeniu wydajności, składając odpowiedzialność za inicjalizację zmiennych na barki programisty.

2 Nie do końca prawda.

3 Fanatycy standardu mogą się z tym nie zgodzić, twierdząc że porażkę trzeba zasygnalizować zwracając wartość EXIT_FAILURE.

4 Jeśli mowa o kompilatorze g++, to przez "stare" rozumiemy starsze od 3.0, a przez "nowe" 3.0 i nowsze. Wersję kompilatora możemy sprawdzić pisząc "g++ --version".

5 No, prawie dowolnego. Pewne typy definiowane przez użytkownika mogą nie spełniać wymagań nakładanych na elementy wektora, które muszą być np. kopiowalne. Na razie ograniczenie to jest dla nas nieistotne.

6 Próba taka skutkuje tzw. zachowaniem niezdefiniowanym. Przy odrobinie szczęścia program natychmiast się zakończy, ale tak naprawdę nie wiadomo co się stanie, tj. język C++ nie precyzuje jak tragiczne będą skutki. W szczególności program może "wydawać się działać", co nie znaczy że działa :).

7 Zaawansowani programiści mogą wyłapać wyjątek, który jest wtedy zgłaszany i ustrzec się przed zakończeniem programu, podejmując w zamian jakieś ambitniejsze działania.

8 Ściścle rzecz biorąc funkcja ta jest zadeklarowana w przestrzeni nazw std.





©operacji.org 2019
wyślij wiadomość

    Strona główna