Słowa kluczowe
Słowa kluczowe to słowa które mają zastrzeżoną nazwę oraz specjalne znaczenie w danym języku programowania.
Wykaz słów kluczowych języka c++ wraz z krótkim opisem:
SŁOWO |
OPIS |
Wstawianie kodu w asemblerze |
|
Klasa zmiennej lokalnej |
|
Typ zmiennej |
|
break |
Przerywa wykonywanie pętli (for, while) oraz instr. switch |
case |
Wskazuje na warunek insturkcji switch |
catch |
Wyłapuje wyjątek |
Typ zmiennej |
|
class |
Deklaracje klas |
Klasa zmiennych, deklaracja stałych funkcji |
|
continue |
Wykonanie kolejnej iteracji pętli |
default |
Wskazuje na dowolny warunek insturkcji switch |
delete |
Zwalnianie pamięci przydzielonej dynamicznie |
do |
Tworzenie pętli do-while |
Typ zmiennej |
|
else |
Alternatywa dla instr. if, gdy warunek nie jest spełniony |
enum |
Typ zmiennej |
Klasa zmiennej |
|
Typ zmiennej |
|
for |
Pętla |
friend |
Wskazuje zaprzyjaźnioną klasę lub funkcję |
goto |
Skok bezwarunkowy |
if |
Instrukcja warunkowa |
Wstawia kod funkcji w miejscu jej wywołania |
|
Typ zmiennej |
|
Kwalifikator zmiennej |
|
namespace |
Przestrzeń nazw |
new |
Przydziela pamięć dynamicznie |
operator |
Przeciążanie operatorów |
private |
Stopień ochrony danych w klasie |
protected |
Stopień ochrony danych w klasie |
public |
Stopień ochrony danych w klasie |
Klasa zmiennej |
|
return |
Zwracanie wartości przez funkcje |
Kwalifikator zmiennej |
|
Kwalifikator zmiennej |
|
sizeof |
Zwraca rozmiar obiektu (typu) w bajtach |
Klasa zmiennej, funkcje statyczne |
|
struct |
Deklaracja struktur |
switch |
Rodzaj instrukcji warunkowej |
template |
Tworzenie wzorców |
this |
Wskaźnik dla klas |
throw |
Rzucanie wyjątkiem |
try |
Przechwytywanie wyjątków |
typedef |
Tworzenia nowego typu danych |
union |
Deklaracja do definiowania unii |
Kwalifikator zmiennej |
|
using |
Wybór przestrzeni nazw |
virtual |
Deklarowanie metod wirtualnych klasy |
Typ zmiennej |
|
Klasa zmiennej |
|
wchar_t |
Typ zmiennej |
while |
Rodzaj pętli |
Kilka słów o c++
C++ jest językiem wysokiego poziomu pozwalającym na programowanie proceduralne, obiektowo-zorientowane oraz generyczne. Opracował go Bjarne Stroustrup na bazię języka C.
Kody źródłowe w c++ wymagają kompilacji przeprowadzanej przez specjalne programy (kompilatory), które tworzą na ich bazie odpowiedni kod maszynowy.
Zatem na początku musimy zaopatrzyć się w jeden z kompilatorów c++. Osobiście polecam g++ pod linuksa oraz Dev-C++ i djgpp pod windows/dos. Są to programy całkowicie darmowe do których odnośniki zamieszczone są w dziale linki.
Pierwszy program
Na początku chciałbym przedstawić bardzo prosty program, na przykładzie którego omówię budowę programu w c++.
1
2
3
4
5
6
7
8
#include <iostream> /* plik nagłówkowy */
using namespace std;
int main() //główny program
{
cout << "Hello world!"; //wypisanie na standardowe wyjście
return 0; //koniec programu - zwracam 0
}
Przeanalizujmy zatem kod programu. Pierwsza linia
1
#include <iostream> /* plik nagłówkowy */
rozpoczyna się od dyrektywy preprocesora. Są to polecenia dla preprocesora, które wykonywane są przed kompilacją kodu programu. Dyrektywa #include nakazuje preprocesorowi dołączenie w tym miejscu kodu pliku podanego po dyrektywie. W tym przypadku dołączony zostanie plik nagłówkowy "iostream" ponieważ będziemy używać strumieni do wypisania tekstu.
Komentarze
Komentarze to tekst w kodzie źródłowym, który nie podlega kompilacji a jedynie pozwala programiście komentować kod. Komentarze znacznie ułatwiają późniejsze utrzymywanie kodu, pomagają także zrozumieć działanie programu innym.
W c++ istnieją 2 sposoby wstawiania komentarzy:
...
/*pierwszy sposób komentowania kodu
kolejna linia komentarza
a to już ostatnia */
...
//to jest drugi sposób wstawiana komentarza
...
Jak widać pierwszy sposób polega na wstawieniu komentarza pomiędzy /*(otwarcie komentarza) i */ (zamknięcie komentarza). Wszystko, co jest zawarte pomiędzy tymi znakami jest traktowane jako komentarz.
Drugi rodzaj komentarza pozwala na podanie komentarza, który rozpoczyna się od sekwencji znaków //, a kończy się wraz ze znakiem nowej linii.
UWAGA: Nie powinno się zagnieżdżać komentarzy pierwszego rodzaju!
Należy pamiętać, że komentarz zaczyna się od sekwencji znaków /* i kończy po wystąpieniu pierwszej sekwencji znaków kończących komentarz */, a więc poniższe zagnieżdżenie komentarzy jest nieprawidłowe:
...
/* początek komentarza 1
komentarz 1
/* początek komentarza 2
komentarz 2
koniec komentarza 2 */
komentarz 1
koniec komentarza 1*/
...
Wróćmy do analizowania kodu naszego programu:
2
using namespace std;
Na razie przyjmijmy, że gdy dołączamy plik nagłówkowy "iostream" to ta linia jest zalecana (a nawet wymagana) ale to wyjaśnię nieco później.
Teraz najważniejsza linia naszego programu, definicja głównej funkcji programu:
1
2
3
4
int main()
{ //ciało funkcji
...
}
Funkcje zostały dokładniej opisane w oddzielnej części kursu, jednak po krótce wyjaśnię budowę funkcji "main". Otóż słowo kluczowe int określa typ zwracanej wartości przez daną funkcję. Dla funkcji "main" będzie to zawsze int.
Następnie podawana jest nazwa funkcji (w tym przypadku jest to "main", czyli główna funkcja programu). Nazwa ta jest zastrzeżona i nie można jej zmienić. Funkcja "main" jest wywoływana przez powłokę, która przekazuje jej także argumenty.
Nawiasy "()" oznaczają, że mamy do czynienia z funkcją.
Następnie następuje otwarcie bloku (znaki "{" - otwarcie, "}" - zamknięcie bloku).
Blok kodu to jedyne miejsce gdzie można umieszczać instrukcje programu.
6
cout << "Hello world!";
Funkcja "cout" pozwala nam na wypisanie tekstu poprzez strumienie na standardowe wyjście - najczęściej jest to konsola.
Dzięki tej funkcji możemy wypisywać nie tylko tekst ale także wartości zmiennych, co można będzie zobaczyć w kolejnych częściach kursu.
7
8
return 0; /* zwrócenie wartości 0 - (zazwyczaj
oznacza poprawne wykonanie programu) */
Jak każda funkcja, (prawie każda ;), funkcja "main" musi zwracać wartość odpowiedniego dla niej typu, czyli w tym przypadku jest to liczba całkowita typu int.
Na koniec mała ale bardzo istotna uwaga:
WSZYSTKIE instrukcje w c++ oraz deklaracje zmiennych muszą kończyć się znakiem ";"!
W ten sposób poznaliśmy ogólną budowę programu w c++. Myśle, że nie jest ona skomplikowana.
Do czego służą zmienne?
Zmienne pozwalają nam na przechowywanie danych w programie. Jest kilka podstawowych typów zmiennych, które umożliwiają przechowywanie różnego rodzaju informacji.
Deklaracja zmiennych
Zmienne deklarujemy w następujący sposób:
<modifikator> <typ> nazwa_zmiennej;
Podstawowe typy zmiennych:
char - zmienna przechowuje znaki (litery, cyfry, znaki interpunkcyjne). Za pomącą tego typu zmiennej można także przechowywać niewielkie liczby.
int - zmienna służy do przechowywania liczb całkowitych.
bool - zmienna służy do przechowywania wartości logicznych true/false (prawda/fałsz).
float - zmienna przechowuje liczby rzeczywiste (zmiennoprzecinkowe - do 7 cyfr po przecinku).
double - zmienna przechowuje liczby rzeczywiste podobnie jak powyższy typ ale posiada dużo większą dokładność (do 15 miejsc po przecinku).
Nie będę omawiał sposobu przechowywania liczb rzeczywistych w pamięci komputera bo jest to nieco zagmatwane ale zaznaczę tylko, że wraz ze wzrostem wartości liczby rzeczywistej precyzja jej zapisu maleje.
Np. liczba 1234567.01234 utraci większą część ułamkową niż liczba 7.01234, która będzie zapisana z nieco większą dokładnością.
C++ pozwala także na użycie pewnych kwalifikatorów przed typem zmiennej:
signed - zmienna może przechowywać wartości dodatnie i ujemne (zmienna posiada znak +/-).
unsigned - zmienna może przechowywać tylko wartości dodatnie.
short - zmienna jest typu krótkiego - wpływa na długość zajmowanej pamięci (a więc również na zakres zmiennej).
long - zmienna jest typu długiego.
Istnieje także możliwość określenia klasy zmiennej. Należy jednak podkreślić, że TYP ZMIENNEJ (char,int itp.) decyduje o sposobie interpretacji przechowywanych w pamięci bitów, natomiast KLASA ZMIENNEJ decyduje o sposobie przechowywania zmiennej w pamięci. W C++ występują następujące klasy zmiennych:
const - stała - nie można zmieniać wartości raz nadanej
auto - przydział miejsca w pamięci dla zmiennej następuje dynamicznie (na stosie) w czasie wykonywania bloku, w którym zmienna została zadeklarowana. Po zakończeniu wykonywania instrukcji z danego bloku pamięć po zmiennej zostaje zwolniona.
Jest to domyślna klasa zmiennych.
register - zmienne lokalne (tzn. dostępne tylko w bloku, w którym zostały zadeklarowane). Zmienne te są umieszczane w miarę możliwości w rejestrach procesora. Jeżeli w programie pojawi się więcej niż 2 zmienne tej klasy to zostaną one umieszczone na stosie. Praktyczne zastosowanie znajduje np. jako licznik w pętlach, co przyspiesza działanie programu.
extern - jeżeli zmienna została - raz i TYLKO RAZ - zadeklarowana w pojedynczym segmencie programu, zostanie w tym segmencie umieszczona w pamięci i potraktowana podobnie jak zmienna klasy static. Po zastosowaniu w innych segmentach deklaracji tej samej zmiennej jako extern, zmienna ta może być używana również w tamtych segmentach programu.
volatile - oznacza zmienną, z której mogą korzystać także inne procesy. Oznacza to np., że inny proces może zmieniać wartość tej zmiennej przez co przed każdym jej użyciem musi zostać odczytana na nowo.
Deklarując zmienne musimy pamiętać, że kompilator rozróżnia małe i duże litery. Zatem zmienna A nie jest zmienną a i vice versa; Zmienne NIE mogą mieć nazwy, która jest jednym ze słów kluczowych.
Przykłady deklaracji zmiennych:
int a; //zmienna typu całkowitego
const double PI = 3.1415926535; //stała - liczba rzeczywista
const unsigned short s = 123; /* zmiennej klasy 'const' musi zostać
nadana wartość przy deklaracji */
char znak = 'A'; /* wartość liczbowa zmiennej "znak" to 65 ponieważ
literze 'A' odpowiada 65 w kodzie ASCII */
Typ void
Dane typu void są to dane "niezdefiniowane". Można im przypisywać wartość tylko po określeniu rozmiaru i przydzieleniu pamięci. Ten typ wykorzystuje przy funkcjach, które nie zwracają wartości, a także jako argumenty funkcji które mogą być różnego typu ale o tym nieco później.
Na koniec mała tabela, określająca rozmiar zmiennej oraz zakres możliwych wartości:
Typ zmiennej: |
Rozmiar: |
Zakres wartości: |
(signed) char |
1B |
-128 ÷ 127 |
unsigned char |
1B |
0 ÷ 255 |
(signed) short |
2B |
-32768 ÷ 32767 |
unsigned short |
2B |
0 ÷ 65535 |
(signed) int |
4B |
-2147483648 ÷ 2147483647 |
unsigned int |
4B |
0 ÷ 4294967295 |
float |
4B |
1,2E-38 ÷ 3,4E+38 |
double |
8B |
2,2E-308 ÷ 1,8E+308 |
long double |
10B |
3,4E-4932 ÷ 1,2E+4932 |
Operatory
W każdym języku programowania istnieją sposoby przypisywania zmiennym wartości, wykonywania działań matematycznych, itp. Służą do tego specjalne znaki zwane operatorami.
Znakiem który pozwala na przypisanie zmiennej wartości jest operator przypisania czyli znak "=":
int liczba;
liczba = 17;
Operatory arytmetyczne
* |
operator mnożenia |
/ |
operator dzielenia |
% |
operator dzielenia modulo |
+ |
operator dodawania |
- |
operator odejmowania |
Bardzo podobne operatory do powyższych to:
*= |
pomnóż przez |
/= |
podziel przez |
%= |
podziel modulo przez |
+= |
dodaj |
-= |
odejmij |
Zapis ten jest skróconym zapisem:
int liczba = 16;
liczba += 3; //to samo co: liczba=liczba+3;
liczba %= 5; //liczba ma teraz wartosc 19(mod)5 = 4
Operatory inkrementacji
Operatory inkrementacji i dekrementacji opisane są w dziale "inkrementacja".
Operatory bitowe
Operatory bitowe operują na bitach zmiennych.
<< |
przesuń bity w lewo |
>> |
przesuń bity w prawo |
~ |
operator negacji bitowej |
& |
koniunkcja bitowa |
| |
alternatywa bitowa |
^ |
różnica bitowa |
wersje skrócone |
|
<<= |
operator przypisania |
>>= |
operator przypisania |
~= |
operator przypisania |
&= |
operator przypisania |
|= |
operator przypisania |
^= |
operator przypisania |
Operator przesunięcia bitów w lewo przesuwa bity w lewo o wskazaną liczbę miejsc, a puste miejsca z prawej strony uzupełnia zerami. Bity które po przesunięciu z lewej strony nie "mieszczą" się już w zmiennej są kasowane:
00011010 - bity początkowe
01101000 - bity po operacji <<2
Operator przypisania <<= działa podobnie jak np. analogicznie jak operatory "+" i "+=".
short a=3;
a <<= 1; /* jest to równoważne pomnożeniu liczby przez 2^1 ponieważ
przesuwamy bity o 1 pozycję w lewo a jest to system dwójkowy */
Kolejnym operatorem bitowym jest operator negacji ~, który zamienia zera na jedynki, a jedynki na zera.
001011001 - przed negacją
110100110 - po negacji (~)
Operator koniunkcji bitowej & ustawia dany bit na 1 wtedy i tylko wtedy gdy oba bity na danej pozycji maja wartość 1:
01011100
10011010
----------- &
00011000
Alternatywa bitowa | ustawia bit na 1 gdy minimum jeden z bitów na danej pozycji ma wartość 1:
11000101
10010001
----------- |
11010101
Różnica bitowa ustawia bit na 1 gdy tylko jeden z bitów jest 1:
10011011
11001010
----------- ^
01010001
Operatory logiczne
&& |
logiczne "i" |
|| |
logiczne "lub" |
! |
negacja logiczna |
Operatory logiczne operują na wartościach logicznych (prawda/fałsz).
Operator && zwraca wartość true (prawda) wtedy i tylko wtedy gdy oba argumenty mają wartość logiczną true.
Operator || zwraca true gdy przynajmniej jeden z argumentów ma wartość true.
Operator ! neguje wartość logiczną, a więc gdy argument ma wartość true to zwraca false, a gdy false to zwraca true.
bool a,b,c;
a = true;
b = false;
c = a && b; //c ma wartość false
c = a || b; //c ma wartość true
c = !a; //c ma wartość false
Operatory relacji
== |
operator porównania |
!= |
operator nierówności |
> |
operator większości |
>= |
większe bądź równe |
< |
operator mniejszości |
<= |
mniejsze bądź równe |
Operatory relacji zwracają wartości logiczne true/false (liczbowo 1/0).
Operator porównania == zwraca wartość true wtedy i tylko wtedy gdy oba argumenty mają tą samą wartość.
Operator nierówności != zwraca wartość true wtedy gdy oba argumenty różnią się co do wartości (nie są sobie równe).
Operator > (>=) zwraca wartość true wtedy pierwszy z argumentów (ten po lewej stronie :) ma wartość większą (większą bądź równą) od drugiego argumentu.
Analogicznie działają operatory < oraz <=.
int a,b;
a = 15;
b = a + 3;
bool w = a > b; //w przyjmie wartość false
w = a >= (b-3); //a teraz wartość true
Operator rozmiaru
Operator rozmiaru to sizeof zwraca rozmiar danego wyrażenia lub typu w bajtach.
Dzięki temu operatorowi możemy na przykład sprawdzić ile bajtów w pamięci zajmuje zmienna (typ zmiennej).
#include <iostream>
using namespace std;
int main()
{
int a;
cout << "Romiar typu char: " << sizeof(char) << endl
<< "Romiar typu int: " << sizeof(int) << endl;
return 0;
}
Operatory selekcji
. |
operator selekcji |
-> |
operator selekcji wskaźników |
.* |
operator selekcji klas |
->* |
operator selekcji wskaźników klas |
Co prawda nie omawiałem jeszcze struktur danych ale zamieszczam już przykład:
struct X { //deklaracja struktury X
int liczba; //pola struktury X
int *wskaznik; //pola struktury X
X(): liczba(0), wskaznik(&liczba) { } //konstruktor struktury
};
X str, *wskstr; //deklaracja zmiennej typu X i wskaźnika do niej
wskstr = &str; //przypisanie wskaźnikowi adresu zmiennej str
cout << str.liczba << endl;
str.liczba = 5; //odwołanie do pola 'liczba' zmiennej str
cout << str.liczba << endl;
wskstr->liczba += 7; //odwołanie do pola przez wskaźnik do struktury
cout << str.liczba << endl;
int X::*pl; //deklaracja wskaźnika do pola
pl = &X::liczba; //przypisanie wskaźnikowi adresu pola
str.*pl += 3; /* odwołanie do pola 'liczba' zmiennej str
przez wskaźnik do pola */
cout << str.liczba << endl;
wskstr->*pl += 20; /* odwołanie do pola 'liczba' przez wskaźnik
do pola dla wskaźnika do struktury ;D */
cout << str.liczba << " = " << *(str.wskaznik) << endl;
Chyba trochę zamieszałem ale jest to nieco zagmatwane.
Operatory pamięci dynamicznej
Operatory pamięci dynamicznej new, new[], delete i delete[] oraz & i * zostały opisane w osobnej części kursu.
Co to są funkcje?
Funkcje w programowaniu przypominają funkcje w matematyce. Np. funkcja sinus pobiera jako argument dowolną liczbę rzeczywistą i zwraca wartość rzeczywistą z przedziału [-1,1]. Podobnie funkcje w c++ mogą pobierać pewne argumenty i zwracać określony typ wartości.
Funkcję deklarujemy w następujący sposób:
<typ_zwracanej_wartości> nazwa_funkcji( <argumenty_funkcji> );
Na początku wskazujemy jaki typ wartości będzie zwracany przez funkcję. Może to być jeden z typów podstawowych (char, int) jak również typy złożone (struktury i klasy).
Następnie podajemy nazwę funkcji a po niej w nawiasach po przecinku wskazujemy jakie argumenty będzie przyjmować.
Funkcje, które nie zwracają żadnej wartości deklarujemy jako void:
#include <iostream>
using namespace std;
void napis()
{ //definicja ciała funkcji
cout << "Funkcja dziala poprawnie." << endl;
}
int dodaj(int a, int b)
{
return a+b;
}
int main()
{
napis(); //wywołanie funkcji
int wynik = dodaj(5,7);
cout << "5 + 7 = " << wynik << endl;
return 0;
}
Jak widać w powyższym przykładzie wartość zwracamy poprzez słowo kluczowe return po którym podajemy wartość jaki ma zwrócić dana funkcja. Jeżeli w ciele funkcji następuje zwrócenie wartości poprzez return to funkcja jest przerywana (jeżeli dalej są jakieś instrukcje to już się nie wykonają).
Przeciążanie funkcji
C++ pozwala nam na przeciążanie nazw funkcji. Polega to na tym, że kilka funkcji może jednocześnie mieć taką samą nazwę. Funkcje te muszą różnić się przyjmowanymi argumentami, tak aby kompilator mógł wybrać odpowiednią funkcję gdy zostanie ona wywołana:
int dodaj(int a, int b)
{
return a+b;
}
double dodaj(double a, double b)
{
return a+b;
}
int main()
{
dodaj(5,4); // dodaj(int,int)
dodaj(5.0,4); // dodaj(double,double)
return 0;
}
Funkcje inline
Jeżeli definicja (nie deklaracja) funkcji jest poprzedzona słowem inline oraz funkcja nie jest rekurencyjna i nie zawiera żadnych pętli to w miejscu wywołania tej funkcji kompilator wstawi jej kod, co spowoduje zwiększenie rozmiaru pliku wykonywalnego ale może spowodować także przyspieszenie jego działania.
inline int dodaj(int a, int b)
{
return a+b;
}
int main()
{
int a,b,c;
a = 5;
b = 9;
c = dodaj(a,b);
return 0;
}
Rekurencja
Rekurencja polega na odwoływaniu się funkcji do samej siebie. Należy przy tym uważać na to, aby taka rekurencja miała koniec.
Dobrym przykładem ilustrowania rekurencji jest obliczanie silni:
unsigned long silnia(unsigned a)
{
if(a <= 1) return 1;
return a*silnia(a-1);
}
Rekurencja jest mało wydajna i gdy tylko można zastąpić ją pętlą (czasem jest to bardzo trudne) należy to czynić:
unsigned long silnia(unsigned a)
{
unsigned long x=1;
while(a > 1) x *= a--;
return x;
}
Wartości domyślne funkcji
Język c++ pozwala nam na nadanie wartości domyślnych argumentom funkcji w czasie jej definicji. Wartości domyślne możemy nadawać tylko argumentom idąc od końca listy argumentów tzn.:
int dodaj(int a = 5, int b) //źle - nie nadano wartości domyślnej
{ return a+b; } //arg. b więc nie można nadać go arg. a
double dodaj(double a=3.1415, double b=2.7181) //ok
{ return a+b; }
float dodaj(float a, float b=0.5772) //ok
{ return a+b; }
int main()
{
dodaj(); //dodaj(3.1415, 2.7181)
dodaj(1); //błąd - niejednoznaczne
dodaj(1.0); //dodaj(1,2.7181)
dodaj(float(1)); //dodaj(1,0.5772)
return 0;
}
Odwołując się do funkcji z parametrami domyślnymi możemy zatem pomijać TYLKO parametry końcowe o ile mają one nadane wartości domyślne.
Funkcje statyczne
Jeżeli deklaracja funkcji globalnej jest poprzedzona słowem kluczowym static to taka funkcja ma zasięg lokalny tzn., że nie możemy używać jej w innej jednostce kompilacji.
Nieco inne znaczenie ma static dla metod i pól ale o tym w dziale struktury.
Pliki nagłówkowe
W tym miejcu chciałbym wyjaśnić kwestię plików nagłówkowych. Otóż pliki te pozwalają nam deklarować prototypy funkcji, klasy (struktury), szablony i tym podobne rzeczy. Takie postępowanie często zwiększa przejrzystość kodu.
W pierwszym programie dołączyliśmy plik nagłówkowy <iostream>, który zawiera deklaracje funkcji, których będziemy używać (oczywiście nie wykorzystujemy wszystkich dostępnych tam funkji ;-).
Bez tego pliku kompilator nie widzałby jaki jest prototyp funkcji do której się odwołujemy - można sprawdzić jakie błędy wyrzuci kompilator jeśli nie dołączymy tego pliku nagłówkowego.
Jeśli przejrzymy kod pliku <iostream> zauważymy, że nie ma tam żadnych definicji funkcji (poza deklaracjami), a mimo to program wykorzystujący te funkcje działa. Jak to możliwe?
Odpowiedź jest prosta. Kompilator w czasie kompilacji nie potrzebuje definicji funcji - musi jedynie znać parametry tej funkcji oraz typ wartości zwracanej przez daną funkcję. Dopiero linker łączy skompilowany kod naszego programu z odpowiednimi bibliotekami. Biblioteki zawierają skompilowane funkcje, których prototypy znajdują się właśnie w plikach nagłówkowych. Dzięki wykorzystaniu plików nagłówkowych możemy zatem "ukryć" sposób działania danych funkcji.
Pliki nagłówkowe zazwyczaj mają rozszerzenie "*.h", rzadziej "*.hpp". Pliki te możemy dołączać do naszego programu na dwa sposoby:
#include <cstdio>
#include "moj.h"
Jeśli nazwa pliku zostanie objęta znakami <_nazwa_> to kompilator będzie szukał w katalogach wymienionych w odpowiedniej zmiennej systemowej lub w katalogach dodanych poprzez odpowiednie opcje kompilatora.
Jeśli natomiast nazwa zostanie ujęta w znaki cudzysłowia "_nazwa_" to plik będzie szukany poprzez ściężkę względem bieżącego katalogu (np. "moj.h" oznacza po prostu plik w biężącym katalogu, "../moj.h" oznacza plik w katalogu macierzystym względem bieżącego). W cudzysowie można także wpisać ścieżkę bezwzględną (np. "C:/moj.h", "/home/user/moj.h").
Definicje funkcji zadeklarowanych w plikach nagłówkowych umieszcza się w plikach "*.cpp". Na początku takiego pliku dołączamy plik nagłówkowy po czym defniujemy nasze funkcje.
Przykład użycia plików nagłówkowych:
figury.h:
#ifndef __figury_h__ /* zabezpieczenie przed wielokrotną deklaracją */
#define __figury_h__ /* dzięki temu plik może być dołączany wielokrotnie
w różnych częściach programu */
struct kolo {
double r;
double pole();
};
#endif
figury.cpp:
#include "figury.h" /* dołączamy nasz plik nagłówkowy */
double kolo::pole()
{
return r*r;
}
main.cpp:
#include "figury.h"
#include <iostream>
using namespace std;
int main()
{
kolo k;
cout << "Podaj promień koła:\t";
cin >> k.r;
cout << "Pole koła o promieniu " << k.r << " wynosi " << k.pole() << endl;
return 0;
}
Przestawiony program jest bardzo prosty i stosowanie pliku nagłówkowego raczej nie zwiększa przejrzystości kodu ale ilustruje sposób wykorzystania plików nagłówkowych.
Kompilacja np. przy użyciu g++ na platormie Linux mogłaby wyglądać następująco:
g++ -c main.cpp
g++ -c kolo.cpp
ld -o program main.o kolo.o
Co prawda to samo uzyskalibyśmy kompilująć całość komendą "g++ -o program main.cpp kolo.cpp" ale chciałem pokazać, że kompilacja pliku main.cpp może zostać wykonana bez definicji funkcji zawartych w naszym pliku nagłówkowym.
Deklaracja tablic
Bardzo często zachodzi potrzeba przechowania wielu danych tego samego typu. Do tego celu najlepiej naddają się tablice, które deklarujemy w następujący sposób:
<typ> nazwa_tablicy[ rozmiar_tablicy ]
Jako <typ> możemy podać jeden z typów podstawowych lub typ złożony (np. struktura). Następnie podajemy nazwę tablicy, a po niej w nawiasach kwadratowych rozmiar tablicy. Rozmiar tablicy musi być wartością stałą znaną w czasie kompilacji programu.
const int a = 5; //stała
int b = 10; //zmienna
int liczby[15]; //deklaracja tablicy 15-elementowej
double liczby_rzeczywiste[a]; //deklaracja tablicy 5-elementowej
char tablica[b]; //BŁĄD - nieznany rozmiar w czasie kompilacji
Do poszczególnych elementów tablicy odwołujemy się poprzez nazwę tablicy po której w nawiasach kwadratowych podajemy numer elementu tablicy.
UWAGA:
Elementy tablicy numerowane są od 0!
Jeżeli stworzyliśmy tablicę 5-elementową to ich indeksy to 0, 1, 2, 3 i 4.
Oto prosty przykład wykorzystania tablicy:
#include <iostream>
using namespace std;
int main()
{
float liczby[3];
cout << "Podaj 3 liczby:" << endl;
cin >> liczby[0] >> liczby[1] >> liczby[2];
cout << "Srednia arytmetyczna podanych liczb wynosi: "
<< (liczby[0]+liczby[1]+liczby[2])/3.0 << endl;
return 0;
}
Tablice wielowymiarowe
Tablice wielowymiarowe to tablice, które posiadają więcej niż jeden wymiar ;) Mogą to być tablice dwu, trój lub więcej wymiarowe.
Deklarujemy je podobnie to zwykłych tablic (jednowymiarowych) z tą różnicą, że podajemy kolejne wymiary tablicy w nawiasach kwadratowych. Przykłady:
int tab1[5][10]; //tablica dwuwymiarowa
char tab2[12][31][50]; //tablica o trzech wymiarach
tab1[0][0] = 17; //odwołanie do elementu tablicy
tab2[11][30][49] = 'a';
Tablice dynamiczne
Tablice opisane powyżej to tablice statyczne, których rozmiar musi być znany już w czasie kompilacji programu. Często jednak zachodzi potrzeba tworzenia tablic, których rozmiar określany jest dopiero w czasie wykonywania programu.
Do tego celu służą tablice dynamiczne, które zostały opisane w osobnym dziale.
Przechowywanie tekstu
W języku c++ tekst można przechowywać w tablicach składających się z elementów typu char.
Jest to metoda stosowana w języku c ale w c++ bardzo często korzysta się z tego rozwiązania.
Deklaracja takich tablic może wyglądać następująco:
char napis1[15]; //deklaracja napisu o długości 15 znaków
char napis2[] = "To jest napis2"; /*deklaracja napisu (następuje auto-
matyczne przypisanie tekstu zmiennej oraz określenie jej długości) */
char *napis3 = "A to jest napis3"; //podobne do powyższej metody
Ostatnia deklaracja wykorzystuje wskaźnik. Z 'napis3' korzysta się tak samo jak z 'napis1' i 'napis2' (sama nazwa zadeklarowanej tablicy np. 'napis1' bez nawiasów kwadratowych oznacza wskaźnik do tablicy elementów typu char).
UWAGA:
Każdy napis w c++ powinien kończyć się znakiem o kodzie 0!
char napis[] = "tekst";
Powyższa tablica znaków będzie miała długość 6 (5 liter + znak '\0').
Jest to bardzo ważne i należy o tym pamiętać zarówno podczas deklaracji jak i korzystaniu z tego rodzaju napisów.
Przypisywanie tekstu
Przypisywanie tekstu wcale nie jest takie proste jak mogłoby się to wydawać na pierwszy rzut oka. Weźmy taki oto przykład:
#include <iostream>
using namespace std;
char* funkcja()
{
char str[] = "to jest przykładowy tekst"; //deklaruje napis
return str;
}
int main()
{
char *napis;
napis = funkcja();
cout << napis << endl;
return 0;
}
Mogłoby się wydawać, że napis zostanie wyświetlony prawidłowo. Proszę jednak sprawdzić, że wcale tak nie musi być i najprawdopodobniej wcale tak nie będzie.
Funkcja 'funkcja()' zwraca wskaźnik do tablicy znaków, który jest następnie przypisywany zmiennej 'napis' (wskaźnik do tablicy znaków - o wskaźnikach będzie nieco później).
Następnie poprzez funkcję 'cout' wypisujemy "zawartość" zmiennej 'napis' (pamiętajmy, że tekst kończy się gdy napotkany zostaje pierwszy znak '\0'). Jednak wskaźnik zwrócony przez 'funkcja()' odnosił się do zmiennej lokalnej tej funkcji a więc do tablicy znaków która nie jest już dla nas dostępna w danym bloku programu (pamięć dla tablicy znaków 'str' mogła zostać już zwolniona i wykorzystana do innych celów) zatem odwołujemy się do przypadkowej wartości w pamięci komputera.
Program wypisałby poprawnie napis gdybyśmy poprzedzili deklarację zmiennej 'str' słowem kluczowym static ponieważ wtedy zmienna ta otrzymałaby stałe miejsce w pamięci programu przez co wskaźnik do niej pozostawałby poprawny przez cały czas wykonywania się programu.
Jak przypisywać wartości tekstowe?
Odpowiedź jest prosta. Daną tablice znaków należy skopiować w całości do nowej zmiennej. Można to wykonać samemu jednak lepiej posłużyć się stworzoną do tego celu funkcją 'strcpy()', której deklaracja znajduję się w pliku nagłówkowym <cstring>:
char* strcpy (char*, const char*)
Funkcja pobiera dwa argumenty. Pierwszy to wskaźnik do tablicy do której będzie skopiowany tekst, a drugi to wskaźnik do tablicy z której będzie kopiowany tekst aż do napotkania pierwszego znaku '\0'.
Funkcja zwraca wskaźnik do tablicy do której kopiowano tekst.
Przy używaniu tej funkcji należy uważać na to, aby tablica docelowa pomieściła (miała odpowiedni rozmiar) kopiowany do niego tekst - w innym przypadku nastąpi zapis do przypadkowej części pamięci co w najlepszym przypadku może skończyć się nadpisaniem innej zmiennej jednak częściej spowoduje to wysypanie się programu.
W pliku <string> są również zadeklarowane inne funkcje do posługiwania się łańcuchami znaków takie jak 'strncpy()', 'strcat()', 'strchr()' czy 'strcmp()'. Nie będę ich tu wszystkich opisywał, funkcje z tej jak i wielu innych bibliotek zostały już obszernie opisane (proszę pytać wujka google o dokumentację :-).
Klasa string
Wygodnym sposobem na posługiwanie się napisami w c++ jest skorzystanie z gotowych struktur, które znacznie ułatwiają operacje na nich poprzez przeciążenie odpowiednich operatorów.
Polecam używanie struktury 'string', która została zaimplementowana w STL'u (Standard Template Library).
Typ wyliczeniowy
Typ wyliczeniowy to dość specyficzny typ zmiennych. Typ ten jest nieco podobny to typu int jednak nie można go używać wymiennie. Typ wyliczeniowy pozwala nam na przyporządkowanie pewnym nazwom wartości liczbowych, które mogą być stosowane wymiennie z tymi wartościami.
Nazwy te stają się wartościami typu w którym zostały określone.
Deklaracja typu wyliczeniowego wygląda następująco:
enum TYP_ZMIENNEJ {nazwa1 = warotsc1, nazwa2 = wartosc2} nazwa_zmiennej;
Po słowie kluczowym enum podajemy TYP_ZMIENNEJ, który będzie służył do deklarowania zmiennych tego typu.
Następnie w nawiasach klamrowych {} podajemy po przecinku nazwy (można im nadać wartość stosując do tego znak równości). Jeżeli danej nazwie nie przyporządkujemy wartości jawnie to kompilator przypisze wartość o 1 większą niż wartość poprzedniej nazwy.
Następnie możemy umieścić także nazwę zmiennej danego typu wyliczeniowego (możemy podać kilka nazw zmiennych po przecinku).
Na samym końcu deklaracji musimy umieścić średnik!
Mam nadzieję, że przykład wyjaśni użycie tego typu zmiennej:
enum POMIESZCZENIA {przedpokoj,kuchnia = 3,lazienka,jadalnia = -3,sypialnia} dom;
POMIESZCZENIA dom2;
dom2 = kuchnia; //to samo co: dom2=3;
dom = -2; //to samo co: dom=sypialnia;
Mały komentarz:
W pierwszej linii deklarujemy typ zmiennej wyliczeniowej, którego nazwą będzie słowo 'POMIESZCZENIA'. W nawiasach klamrowych podajemy nazwy kilku pomieszczeń ale tylko niektórym przyporządkowujemy wartość jawnie zatem pozostałym nazwom zostaną przyporządkowane wartości przez kompilator.
Pierwsza domyślna wartość to 0, która zostanie przyporządkowana nazwie 'przedpokoj', 'kuchnia' będzie oczywiście posiadała wartość 3, 'lazienka' wartość 4 (ponieważ poprzednia wynosiła 3), 'jadalnia' -3 oraz 'sypialnia' -2.
Na końcu podajemy nazwę zmiennej 'dom'.
W drugiej linii deklarujemy zmienną 'dom2'. W kolejnych liniach korzystamy już ze zmiennych przypisując im wartości liczbowe lub stosując odpowiednie nazwy.
Należy pamiętać, że typ wyliczeniowy nie jest zwykłym typem liczbowym takim jak int, a więc nie możemy przechowywać w nim dowolnych wartości. Jego rozmiar jest bowiem zależny od zakresu wartości jakie ma przechowywać.
W powyższym przykładzie zadeklarowany typ wyliczeniowy przechowuje wartości z zakresu [-3;4].
Jeśli typ wyliczeniowy przechowuje wartości ujemne to zakresem jest najmniejsza ujemna potęga dwójki (-2^m), która jest niewiększa niż najmniejsza wartość w danym typie.
Podobnie dla liczb dodatnich zakresem jest najmniejsza ujemna potęga 2 minus 1 (2^n-1).
Zatem w naszym przypadku m=2 (-2^2 = -4), a n=3 (2^3-1 = 7). Zatem minimalny rozmiar wynosi 3 bity + 1 jako znak co daje w sumie 4 bity. Jednak kompilator zwiększy rozmiar do rozmiaru "najbliższego" używanego typu (np. char, int).
Operatory inkrementacji
Inkrementacja (ang. increment - przyrost) i dekrementacji polega na dodaniu/odjęciu do/od zmiennej jedynki.
Przykład:
#include <iostream>
using namespace std;
int main()
{
int a = 5;
cout << a << endl;
cout << ++a << endl; //preinkrementacja
cout << a++ << endl; //postinkrementacja
cout << a << endl;
return 0;
}
Powyższy program wypisze nam na ekranie:
5
6
6
7
Jak widać na powyższych przykładach rozróżniamy 2 typy inkrementacji - tzw. preinkrementację i postinkrementację. Obie powodują zwiększenie wartości zmiennej o 1 jednak jest między nimi pewna różnica.
Otóż operator preinkremencacji (++a) zwraca wartość już zwiększoną o 1 (w tym przypadku będzie to 6) natomiast operator postinkrementacji (a++) zwraca wartość zmiennej przed procesem inkrementacji (tutaj zwróci wartość 6 natomiast wartość zmiennej 'a' będzie już wynosić 7).
Dokładnie tak samo sprawa wygląda dla operatora dekrementacji - predekrementacji (--a) i postdekrementacji (a--).
UWAGA:
Gdy używamy operatora inkrementacji/dekrementacji należy zwrócić uwagę czy zwracana wartość jest w jakikolwiek sposób wykorzystywana ponieważ wtedy musimy zastosować preinkrementację albo postinkrementację.
Gdy chcemy tylko zwiększyć wartość zmiennej i NIE wykorzystujemy wartości zwróconej przez operator to możemy stosować pre i postinkrementację wymiennie.
Instrukcje warunkowe
Jak sama nazwa wskazuję są to instrukcje, które wykonują się tylko wtedy gdy spełniony jest podany warunek. Dzięki instrukcjom warunkowych program może "zachowywać" się zależnie od spełnienia pewnych warunków.
Intstrukcja if - else
Podstawowa instrukcja warunkowa wygląda następująco:
if( <warunek> )
{//blok1
...
}
else
{//blok2
...
}
Jeżeli spełniony jest warunek to wykonane zostaną instrukcje zawarte w bloku1, a gdy warunek nie jest spełniony to wykonane zostaną instrukcje z bloku2.
UWAGA:
Wartość zwracana przez warunek w instrukcjach warunkowych nie musi być typu bool ale może być także wartością liczbową i w takim przypadku warunek nie jest spełniony wtedy i tylko wtedy gdy wartość ta wynosi 0!
Gdy wartość jest różna od zera (również mniejsza od zera) to warunek jest spełniony.
Przykład:
#include <iostream>
using namespace std;
int main()
{
int x;
cout << "Podaj liczbę całkowitą:" << endl;
cin >> x;
if(x & 1) cout << "Liczba nieparzysta.";
else cout << "Liczba parzysta.";
return 0;
}
Po instrukcji warunkowej if nie musi występować słowo kluczowe else (oczywiście nie ma wtedy bloku2 ).
Jeżeli w jednym z bloków znajduję się tylko jedna instrukcja to nie musi być ona zawarta w klamrach {} (czyli nie musimy tworzyć bloku) tak jak ma to miejsce w przedstawionym przykładzie.
Operator warunkowy
Operator warunkowy bardzo przypomina konstrukcje if - else:
( <warunek> ) ? <wyrażenie1> : <wyrażenie2>;
Jeżeli spełniony jest <warunek> to operator zwróci wartość <wyrażenia1>, w przeciwnym wypadku wartość <wyrażenia2>.
UWAGA:
<wyrażenie1> nie jest zakończone średnikiem natomiast <wyrażenie2> może (a nawet musi) być zakończone średnikiem gdy wartość zwracana przez operator nie jest przekazywana jako argument.
Można sobie zadać pytanie czy operator ten jest potrzebny skoro to samo możemy zrealizować przez instrukcję if - else?
Otóż operator warunkowy pozwala w wielu sytuacjach skrócić zapis kodu i zwiększać przejrzystość kodu.
Teraz powyższy przykład zapiszmy z wykorzystaniem operatora warunkowego:
#include <iostream>
using namespace std;
int main()
{
int x;
cout << "Podaj liczbę całkowitą:" << endl;
cin >> x;
cout << ( (x & 1) ? "Liczba nieparzysta." : "Liczba parzysta.") << endl;
return 0;
}
Instrukcja switch - case
Ostatnią instrukcją warunkową jest konstrukcja switch - case, która pozwala nam na wybór więcej niż jednej opcji:
switch( <wyrażenie> )
{
case <wartość1>:
instrukcja1;
instrukcja2;
//...
break;
case <wartość2>:
instrukcja1;
instrukcja2;
//...
break;
//...
default:
instrukcja1;
instrukcja2;
//...
}
Instrukcja switch oblicza wartość <wyrażenia> i dopasowywuje go do jednej z podanych opcji.
Wszystkie opcje muszą być zawarte w bloku. Po słowie kluczowym case podajemy <wartość>, a następnie dwukropek. Po dwukropku podajemy instrukcje, które z kolei nie muszą być już zawarte w nowym bloku.
Jeżeli wartość <wyrażenia> będzie odpowiadała jednej z <wartości> to wykonywane są wszystkie instrukcje występujące po niej aż do napotkania słowa kluczowego break!
Jeżeli żadna z <wartości> nie będzie pasowała do wartości <wyrażenia> to wykonane zostaną instrukcje znajdujące się po słowie kluczowym default.
Jeżeli nie występuje opcja default i inne możliwości także nie pasują to nie zostanie wykonana żadna instrukcja.
Przyład:
#include <iostream>
using namespace std;
int main()
{
short x;
cout << "Podaj cyfrę od 0 do 5" << endl;
cin >> x;
switch(x)
{
case 0: cout << "Wybrano 0" << endl;
break;
case 1: cout << "Wybrano 1" << endl;
break;
case 2: cout << "Wybrano 2" << endl;
break;
case 3:
case 4:
case 5: cout << "Wybrano 3, 4 lub 5" << endl;
break;
default: cout << "Wybrana liczba nie zawiera się w [0,5]" << endl;
}
return 0;
}
Mam nadzieję że ten program nie wymaga już komentarza. Chciałbym tylko zwrócić uwagę na brak słowa kluczowego break po 2 instrukcjach case co pozwala nam na wykonanie tego samego kodu jeśli zostanie spełniona jedna z tych opcji.
Instrukcja goto
Instrukcja goto jest instrukcją skoku bezwarunkowego. Powoduje ona "przeskoczenie" programu do innego miejsca w tym samym bloku wskazanego etykietą:
goto <etykieta>;
//...
<etykieta>:
//...
Instrukcja skoku bezwarunkowego goto może być najczęściej zastąpiona przez pozostałe instrukcje warunkowe, może być także używana w połączeniu z nimi. Powinna być używana w sposób rozsądny i tylko tam gdzie rzeczywiście powoduje ona przyspieszenie programu bądź zwiększa jego czytelność (np. przerywanie zagnieżdżonych pętli).
Pętle
Pętle służą do wykonywania wielokrotnie tego samego bloku instrukcji.
Pętla while
Składnia:
while( <warunek> )
{
instrukcja1;
instrukcja2;
//...
}
Po słowie kluczowym while w nawiasach podajemy warunek warunek a następnie instrukcje zawarte w bloku.
Na początku sprawdzany jest warunek. Jeżeli jest on spełniony to wykonywane są instrukcje zawarte w bloku. Następnie ponownie jeśli warunek jest spełniony wykonywanie są te same instrukcje z bloku.
UWAGA:
Warunek nie jest spełniony wtedy i tylko wtedy gdy jego wartość logiczna wynosi false co liczbowo odpowiada 0 (zeru). W przeciwnym wypadku warunek jest spełniony.
Pętla kończy się jeżeli warunek nie zostanie jest spełniony (możliwe jest, że instrukcje nie zostaną wykonane ani raz jeżeli warunek nie zostanie spełniony już przy pierwszym sprawdzaniu).
Istnieją jeszcze inne możliwości przerwania pętli:
Jeżeli wewnątrz bloku pętli wystąpi słowo kluczowe break to kolejne instrukcje zawarte w tym bloku nie zostaną już wykonane i pętla kończy swoje działanie.
Instrukcja goto zawarta w bloku może spowodować skok to etykiety znajdującej się poza blokiem pętli tym samym powodując jej przerwanie.
Wewnątrz bloku pętli może znajdować się także instrukcja continue, która co prawda nie powoduje natychmiastowego przerwania pętli ale sprawia, że kolejne instrukcje zawarte w bloku nie zostaną już wykonane i nastąpi ponowne sprawdzenie warunku pętli.
Pomijam słowo kluczowe return, które oczywiście przerywa wykonywanie pętli i całej funckji ;D.
A teraz przykłady:
int i = 5;
while(i < 10) i++; //po wykonaniu pętli i = 10
while(i++) if(i==15) break; //jeśli i wynosi 15 to przerywamy wykonywanie pętli
while(true) goto etykieta1; //skok do etykiety
etykieta1:
while(i <= 20) //wyswietla liczby 15, 16, 17, 19 i 20
{
if(i==18) { i++; continue; }
cout << i++ << endl;
}
Pętla do - while
Składania:
do {
instrukcja1;
instrukcja2;
//...
}while( <warunek> );
Zwracam uwagę, że po nawiasach w których zawarty jest warunek musimy postawić średnik!
Pętla do-while bardzo przypomina pętlę while. Działa ona niemal tak samo z tą różnicą, że instrukcje zawarte w bloku są wykonane minimum jeden raz! Dopiero po ich wykonaniu sprawdzany jest warunek i zależnie od jego spełnienia pętla ponownie wykonuje dane instrukcje bądź jest przerywana.
Pętlę tą możemy przerywać tymi samymi sposobami co pętlę while.
Pętla for
Tradycyjnie zacznę od przedstawienia składni:
for(intrukcja1 ; <warunek> ; instrukcja2)
{
//instrukcje
}
Na początku wykonywana jest instruckcja1 (może być to kilka instrukcji oddzielonych przecinkami). Następnie sprawdzany jest warunek. Jeżeli jest spełniony to wykonywane są instrukcje zawarte w bloku a jeśli nie to pętla kończy swoje działanie. Po wykonaniu tych instrukcji (oczywiście jeżeli pętla nie została zakończona :-) wykonywana jest instruckcja2 (znowu może to być kilka instrukcji oddzielonych przecinkami) po której ponownie sprawdzany jest warunek.
Podsumowując:
Instruckcja1 wykonywana jest zawsze tylko raz na samym początku. Następnie sprawdzany jest warunek i wykonywane są instrukcje zawarte w bloku (war. spełniony). Instruckcja2 wykonywana jest zawsze po wykonaniu instrukcji z bloku (przed sprawdzeniem warunku) - wykonuje się zatem tyle razy ile wykonane zostały instrukcje z bloku chyba, że pętla została przerwana wewnątrz bloku (break, goto czy return).
Jeśli wewnątrz bloku wystąpi instrukcja continue to instrukcje w tym bloku występujące dalej zostaną pominięte i wykonana zostanie instruckcja2 po której sprawdzony zostanie ponownie warunek.
Chyba nieco zagmatwałem opis ostatniej pętli, ale w rzeczywistości jest to bardzo proste.
Dla formalności jeszcze krótki przykład:
for(int i=1; i<=10 ;i++) cout << i << endl;
//pętla wypisze liczby od 1 do 10 włącznie
Co to są wskaźniki
Wskaźniki to bardzo ważne pojęcie w programowaniu więc warto je zrozumieć.
Wskaźnik na zmienną danego typu jest to zmienna, która przechowuje adres zmiennej danego typu. Może brzmi to nieco niezrozumiale jest to bardzo proste. Wszystkie zmienne jakie deklarujemy są niczym innym jak tylko komórkami pamięci operacyjnej RAM (może być inaczej ale nie jest to teraz istotne). Zatem odwołując się do zmiennej poprzez jej nazwę odwołujemy się do przydzielonej jej (zmiennej) pamięci. Wartością wskaźnika jest natomiast adres pamięci RAM, gdzie znajduje się taka zmienna. Żeby wytłumaczyć jak dokładnie to działa posłużymy się prostym przykładem:
1
2
3
4
int x=1; //deklaracja zmiennej int
int *wskaznik; //deklaracja wskaźnika na typ int
wskaznik = &x; //przypisanie adresu zmiennej wskaźnikowi
*wskaźnik = 99; //zapis równoważny z "x=99;"
Pierwsza linia przedstawionego kodu jest oczywista więc nie będziemy jej spejcajlnie analizować. W drugiej lini zadeklarowaliśmy wskaźnik na typ int. Zatem jak można się domyślać deklaracja wskaźnika wygląda następująco:
<modyfikator> <typ> *nazwa;
Zatem sama deklaracja wskaźnika na zmienną danego typu różni się od deklaracji zmiennej jedynie dodatkowym znakiem '*' poprzedzającym nazwę zmiennej.
Tutaj chciałbym od razu uświadomić pewną sprawę. Niektórzy bowiem mylnie sądzą, że jeśli znak '*' następuje bezpośrednio po typie zmiennej (bez spacji) to wtedy wszystkie zmienne zadeklarowane po przecinku będą wskaźnikami. Jest to nie prawda. Jedynie pierwsza zmienna będzie wskaźnikiem na dany typ, a reszta będzie zwykłymi zmiennymi tego typu. Zalecam zatem dodawanie spacji po typie zmiennej, żeby uniknąć tego skojarzenia:
int* x,y; //zmienna x jest wskaźnikiem na typ int
//zmienna y jest zwykłą zmienną typu int !
int *a,b,*c; //zmienne a i c są wskaźnikami na int
Załóżmy teraz, że nasza zmienna 'x' znajduje się w pamięci operacyjnej pod adresem 0x00F8 (system szesnastokowy). Oznacza to, że cztery bajty (bo taki jest rozmiar int) począwszy od adresu 0x00F8 przechowują wartość zmiennej 'x'. Wartość wskaźnika ('wskaźnik') do zmiennej niech będzie pod adresem 0x01C0.
W trzecim wierszu przypisaliśmy adres zmiennej 'x' wskaźnikowi. Jak widać jeśli przed nazwą zmiennej pojawi się operator adresu czyli znak '&' to dostaniemy nie wartość jaką przechowuje ta zmienna ale adres tej zmiennej (gdzie w pamięci operacyjnej znajduję się wartość zmiennej).
Zatem wartością liczbową naszego wskaźnika będzie wartość 0x00F8 czyli adres naszej zmiennej 'x'. Teraz jeśli chcemy się odwołać do zmiennej wskazywanej przez wskaźnik to przed jego nazwą musimy umieścić znak '*'. Wtedy odwołujemy się do adresu na jaki on wskazuje czyli de facto zmiennej znajdującej się pod tym adresem (u nas jest to zmienna 'x').
Dla jasności może jeszcze skromny rysunek ilustrujący pamięć operacyjną (znaki '_' oznaczają dowolne bity, które nas w danym momencie nie interesują):
adres [ baj1 ] [ baj2 ] [ baj3 ] [ baj4 ] [ baj5 ] [ baj6 ] [ baj7 ] [ baj8 ]
0x00F0 ________ ________ ________ ________ ________ ________ ________ ________
0x00F8 00000001 00000000 00000000 00000000 ________ ________ ________ ________
0x0100 ________ ________ ________ ________ ________ ________ ________ ________
0x0108 ________ ________ ________ ________ 11111000 00000000 00000000 00000000
Jak widać w pamięci operacyjnej pod adresem 0x00F8 znajduje się wartość 1 - zakładam, że jest to system Little-Endian czyli zapis używany w architekturze komputerów x86.
W systemie tym kolejne bajty liczby zapisanej szesnastkowo 0x12345678 będą zapisane w kolejności 0x78 0x56 0x34 0x12 (najpierw najmniej znaczączy bajt, następnie drugi najmłodszy bajt, trzeci najmłodszy bajt a na końcu bajt najbardziej znaczący). Taki system zapisu może wydawać się dziwny ale ma on swoje zalety - proste rzutowanie zmiennych liczbowych.
Pod adresem 0x010C (wartość zmiennej 'wskaźnik') znajduje się wartość 0x00F8 czyli adres zmiennej 'x'. Jeśli teraz odwołaliśmy się do wskaźnika stosując przed jego nazwą znak '*' to odwołujemy się do komórki pamięci operacyjnej pod adresem 0x00F8 (bo taki adres wskazuje wskaźnik).
Znak '*' jest w tym momencie operatorem wyłuskania. Nazwa ta jak widać nie jest przypadowa bo operator ten niejako "wyłuskuje" zmienną przechowywaną pod danym adresem (czyli wartością wskaźnika).
W lini czwartej naszego programu zapisujemy więc wartość 99 pod adresem wskazywanym przez nasz wskaźnik zatem zmieniamy tym samym wartość zmiennej x.
Skoro wiemy już, że wskaźnik to jedynie liczba (adres) wskazujący gdzie w pamięli leży jakiś obiekt (zmienna) to można sobie zadać pytanie po co przy deklaracji musimy podawać typ zmiennej na jaką będzie wskazywał wskaźnik. Otóż odpowiedź jest bardzo prosta. Jest to wymuszone ścisłą kontrolą typów w C++. Gdyby wskaźnik nie miał podanego typu przy deklaracji kompilator nie miałby możliwości kotrolowania tego w jaki sposób wykorzystujemy nasz wskaźnik.
W C++ istnieje wskaźnik na dowolną zmienną ale zostanie on opisany nieco dalej.
Modyfikator const
Modyfikator const przy deklaracji wskaźnków podobnie jak przy deklaracji zwykłych zmiennych powoduje, że obiekt jest stały. Oznacza to, że nie możemy zmieniać wartości takiego obiektu.
Przy wskaźnikach możemy użyć słowa kluczowego const na dwa sposoby. Możemy deklarować bowiem wskaźnik na stały obiekt (wskaźnik na stałą) lub stały wskaźnik na obiekt (stały wskaźnik na zmienną). Już wyjaśniam o co chodzi w tym całym zamieszaniu.
Wskaźnik na stałą deklarujemy w następujący sposób:
const <typ> *nazwa;
Deklaracja taka oznacza, że wskazywaną wartość możemy jedynie odczytywać, natomiast nie wolno nam próbować zmieniać wartości wskazywanej przez wskaźnik:
int x=5,y; //deklaracja zmiennych
const int *ptr = &x;//dekaracja wskaźnika na stałą i jego inicjalizacja
y = *ptr; //poprawnie - możemy odczytywać zmienną wskazywaną przez wskaźnk
*ptr = 3; //błąd - nie wolno nam zapisywać do zmiennej wskazywanej przez wskaźnik
//mimo tego, że zmienna ta przy deklaracji nie miała modyfikatora const
Sposób deklaracji stałego wskaźnika na zmienną różni się tym, że słowo kluczowe const występuje po znaku '*':
<typ> * const nazwa;
Zapis ten oznacza, że to wskaźnik jest stały a nie obiekt przez niego wskazywany. Oznacza to, że nie wolno nam zmieniać wartości wskaźnika czyli adresu na jaki on wskazuje. Zatem przy deklaracji wskaźnika musimy go od razu zainicjować. Wolno nam za to zmieniać wartość wskazywaną przez taki wskaźnik:
int x,y;
int * const ptr = &x; //obowiązkowa inicjalizacja
*ptr = 5; //ok
ptr = &y; //błąd - nie wolno zmieniać wartości wskaźnika
Można także deklarować stałe wskaźniki na stałe. Taki wskaźnik dziedziczy cechy obu deklaracji:
int x,y;
const int * const ptr = &x;
y = *ptr; //ok - możemy czytać wskazywaną wartość
ptr = &y; //błąd - nie wolno zmieniać wartości wskaźnika
*ptr = y; //bląd - nie wolno zmieniać wskazywanej wartości
Co prawda nie omawiałem jeszcze tematu obiektów ale chciałbym już w tym miejscu wspomnieć o pewnym ograniczeniu. Otóż jeśli mamy zadeklarowany wskaźnik na stały obiekt to wolno nam jedynie wywoływać stałe metody takiego obiektu (funkcje zadeklarowane w obiekcie ze słowem kluczowym const, które zapewnia kompilator, że funkcja taka nie będzie modyfikować zmiennych zawartych w obiekcie. Omówię to dokładniej w dziale poświęconemu strukturom danych.
Operator wyłuskania
Jeśli już wspomniałem o obiektach to od razu wspomnę na temat operatora wyłuskania '->'. Operator ten pozwala nam na "wyłuskiwanie" pól obiektów na który wskazuje wskaźnik. Co prawda jego użycie możnaby zastąpić znanym nam już opratorem wyłuskania '*' ale byłoby to niewygodne - także sam kod z użyciem '->' wygląda nieco lepiej:
struct A { //deklaracja struktury A
int x;
int getx() { return x; }
} a, *ptr; //deklarujemy obiekt typu A oraz wskaźnik na struturę A
ptr = &a; //przypisujemy adres struktury do wskaźnika
(*ptr).x = 5; //przypisujemy wartość 5 polu x
ptr->x = 10; //przypisujemy polu wartość 10
Użycie nawiasu podczas dereferencji z użyciem operatora '*' jest konieczne ponieważ operator '.' ma wyższy priorytet niż operator '*'. Jeśli zatem wyrażenia '*ptr' nie otoczylibyśmy nawiasami to byłoby to równoznaczne zapisowi:
*(ptr.x) = 5; //to samo co: *ptr.x=5;
Tutaj nawiasy dałem aby pokazać jak zinterpretowałby to kompilator. Wyrzuciłby oczywiście błąd ponieważ najpierw chciałby on wyciągnąć pole 'x' od obiektu 'ptr', który nie jest obiektem typu A ale wskaźnikiem na taki obiekt.
Operator '->' pozwala nam tak samo na wywoływanie metod (tak nazywają się funkcje zawarte w strukturze):
int x = ptr->getx(); //równoważne (*ptr).getx();
Zalecam zatem stosowanie nawiasów zawsze, gdy nie pamiętamy priorytetów operatorów. Kilka niepotrzebnych nawiasów w kodzie źródłowym nie zaszkodzi skompilowanemu programowi natomiast może zwiększyć przejrzystość kodu (zwłaszcza dla mniej zaawansowanych programistów :-).
Wskaźnik uniwersalny
Sama nazwa brzmi ciekawie. Wskaźnik uniwersalny deklarujemy w następujący sposób:
void *nazwa;
Czym różni się on od zwykłego wskaźnika. Otóż jak widać w deklaracji nie podajemy na jaki typ będzie wskazywał dany wskaźnik. Pozwala nam to na przypisanie każdego typu wskaźnika.
Ale żeby nie było tak różowo to musimy być świadomi, że tracimy możliwości użycia operatorów dereferencji (wyłuskania) - '*' oraz '->'. Jest tak ponieważ, wskaźnik typu void wskazuje tylko sam adres w pamięci operacyjnej ale nie mówi kompilatorowi nic o typie wartości przechowywanej pod tym adresem. Zatem kompilator nie posiada żadnej informacji o typie, tym samym nie może wykonać operacji wyłuskania.
Tutaj z pomocą przychodzi nam operator rzutowania reinterpret_cast. Dzięki niemu możemy zamienić zmienną na wskaźnik dowolnego typu. W tym momencie to my jesteśmy odpowiedzialni za kontrolę typów. Kompilator nie może w czasie kompilacji stwierdzić czy rzutowanie takie jest poprawne i musi nam zaufać. Proszę zatem o czujność w czasie wszelkiego rodzaju rzutowania wskaźników bowiem są one przyczyną wielu błędów, które niestety często trudno jest wykryć.
unsigned x = 0x12345678;
void *p = &x;
unsigned short y = *reinterpret_cast<unsigned short*>(p);
W przykładzie tym deklarujemy wskaźnik typu void i przypisujemy mu adres zmiennej x (która jest typu unsigned int i zajmuje 4 bajty). W następnej lini deklarujemy zmienną typu unsigned short i przypisujemy jej wartość wyłuskaną ze wskaźnika zwróconego przez operator reinterpret_cast, który "przerobił" zmienną 'p' na wskaźnik do typu unsigned short. Wartość 'y' wynosić będzie 0x5678. Na maszynach z rodziny x86, gdzie stosowany jest wspomniany już system Little-Endian takie przypisane będzie poprawne mimo tego, że rozmiar zmiennej 'y' (2 bajty) jest mniejszy niż rozmiar zmiennej 'x'.
W systemie zapisu liczb Big-Endian takie rzutowanie przypisałoby zmiennej 'y' wartość 'x' przesuniętą o 16 bitów w prawo czyli wartość 0x1234.
Wskaźniki do funkcji
W języku C++ mamy możliwość deklarowania wskaźników nie tylko na zmienne ale także na funkcje. Znajdują one zastosowanie na przykład w czasie korzystania z bibliotek dynamicznych.
Sama idea wskaźnika jest taka sama jak w przypadku zwykłego wskaźnika z tym wyjątkiem, że adres wskaźnika oznacza miejsce w pamięci w którym rozpoczyna się kod funkcji.
Deklaracja wygląda następująco:
<typ> (*nazwa)( <parametry_funkcji> );
Jak widać w czasie deklaracji podajemy zarówno typ zwracanej wartości jak i wszystkie parametry jakie pobiera wskazywana funkcja. Można się zatem domyślać, że nie ma uniwersalnego wskaźnika na funkcję - wynika to między innymi z kontroli typów w C++.
Warto wiedzieć, że funkcję wskazywaną przez wskaźnik możemy wywołać na dwa sposoby:
int dodaj(int a, int b) { return a+b; }
int (*f)(int,int) = &dodaj;
int suma_5_7 = f(5,7);
//lub z użyciem operatora wyłuskania
int suma_6_7 = (*f)(6,7);
Mimo, że drugi sposób jest dłuższy i może mniej wygodny to jednak zachęcam do jego stosowania. Spowodowane jest to tym, że przeglądając kod od razu widzimy, że 'f' jest wskaźnikiem na funkcję a nie samą funkcją. Przykładowo, gdy przerabialibyśmy kod programu gdzie stosowanoby pierwszy sposób, a do programu dołączane byłoby wiele plików źródłowych to możemy nie wiedzieć, że mamy do czynienia ze wskaźnikiem a nie funckją i tracić czas na poszukiwanie definicji funkcji która nie istnieje.
Wskaźniki a tablice i funkcje
Warto wiedzieć, że nazwa tablicy statycznej jak i dynamicznej jest po prostu wskaźnikiem na elementy typu z którego składa się taka tablica:
int tab[5];
char c[5][11];
Zatem 'tab' jest typu int *, a 'c' jest typu char **. Zwracam uwagę, że c jest podwójnym wskaźnikiem (tablica jest dwuwymiarowa). Podobnie jak w języku C możemy stosować arytmetykę wskaźników. Polega ona na tym, że do wskaźnika możemy dodawać i odejmować liczby całkowite. Kompilator sam pomnoży dodawaną liczbę przez rozmiar typu wskaźnika aby dodać do adresu odpowiednią ilość bajtów.
*tab = 0; //równoznaczne z tab[0]=0
*(tab+1) = 1; //równoznaczne z tab[1]=1
**c = 'a'; //równoznaczne z c[0][0]='a'
*(c[4]+5) = '5'; //równoznaczne z c[4][5]='5'
Tablica może składać się z zadeklarowanych przez nas struktur danych i arytmetyka wskaźników będzie działać poprawnie. Przed dodawaniem/odejmowaniem liczby będą automatycznie pomnożone przez rozmiar naszej struktury więc nie należy samemu mnożyć tych wartości przez operator sizeof - kompilator zrobi to za nas.
Podobna sytuacja ma miejsce w przypadku funckji. Sama nazwa funkcji jest wskaźnikiem do funkcji o prototypie takim jaka jest ta funkcja.
int min(int a, int b) { return a < b ? a : b; }
//...
int (*f)(int,int);
f = min; //poprawnie
f = &min; //poprawnie
int m = f(5,7);
int n = (*f)(9,8);
Zastosowanie wskaźników
Mogłoby się wydawać, że wskaźniki niepotrzebnie komplikują kod i nie są nam potrzebne skoro mamy normalne zmienne. Wcześniej czy później docenimy jednak ich rolę i przekonamy się, że dają one duże możliwości programiście - pozwalają one przyspieszyć działanie programu, a ich użycie czasami może okazać się nieuniknione!
Struktury danych
Struktury danych pozwalają nam "zebrać" różne dane. Pozwala to na wygodniejsze operowanie danymi. Deklaracja struktury wygląda następująco:
struct <nazwa> {
... //deklaracja pól i metod strutury
};
Od razu wyjaśnie co to pola i metody struktury. Otóż polami struktury nazywamy wszystkie zmienne zawarte w strukturze danych, a metodami wszystkie funkcje, jakie posiada dana struktura.
Posłużmy się następującym przykładem:
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <string>
#include <iostream>
using namespace std;
struct Osoba {
string imie, nazwisko;
unsigned char wiek;
void wyswietl();
};
void Osoba::wyswietl()
{
cout << imie << " " << nazwisko << " ma " << wiek << " lat." << endl;
}
int main()
{
struct Osoba osoba;
cout << "Podaj imię, nazwisko i wiek" << endl;
cin >> osoba.imie >> osoba.nazwisko >> osoba.wiek;
osoba.wyswietl();
return 0;
}
Nasza struktura danych zawiera 3 pola: "imie", "nazwisko" i "wiek" oraz jedną metodę "wyswietl". Deklaracja funkcji nie różni się od deklaracji zwykłych funkcji. Zauważmy, że w linii 11 gdzie rozpoczyna się definicja naszej funkcji jej nazwa poprzedzona jest nazwą typu naszej struktury po której występuję podwójny dwukropek. W definicji naszej metody "wyswietl" odwołujemy się bezpośrednio do pól struktury, bowiem każda metoda posiada pełny dostęp do wszystkich pól swojej struktury.
W głównej funkcji najpierw deklarujemy obiekt typu "Osoba", czyli naszą stukturę. Następnie program pyta użytkownika o imię, nazwisko i wiek. Jak widzimy do poszczególnych pól naszej stuktury odwołujemy się poprzez postawienie kropki za nazwą zmiennej, po której podajemy nazwę żądanego pola.
Następnie wywołujemy metodę "wyswietl" na rzecz obiektu "osoba".
Konstuktor i destruktor
Konstuktor to metoda (funkcja) w strukturze danych posiadająca taką samą nazwę jak typ naszej struktury. Jeżeli obiekt nie ma jawnie zadeklarowanego konstruktora (np. w naszym poprzednim przykładzie) to jest on tworzony automatycznie w czasie kompilacji (podobnie sytacja wygląda z operatorem kopiującym tzn. metodą "operator =()").
Konstutor jest wywoływany tylko w czasie tworzenia obiektu. Jeżeli przeciążyliśmy konstruktor (zadeklarowaliśmy kilka funkcji o różnych argumentach) to o wywołaniu konkretnego konstruktora decydują argumenty podane przy definicji naszego obiektu. Kompilator tak jak w przypadku zwykłego przeciążania funkcji postara się o wywołanie konstruktora do którego najlepiej pasują podane argumenty. Jeśli ich nie ma wtedy wywoływany jest konstruktor nie posiadający argumentów.
UWAGA:
Jeśli zadeklarowaliśmy choć jeden konstruktor to kompilator nie doda już za nas konstruktora, który nie posiada żadnych argumentów. W takim przypadku musimy już jawnie zadeklarować i zdefiniować taki konstruktor. Oczywiście nie jest on konieczny, ale bez niego nie możemy tworzyć obiektów bez podawania żadnych argumentów.
Jeżeli jawnie nie zadeklarujemy konstruktora kopiującego to jest on zawsze automatycznie dodany.
Konstuktor nie zwraca żadnego typu i w czasie jego deklaracji nie podajemy żadnego typu.
Destruktor to funkcja o nazwie identycznej z nazwą typu struktury poprzedzona znakiem tyldy "~". Destruktor nie zwraca żadnego typu ani nie posiada, żadnego argumentu. Jego deklarację (i definicję) również nie poprzedzamy żadną nazwą nazwą typu.
Jak można się domyślać destruktor wywoływany jest automatycznie gdy niszczony jest obiekt. Dzieje się tak gdy program wychodzi z bloku kodu w którym zadeklarowy był nasz obiekt. Jeżeli był on tworzony dynamicznie to destuktor wywoływany jest gdy użytkownik jawnie usuwa zmienną z pamięci. Destruktor możemy także wywołać jawnie - nie oznacza to jednak automatycznego zwolnienia miejca po obiekcie na rzecz którego został on wywołany.
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
using namespace std;
struct Struktura {
int x;
double y;
Struktura(int);
Struktura(double);
~Struktura() { cout << "x=" << x << ", y=" << y << endl; };
};
Struktura::Struktura(int _x): x(_x)
{
std::cout << "Struktura(" << _x << ") int" << std::endl;
}
Struktura::Struktura(double _y): y(_y)
{
std::cout << "Struktura(" << _y << ") double" << std::endl;
}
int main()
{
struct Struktura a(5); //Struktura(int)
struct Struktura b(3.14); //Struktura(double)
struct Struktura c; //blad - brak jawnej deklaracji konstruktora "Strukutra()"
a.~Struktura(); //wywołanie destruktora na rzecz obiektu a
a.y = 2.71; //możemy korzystać z obiektu ponieważ pamięć nie została zwolniona
std::cout << a.y << std::endl;
return 0;
}
Zachęcam do skompilowania i uruchomienia powyższego kodu (przed kompilacją należy usunąć bądź zakomentować wiersz 25).
Przeanalizujmy teraz działanie całego programu. W wierszu 9-tym deklarujemy i odrazu definiujemy destruktor naszej struktury. Definicja metody wewnątrz deklaracji różni się tym od jej definicji poza deklaracją struktury tym, że jest metoda ta jest traktowana jak funkcja inline. Dla przejrzystości kodu raczej unika się definiowania metod od razu przy ich deklaracji ze względu na przejrzystość kodu.
Komentarza wymaga na pewno linia 12-sta i 16-sta. Otóż po nazwie konstruktora i jego argumentach zawartych między nawiasami występuję dwukropek po którym możemy podać argumenty do konstruktorów pól jakie posiada klasa oddzielone przecinkami. W linii 12-stej dla obiektu "x" zostanie wywołany konstruktor z argumentem, którym jest argument konstruktora "Struktura(int x)". W tym przypadku takie postępowanie nie jest niezbędne gdyż zamiast tego wystarczyłoby w konstruktorze przypisać polu "x" wartość argumentu "_x", jednak chciałem pokazać, w jaki sposób przekazuje się argumenty do konstruktorów pól struktury. Jest to bardzo ważne, zwłaszcza gdy mamy do czynienia z polami, które nie są typami prostymi ale np. strukturami.
Klasa przypomina troch� rozbudowan� struktur�. Tak jak struktura mo�e posiada� pola i funkcje, kt�re tu nazywaj� si� metodami.
Deklaracja klasy wygl�da nast�puj�co:
class nazwa_klasy {
public:
pola;
metody;
};
stopnie ochrony danych
W c++ istniej� trzy storpnie ochrony danych:
private
Jest to najbardziej rygorystyczny stopie� ochrony danych. Dost�p do danych typu private maj� tylko metody klasy oraz klasy i metody zaprzyja�nione z dan� klas�. Np.:
class klasa1 {
private:
int a;
public:
void puta(int x) {a=x;};
};
int main()
{
klasa1 klasa;
klasa.x=5; /*brak dost�pu*/
klasa.puta(5); /*OK*/
};
protected
Dost�p do danych typu protected maj� metody klasy, klasy i metody zaprzyja�nione oraz klasy pochodne. Np.:
class klasa1 {
protected:
int a;
};
class klasa2: protected klasa1
{
public:
int geta(void) {return a;};
};
int main()
{
klasa1 klas1;
klasa2 klas2;
klas1.a=5; /*brak dost�pu*/
klas2.a=5; /*brak dost�pu*/
cout << klas2.geta(); /*OK*/
return 0;
}
public
Tu dost�p do danych jest prawie nieograniczony. Je�eli w powy�szych przyk�adach wsz�dzie zamieniliby�my stopnie ochrony danych na public to nigdzie nie wyt�pi� by b��d dost�pu.
dziedziczenie
Dziedziczenie klas pozwala nam na "odziedziczenie" p�l i metod klasy bazowej przez klasy pochodne. Przyk�ad:
class G
{
public: int a,b;
};
class D : public G
{
public: char c;
};
int main()
{
D klasa;
klasa.a=5;
klasa.b=10;
klasa.c='c';
return 0;
}
virtual
Metody virtulane tworzymy dodaj�c przed deklaracj� metody s�oso virtual.
class CKLASA
{
public:
int a,b;
virtual int pomnoz()=0;
};
Przyr�wnanie do zera metody m�wi kompilatorowi, �e ma do czynienia z funkcj� w pe�ni wirtualn�. Stworzyli�my klas� abstrakcyjn�, dla ktorej nie definiujemy cia�a metody. Teraz klasy pochodne mog� korzysta� z metody typu virtual np.::
class CDER: public CKLASA
{
public:
int pomnoz() {return a*b;};
};
class CDER2: public CKLASA
{
public:
int pomnoz() {return a*b*a;};
};
int main()
{
CDER klasa1;
CDER2 klasa2;
klasa1.a=3;
klasa1.b=4;
klasa2.a=2;
klasa2.b=3;
cout << klasa1.pomnoz() << endl << klasa2.pomnoz();
return 0;
};
friend
Klasy i funkjce zaprzyja�nione maj� dost�p do danych prywatnych danej klasy. Przyk�ad:
//biblioteki
class klasa2; //niepe�na deklaracja
class klasa1
{
int x; //zmienna private
friend klasa2;
};
class klasa2
{
public:
void putx(klasa1& k, int a) {k.x=a;};
int getx(klasa1* k) {return k->x;};
};
int main()
{
klasa1 k1;
klasa2 k2;
k2.putx(k1,5);
cout << k2.getx(&k1);
return 0;
}
Wszystkie pola (prywatne,chronione i publiczne) w klasie zaprzyja�nionej s� jako pola publiczne. Nie mo�emy jednak w powy�szym przyk�adzie odwo�a� si� tak:
int main()
{
klasa2 k2;
k2.x=5; //b��d
}
Funkcje tak�e mog� by� zaprzyja�nione:
class K
{
int liczba;
public:
K(int a) {liczba=a;};
friend int wyswietl(K&);
};
int wyswietl(K& k) {return k.liczba;}
int main()
{
K klasa(5);
cout << wyswietl(klasa);
return 0;
}
tworzenie plik�w nag��wkowych
Gdy tworzymy bardziej z�o�one programy, dobrze jest klasy deklarowa� w oddzielnych plikach nag��wkowych. Kod programu jest wtedy bardziej przejrzysty.
Przyk�ad pliku nag��wkowego:
Tworzymy plik np. o nazwie "mojplik.h" i w nim deklarujemy klase:
#ifndef _MOJPLIK_H_ /*zabezpieczenie*/
#define _MOJPLIK_H_
class mojaklasa {
private:
int x;
public:
mojaklasa(int);
int wypisz();
};
#include "mojplik.cpp"
/*doloczenie pliku*/ #endif
Teraz tworzymy plik o nazwie "mojplik.cpp" i w nim konczymy deklaracje:
mojaklasa::mojaklasa(int a) {x=a;};
int mojaklasa::wypisz() {return x;};
Teraz w programie mo�emy napisa�:
#include "mojplik.h"
...
int main()
{
mojaklasa MYCLASS(3); /*deklaracja klasy i wywo�anie konstruktora*/
cout >> MYCLASS.wypisz();
return 0;
};
Program powinien wyswietli� 3(bo taki parametr by� podany do konstruktora klasy).
Unia bardzo przypomina klas� sposobem deklaracji, jednak jest mi�dzy nimi bardzo istotna r��nica. Polega ona na tym, �e unia zajmuje tyle pami�ci ile zajmuje jej najwi�kszy element. Przyk�ad:
union abc
{
public:
int liczba;
double liczba2;
};
class ABC
{
public:
int liczba;
double liczba2;
};
W powy�szym przyk�adzie unia(abc) zajmuje w pami�ci 2 Bajt�w(najwi�kszy element double zajmuje 2B), a klasa(ABC) zajmuje 10 Bajt�w (bo int-2B + double-8B = 10B).
Ma to jednak swoje konsekwencje. Mianowice w klasie mo�emy jednocze�nie przechowywa� warto�ci we wszystkich zmiennych(w powy�szej klasie mo�emy w tym samym czasie przechowywa� warto�� typu int i double). W unii natomiast mo�na przechowywa� jedynie warto�� w jednej zmiennej. Np. w powy�szym przyk�adzie gdy przypiszemy warto�� zmiennej 'liczba' to automatycznie tracimy warto�� drugiej zmiennej('liczba2'). Przyk�ad:
#include <iostream.h>
union UNIA
{
public:
short wiek;
short wzrost;
short show(bool);
};
short UNIA::show(bool xxx)
{
if (xxx) return wiek; else return wzrost;
};
int main()
{
bool rodz;
UNIA dziecko;
cout << "Je�li chcesz poda� wiek wprowadz '1'\n\
Je�li chcesz poda� wzrost wprowad� '0'\n";
cin >> rodz;
cout << "Podaj warto��:\t\t";
if (rodz) cin >> dziecko.wiek; else cin >> dziecko.wzrost;
if (rodz) cout << "wiek dziecka wynosi: ";
else cout << "wzrost dziecka wynosi: ";
cout << dziecko.show(rodz);
return 0;
}
Powy�szy przyk�ad jest beznadziejny ale prezentuje, �e mo�na jednocze�nie przechowywa� tylko jedn� warto�� w unii. Gdyby�my wy�ej wprowadzili wiek, a nast�pnie wzrost to utraciliby�my warto�� przechowywan� w zmiennej wiek.
cout << "Podaj wiek:\t\t";
cin >> dziecko.wiek; //przypisanie wieku
cout << "Podaj wzrost\t\t";
cin >> dziecko.wzrost; /*przypisanie wzrostu i jednoczesna utrata warto��i wieku ('dziecko.wiek')*/
W unii nie mo�na deklarowa� obiekt�w statycznych (static). W unii, tak jak w klasie obowi�zj� 3 stopnie ochrony(public, protected i private. W unii mo�na deklarowa� konstruktor i destruktor.
Czesto chcemy uzywac zmiennej, kt�rej rozmiar nie jest znany w czasie kompilacji programu. Przydzielenie pamieci dynamicznie pozwala przydzielic pamiec w czasie wykonywania programu (rozmiar moze podac uzytkownik):
W c++ pamiec dynamicznie przydziela sie tak:
typ_zmiennej *zmienna = new typ_zmiennej;
Czyli najpierw typ zmiennej, nastepnie wskaznik to zmiennej, operator przypisania ("="), operator new i ponownie typ zmiennej.
tablice
Deklaracja tablicy wyglada nastepujaco:
typ_zmiennej *tablica= new typ_zmiennej[rozmiar];
Czyli tak samo jak dla zwyklej zmiennej tylko na koncu w nawiasach kwadratowych podajemy rozmiar tablicy.
Przykladowe zastosowanie:
//biblioteki
int *tablica;
int main()
{
int rozmiar,suma;
suma=0;
cout << "Podaj rozmiar tablicy:\t";
cin >> rozmiar;
tablica= new int[rozmiar]; /*przydzielenie pamieci*/
if (tablica==0) { cout << "Zabraklo pamieci!!!\n"; return 1;};
cout << "\nPodaj " << rozmiar << " liczb\n";
for (int i=0;i<rozmiar;i++) cin >> *(tablica + i);
for (int i=0;i<rozmiar;i++) suma+=*(tablica + i);
cout << "\nSuma liczb wynosi:\t" << suma;
return 0;
}
Na pewno wiesz, jak dziala powyzszy program. Do tablicy odwolujemy sie przez wskaznik. Sama nazwa tablicy podaje adres poczatku tablicy(nie stosujemy operatra adresu "&").
Sprawa troche sie komplikuje, kiedy chcemy stworzyc tablice kilku wymiarowa. Nie mozemy zrobic tego tak:
typ_zmiennej *zmienna=new typ_zmiennej[x][y] /*blad*/
Jednak jest prosty spos�b:
int i,x,y,**tablica; //deklaracja wska�nika
int a,b; //zmienne pomocnicze
x=5; y=10; //wymiary tablicy
tablica=new int*[x]; //przypisanie tablicy wska�nik�w do wska�nik�w :-)
for (i=0;i<x;i++) tablica[i]=new int[y]; //przypisanie wska�nik�w
for (a=0;a<x;a++)
{
for (b=0;b<y;b++)
{
tablica[a][b]=a*10+b+1;
};
};
for (a=0;a<x;a++)
{
for (b=0;b<y;b++)
{
cout << tablica[a][b] << " ";
};
};
Powy�szy przyk�ad powinien wy�wietli� liczby od 1 do (x*y) czyli 50.
Stworzylismy tablice dwuwymiarowa. W ten sam spos�b tworzy sie tablice 3,4..- wymiarowe. Mysle, ze juz potrafisz takie stworzyc. Gdyby to bylo jeszcze niejasne to piszcie.
Preprocesor to dodatkowy program, kt�ry posiadaj� kompilatory c++. Wykonywany jest on przed kompilacj� programu.
Dyrektywy preprocesora poprzedzone s� znakiem # i nie s� zako�czone �rednikiem.
Oto niekt�re z nich:
#include
T� dyrektywe ju� znasz. Do��czamy ni� do programu pliki dodatkowe.(biblioteki)
Po tej dyrektywie wyst�puje parametr w cudzys�owiu lub w nawiasach k�towych. R��nica polega na tym, �e je�li nazwa pliku podana zostanie w cudzys�owiu to kompilator b�dzie szuka� pliku w katalogu, w kt�rym znajduje si� program. Je�eli nazwa pliku wyst�pi w nawiasach k�towych, to plik b�dzie szukany w katalogu z bibliotekami kompilatora (include).
Przyk�ad:
#include <iostream.h>
#include "moj_plik.h"
#define
T� dyrektyw� mo�emy definiowa� w�asne zmienne:
#define dwa 2
Tu zdefiniowali�my zmienn� "dwa"(warto�� wynosi 2). Teraz w programie mo�emy u�y� jej tak:
int x;
cin >> x;
if (x==dwa) cout << "liczba jest r�wna 2" else cout << "liczba jest r��na od 2";
T� dyrektyw� mo�na tak�e zabezpiecza� pliki nag��wkowe:
#ifndef __nazwa_h__
#define __nazwa_h__
...
#endif
#undef
Ju� po nazwie mo�na si� domy�li�, �e gdy zdefiniowali�my jak�� zmienn� dyrektyw� #define to dyrektywa #undef uniewa�ni poprzedni� definicje. Przyk�ad:
Przyk�ad:
#define zmienna 5
...
#undef zmienna
#ifndef
Jest to dyrektywa warunkowa ("je�eli nie zdefiniowano <parametr>"), czyli je�li wcze�niej nie zdefiniowano zmiennej globalnej to wykonuje kod. Na ko�cu tego kodu wyst�puje dyrektywa #endif
Przyk�ad:
#ifndef zmienna /*je�eli nie zdefiniowano zmiennej*/
...../*wykonuje kod*/
#endif
#ifdef
To samo co wy�ej tylko, �e tutaj wykonywany jest kod gdy zdefiniowano jak�� zmienn�.
Przyk�ad:
#ifdef zmienna
......./*kod*/
#endif /*zako�czenie*/
#else
Ta dyrektywa jest podobna w dzia�aniu do else w instrukcji warunkowej. Dyrektywa #else mo�e wyst�powa� po dyrektywach warunkowych (#if, #ifdef, #ifndef).
Przyk�ad:
#ifdef zmienna /*gdy zdefiniowano zmienn�*/
... /*kod gdy warunek spe�niony*/
#else
... /*kod gdy warunek nie jest spe�niony*/
#endif
#endif
Ta dyrektywa zaka�cza dyrektywy warunkowe. W powy�szych przyk�adach wida�, gdzie nale�y j� stosowa�.
Kto�, kto mia� ju� do czynienia z programowaniem zapewne docenia zalety stosowania instrukcji assemblerowskich. Stosowanie takich instrukcji mo�e znacznie przyspieszy� dzia�anie programu.
Instrukcje assemblera wstawiamy stosuj�c s�owo kluczowe asm, a nast�pnie piszemy instrukcje assemblera. Mo�na tak�e zapisa� kilka instrukcji w bloku (klamra pocz�tku bloku musi znajdowa� si� w tej samym wierszu co s�owo asm).
Przyk�ad:
#include <stdio.h> /*biblioteka*/
char g,m,s; //deklaracja zmiennych
void main()
{
asm {
mov ah,0x2c
int 0x21
mov g,ch /*godzina*/
mov m,cl /*minuta*/
mov s,dh /*sekunda*/
}
printf("czas: %2d:%2d:%2d\n",g,m,s);
}
Program powinien wy�wietli� aktualn� godzin�.
Jescze jedna uwaga. W c++ we wstawkach asseblerowskich liczby w systemie szesnastkowym musz� by� zapisane tak jak w c++:
int a;
asm mov a,0xFF /*dobrze*/
asm mov a,FFh /*b��d*/
Funkcja main() mo�na deklarowa� nast�puj�co:
int main()
int main(int argc)
int main(int argc,char *argv[])
int main(int argc,char **argv,char **env)
Zapis "char *argv[]" jest r�wnoznaczny z zapisem "char **argv".
Pierwszy argument (argc) jest typu int, a jego warto�� to ilo�� argument�w funkcji main.
Drugi argument (argv) to tablica wska�nik�w do argument�w funckji (pierwszy argument to nazwa programu).
Trzeci argument (env) to tablica wska�nik�w do zmiennych �rodowiskowych.
Przyk�ad:
#include <iostream.h>
int main(int argc,char *argv[],char *env[])
{
cout << "Nazwa programu: " << argv[0] << endl;
cout << "Ilo�� argument�w funkcji main: " << argc << endl;
int a;
for(a=1;a<argc;a++) cout << "Argument " << (a+1) << ": " << argv[a] << endl;
a=0;
while(env[a])
{
cout << "Zmienna srodowiskowa nr " << (a+1) << ": " << env[a] << endl;
a++;
}
return 0;
}
Obsługa plików
Chyba nie trzeba specjalnie pisać dlaczego obsługa zapisu/odczytu danych do/z pliku jest tak ważna. Zatem przejdźmy od razu do meritum sprawy.
Podejście z języka c
Niektórzy mogą zapytać dlaczego podaję sposób obsługi plików jaki stosowany jest w języku c skoro jest to kurs c++. Otóż sposób ten jest nadal często wykorzystywany i uważam, że warto go znać.
Deklaracje funkcji, które teraz omówię znajdują się pliku nagłówkowym csdtio (a dokładnie w "stdio.h"):
Otwieranie pliku:
FILE * fopen(const char *filename, const char *mode);
Argumenty funkcji:
filename - nazwa pliku
mode - tryb otwarcia pliku. Możliwe wartości przedstawia poniższa tabela:
tryb |
opis |
"r" |
otwiera plik jedynie do odczytu - plik musi istnieć. |
"w" |
tworzy plik i otwiera go do zapisu - jeżeli plik istniał to jego zawartość zostanie skasowana. |
"a" |
tworzy plik i otwiera go do zapisu - jeżeli plik istniał to zostaje otwarty do zapisu tylko w trybie dopisywania (ustawia kursor na końcu zawartości pliku). |
"r+" |
otwiera plik do odczytu i zapisu - plik musi istnieć. |
"w+" |
tworzy i otwiera plik do odczytu i zapisu - jeśli plik istniał to jego zawartość zostanie skasowana. |
"a+" |
otwiera plik do w trybie odczytu i dopisywania - wszystkie operacje zapisu są wykonywane na końcu pliku i nie powodują utraty poprzedniej zawartości pliku (nawet gdy wcześniej przesuniemy kursor funkcją fseek). Jeśli plik nie istniał to zostanie utworzony. |
Podczas otwierania pliku możemy jeszcze do trybu dodać literkę "b" (np. "rb", "wb+", "a+b"), która oznacza, że plik ma być otwarty w trybie binarnym a nie tekstowym - nie wszystkie systemy obsługują tryby otwarcia plików ale można go stosować w celu zwiększenia "przenośności" kodu.
Funkcja zwraca wskaźnik do struktury "FILE" (jej deklaracja znajduje się w pliku nagłówkowym) powiązanej z otwartym plikiem albo 0 jeśli wystąpił błąd.
Zamykanie pliku:
int fclose(FILE *stream);
Argumenty funkcji:
stream - wskaźnik do strukury FILE określającej otwarty plik
Funkcja zamyka plik powiązany ze strumieniem i powoduje opróżnienie wszystkich buforów z nim powiązanych.
Funkcja zwraca 0 jeśli wykona się poprawnie albo EOF w przeciwnym wypadku (EOF to wartość zdefiniowana w pliku nagłówkowym).
Czytanie z pliku:
size_t fread(void *ptr, size_t size, size_t count, FILE *stream );
Argumenty funkcji:
ptr - wskaźnik określający gdzie mają zostać zapisane odczytane dane (minimalny rozmiar to size*count bajtów!)
size - wielkość czytanego bloku w bajtach
count - ilość bloków do odczytu
stream - plik z którego odczytać dane
Funkcja odczytuje dane z pliku blokami o podanym rozmiarze. Dane te są zapisywane pod podanym adresem. Wewnętrzny kursor w pliku jest automatycznie zwiększany o ilość przeczytanych bajtów (jeśli wszystko się powiedzie to jest to size*count bajtów).
Funkcja zwraca ilość poprawnie przeczytanych bloków (nie bajtów!). Jeśli jest ona mniejsza od argumentu count to oznacza, że wystąpił błąd lub osiągnięto koniec pliku. Należy wtedy sprawdzić to funkcją ferror() lub feof().
Zapis do pliku:
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
Argumenty funkcji:
ptr - wskaźnik określający początek zapisywanych danych
size - rozmiar zapisywanego bloku w bajtach
count - ilość bloków do zapisania
stream - plik do którego zapisać dane
Funkcja zapisuje dane do pliku blokami o rozmiarze podanym w parametrze "size", przesuwając kursor pliku o ilość zapisanych bloków.
Funkcja zwraca ilość poprawnie zapisanych bloków. Jeśli wartość ta różni się od argumentu "count" oznacza to, że wystąpił błąd.
Odczyt sformatowanych danych z pliku:
int fscanf(FILE * stream, const char *format, ... );
Argumenty funkcji:
stream - wskaźnik do struktury określającej plik
format - format odczytywanych danych
... - opcjonalne argumenty określające adresy odczytywanych pól
Funkcja pozwala na odczytywanie sformatowanych danych. Nie będe tutaj szczegółowo opisywał sposobu formatowania danych gdyż jest on analogiczny do tego w funkcji scanf().
Funkcja zwraca ilość poprawnie odczytanych pól. Jeśli jest ona mniejsza od spodziewanej ilości oznacza to, że nie udało odczytać się wszystkich pól ponieważ nie udało się dopasować podanego wzoru formatowania. W razie niepowodzenia odczytu danych z pliku funkcja zwraca EOF.
Zapis sformatowanych danych do pliku:
int fprintf(FILE * stream, const char *format, ... );
Argumenty funkcji:
stream - wskaźnik do struktury określającej plik
format - wzór formatowania danych
... - opcjonalne argumenty zapisywanych pól
Funkcja zapisuje sformatowane dane. Samo formatowanie jest analogiczne do używanego w funkcji printf().
Funkcja zwraca ilość poprawnie zapisanych znaków. Jeśli wystąpił błąd funkcja zwraca liczbę ujemną.
Odczyt znaku z pliku:
int fgetc(FILE *stream);
Argument funkcji:
stream - wskaźnik do struktury określającej plik
Funkcja odczytuje jeden znak z pliku i zwiększa pozycję kursora pliku o 1.
Jeśli odczyt się powiedzie funkcja zwraca odczytany znak albo EOF w przeciwnym wypadku.
Zapis znaku do pliku:
int fputc(int character, FILE *stream);
Argument funkcji:
character - znak do zapisania
stream - wskaźnik do struktury określającej plik
Funkcja zapisuje znak do pliku i przesuwa kursor pliku o 1.
Jeśli zapis się powiedzie funkcja zwraca zapisany znak albo EOF jeśli wystąpił błąd (sprawdzamy funkcją ferror()).
Odczyt stringu z pliku:
char * fgets(char *str, int num, FILE *stream);
Argumenty funkcji:
str - wskaźnik do buforu odczytywanych danych
num - maksymalna ilość odczytanych znaków
stream - wskaźnik do struktury określającej plik
Funkcja wczytuje znaki do bufora aż do czasu gdy wczyta "num"-1 znaków lub jeśli wystąpi znak końca lini ('\n') lub koniec pliku (EOF). Ponieważ znak końca lini jest uznawany za poprawny jest on także zapisywany do bufora.
Funkcja automatycznie dodaje znak '\0' na końcu wczytanego bufora.
Jeśli odczyt się powiedzie funkcja zwraca wskaźnik podany w jej argumencie "str". Jeśli od razu napotkano koniec pliku lub odczyt nie powiódł się funkcja zwraca 0 (sytuację sprawdzamy przy użyciu funkcji feof() lub ferror()).
Zapis C stringu do pliku:
int fputs(const char *str, FILE *stream);
Argumenty funkcji:
str - wskaźnik do tablicy zapisywanych znaków
stream - wskaźnik do struktury określającej plik
Funkcje zapisuje tablicę znaków do pliku aż do napotkania znaku '\0' (jednak znak ten nie jest już zapisywany do pliku!).
Jeśli zapis się powiedzie funkcja zwraca wartość nieujemną. W razie wystąpienia błędu funkcja zwraca EOF.
Sprawdzenie czy ustawiona jest flaga końca pliku EOF:
int feof(FILE *stream);
Argument funkcji:
stream - wskaźnik do struktury określającej plik
Funkcja sprawdza czy ustawiona jest flaga EOF (End-Of-File) w strukturze "stream" powiązanej z otwartym plikiem. Flaga ta jest ustawiana przez funkcje, które w czasie działania napotkały koniec pliku.
Funkcja zwraca wartość różną od zera jeśli flaga EOF jest ustawiona, 0 w przeciwnym wypadku.
Sprawdzanie błędów:
int ferror(FILE *stream);
Argument funkcji:
stream - wskaźnik do struktury określającej plik
Funkcja sprawdza czy flaga wskazująca błąd jest określona - jest ona ustawiana przez poprzednie operacje, które zakończyły się niepowodzeniem.
Funkcja zwraca wartość różną od zera jeśli flaga błędu jest ustawiona, 0 w przeciwnym wypadku.
Opróżnianie buforów:
int fflush(FILE *stream );
Jeśli plik został otwarty do zapisu i ostatnią operacją był zapis danych funkcja zapisuje wszystkie niezapisane dane z buforu do pliku. Jeśli plik został otwarty do odczytu zachowanie funkcji zależy od implementacji. W niektórych z nich funkcja powoduje wyczyszczenie buforu odczytu.
Jeśli plik jest zamykany funkcją fclose() albo z powodu zakończenia programu wszystkie powiązane bufory są automatycznie opróżniane.
Funkcja zwraca 0 jeśli wszystko się powiedzie albo EOF jeśli wystąpi błąd.
Ustawianie kursora w pliku:
int fseek(FILE *stream, long int offset, int origin);
Argumenty funkcji:
stream - wskaźnik do struktury określającej plik
offset - liczba bajtów określająca pozycję od "origin" (może być liczbą ujemną)
origin - pozycja do której dodana zostanie wartość "offset". Może to być jedna z wartości:
SEEK_SET |
oznacza początek pliku |
SEEK_CUR |
oznacza bieżącą pozycję kursora w pliku |
SEEK_END |
oznacza koniec pliku |
Ustawia pozycję kursora w pliku na nową pozycję okręśloną poprzez argument "offset" względem "origin". Po wywołaniu tej funkcji czyszczona jest flaga EOF oraz tracone są wszystkie efekty wywołania funkcji ungot().
Należy pamiętać, że jeśli plik otwarty jest w trybie tekstowym to argument "offset" oznacza liczbę znaków a nie bajtów.
Jeśli funkcja się powiedzie zwracana jest wartość 0, wartość niezerowa w przeciwnym wypadku.
Odczyt aktualnej pozycji kursora:
long int ftell(FILE *stream);
Argumenty funkcji:
stream - wskaźnik do struktury określającej plik
Funkcja zwraca aktualną pozycję kursora w pliku. Dla plików binarnych jest to wartość odpowiadająca ilości bajtów od początku pliku. Dla plików tektstowych nie ma gwarancji, że jest to dokładnie liczba bajtów od początku pliku ale nadal może być używania do przywracania pozycji kursora poprzez funkcję fseek().
Funkcja zwraca aktualną pozycję kursora. W razie niepowodzenia zwraca -1L i ustawiana jest dodatnia wartość zmiennej globalnej "errno", której wartość może być zinterpretowana funkcją perror().
ALGORYTM - definicja
Algorytm, opis rozwiązywania problemu (zadania) wyrażony za pomocą takich operacji, które wykonawca algorytmu rozumie i potrafi wykonać. Pierwsze opisy, które później nazywano algorytmami, dotyczyły rozwiązywania zadań matematycznych. Już w starożytnym Egipcie i Grecji stworzono wiele takich metod, które pozwalały rozwiązywać pewne zadania w sposób "algorytmiczny". Spośród nich najbardziej znane to opracowana przez matematyka gr. Euklidesa metoda znajdowania największego wspólnego dzielnika dwóch liczb naturalnych, zwany obecnie algorytmem Euklidesa, oraz sposób podziału kąta na dwie równe części za pomocą cyrkla i linijki, zw. bisekcją kąta.
Nazwa "algorytm" wywodzi się od nazwiska arabskiego matematyka i astronoma Alchwarizmiego (IX w.). W tłumaczeniu łacińskim pracy Alchwarizmiego poświęconej arytmetyce występuje on pod nazwiskiem Algoritmi; jej wersja arabska nie zachowa ła się (w odróżnieniu od pracy poświęconej algebrze).
Intuicyjnie algorytm kojarzy się z metodą rozwiązywania zadania, z przepisem postępowania czy ze schematem działania. Jednak nie każda metoda, nie każdy schemat jest algorytmem. Przyjmuje się, że algorytm powinien mieć wyraźnie określony początek (start), tzn. wskazaną operację (czynność), od której zaczyna się realizacja tego algorytmu, precyzyjnie określoną kolejność wykonywania poszczególnych operacji (działań) oraz wyróżniony koniec (stop). Wymaga się także, aby algorytm składał się ze skończonej liczby operacji (poleceń) sformułowanych w sposób jednoznaczny i możliwy do wykonania w skończonym (rozsądnym) czasie. Cały algorytm musi być sformułowany w sposób zrozumiały dla określonego wykonawcy, czy inaczej mówiąc, musi być napisany w języku zrozumiały dla danego wykonawcy.
Jako przykład posłuży algorytm wyznaczania wartości całkowitej pierwiastka kwadratowego z danej liczby naturalnej n, czyli algorytmicznego obliczania wartości funkcji (sqrt(n)), gdzie n jest liczbą naturalną. Algorytm ten jest oparty na bardzo prostym pomyśle. Bierzemy liczbę naturalną i, a następnie sprawdzamy czy (i+1)²>n; Jeśli warunek ten nie jest spełniony, to bierzemy kolenją liczbę naturalną i dla niej sprawdzamy warunek. Jeżeli warunek jest spełniony, to i jest szukaną wartością funkci E(sqrt(n)). Przyjmuje się, że wykonawca tego algorytmu potrafi dodawać, podnosić do kwadratu i porównywać dwie liczby naturalne.
Powyższa definicja pochodzi z Encyklopedi szkolnej WSiP.
Przykładowa implementacja w c++
int n,i=0;
cin >> n; //wczytujemy zmienną n
while((i+1)*(i+1) <= n) ++i;
cout << "Część całkowita pierwiastka z " << n << " wynosi " << i << endl;