Cpp, Programowanie, C++


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

asm

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

int

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*/
...

0x01 graphic

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:

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:


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:


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.

0x01 graphic

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
(signed) short int

2B

-32768 ÷ 32767

unsigned short
unsigned short int

2B

0 ÷ 65535

(signed) int
(signed) long

4B

-2147483648 ÷ 2147483647

unsigned int
unsigned long

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.

0x01 graphic

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:


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

0x01 graphic


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.

0x01 graphic


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;
};

0x01 graphic

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.

0x01 graphic

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;
}

0x01 graphic

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;
};

0x01 graphic

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;
}

0x01 graphic

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"):

FILE * fopen(const char *filename, const char *mode);

Argumenty funkcji: