Monika Dekster

C++: Spis treści

  1. Historia C++
  2. Cechy C++
  3. Struktura Programu
  4. Typy Danych
  5. Operatory
  6. Instrukcje Sterujące
  7. Funkcje
  8. Wskaźniki i tablice
  9. Programowanie obiektowe
  10. Inicjalizacja klas - konstruktory
  11. Przeładowanie operatorów
  12. Dziedziczenie
  13. Polimorfizm
  14. Templates
  15. Podstawowe wejście / wyjście - Formatowanie
  16. Operacje na plikach
  17. Wyjątki
  18. Wyrażenia lambda
  19. Biblioteka STL: kontenery
  20. Biblioteka STL: algorytmy

Historia C / C++

Wersje:

Cechy C / C++

Cechy C

  1. Prostota, szybki mały kompilator
  2. Operacje niskopoziomowe, dające programiście dużą kontrolę nad działaniem programu (dlatego możliwe jest np. napisanie jądra systemu operacyjnego)
  3. Ze względów optymalizacyjnych autorzy nie wbudowali wielu mechanizmów kontroli (np. zakresu tablic, legalności odwołań wskaźnikowych, inicjalizacji zmiennych, itd.)
  4. Brak automatycznego zwalniania pamięci (może prowadzić do "memory leaks")
  5. Nie wymaga ścisłej zgodności typów między parametrem i argumentem.
  6. Dopuszcza prawie każdy typ argumentu, o ile da się go rozsądnie przekształcić do typu parametru. Konwersja następuje automatycznie.
  7. C nie wykonuje prawie żadnego sprawdzania błędów wykonania. Za obsługę błędów odpowiada programista.
  8. Język C umożliwia odczyt i zapis poza zasięgiem tablicy!
  9. Język dla programistów, którzy są świadomi tego co robią.

Cechy C++

Język C++ został stworzony w latach osiemdziesiątych XX wieku (pierwsza wersja pojawiła się w 1979 r.) przez Bjarne Stroustrupa jako obiektowe rozszerzenie języka C. Poza językiem C, na definicję języka C++ miały wpływ takie języki, jak Simula (z której zaczerpnął właściwości obiektowe) oraz Algol, Ada, ML i Clu.

Początkowo język C++ był dostępny w takim standardzie, w jakim opracowano ostatnią wersję kompilatora Cfront (tłumaczący C++ na C), później opublikowano pierwszy nieformalny standard zwany ARM (Annotated Reference Manual), który sporządzili Bjarne Stroustrup i Margaret Ellis. Standard języka C++ powstał w 1998 roku (ISO/IEC 14882-1998 „Information Technology – Programming Languages – C++”). Standard ten zerwał częściowo wsteczną zgodność z ARM w swojej bibliotece standardowej; jedyne, co pozostało w stanie w miarę nienaruszonym to biblioteka iostream.

Początkowo najważniejszą zmianą wprowadzoną w C++ w stosunku do C było programowanie obiektowe, później jednak zaimplementowano wiele innych ulepszeń, mających uczynić ten język wygodniejszym i bardziej elastycznym od swojego pierwowzoru. Niektóre zmiany w standardzie języka C były zainspirowane językiem C++ (np. słowo inline w C99).

Nazwa języka została zaproponowana przez Ricka Mascitti w 1983 roku, kiedy to po raz pierwszy użyto tego języka poza laboratorium naukowym. Odzwierciedla ona fakt, że język ten jest rozszerzeniem języka C. Wcześniej używano nazwy „C z klasami”. Nazwa języka C++ nawiązuje do faktu bycia „następcą języka C”, przez użycie w niej operatora inkrementacji ”++”.

Technical Report (TR) to zbiór proponowanych rozszerzeń do biblioteki standardowej C++, takich jak wyrażenia regularne, sprytne wskaźniki, tablice haszujące i generatory liczb losowych.

Standardy C++:

C++ standards



Kompilator gcc / g++

Program gcc (g++) jest tylko interfejsem. Jako pakiet składa się z wielu programów. Każda faza budowania ma przydzieloną osobną aplikację.

  1. Preprocesor : cpp (C PreProcessor), tworzy plik gotowy do kompilacji (-E)
  2. Kompilator : cc (lub cc1, C Compiler), kompiluje do assemblera (-S)
  3. Assembler : as, assembluje kod do kodu maszynowego platformy docelowej (-c)
  4. Konsolidator : ld – tworzy końcowy program z utworzonych obiektów.

Każdemu etapowi tworzenia obrazu procesu w pamięci towarzyszy odpowiednie przekształcanie adresów, począwszy od etykiet i innych symboli, a skończywszy na fizycznych adresach komórek pamięci. Jeśli w programie źródłowym występują adresy, to mają one najczęściej postać symboliczną (etykiety w asemblerze) lub abstrakcyjną (wskaźniki w C). Adresy związane z lokalizacją pojawiają się na etapie translacji i są odpowiednio przekształcane aż do uzyskania adresów fizycznych.

Struktura programu


#include <iostream>
/*  to jest komentarz
	kilkuwierszowy
*/
int main() {
	std::cout << "Hello, world" << std::endl;	//	komentarz jednowierszowy
	return 0;
}
Wersja dla "leniwych" ale nie polecana:

#include <iostream>
using namespace std;
/*  to jest komentarz
	kilkuwierszowy
*/
int main() {
	cout << "Hello, world" << endl;
	//	"return jest automatycznie dopisywane
	//	przez kompilator, ale oczywiście nie jest
	//	błędem umieszczenie tej instrukcji
}

Typy danych

  1. typ wartości logicznych: bool,
  2. typy całkowitoliczbowe:
    • char / unsigned char / signed char,
    • int / unsigned int,
    • short / unsigned short,
    • long / unsigned long,
    • long long / unsigned long long,
  3. typy zmiennopozycyjne:
    • float,
    • double,
    • long double,

Rozmiary poszczególnych typów są zależne od implementacji (operator sizeof()).

Stałe

  1. stałe logiczne: true, false
  2. stałe całkowite
    • int: 536230
    • unsigned int: 536230u
    • int: 012300 wartość oktalna
    • int: 0xff, 0xab12 wartość heksadecymalna
    • long: 536230l
    • unsigned long: 536230ul
    • long long: 536230ll
    • unsigned long long: 536230ull
  3. stałe znakowe:
    • 'a'
    • '\t' tab
    • '\n' linefeed
    • '\f' form feed
    • '\r' carriage return
    • '\"' double quote
    • '\\' backslash
    • '\'' single quote
    • '\xff' kod heksadecymalny znaku
    • '\177' kod oktalny znaku
  4. literały tekstowe (tablice znakowe): "to jest tekst\n"
  5. stałe zmiennoprzecinkowe
    • float: 1e1f, 2.f, .3f
    • double: 1e1, .3, 0.0, 2.d
    • long double: 1e1l, 0.3L

Zmienne

C++ standards

Skompilowany program tworzy następujące obszary pamięci:

  1. Text segment (kod wykonywalny programu, zwykle dzielony żeby nie duplikować kodu, read-only)
  2. Initialized data segment (zmienne globalne i statyczne zainicjalizowane przez programistę)
  3. Uninitialized data segment (zmienne globalne i statyczne zainicjalizowane przez jądro systemu na 0)
  4. Stack (stos, zmienne automatyczne)
  5. Heap (pamięć alokowana dynamicznie)

Zmienne są (zwykle) nazwanymi pojemnikami na pojedyncze wartości typu z jakim zostały zadeklarowane.

Wyróżniamy następujące rodzaje zmiennych:

  1. zmienne lokalne (local variables),
  2. zmienne globalne (global variables),
  3. zmienne statyczne lokalne (static local variables),
  4. zmienne statyczne globalne (static global variables),

Przy deklaracji zmiennych można jawnie podawać ich wartości początkowe. To bardzo dobra praktyka. Jeśli w deklaracji zmiennych statycznych lub globalnych nie podano ich wartość początkowej, to taka zmienna zostanie automatycznie zainicjalizowana wartością 0 odpowiedniego typu. W przypadku zmiennych lokalnych programista musi sam zadbać o zainicjalizowanie zmiennej przed jej odczytaniem.

Operatory

  1. operatory numeryczne:
    • +, − unarny
    • *, ⁄, % dwuargumentowe
    • +, − dwuargumentowe
    • ++, −− inkrementacja i dekrementacja (pre i postfiksowa)
  2. operatory relacyjne: <, <=, >, >=, ==, !=
  3. logiczne operatory warunkowe: &&, ||
  4. ?: operator warunkowy
  5. operator zmiany typu (cast)
  6. <<, >> operatory przesunięcia (tylko dla typów całkowitych),
  7. logiczne operatory bitowe (tylko dla typów całkowitych)
    • ~ operator dopełnienia bitowego
    • &, |, ^ (and, or, xor)
  8. +=, −=, *=, ⁄=, &=, |=, ^=, %=, <<=, >>= operatory modyfikacji

Uwagi dotyczące operatorów

Operator dzielenia 

Dzielenie całkowite daje wynik całkowity, np. 5 ⁄ 2 daje w wyniku 2 (a nie 2.5)

Operator modulo  %

Dla operandów całkowitych daje wynik taki, że (a ⁄ b) * b + (a % b) jest równe a

Operator warunkowy

E1 ? E2 : E3

wartością wyrażenia jest E2 jeżeli E1 jest true i E3 jeżeli E1 jest false

np. c = (a > b) ? a : b oblicza maximum liczb a, b

Operatory zwiększania i zmniejszania

n++, n−− postfixowy

++n, −−n prefixowy

n = 5; m = n++; daje m = 5

n = 5; m = ++n; daje m = 6

Złożone operatory przypisania

Postać: E1 op= E2 jest równoważne:

E1 = (T)((E1) op (E2))

T jest typem E1, op jest jednym z:

+, −, *, ⁄, %, &, |, ^, <<, >>

Konwersja typów

Binarne promocje numeryczne

Dokonywane dla operandów następujących operatorów dwuargumentowych:

Zasady konwersji

Sterowanie przebiegiem programu

instrukcja - wyrażenie zakończone średnikiem;

blok - grupa instrukcji ujęta w nawiasy klamrowe {} (składniowo równoważna jednej instrukcji)

Instrukcja warunkowa (if)


if (wyrażenie)
	instrukcja-1
else
	instrukcja-2

Instrukcja rozgałęzienia (switch)


switch (wyrażenie) {
	case stała-1: instrukcja-1
	case stała-2: instrukcja-2
				  ... 
	case stała-n: instrukcja-n
	default: instrukcja
}

Pętle


while (wyrażenie) instrukcja;

do {
	instrukcja;
} while (wyrażenie);

for (wyrażenie1; wyrażenie2; wyrażenie3) 
	instrukcja;

Równoważne:


wyrażenie1;
while (wyrażenie2) { 
	instrukcja; 
	wyrażenie3;
}

Funkcje

Definicja funkcji


<type> name(<type> arg1, <type> arg2, ...) {
definitions and instructions;
...
}

Deklaracja funkcji


<type> name(<type> arg1, <type> arg2, ...);

Przykłady:


double sqr(double x);

double sqr(double x) {
	return x*x;
}

int add(int a, int b);

int add(int a, int b) {
	return a + b;
}

void print (char* s, double x);

void print (char* s, double x) {
	printf("%s %f\n", s, x);
}

void message();

void message() {
	printf("Function with empty parameter list\n");
}

Funkcja o typie różnym od void musi mieć przynajmniej jedną instrukcję return wyr;, gdzie wyr jest wyrażeniem zgodnym z typem funkcji. Funkcja typu void może zawierać instrukcję return; (bez wyrażenia). Jeżeli tej instrukcji nie ma to funkcja kończy się po wykonaniu wszystkich jej instrukcji.

Parametry do funkcji są przekazywane przez wartość. Oznacza to, że aktualne wartości parametrów są kopiowane do obszaru roboczego funkcji, a sama funkcja nie ma dostępu do oryginału. Wniosek: Funkcja nie może zmienić oryginalnych wartości parametrów. Inne sposoby przekazywania parametrów są opisane w rozdziale o wskaźnikach.

Wskaźniki i tablice

Wskaźniki

Pointers



Pointers



Pointers


Tablice

Deklaracja tablicy


int ia[10];

const int len = 10;
char ca[len];

Tworzenie tablicy:

Inicjalizacja

int silnia[] = { 1, 1, 2, 6, 24 };
char *as = "Tablica";

Tablice muszą być indeksowane wyrażeniami całkowitymi. Tablica o długości n może być indeksowana od 0 do n-1.

Pointerss

Tablice wielowymiarowe statyczne

Tablica 2D w języku C jest tablicą 1D, której elementami są tablice 1D (wiersze). Na przyklad tablica T a[4][3] (T jest jakimś typem) może być przedstawiona następująco:

Pointerss

a[0] ---> a[0][0] a[0][1] a[0][2]
a[1]---> a[1][0] a[1][1] a[1][2]
a[2]---> a[2][0] a[2][1] a[2][2]
a[3]---> a[3][0] a[3][1] a[3][2]

Elementy tablicy są przechowywane w pamięci wierszami, tak więc adres elementu a[i][j] tablicy a[m][n] typu T jest wyznaczany następująco:


address(a[i][j]) = address(a[0][0]) + (i * n + j) * size(T)
  1. Powyższe równanie jest ważne. Stanowi ono połączenie między abstrakcyjnym typem danych a jego implementacją. Z punktu widzenia programisty jest ono niewidoczne; kompilator automatycznie generuje odpowiedni kod gdy w programie pojawi się odniesienie do tablicy.
  2. Dla tablic 3 i więcej wymiarowych równanie to staje się coraz bardziej skomplikowane.
  3. Liczba wierszy (ogólnie pierwszy wymiar tablicy) nie pojawia się w równaniu - nie jest potrzebna by obliczyć adres danego elementu. Dlatego przy przekazywaniu tablic do funkcji nie jest konieczne podawanie pierwszego wymiaru.

Metoda K&R redukcji tablic do wskaźników

K&R stworzyli zunifikowane pojęcie tablicy i wskaźnika. Ich rozwiązanie można przedstawić w postaci pięciu reguł:

  1. Tablica N-wymiarowa jest tablicą 1D elementów, które są tablicami N-1-wymiarowymi.
  2. Tablica jest traktowana jako wskaźnik do swojego pierwszego elementu (decay convention). Decay convention nie powinna być używana więcej niż raz do tego samego obiektu.
  3. Pobranie elementu o indeksie i jest równoważne operacji "dodaj wskaźnikowo indeks do adresu początku tablicy i pobierz element wskazany przez tak otrzymaną sumę"
  4. Dla tablic wielowymiarowych reguły 2 i 3 są stosowane rekursyjnie, ale wyłuskiwany jest tylko typ danych, a nie wskaźnik (z wyjątkiem ostatniego kroku).

Powyższe reguły prowadzą do równania tablic. Dla tablicy 2D elementów typu T mamy:


T a[m][n];
a[i] = a + i * sizeof(row) // no '*' since it is an address (rule 4.)
a[i][j] = *(a[i] + j * sizeof(T))
a[i][j] = *(a + i * sizeof(row) + j * sizeof(T))
a[i][j] = *(a + (i * n + j) * sizeof(T)) // since sizeof(row) = n * sizeof(T)
&a[i][j] = a + (i * n + j) * sizeof(T)

Uzyskaliśmy więc równanie jak wyżej.

Sposoby dostępu do tablicy 2D.

  1. Tablica 2D (może być bez pierwszego wymiaru)
  2. Wskaźnik do tablicy; drugi wymiar podany jawnie
  3. Pojedynczy wskaźnik; "spłaszczenie" tablicy do 1D. W ten sposób można tworzyć funkcje ogólnego zastosowania; rzeczywiste wymiary nie pojawiają się nigdzie i mogą być przekazane przez listę parametrów.
  4. Podwójny wskaźnik; użycie pomocniczej tablicy wskaźników.
  5. Pojedynczy wskaźnik; użycie pomocniczej tablicy wskaźników.

Tablice wielowymiarowe dynamiczne

Pointerss

Stos i sterta (stack and heap)

Stos (stack) to obszar pamięci w przestrzeni adresowej programu wykorzystywany do specyficznych celów. Stos obsługiwany jest w sposób automatyczny, obsługa ta nie wymaga ingerencji programisty. Na stosie dostępny jest wyłącznie element położony na jego wierzchołku, a elementy zdejmowane są ze stosu w odwrotnej kolejności niż były na nim umieszczane (rejestr LIFO).

Na stosie przechowywane są:

Sterta (heap) to obszar pamięci udostępniany przez system operacyjny wszystkim działającym programom (procesom). Na stercie przechowywane są dynamicznie przydzielane obszary pamięci

Pamięć statyczna

Wszystkie zmienne globalne oraz zmienne statyczne. Dodatkowo, pamięć w której przechowywany jest binarny kod wykonywalny programu również jest statycznie zaalokowana. Pamięć statyczna jest alokowana w momencie uruchomienia procesu (zawsze w tym samym miejscu).

Wskaźniki do funkcji

Składnia:


int (*foo)(int);

foo jest wskaźnikiem do funkcji posiadającej jeden argument typu int i zwracającej wartość typu int.

Deklarując wskaźniki funkcyjne postepujemy jak przy deklaracji funkcji, zamieniając nazwę funkcji (np. foo) na (*foo) (pamiętając o nawiasach).

Co oznacza deklaracja:


void *(*foo)(int *);

Czytamy 'od środka': najbardziej wewnętrznym elementem deklaracji jest *foo, reszta wygląda jak normalna deklaracja funkcji. Czyli *foo jest funkcją, która przyjmuje argument int* i zwraca void*. W takim razie foo jest wskaźnikiem do takiej funkcji.

Inicjalizacja wskaźników funkcyjnych:

Aby zainicjalizować wskaźnik funkcyjny musimy podać adres funkcji o parametrach zgodnych z deklaracją


void my_int_func(int x) {
	printf("%d\n", x);
}

int main(void) {
	void (*foo)(int);
	foo = &my_int_func;	// the ampersand is optional
	foo(2);	// call my_int_func; you do not need to write (*foo)(2)
	(*foo)(2);	// but if you want to, you may
	return 0;
}

Z powyższego przykładu wynika, że składnia wskaźników funkcyjnych jest elastyczna: można stosować konwencję 'wskaźnikową', z '*' i '&', albo ominąć tę część.

Obiektowość - własne typy danych

  1. C++ pozwala na zdefiniowanie własnego typu danych, z którego można korzystać tak samo jak z typu wbudowanego
  2. Typ, to nie tylko dane (liczby, napisy, etc.) ale także i zbiór operacji, które można na obiekcie tego typu wykonać (np. typ int z operacjami +, -, *, /, %)
  3. Łatwiej jest zrozumieć i zmodyfikować program, który zawiera typy ściśle odpowiadające pojęciom z dziedziny zastosowań, niż program, który tego nie robi
  4. Głównym pomysłem w definiowaniu nowych typów jest oddzielenie szczegółów implementacji od cech istotnych dla właściwego korzystania z tego typu) - enkapsulacja

Definicja klasy

  1. Do opisu nowego typu definiujemy nową klasę
    
    class nazwa {
    	// ciało klasy
    };
    
  2. Tworzenie obiektu klasy:
    
    nazwa obiekt;
    
  3. Ta definicja jest analogiczna do definicji zmiennych typów wbudowanych, np. (int n;)
  4. Mozna również tworzyć wskaźniki do obiektów i tablice obiektów:
    
    nazwa* ptr; // wskaźnik do typu nazwa
    nazwa tab[10]; // tablica obiektów typu nazwa
    

Składowe klasy

  1. Podejście funkcjonalne:
    • Atrybuty (dane):
      
      struct date {
      	short day;
      	short month;
      	int year;
      };
      
    • Metody (funkcje):
      
      void set_date(date&, short, short, int);
      date& next_date(const date&);
      void print(const date&);
      
      Nie ma powiązania między danymi i funkcjami.
  2. Podejście obiektowe:
    • Aby powiązać dane z funkcjami, czyli aby wskazać, że na obiektach typu date mogą działać tylko te, a nie inne funkcje, definiujemy klasę:
      
      class date {
      	short day;
      	short month;
      	int year;
      public:	
      	void set_date(short, short, int);
      	date& next_date() const;
      	void print() const;
      };
      
    • Widać, że powyższe funkcje są także składowymi klasy date
    • Nazwy deklarowane w klasie mają zakres ważności równy obszarowi całej klasy

Odwoływanie się do składowych klasy

Aby odnieść się do atrybutów obiektu lub metod klasy można używać jednej z poniższych notacji


obiekt.atrybut;
wsk_obiektu->atrybut;
referencja_obiektu.atrybut;
obiekt.metoda();
wsk_obiektu->metoda();
referencja_obiektu.metoda();			
np.

date tomorrow, *birthday, *day_after_tomorrow;
date& tom = tomorrow;
birthday = &tomorrow;
tomorrow.day = 4;
birthday->year = 2003;
tomorrow.print();
day_after_tomorrow = birthday->next_date();

Enkapsulacja (ukrywanie informacji)

Nie wszystkie składowe klasy muszą być widoczne na zewnątrz (jak to było w przypadku struktur), tzn. nie wszystkie atrybuty czy metody klasy mogą być dostępne spoza tej klasy.

Etykieta public: dzieli składowe klasy na dwie części:

Korzyści z enkapsulacji danych:

Reguły składniowe:

Klasa a obiekt

Definicja klasy nie definiuje żadnych obiektów. Jest to tylko określenie nowego typu danych. Dopiero mając gotowy projekt (definicję klasy) można utworzyć obiekty tej klasy


class person { // klasa
	char name[40]; // część prywatna
	int age;
public: // część publiczna
	void set(const char*, int);
	void print();
}; // koniec definicji klasy
...
person student1, student2, professor; // obiekty; odpowiednik zmiennych

Każdy obiekt klasy znajduje się w swoim odrębnym miejscu w pamięci.

Metody klasy są składowane jednokrotnie(bo są wspólne dla wszystkich obiektów tej klasy).

Metoda jest narzędziem, za pomocą którego dokonujemy operacji na atrybutach klasy. Wywołanie:


student1.set("Jan Kowalski", 21);
professor.set("Albert Einstein", 57);
należy rozumieć: na rzecz obiektu student1 wykonaj funkcję set z danymi argumentami.

Definiowanie funkcji składowych

Funkcja składowa może być zdefiniowana:

Inicjalizacja klas - konstruktory

Konstruktor jest specyficzną funkcją, która jest wywoływana zawsze gdy tworzony jest obiekt. Jeśli programista nie utworzy konstruktora dla klasy, kompilator automatycznie utworzy konstruktor, który nic nie będzie robił. Konstruktor nie pojawi się nigdzie w kodzie, jednak będzie on istniał w skompilowanej wersji programu i będzie wywoływany za każdym razem, gdy będzie tworzony obiekt klasy. Jeśli chcemy zmienić domyślne własności konstruktora jaki jest tworzony przez kompilator C++ wystarczy, że utworzymy własny konstruktor dla klasy.

W odróżnieniu od dotąd poznanych funkcji, konstruktor nie posiada zwracanego typu danych. Druga istotna własność konstruktora to jego nazwa. Konstruktor musi nazywać się tak samo jak nazwa klasy. Konstruktory mogą posiadać parametry tak samo jak zwykłe funkcje. C++ umożliwia również tworzenie kilku konstruktorów dla jednej klasy (na ogólnych zasadach obowiązyjących przy przeładowaniu funkcji, czyli konstruktory muszą się różnić listą parametrów).

Gdy tworzymy klasę, wszystkie zmienne jakie są zadeklarowane wewnątrz niej są zainicjalizowane przypadkowymi wartościami, które zmieniamy w konstruktorze.

Przykład:


class JakasKlasa {
	int a;
	char b;
public:
	JakasKlasa() {
		a = 123;
		b = 'x';
	}
};

Czasami zachodzi potrzeba zainicjalizowania zmiennej w trakcie tworzenia klasy, a nie po jej utworzeniu. Aby to zrobić, należy użyć następującego zapisu:


class JakasKlasa {
	int a;
	char b;
public:
	JakasKlasa() : a{123}, b{'x'} { }
};

Taki zapis ma kilka bardzo istotnych zalet:

Lista inicjalizacyjna

Lista inicjalizacyjna to lista oddzielonych przecinkami identyfikatorów pól (składowych) z podanymi w nawiasach okrągłych argumentami dla konstruktorów obiektów będących składowymi tworzonego obiektu. Zwykle są to jednocześnie argumenty formalne definiowanego konstruktora, choć nie musi tak być. Jeśli argumentem przesyłanym do konstruktora obiektu składowego na liście inicjalizacyjnej jest obiekt tego samego typu, co ta składowa, to traktowane to będzie jako wywołanie konstruktora kopiującego. Taka składnia działa również dla składowych, które są typu wbudowanego - twórcy języka starali się, aby reguły składniowe dla typów obiektowych i wbudowanych były do siebie tak podobne, jak to tylko możliwe.

Listę inicjalizacyjną, poprzedzoną dwukropkiem, umieszcza się bezpośrednio po nawiasie zamykającym listę parametrów konstruktora, a przed nawiasem klamrowym otwierającym definicję tego konstruktora.

Jeśli w klasie tylko deklarujemy konstruktor, a jego definicję podajemy poza klasą, to w deklaracji listy inicjalizacyjnej nie umieszczamy.

To jest logiczne: lista inicjalizacyjna należy logicznie do implementacji, a nie do interfejsu (kontraktu). Jak pamiętamy, odwrotnie było z argumentami domniemanymi (domyślnymi) - nie tylko konstruktorów, ale w ogóle funkcji; jeśli deklaracja występuje, to argumenty domniemane muszą być zdefiniowane właśnie w deklaracji, ale nie w definicji, bowiem ich wartość, i sama ich obecność, należy jak najbardziej do kontraktu z użytkownikiem (interfejs).

Niezależnie od kolejności na liście inicjalizacyjnej, składowe obiektu są inicjowane zawsze w kolejności ich deklaracji w ciele klasy.

Niektóre kompilatory wysyłają ostrzeżenia, jeśli kolejność deklaracji w definicji klasy i kolejność na liście inicjalizacyjnej nie są zgodne.

Na liście inicjalizacyjnej nie musimy wymieniać wszystkich składowych klasy. Te składowe, które nie zostały wymienione na liście, zainicjalizowane będą (przed rozpoczęciem wykonania ciała konstruktora!) przez:

Pola stałe stosuje się rzadko. Zazwyczaj wystarczy mechanizm ochrony danych poprzez umieszczenie składowej w sekcji prywatnej klasy. Również pola odnośnikowe nie występują często: składowa odnośnikowa jest inną nazwą czegoś spoza klasy, co stwarza niepotrzebną zwykle więź między obiektem a danymi spoza obiektu. Natomiast pola obiektowe, występują często i w takich przypadkach stosowanie listy inicjalizacyjnej może być konieczne.

Konstruktor kopiujący

Konstruktor kopiujący jest wywoływany, gdy inicjujemy nowo tworzony obiekt obiektem już istniejącym. Konstruktor kopiujący kopiuje wartość każdej niestatycznej składowej klasy do jej odpowiednika w nowo tworzonym obiekcie. Jest to tzw. kopiowanie płytkie. Prototyp konstruktora kopiującego ma postać


Nazwa_klasy(const Nazwa_klasy &);

Mogłoby się wydawać, że tego rodzaju konstruktor często w ogóle nie będzie potrzebny. Tak jednak nie jest: jest on potrzebny praktycznie zawsze, choć nie zawsze sami musimy go definiować. Zauważmy bowiem, że kopiowanie obiektów zachodzi zawsze, gdy obiekt pełni rolę argumentu wywołania funkcji, jeśli tylko przekazywany jest do funkcji przez wartość, a nie przez wskaźnik lub referencję; na stosie musi zostać położona kopia obiektu. Podobnie rzecz się ma przy zwracaniu przez wartość obiektu jako wyniku funkcji: tu również jest wykonywana kopia obiektu. Za każdym razem w takim przypadku używany jest konstruktor kopiujący.

Ponieważ niejawny konstruktor kopiujący nie kopiuje składowych statycznych oraz nie kopiuje łańcuchów, tylko ich adresy, w klasach w których występują składowe statyczne albo łańcuchy dynamiczne, należy zdefiniować jawnie konstruktor kopiujący.

Dlaczego argument musi być referencją, a nie obiektem przekazywanym przez wartość? Gdyby był obiektem, to ponieważ argumenty przekazywane przez wartość są podczas wywołania kopiowane i kładzione na stosie, musiałaby najpierw zostać wykonana kopia tego obiektu. Ale do tego potrzebne byłoby ... wywołanie konstruktora kopiującego i przesłanie do niego przez wartość argumentu, a do tego znowu trzeba by wykonać kopię, a więc wywołać konstruktor kopiujący, i tak dalej, bez końca. Konstruktor kopiujący byłby zatem wywoływany rekursywnie w nieskończoność.

Z drugiej strony, przekazując do konstruktora obiekt-wzorzec przez referencję, dajemy mu możliwość zmiany tego obiektu-wzorca, dostaje on bowiem wtedy oryginał obiektu, a nie jego kopię. Najczęściej taka zmiana byłaby niepożądana. Dlatego właśnie, aby się przed możliwością takiej zmiany zabezpieczyć, parametr konstruktora kopiującego deklarujemy z modyfikatorem const. Sam kompilator zadba wtedy o to, abyśmy nawet przypadkowo nie zmodyfikowali obiektu-wzorca.

Konstruktor konwertujący (C++03)

Konstruktor, którego jedynym argumentem niedomyślnym jest obiekt dowolnej klasy lub typ wbudowany. Powoduje niejawną konwersję z typu argumentu na typ klasy własnej konstruktora. Na przykład:

W C++11 konstruktorem kowertującym może być każdy konstruktor nie posiadający własności explicit.


class MojaKlasa {
public:
	MojaKlasa(int parametr) { // konstruktor konwertujący z typu int na typ MojaKlasa
		// ciało konstruktora
	}
};
void funkcja(MojaKlasa obiekt) { /* ciało funkcji */ }

int main () {
	int zmienna = 5;
	funkcja(zmienna); // wywołanie konstruktora konwertującego z int na MojaKlasa
	return 0;
}

Przeładowanie operatorów

Klasy definiowane przez użytkownika muszą być co najmniej tak samo dobrymi typami jak typy wbudowane. Oznacza to, że:

To drugie wymaga, by twórca klasy mógł definiować operatory.

Definiowanie operatorów wymaga ostrożności.

Operatory definiujemy przede wszystkim po to, by móc czytelnie i wygodnie zapisywać programy. Jednak bardzo łatwo można nadużyć tego narzędzia (np. definiując operację + na macierzach jako odejmowanie macierzy). Dlatego projektując operatory (symbole z którymi są bardzo silnie związane pewne intuicyjne znaczenia), trzeba zachować szczególną rozwagę.

Większość operatorów języka C++ można przeciążać, tzn. definiować ich znaczenie w sposób odpowiedni dla własnych klas.

Przeciążanie operatora polega na zdefiniowaniu metody (prawie zawsze może to też być funkcja) o nazwie składającej się ze słowa operator i nazwy operatora (np. operator=).

Można przeciążać wszystkie zdefiniowane w języku operatory z wyjątkiem:

., .*, ::, ?:, sizeof

Tak zdefiniowane metody (funkcje) można wywoływać zarówno w notacji operatorowej:


a = b + c;   
jak i funkcyjnej (tej postaci praktycznie się nie stosuje):

a = (b.operator+(c));   

Uwagi dotyczące definiowania operatorów

Operatory jednoargumentowe

Operator jednoargumentowy (przedrostkowy) @ można zadeklarować jako:

Operatorów ++ oraz −− można używać zarówno w postaci przedrostkowej jak i przyrostkowej. W celu rozróżnienia definicji przedrostkowego i przyrostkowego ++ (−−) wprowadza się dla operatorów przyrostkowych dodatkowy parametr typu int.


class X{
public:
	X operator++();     // przedrostkowy ++a
	X operator++(int);  // przyrostkowy a++
};

int main(){
	X a;
	++a;  // to samo co: a.operator++();
	a++;  // to samo co: a.operator++(0);
}

Operatory dwuargumentowe

Operator dwuargumentowy @ można zadeklarować jako:

Kopiujący operator przypisania

Kopiujący operator przypisania jest czymś innym niż konstruktor kopiujący!

O ile nie zostanie zdefiniowany przez użytkownika, to będzie zdefiniowany przez kompilator, jako przypisanie składowa po składowej (więc nie musi to być przypisywanie bajt po bajcie). Język C++ nie definiuje kolejności tych przypisań.

Zwykle typ wyniku definiuje się jako X&, gdzie X jest nazwą klasy, dla której definiujemy operator=.

Uwaga na przypisania x = x, dla nich operator= też musi działać poprawnie!

Jeśli nie chcemy, by dana klasa miała zdefiniowany operator=, to nie wystarczy go nie definiować (bo zostanie wygenerowany automatycznie). Musimy zabronić jego stosowania. Można to zrobić na dwa sposoby:

Operator wywołania funkcji

Wywołanie:


wyrażenie_proste(lista_wyrażeń);

uważa się za operator dwuargumentowy z wyrażeniem prostym jako pierwszym argumentem i, być może pustą, listą wyrażeń jako drugim. Zatem wywołanie:


x(arg1, arg2, arg3); 

interpretuje się jako:


x.operator()(arg1, arg2, arg3) 

Operator indeksowania

Wyrażenie:


wyrażenie_proste [ wyrażenie ];

interpretuje się jako operator dwuargumentowy. Zatem wyrażenie:


x[y];

interpretuje się jako:


x.operator[](y) 

Operator dostępu do składowej klasy

Wyrażenie:


wyrażenie_proste -> wyrażenie_proste;

uważa się za operator jednoargumentowy. Wyrażenie:


x -> m;

interpretuje się jako:


(x.operator->())->m;

Zatem operator->() musi dawać wskaźnik do klasy, obiekt klasy albo referencję do klasy. W dwu ostatnich przypadkach, ta klasa musi mieć zdefiniowany operator -> (w końcu musimy uzyskać coś co będzie wskaźnikiem).

Dziedziczenie

Dziedziczenie polega na tworzeniu nowych klas na podstawie już istniejących. Powiedzmy, że piszemy program dotyczący różnych pojazdów. Podstawowym elementem programu będą właśnie owe pojazdy. Tak więc zdefiniujmy sobie klasę pojazd.


class pojazd {
public:
	int predkosc;
	int przyspieszenie;
	int ilosc_kol;
	int kolor;
};

Dla ułatwienia wszystkie składowe one publiczne. Chcemy teraz zdefiniować ciężarówkę. Dopisujemy więc pole ladownosc.


class ciezarowka {
public:
	int predkosc;
	int przyspieszenie;
	int ilosc_kol;
	int kolor;
	int ladownosc;
};

Obie klasy są niemal jednakowe. Odróżnia je zaledwie jeden detal. Jest to ladownosc w klasie drugiej. Zatem klasa ciezarowka jest jakby rozbudowaną klasą pojazd. Można zrobić to szybciej i krócej korzystając z dziedziczenia. Zamiast pisać od początku całą klasę ciezarowka wystarczy poinformować kompilator, że ta klasa jest rozwiniętą wersją klasy pojazd. W C++ robi się to tak:


class ciezarowka : public pojazd {
public:
	int ladownosc;
};

W wyniku takiego zapisu otrzymaliśmy klasę ciezarowka, która jest pochodną klasy pojazd. Oznacza to, że zawiera ona wszystkie wady i zalety klasy swojego przodka. Za nazwą klasy pochodnej stawiamy dwukropek a następnie określamy sposób dziedziczenia. Ustala się to za pomocą znanych etykiet. Dziedziczenie prywatne oznacza, że wszystkie składniki klasy bazowej staną się niedostępne w klasie pochodnej. Niezależnie od sposobu dziedziczenia, prywatne składniki klasy podstawowej zawsze pozostaną niedostępne w klasie pochodnej! Podczas dziedziczenia chronionego, składniki publiczne i chronione w klasie pochodnej będą chronione. Dziedziczenie publiczne jest najprostsze i chyba najczęściej stosowane. Praktycznie nie powoduje żadnych zmian. Dostęp do składników odziedziczonych jest nadal taki sam, jak w klasie podstawowej.

składniki w klasie podstawowej sposób dziedziczenia składniki w klasie pochodnej
prywatne
chronione
publiczne
prywatne niedostępne
niedostępne
niedostępne
prywatne
chronione
publiczne
chronione niedostępne
chronione
chronione
prywatne
chronione
publiczne
publiczne niedostępne
chronione
publiczne

Podczas dziedziczenia zawartość klasy bazowej staje się automatycznie zawartością klasy pochodnej. Zatem wszystkie składniki i funkcje składowe stają się dostępne w klasie pochodnej. Ta dostępność jest uwarunkowana sposobem dziedziczenia. W powyższym przykładzie składowe były publiczne. Gdybyśmy jednak użyli składowych prywatnych, nie mielibyśmy do nich dostępu. Jak w takim razie można odnieść się do prywatnych składników klasy podstawowej z wnętrza klasy pochodnej? Z myślą o dziedziczeniu została zaprojektowana etykieta protected. Składowe klasy oznaczone tą etykietą są traktowane w klasie podstawowej jako prywatne. Jednakże w przeciwieństwie do składników prywatnych są dostępne w klasach pochodnych.

Składowe, które NIE są dziedziczone:

  1. Konstruktory
  2. Destruktor
  3. Operator przypisania

Polimorfizm

W C++ możemy mówić o dwóch typach polimorfizmu. Jeden z nich poznaliśmy: polega on na wykorzystaniu mechanizmu przeładowania funkcji i operatorów (polimorfizm czasu kompilacji). Drugim typem jest polimorfizm działający w czasie wykonywania programu. Poniższy schemat przedstawia ideę.

C++ virtual

Wskaźniki do klasy bazowej

Jednym z kluczowych cech dziedziczenia jest fakt, że wskaźnik do klasy pochodnej jest kompatybilny (w sensie typu) ze wskaźnikiem do klasy bazowej. Oznacza to, że wskaźnik do klasy bazowej może z powodzeniem być użyty również do wskazywania na obiekty klas pochodnych.

Funkcje wirtualne

Rozpatrzmy następujący przykład:


// virtual members
#include <iostream>
using namespace std;

class Polygon {
protected:
	int width, height;
public:
	Polygon(int w, int h) : width(w), height(h) {}
	virtual int area() {
		return 0;
	}
};

class Rectangle: public Polygon {
public:
	int area() {
		return width * height;
	}
};

class Triangle: public Polygon {
public:
	int area() {
		return width * height / 2;
	}
};

int main () {
	Rectangle rect(4, 5);
	Triangle trgl(4, 5);
	Polygon poly(4, 5);
	Polygon* ppoly1 = &rect;
	Polygon* ppoly2 = &trgl;
	Polygon* ppoly3 = &poly;
	cout << ppoly1-<area() << endl;
	cout << ppoly2-<area() << endl;
	cout << ppoly3-<area() << endl;
	return 0;
}

Składowa wirtualna jest funkcją składową, która może być zredefiniowana w klasie pochodnej z zachowaniem jej własności w momencie wywałania jej przez wskaźnik / referencję. Aby funkcja stała się wirtualna poprzedzamy jej deklarację słowem kluczowym virtual.

W powyższym przykładzie wszystkie trzy klasy (Polygon, Rectangle, Triangle) mają te same składowe. Funkcja area() została zadeklarowana jako wirtualna w klasie bazowej, ponieważ będzie później zredefiniowana w klasach pochodnych. Składowe niewirtualne również mogą być redefiniowane w klasach pochodnych, ale jeżeli wywołamy je przy pomocy referencji do klasy bazowej, faktycznie zostanie wywołana funkcja z klasy bazowej (to wiązanie jest dokonywane na etapie kompilacji - early binding). Gdybyśmy usunęli atrybut virtual z deklaracji funkcji area(), to wszystkie wywołania tej funkcji zwróciłyby wartość zero (ponieważ tę wartość zwraca funkcja z klasy bazowej).

Tak więc atrybut virtual umożliwia tzw. late binding, czyli decyzja o tym, która z funkcji zostanie wywołana jest podejmowana dopiero w czasie wykonania programu, na podstawie rzeczywistego typu obiektu, a nie typu wkaźnika / referencji. W powyższym przykładzie odpowiednia wersja funkcji area() jest wywoływana dla obiektu klasy Rectangle, Triangle i Polygon.

Klasy abstrakcyjne

Zauważmy, że w klasie Polygon wartość zwracana przez funkcję area() jest arbitralna. Wielokąt nie jest żadną konkretną figurą i nie można mówić o jego powierzchni. W takich sytuacjach, kiedy klasa jest używana wyłącznie jako baza dla innych klas, możemy zdefiniować klasę abstrakcyjną. Klasa taka może (a nawet powinna) mieć wirtualną / e funkcję / e składową / e, ale bez podawania jej / ich definicji (które w tym przypadku nie mają sensu). Funkcje te zwane są funkcjami czysto wirtualnymi - pure virtual functions i w programie ich definicje są zastępowane przez = 0.

Nowa wersja klasy Polygon mogłaby wyglądać następująco:


class Polygon {
protected:
	int width, height;
public:
	Polygon(int w, int h) : width(w), height(h) {}
	virtual int area() = 0;
};

Klasy, które zawierają przynajmniej jedną funkcję czysto wirtualną są nazywane klasami abstrakcyjnymi. Klasy abstrakcyjne nie mogą być używane to tworzenia obiektów (czyli nie możemy użyć instrukcji Polygon poly;). Możemy natomiast definiować odpowiednie wskaźniki i wykorzystywać ich polimorficzne możliwości.

Ostatni program demonstruje cechy polimorfizmu, łącząc je z dynamiczną alokacją pamięci, zastosowaniem konstruktorów i inicjalizacji.


class Polygon {
protected:
	int width, height;
public:
	Polygon (int w, int h) : width(w), height(h) {}
	virtual int area (void) = 0;
	void printarea() {
		cout << this-<area() << '\n';
	}
};

class Rectangle: public Polygon {
public:
	Rectangle(int a,int b) : Polygon(a,b) {}
	int area() {
		return width*height;
	}
};

class Triangle: public Polygon {
public:
	Triangle(int a,int b) : Polygon(a,b) {}
	int area() {
		return width*height/2;
	}
};

int main () {
	Polygon * ppoly1 = new Rectangle (4,5);
	Polygon * ppoly2 = new Triangle (4,5);
	ppoly1-<printarea();
	ppoly2-<printarea();
	delete ppoly1;
	delete ppoly2;
	return 0;
}

Praktyczna implementacja mechanizmu polimorfizmu

Każda klasa, która posiada co najmniej jedną funkcję wirtualną, ma dodatkowe pole będące adresem tzw. tablicy funkcji wirtualnych. Jest to tablica adresów funkcji właściwych dla danej klasy. Umożliwia ona zastosowanie odpowiedniej do typu obiektu funkcji, niezależnie od typu wskaźnika / referencji

C++ virtual

C++ templates

Java Generics vs. C++ templates
  1. In C++ generic functions/classes can only be defined in headers, since the compiler generates different functions for different types (that it's invoked with). So the compilation is slower. Java uses a technique called "erasure" where the generic type is erased at runtime.
  2. In C++ templates, parameters can be any type or integral but in Java, parameters can only be reference types.
  3. For C++ templates, separate copies of the class or function are likely to be generated for each type parameter when compiled. However for Java generics, only one version of the class or function is compiled and it works for all type parameters.
  4. C++ templates can be specialized - a separate implementation could be provided for a particular template parameter. In Java, generics cannot be specialized. C++ does not support wildcard. Java supports wildcard as type parameter if it is only used once.
  5. In C++, static variables are not shared between classes of different type parameters. In Java, static variables are shared between instances of a classes of different type parameters.

In summary:

Generics in Java

  1. are generated at runtime
  2. use Object substitution and casting instead of multiple native versions of the class
  3. support explicit constraints

Templates in C++

  1. are generated at compile time
  2. create native codebase per template parameter
  3. support explicit constraints since C++ 20 (concepts)
Function templates

The format for declaring function templates with type parameters is:


template <class identifier> function_declaration;
template <typename identifier> function_declaration;
The only difference between both prototypes is the use of either the keyword class or the keyword typename. Its use is indistinct, since both expressions have exactly the same meaning and behave exactly the same way. For example, to create a template function that returns the greater one of two objects we could use:

template <class T>
T get_max (T a, T b) {
	return (a > b ? a : b);
}

Here we have created a template function with T as its template parameter. This template parameter represents a type that has not yet been specified, but that can be used in the template function as if it were a regular type. As you can see, the function template GetMax returns the greater of two parameters of this still-undefined type.

To use this function template we use the following format for the function call:


function_name<type>(parameters);

For example, to call get_max() to compare two integer values of type int we can write:


int x, y;
get_max<int>(x,y);

When the compiler encounters this call to a template function, it uses the template to automatically generate a function replacing each appearance of T by the type passed as the actual template parameter (int in this case) and then calls it. This process is automatically performed by the compiler and is invisible to the programmer.

If the generic type T is used as a function parameter, the compiler can find out automatically which data type has to instantiate without having to explicitly specify it within angle brackets. So we can write:


int x, y;
get_max(x,y);

Because our template function includes only one template parameter (class T) and the function template itself accepts two parameters, both of type T, we cannot call our function template with two objects of different types as arguments:


int i;
long l;
get_max(i, l); // Error!!! Different types!!!

This is not correct, since our function template expects two arguments of the same type, and in this call to it we use objects of two different types.

But we can define function templates that accept more than one type parameter, simply by specifying more template parameters between the angle brackets. For example:


template <class T, class U>
T get_min (T a, U b) {
	return (a < b ? a : b);
}

This function template accepts two parameters of different types and returns an object of the same type as the first parameter (T) that is passed. For example, after that declaration we could call get_min() with:


int i;
long l;
get_min(i, l); // OK; equivalent to get_min<int, long>(i, l);

even though the parameters have different types, since the compiler can determine the appropriate instantiation anyway.

Class templates

We also have the possibility to write class templates, so that a class can have members that use template parameters as types. For example:


template <class T, class U> class mypair {
	T first;
	U second;
public:
	mypair (T first, U second) : first{first}, second{second} {}
	void print();
};

template <class T, class U>
void mypair<T, U>::print () {
	cout << "(" << first << ", " << second << ")" << endl;
}

int main () {
	mypair p(1, 7.5); // mypair<int, double> p(1, 7.5);
	p.print();
	return 0;
}

In case that we define a function member outside the declaration of the class template, we must always precede that definition with the template prefix.

Template specialization and overloading

If we want to define a different implementation for a template when a specific type is passed as template parameter, we can declare a specialization of that template.

For example, let's suppose that we have a very simple function called min_tmpl() that returns miniumum value of its parameters. It uses the < operator, so it is not suitable for those types for which this operator is no defined, for instance char array. For such types it would be convenient to have a completely different implementation, so we decide to declare a function template specialization.

template<class T> T min_tmpl (T a, T b) { // overload (a)
	return a < b ? a : b;
}

template<class T> T* min_tmpl (T* a, T* b) { // overload (b)
	return *a < *b ? a : b;
}

const char *min_tmpl (const char *n1, const char *n2) { // specialization (c)
	return (strcmp(n1, n2) < 0 ? n1 : n2);
}

int main () {
	int n = min_tmpl (10, 20); // template version (a)
	cout << n << endl;

	double d = min_tmpl (10.0, 20.0); // template version (a)
	cout << d << endl;

	int a1 = 5;
	int a2 = 4;
	int* a3 = min_tmpl(&a1, &a2); // template version (b)
	cout << *a3 << endl;

	const char *s1 = "One";
	const char *s2 = "Two";
	const char *s3 = min_tmpl (s1, s2); // specialized version (c)
	cout << s3 << endl;

	return 0;
}
Non-type parameters for templates

Besides the template arguments that are preceded by the class or typename keywords, which represent types, templates can also have regular typed parameters, similar to those found in functions.


template <class T, int N = 10> class mysequence {
	T memblock[N];
public:
	void setmember(int n, T value) {
		memblock[n]=value;
	}
	T getmember(int n) {
		return memblock[n];
	}
};

int main () {
	mysequence <int> myints; // 10 ints
	myints.setmember(0, 100);
	cout << myints.getmember(0) << '\n';

	mysequence <double, 5> myfloats; // 5 doubles
	myfloats.setmember(3, 3.1416);
	cout << myfloats.getmember(3) << '\n';
	return 0;
}

Podstawowe wejście / wyjście - Formatowanie

Podstawowe klasy wejścia / wyjścia

Cztery podstawowe klasy we/wy przedstawiono na poniższym diagramie. Klasa ios jest (wirtualną) klasą bazową dla klas istream i ostream, które z kolei są klasami bazowymi dla iostream.

Klasy te są są dostępne przez włączenie pliku nagłówkowego <iostream>.

Pointers

Flagi formatujące

Każda flaga posiada nazwę (zdefiniowaną poprzez stałą enum). Dla wygody zdefiniowano również pewne grupy flag (zwane polami bitowymi - bit fields), również o zdefiniowanych nazwach.

Format-State Flags
Flag Bit Field Flag Purpose
ios::left ios::adjustfield Left-justify all output
ios::right Right-justify all output
ios::internal Left-justify sign or base indicator, and right-justify rest of number
ios::dec ios::basefield Display integers in base 10 (decimal) format
ios::oct Display integers in base 8 (octal) format
ios::hex Display integers in base 16 (hexadecimal) format
ios::fixed ios::floatfield Use fixed point notation when displaying floating-point numbers (the usual convention)
ios::scientific Use exponential (i.e., scientific) notation when displaying floating-point numbers
ios::showbase   Show base indicator when displaying integer output
ios::showpoint Show decimal point when displaying floating-point numbers (default is 6 significant digits)
ios::showpos Show a leading plus sign (+) when displaying positive numbers
ios::uppercase Use uppercase E when displaying exponential or hexadecimal numbers
ios::boolalpha Use textual rather than numerical format to read and write boolean values (true and false, rather than 1 and 0)
ios::skipws Skip whitespace on input
ios::unitbuf Flush output buffer after each insertion (which really has nothing to do with formatting)

Uwagi dotyczące powyższej tabeli:

  1. Defaultowe wartości flag:
    • ios::skipws i ios::dec ustawione.
    • Wszystkie pozostałe: wyzerowane.
  2. W każdym z trzech pól bitowych (ios::adjustfield, ios::basefield i ios::floatfield) odpowiednie flagi są wzajemnie wykluczające.
  3. Jeżeli w jednym z trzech pól bitowych żadna z flag nie jest ustawiona, to:
    • Dla ios::adjustfield, default: dosunięcfie do prawego marginesu.
    • Dla ios::basefield, default: na wyjściu decimal, na wejściu stosuje się konwencję C++ dotyczącą stałych całkowitych (wiodące 0 - notacja oktalna; wiodące 0x lub 0X - notacja heksadecymalna).
    • Dla ios::floatfield, zależnie od wartości liczby.
  4. Operator alternatywy bitowej ('|') może być użyty do łączenia flag (jeżeli nie są wzajemnie wyłączające), np.:
    
    file.setf(ios::dec | ios::showpoint | ios::fixed)
    

Pozostałe parametry

Przy formatowaniu we/wy można również używać następujących parametrów:

Ustawianie i zerowanie flag i parametrów we/wy

Zarówno flagi jak i parametry mogą być zmieniane przez funkcje składowe klas we/wy lub manipulatory strumieni.

Użycie funkcji składowych

Następujące trzy funkcje składowe mogą być używane do zmiany wartości flag we/wy:

stream.flags()
Zwróć bieżące wartości wszystkich flag.
stream.flags(fmtflags_value)
Zwróć bieżące wartości wszystkich flag, ustaw flagi z fmtflags_value, i wyzeruj pozostałe
stream.setf(fmtflags_value)
Ustaw flagi z fmtflags_value, a pozostałe pozostaw niezmienione. Zwróć poprzednie wartości wszystkich flag
stream.setf(fmtflags_value, fmtflags_group)
Ustaw flagi z fmtflags_value, uprzednio zerując flagi należące do grupy fmtflags_group. Pozostałe flagi pozostaw niezmienione. Zwróć poprzednie wartości wszystkich flag

Typowe użycie tej wersji funkcji jest następujące:


stream.setf(0, ios::floatfield)

Drugi z powyższych przykładów zeruje wszystkie flagi z grupy ios::floatfield

stream.unsetf(fmtflags_value)
Wyzeruj flagi z fmtflags_value, a pozostałe pozostaw niezmienione.

Kolejne trzy funkcje pozwalaja zmienić wartości parametrów we/wy.

stream.fill()
Zwróć bieżący znak wypełnienia
stream.fill(char_value)
Zwróć bieżący znak wypełnienia i ustaw nową wartość znaku wypełnienia na podaną parametrem wywołania funkcji
stream.precision()
Zwróć bieżącą wartość parametru precision
stream.precision(streamsize_value)
Zwróć bieżącą wartość parametru precision i ustaw nową wartość precyzji na podaną parametrem wywołania funkcji
stream.width()
Zwróć bieżącą wartość parametru width
stream.width(streamsize_value)
Zwróć bieżącą wartość parametru width i ustaw nową wartość szerokości pola na podaną parametrem wywołania funkcji

Manipulatory

Manipulator jest obiektem funkcyjnym, który może być użyty jako operand operacji we/wy w celu zmiany stanu strumienia we/wy. Rozróżniamy dwa typy manipulatorów: bezparametrowe i z parametrem (-ami)

Następujące tabele zawierają listę dostępnych manipulatorów

Funkcje składowe i manipulatory: kiedy czego używać?

Istotną różnicą między funkcjami składowymi a manipulatorami jest fakt, że funkcje składowe zwracają bieżące wartości parametrów, które zamierzamy zmienić. Manipulatory nie mają tej możliwości. W związku z tym, jeżeli potrzebne nam jest zachowanie poprzedniego stanu strumienia (np. żeby powrócić potem do defaultowych wartości), powinniśmy użyć funkcji składowych. Jeżeli nie jest to potrzebne, to obie metody są równoważne.

Manipulators without Parameters
Manipulators Corresponding
Format-State Flags
Manipulator Purpose Input/Output
left ios::left Turn the flag on output
right ios::right Turn the flag on output
internal ios::internal Turn the flag on output
dec ios::dec Turns flag on output
oct ios::oct Turn the flag on input/output
hex ios::hex Turn the flag on input/output
fixed ios::fixed Turns the flag on output
scientific ios::scientific Turn the flag on output
showbase
noshowbase
ios::showbase Turn the flag on/off output
showpoint
noshowpoint
ios::showpoint Turn the flag on/off output
showpos
noshowpos
ios::showpos Turn the flag on/off output
uppercase
nouppercase
ios::uppercase Turn the flag on/off output
boolalpha
noboolalpha
ios::boolalpha Turns flag on/off input/output
skipws
noskipws
ios::skipws Turn the flag on/off input
unitbuf
nounitbuf
ios::unitbuf Turn the flag on/off output
endl N/A Output a newline character
and flush the stream
output
ends N/A Output a null output
flush N/A Flush the stream output
ws N/A Skip leading whitespace input

Manipulators with Parameters
Manipulator Corresponding
Format-State
Flag(s)
Manipulator Purpose Input/Output
setbase (int_radix_value) ios::dec,
ios::oct, or
ios::hex
Set the number base to int_radix_value (10, 8 or 16) input/output
setiosflags (fmtflags_value)
resetiosflags (fmtflags_value)
As specified by fmtflags_value Turn the flags specified in fmtflags_value on/off input/output
Manipulator Corresponding
Format-State
Parameter
Manipulator Purpose Input/Output
setfill (int_char_code) fill character Set the fill character to the character whose code is specified by int_char_code output
setprecision (int_num_digits) precision Set the number of digits of precision to int_num_digits output
setw (int_width) width Set the fieldwidth in the output to int_width, which must be >= 0; for input of strings, the maximum number of characters read in will be one less than int_width (ensures that the input will fit into a char array whose size is given by int_width) input/output

Implementacja manipulatorów

Przykład: funkcja ostream& flush(ostream&). Aby jej użyć, musimy wykonać dwie operacje


flush(cout);
cout << x;

Możemy też zastosować następującą technikę:


class Flush {};
ostream& operator <<(ostream& os, const Flush&) {
	return flush(os);
}
...
Flush FLUSH;
...
cout << FLUSH << x; // bo mamy operator << dla klasy Flush;

Można też krócej i bardziej ogólnie (tak to jest zrobione w bibliotece ios)


typedef ostream&(*__omanip)(ostream&); // wskaźnik do funkcji (ostream) (ostream)
...
ostream& operator << (ostream& os, __omanip f) {
	return f(os);
}
...
cout << flush << x;

Czyli operator << przyjmuje jako argument wskaźnik do funkcji typu __omanip. Takie podejście umożliwia bardzo proste definiowanie swoich manipulatorów.

Niestety w ten sposób nie da się zdefiniować manipulatorów z argumentami, bo nie ma jak przekazać parametrów (umieszczenie na liście we/wy byłoby wywołaniem funkcji a nie wskaźnikiem funkcyjnym). Musimy więc wrócić do pomysłu z klasami.


ostream& setwidth(ostream& stream, int width) {
	stream.width(width);
	stream.precision(4);
	stream.fill('*');
	stream.setf(ios::scientific, ios::floatfield);
	return stream;
}
class Setw {
	int M_w;
public:
	Setw(int n) : M_w(n) {}
	friend ostream& operator<<(ostream& os, Setw f) { 
		setwidth(os, f.M_w); 
		return os; 
	}
};
inline Setw mysetw(int n) { 
	return Setw(n); 
}
...
cout << mysetw(15) << 123.2345678 << endl;

Operacje na plikach

Porównanie plików tekstowych i binarnych

Textfiles Binary Files
Easy for humans to read Impossible for humans to read
Easy to modify Harder to modify
Easy to transfer Harder to transfer
Slower access Faster access
Less accurate (subject to
conversion and roundoff errors)
Data stored exactly
Less compact storage More compact storage

Otwieranie plików

Zanim program będzie mógł uzyskać dostęp do fizycznego pliku, musimy utworzyć odpowiedni obiekt i połączyć go z tym plikiem. Takie połączenie nazywane jest otwarciem pliku. Można to zrobić używając konstruktora odpowiedniej klasy z biblioteki we/wy (czyli w momencie tworzenia obiektu) lub później, wywołując funkcję składową open() na rzecz tego obiektu.

Pliki mogą być otwierane z różnymi parametrami, przedstawionymi w tabeli

File Stream Modes (Values of Type openmode)
openmode Literal Description
ios::in Open in input (read) mode
ios::out Open in output (write) mode
ios::app Open in append mode
ios::ate Go to end of file when opened
ios::binary Open in binary mode (default is text mode)
ios::trunc If file exists, delete file contents

Istnieją też dodatkowe tryby otwarcia pliku: ios::nocreate (jeżeli plik nie istnieje, próba otwarcia się nie powiedzie) lub ios::noreplace (jeżeli plik istnieje, próba otwarcia się nie powiedzie), ale nie powinny być używane, gdyz nie są częścią standardu języka.

  1. Defaultowo pliki są otwierane w trybie tekstowym. Aby otworzyć plik w trybie binarnym, należy explicite użyć opcji ios::binary.
  2. Obiekt typu istream jest otwierany domyślnie w trybie wejściowym (ios::in).
  3. Obiekt typu ostream jest otwierany domyślnie w trybie wyjściowym (ios::out). Spowoduje to zniszczenie ewentualnej zawartości pliku. Jeżeli chcemy tego uniknąć możemy otworzyć plik w trybie dopisania (ios::app).
  4. Flagi dotyczące trybu otwarcia mogą być łączone operatorem |.
  5. Obiekt typu fstream jest defaultowo otwierany w trybie input/output (ios::in | ios::out), tzn. może być użyty zarówno do odczytu jak i zapisu.

Wykrywanie błędów i warunku końca pliku

Strumienie mogą być w różnych stanach (oznaczanych bitami stanu strumienia), przedstawionych w tabeli

Stream States (Values of Type iostate)
iostate Literal Description
ios::goodbit No problems with the stream
ios::badbit Irrecoverable error
ios::failbit Failure to read or write expected characters
ios::eofbit End-of-file encountered when reading

Klasa ios definiuje cztery funkcje, testujące stan strumienia.

stream.good()
Zwróć true w przypadku braku błędu (czyli kolejne trzy funkcje zwróciły false)
stream.eof()
Zwróć true w przypadku napotkania końca pliku
stream.fail()
Zwróć true jeżeli we/wy zawiodło
stream.bad()
Zwróć true jeżeli błąd we/wy był poważny i naprawa jest niemożliwa
  1. Gdy good() zwraca false, kolejne operacje na strumieniu nie są możliwe (przynajmniej do chwili wywołania clear(), resetująjej flagi). Gdy bad() zwróci true, jest to zwykle niemożliwe, ale gdy tylko fail() zwraca true można wywołać clear() i spróbować jeszcze raz.
  2. Przy próbie czytania poza końcem pliku, obie funkcje eof() i fail() zwracaja true, eof() ponieważ napotkaliśmy koniec pliku, a fail() ponieważ operacja zawiodła. Ten stan zwykle nie wskazuje błędu, lecz fakt, że cały plik został przeczytany.

Pliki o dostępie swobodnym ("Random Access Files")

Domyślnie odczyt lub zapis do pliku odbywa się sekwencyjnie. Jest jednak możliwe by przesunąć się w pliku do dowolnej zadanej pozycji i rozpocząć operację we/wy od tej pozycji.

Następujące typy zostały zdefiniowane do pracy z funkcjami bezpośredniego we/wy:

streamoff
Typ całkowity reprezentujący przesunięcie w pliku.
streamsize
Typ całkowity reprezentujący rozmiar pliku (lub np.liczbę znaków do odczytu / zapisu).
seekdir
Typ całkowity reprezentujący sposób przemieszczania w pliku (jak pokazano w tabeli poniżej).
Seek Origin for Seek Direction (Values of Type seekdir)
seekdir Literal Description
ios::beg Relative to beginning of stream (positive offset is absolute position)
ios::cur Relative to current stream position (negative offset toward beginning of stream, positive offset toward end of stream)
ios::end Relative to end of stream (negative offset toward begining of stream)

Funkcje stosowane do bezpośredniego odczytu z pilku:

inStream.read(c_string_variable, streamsize_value)
Czytaj co najwyżej streamsize_value znaków, umieść je w c_string_variable i zwróć *this.
inStream.gcount()
Zwróć liczbę znaków przeczytanych przez ostatnie wywołanie jednej z funkcji get(), getline(), ignore(), peek(), putback(), read(), unget().
inStream.tellg()
Zwróć bieżącą pozycję w pliku wejściowym (typu streamoff). Nazwa tellg jest skrótem od "tell get", czyli, "tell" the position at which you may "get" a value.
inStream.seekg(streamoff_value)
Przesuń wskaźnik odczytu do pozycji wyspecyfikowanej przez streamoff_value. Domyślnie przesunięcie jest mierzone od początku pliku (ios::beg). Nazwa seekg jest skrótem od "seek get", czyli, "seek a (new) position at which you may "get" a value.
inStream.seekg(streamoff_value, seekdir_value)
Przesuń wskaźnik odczytu do pozycji wyspecyfikowanej przez streamoff_value w kierunku podanym przez seekdir_value.

Funkcje stosowane do bezpośredniego zapisu do pilku:

outStream.write(c_string_variable, streamsize_value)
Pisz streamsize_value znaków z c_string_variable i zwróć *this.
outStream.tellp()
Zwróć bieżącą pozycję w pliku wyjściowym (typu streamoff). Nazwa tellp jest skrótem od "tell put", czyli, "tell" the position at which you may "put" a value.
outStream.seekp(streamoff_value)
Przesuń wskaźnik zapisu do pozycji wyspecyfikowanej przez streamoff_value. Domyślnie przesunięcie jest mierzone od początku pliku (ios::beg). Nazwa seekp jest skrótem od "seek put", czyli, "seek a (new) position at which you may "put" a value.
outStream.seekp(streamoff_value, seekdir_value)
Przesuń wskaźnik zapisu do pozycji wyspecyfikowanej przez streamoff_value w kierunku podanym przez seekdir_value.

Wyjątki

Co to są wyjątki

Jeżeli w jakimś miejscu programu zajdzie nieoczekiwana sytuacja, programista piszący ten kod powinien zasygnalizować o tym. Dawniej polegało to na zwróceniu specyficznej wartości, co nie było zbyt szczęśliwym rozwiązaniem, bo sygnał musiał być taki jak wartość zwracana przez funkcję. W przypadku obsługi sytuacji wyjątkowej mówi się o obiekcie sytuacji wyjątkowej, co często zastępowane jest słowem wyjątek. W C++ wyjątki się "rzuca", służy do tego instrukcja throw.

Tam gdzie spodziewamy się wyjątku umieszczamy blok try, w którym umieszczamy "podejrzane" instrukcje. Za tym blokiem muszą (tzn. musi przynajmniej jedna) pojawić się bloki catch. Wygląda to tak:


try {
	fun(); // fun() can throw an exception
} catch(ExceptionType e) {
	// exception handling
}

W instrukcji catch umieszczamy typ jakim będzie wyjątek. Rzucić możemy wyjątek dowolnego typu (wbudowany, biblioteczny, własna klasa wyjątku), dlatego tu określamy co nas interesuje. Nazwa tego obiektu nie jest konieczna, ale jeżeli chcemy znać wartość musimy ten obiekt nazwać. Bloków catch może być więcej, najczęściej tyle ile możliwych typów do wyłapania. Co ważne jeżeli rzucimy wyjątek konkretnego typu to "wpadnie" on do pierwszego pasującego catch nawet jeżeli inne nadają się lepiej. Dotyczy to zwłaszcza klas dziedziczonych.

Po co nam wyjątki?

Można zadać pytanie, po co nam wyjątki. Możemy przecież użyć starych, dobrych instrukcji warunkowych. Przyczyn jest kilka:

Wbrew pozorom nie wszystko da się zrobić bez użycia wyjątków. Jednym z przypadków jest wystąpienie błędu w obrębie konstruktora. Jak wtedy zasygnalizować ten błąd? Konstruktor z definicji nie zwraca wartości. Natomiast może rzucić wyjątek. To jest podstawowa cecha podejścia RAII (Resource Acquisition Is Initialization). Zadaniem konstruktora jest zapewnienie inicjalizacji pól klasy i środowiska, w jakim będą wykonywane funkcje składowe. To często wymaga akwizycji zasobów, takich jak pamięć, pliki, sockety, itp.


SomeClass {
    vector<double> v(100000); // needs to allocate memory
    ofstream os("myfile"); // needs to open a file
	...
};

W obu przypadkach musielibyśmy sprawdzić, czy alokacja pamięci dla wektora / otwarcie pliku się powiodło. Ponieważ konstruktory klasy vector jak i ofstream generują wyjątki w przypadku niepowodzenia, nie musimy tego robić i dodatkowo nie ma obawy, że zapomnimy to zrobić i program nie będzie działał poprawnie. Jest to szczególnie istotne dla klas złożonych z wielu zależnych od siebie obiektów

Dla zwykłych funkcji możemy albo zwrócić kod błędu, bądź ustawić globalną zmienną (np. errno). Ustawienie zmiennej globalnej nie działa za dobrze - musimy testować ją bezpośrednio po zmianie wartości, inaczej inna funkcja może ją ponownie zmienić. Jeszcze gorzej wygląda sprawa w przypadku programów wielowątkowych.

Zwrócenie kodu błędu przez funkcję też nie zawsze jest łatwe. Często każda wartość jest wartością poprawną i trudno jest ustalić jakąś jako błędną.


int divide(int a, int b) {
	if(b != 0) return a / b;
	return ???; // any int value is a legal quotient
};

W takich przypadkach musielibyśmy zwracać pary wartości (wynik i kod błędu).

Jak używać wyjątków?

Nie powinno się używać wyjątków jako kolejnej wartości zwracanej przez funkcję. Kluczową techniką jest RAII (resource acquisition is initialization), która używa destruktorów klas w celu wymuszenia odpowiedniego zarządzania zasobami.


void f(string s) {
	File_handle f(s,"r"); // File_handle's constructor opens the file called "s"
	... // use f
} // here File_handle's destructor closes the file  

Jeżeli część funkcji f() oznaczona "use f" wygeneruje wyjątek, destruktor File_handle jest mimo to wywołany (zapewnia to mechanizm obsługi wyjątków) i plik jest prawidłowo zamknięty, w przeciwieństwie do następującego "starego" przypadku


void old_f(const char* s) {
	FILE* f = fopen(s, "r"); // open the file named "s"
	... // use f
	fclose(f); // close the file
}

Tutaj, jeżeli funkcja old_f() zgłosi wyjątek (lub użyje zdania return, plik nie zostanie zamknięty.

Propagacja błędów

W większych systemach, funkcja, która wykryła błąd musi przekazać go dalej do innej funkcji, odpowiedzialnej za obsługę tego błędu. Taka "propagacja błędu" często przechodzi przez dziesiątki funkcji, z których dopiero ostatnia zajmuje się obsługą (bo tylko ona ma wystarczająco informacji, żeby wiedzieć, co należy zrobić w konkretnym przypadku). Wyjątki pozwalają na bardzo prostą propagację błędów, nie angażując w ten proces funkcji pośredniczących.


void f1() {
	try {
		// ...
		f2();
		// ...
	} catch (some_exception& e) {
		// ...code that handles the error...
	}
}
void f2() { ...; f3(); ...; }
void f3() { ...; f4(); ...; }
void f4() { ...; f5(); ...; }
void f5() { ...; f6(); ...; }
void f6() { ...; f7(); ...; }
void f7() { ...; f8(); ...; }
void f8() { ...; f9(); ...; }
void f9() { ...; f10(); ...; }
void f10() {
	// ...
	if ( /*...some error condition...*/ )
		throw some_exception();
	// ...
}

Jak widać tylko dwie "końcowe" funkcje: ta, która wykryła błąd, f10() i ta odpowiedzialna za jego obsługę, f1(), są "zaśmiecone" informacją o ewentualnym błędzie. Pozostałe nie uczestniczą w propagacji.

Jednak używając kodów powrotu, wszystkie funkcje muszą aktywnie uczestniczyć w procesie propagacji błędu.


int f1() {
	// ...
	int rc = f2();
	if (rc == 0) {
		// ...
	} else {
		// ...code that handles the error...
	}
}
int f2() {
	// ...
	int rc = f3();
	if (rc != 0) return rc;
	// ...
	return 0;
}

// ... more functions as above
// ... f3() ... f8()

int f9() {
	// ...
	int rc = f10();
	if (rc != 0) return rc;
	// ...
	return 0;
}
int f10() {
	// ...
	if (...some error condition...)
		return some_nonzero_error_code;
	// ...
	return 0;
}

Wady:

  1. Zaśmiecanie funkcji f2() do f9() niepotrzebnymi warunkami, które ich nie dotyczą
  2. Zwiększenie objętości kodu
  3. Zwiększenie stopnia skomplikowania kodu
  4. Wymaga, by kod powrotu z funkcji odpowiadał za dwie różne rzeczy: faktyczną wartość zwracaną przez funkcję w przypadku sukcesu i kod błędu w przypadku jego wystąpienia. Czasem wymaga to dodatkowego parametru mówiącego, z którym z tych wariantów mamy do czynienia

Wyjątki i destruktory

Co w przypadku, gdy zawodzi destruktor?

Destruktor jest jednym z miejsc, gdzie nie wolno generować wyjątków. Dlaczego?


class A {
public:
	void f() {
		// ...
		throw Ex1();
	}
	~A() {
		// ...
		throw Ex2(); // Design error
	}
};

int main() {
	try{
		A a;
		a.f(); // Before jumping to 'catch', destructor for 'a' is called
	} catch(Ex1 e) {
		// ...
	} catch(Ex2 e) {
		// ...
	}
}

Zasadą C++ jest, że w momencie wykrycia wyjątku przez blok try, zanim sterowanie zostanie przeniesione do bloku catch, zostaną wywołane destruktory dla wszystkich lokalnych obiektów w pełni skonstruowanych. W przypadku powyżej oznacza to, że podczas działania funkcji f() na obiekcie a zostanie wyłapany wyjątek typu Ex1 (ponieważ generuje go funkcja f()). Ale zanim sterowanie zostanie przeniesione do bloku catch, zostanie uruchomiony destruktor dla obiektu a, który wygeneruje kolejny wyjątek, tym razem typu Ex2! W takim razie jak powinna wyglądać obsługa takiej sytuacji? Do którego z bloków powinno przenieść się sterowanie programu? Nie ma na to pytanie dobrej odpowiedzi i w takiej sytuacji automatycznie wołana jest funkcja terminate(), która zabija proces!

Dlatego jako regułę przyjmuje się, że destruktor nie generuje wyjątków.

Co jeżeli konstruktor zawodzi?

Należy pamiętać, że zasada o destrukcji obiektów lokalnych w momencie wyłapania wyjątku dotyczy obiektów w pełni skonstruowanych. Jeżeli wyjątek jest generowany w konstruktorze, to budowa obiektu nie zostanie ukończona i destruktor nie zostanie wywołany. Oznacza to, że przydzielone do tej pory zasoby powinny zostać zwolnione ręcznie, bądż, co lepsze, korzystając z RAII (klas automatycznie zwalniających zasoby). Na przykład zamiast zwykłych wskaźników do alokacji pamięci lepiej używać tzw. smart pointers, które mają wbudowany mechanizm zwalniania zaalokowanej pamięci.

Jakich typów wyjątków używać?

C++, w odróżnieniu od większości języków z wyjątkami, jest bardzo liberalny w sensie typów, jakie mogą być generowane w instrukcji throw. Formalnie można wygenerować wyjątek dowolnego typu. Rodzi to kolejne pytanie: jakich typów powinno się używać?

Po pierwsze lepiej używać obiektów, niż typów wbudowanych. Jeżeli to możliwe, należy korzystać z typów pochodnych bibliotecznej klasy std::exception. Umożliwia to wyłapanie takich wyjątków przez blok catch(std::exception e). Możemy oczywiście stosować bardziej specyficzne wyjątki, np std::runtime_error.

Najpopularniejszą praktyką jest rzucanie obiektów tymczasowych, np.


class MyException : public std::runtime_error {
public:
	MyException() : std::runtime_error("MyException") {}
};

void f() {
   // ...
   throw MyException();
}

Tutaj tymczasowy obiekt typu MyException jest tworzony i rzucany. Klasa MyException dziedziczy z klasy std::runtime_error, a ta z kolei z std::exception.

Wyrażenia lambda

Wprowadzenie do wyrażeń lambda

Funktor jest klasą, która posiada implementację operatora (). Tworzenie takich funktorów może być kłopotliwe i pracochłonne, szczególnie jeżeli funktor ma być użyty jednokrotnie. Wtedy kod niepotrzebnie sie komplikuje.


class X {
public:
	int operator()(int n) { return 2 * n; }
};

Wyrażenia lambda są uproszczonym zapisem funktorów.

Prosty przykład:


auto mul2 = [](int n) -> int { return 2 * n; };

lub nawet prościej - typ zwracany zostanie odgadnięty przez kompilator:


auto mul2 = [](int n) { return 2 * n; };

Nawiasy kwadratowe, [] zaczynają definicję lambdy. Podobnie jak funkcja, lambda może mieć parametry, po których następuje część wykonywalna. Przekazywanie parametrów odbywa się tak samo jak dla zwykłych funkcji. Po symbolu strzałki -> może występować typ wartości zwracanej (trailing return type), jednak najczęściej jest on wnioskowany z treści lambdy.

Część wykonywalna może być dowolna, aczkolwiek w praktyce ogranicza się ją do kilku instrukcji.

Lambda jest obiektem, tak więc posiada typ i może być przechowywana (na ogólnych zasadach zasięgu zmiennych). Należy jednak pamiętać, że typ lambdy jest definiowany przez kompilator i znany tylko dla niego. Dlatego przy definiowanu instancji lambdy zawsze używamy specyfikacji auto.

Lambdy świetnie nadają się do wykorzystania w funkcjach STL (głównie sekcja algorithms), np.:


int main() {
	vector<int> v { 1, 2, 3 };
	auto print = [] (int n) { cout << n << endl; };
	for_each(v.begin(), v.end(), print);
}

W powyższym przykładzie algorytm for_each powoduje wywołanie lambdy dla każdego elementu kontenera.

Można to zapisać nawet prościej, wpisując lambdę bezpośrednio w miejsce wywołania. Istnieje ona wtedy tylko w obrębie wywołującej ją funkcji, czyli dokładnie tam gdzie jest potrzebna!


int main() {
	vector<int> v { 1, 2, 3 };
	for_each(v.begin(), v.end(), [](int n) { cout << n << endl; });
	//	find iterator to the first even number
	auto it = find_if(v.begin(), v.end(), [](int n) { return n % 2 == 0; });
	cout << *it << endl;
}

Under the hood, czyli jak to jest zrobione

Kiedy definiujemy lambdę, kompilator używa tej definicji do zbudowania tymczasowej klasy (funktora), o nazwie znanej tylko dla kompilatora.

Kod użytkownika:


[](int& n) { n *= 2; };

Po stronie kompilatora powstanie coś w stylu:


class _SomeCompilerGeneratedName_ {
public:
	void operator()(int& n) const { n *= 2; }
};

Czyli program


int main() {
	vector<int> v { 1, 2, 3 };
	for_each(v.begin(), v.end(), [](int& n) { n *= 2; });
}

zostanie przez kompilator przetłumaczony mniej więcej tak:


class _SomeCompilerGeneratedName_ {
public:
	void operator()(int& n) const { n *= 2; }
};
int main() {
	vector<int> v { 1, 2, 3 };
	for_each(v.begin(), v.end(), _SomeCompilerGeneratedName_{});
}

Widać różnicę!!!

Przechwytywanie kontekstu (lambda captures)

Często chcielibyśmy odwołać się z wnętrza lambdy do obiektów z jej otoczenia (czyli zakresu w jakim została zdefiniowana). Moglibyśmy przesłać te obiekty jako parametry, ale to nie zadziała z algorytmami, ponieważ nie posiadają mechanizmu przekazywania dodatkowych parametrów.

Gdybyśmy pisali własny funktor, moglibyśmy przesłać odpowiednie parametry do konstruktora klasy. W przypadku lambd wykorzystujemy mechanizm przechwytywania kontekstu.

Kontekstem lambdy jest zbiór obiektów istniejących w momencie jej wywołania. Obiekty te mogą zostać przechwycone i użyte wewnątrz lambdy.

Przechwycenie obiektu przez nazwę oznacza, że wewnątrz lambdy powstaje lokalna kopia tego obiektu. Wartość factor nie może zostać zmieniona - to tylko lokalna kopia.


int main() {
	int factor = 10;
	vector<int> v { 1, 2, 3 };
	for_each(v.begin(), v.end(), [factor](int n) { cout << n * factor << endl; });
}

Przechwycenie obiektu przez referencję oznacza, że lambda może zmieniać jego zawartość (i ta zmiana będzie widoczna po zakończeniu działania lambdy). W poniższym przykładzie sumujemy elementy wektora.


int main() {
	int total{};
	vector<int> v { 1, 2, 3 };
	for_each(v.begin(), v.end(), [&total](int n) { total += n; });
	cout << total << endl;
}

Uwaga: Lambda jest obiektem i jak każdy obiekt może być kopiowana, przekazywana jako parametr, przechowywana w kontenerze, itp. Ma też swój wyznaczony zasięg i czas życia, który czasem może być inny niż zasięg / czas życia przechwyconego obiektu. W związku z tym należy zachować ostrożność przy przechwytywaniu obiektów lokalnych przez referencję, ponieważ może się zdarzyć, że lambda będzie posiadała referencję do już nieistniejącego obiektu.

Można przechwycić wszystkie zmienne w zasięgu lambdy wykorzystując tzw. default-capture.


int i;
double d;
vector<double> v(1000);

auto lam1 = [&]() { /* some code */ }; // capture everything by reference 
auto lam1 = [=]() { /* some code */ }; // capture everything by value

Poniższa tabela pokazuje różne warianty lambda captures

[ ] ( ) { } no captures
[=] ( ) { } captures everything by copy (not recommendded)
[&] ( ) { } captures everything by reference (not recommendded)
[x] ( ) { } captures x by copy
[&x] () { } captures x by reference
[&, x] () { } captures x by copy, everything else by reference
[=, &x] () { } captures x by reference, everything else by copy

Under the hood (again)

Jeżeli lambda posiada niepustą listę przechwytywanych obiektów, kompilator dodaje odpowiednie pola składowe do klasy funktora i konstruktor inicjalizujący te pola. Należy jednak pamiętać, że dla każdego obiektu przechwytywanego przez wartość tworzona jest kopia oryginału. Dla każdego obiektu przechwytywanego przez referencję, ta referencja jest przechowywana.


int main() {
	int total{};
	int offset{1};
	vector<int> v { 1, 2, 3 };
	for_each(v.begin(), v.end(),
		[&total, offset](int& n) { total += n + offset; });
	cout << total << endl;
}

Kompilator generuje kod podobny do następującego


class _SomeCompilerGeneratedName_ {
	int& total_; // context captured by reference
	int offset_; // context captured by value
public:
	_SomeCompilerGeneratedName_(int& t, int o) : total_{t}, offset_{o} {}
	void operator()(int& n) const {
		total_ += n + offset_;
	}
};
int main() {
	int total{};
	int offset{1};
	vector<int> v { 1, 2, 3 };
	for_each(v.begin(), v.end(), _SomeCompilerGeneratedName_{total, offset});
	cout << total << endl;
}

Mutable Lambda Function

Jak można zauważyć w powyższych przykładach, operator wywołania funkcji generowany przez kompilator, ma atrybut const. Oznacza to, że jeżeli lambda przechwytuje obiekt przez wartość to nie może jej zmienić. Aby było to możliwe używamy atrybutu mutable, który powoduje opuszczenie const, jak poniżej:


[n]() mutable { ++n; }
// is equivalent to
class _SomeCompilerGeneratedName_ {
	int n_;
public:
	_SomeCompilerGeneratedName_(int n) : n_{n} {}
    void operator()() { ++n_; }
};

Lambdy w funkcjach składowych klas

Lambdy mogą i często są używane wewnątrz funkcji składowych. Ponieważ są to osobne obiekty, nie mają one bezpośredniego dostępu do innych składowych otaczającej klasy. Aby przechwycić te składowe (łącznie ze składowymi prywatnymi), przechwytujemy wskaźnik this.


class Filter {
	vector<int> v;
	int level;
public:
	Filter(vector<int> v, int l) : v{v}, level{l} {}
	void filter() {
		remove_if(v.begin(), v.end(), [this](int n) { return n < level; });
	}
};

Callable objects

Callable object jest ogólną nazwą każdego obiektu, który może być wywoływany jak funkcja.

W C / C++ mamy pojęcie wskaźnika funkcyjnego, który pozwala na zapamiętanie adresu dowolnej funkcji (pod warunkiem zgodności sygnatury). Jednak wskaźnik do funkcji zewnętrznej ma inną sygnaturę niż wskaźnik do funkcji składowej i inną niż lambda. Byłoby dobrze mieć ogólny wskaźnik do dowolnego obiektu callable o danej sygnaturze.

std::function jest wzorcem, który może przechować dowolny obiekt callable o zgodnej sygnaturze.


#include <functional>

void f() { cout << "Hello from f()" << endl; }

class SimpleCallBack {
	std::function<void(void)> callback; // void (*callback)(void)
public:
	SimpleCallBack(std::function<void(void)> callback) : callback{callback} {}
	void execute() {
		if(callback != nullptr) { // is the function valid?
			callback();           // call like a normal function
		}
	}
};

class Functor {
public:
	void operator()() { cout << "Hello from Functor" << endl; }
};

int main() {
	SimpleCallBack(f).execute();
	SimpleCallBack(Functor{}).execute();
	SimpleCallBack([]() { cout << "Hello from lambda" << endl; }).execute();
}

STL: Kontenery

Kontener jest sekwencją elementów (obiektów), dysponującym mechanizmami alokacji i dealokacji pamięci i dostępu do poszczególnych elementów przy użyciu iteratorów i / lub funkcji składowych

Standardowe kontenery implementują struktury danych takie jak:

Kontenery oszczędzają bardzo dużo czasu poświęcanego na reimplementację typowego kodu. Oprócz tego biblioteka ma standardowy interfejs dla funkcji składowych. Dzięki temu w prosty sposób mogą być stosowane w algorytmach STL.

Można wyróżnić cztery typy kontenerów:

Kontenery sekwencyjne (Sequence Containers)

Kontenery sekwencyjne są używane do tworzenia struktur danych obiektów tego samego typu w porządku liniowym. Do kontenerów sekwencyjnych zaliczamy:

While std::string is not included in most container lists, it does in fact meet the requirements of a SequenceContainer.

Container Adapters

Kontenery adaptacyjne nie są pełnymi kontenerami a raczej otoczkami (wrappers) innych kontenerów, takich jak vector, deque, lub list. Kontenery te limitują interfejs użytkownika zgodnie z przeznaczeniem kontenera.

Rozpatrzmy kontener std::stack, który praktycznie jest kolejką LIFO. Deklaracja stosu wygląda następująco:


template<
    class T,
    class Container = std::deque<T>
> class stack;

Czyli implementacja kontenera polega na wykorzystaniu std::deque<T>. Biblioteka daje możliwość zmiany std::deque na inny typ kontenera, który powinien spełniać następujące kryteria:

Kontenery std::vector, std::deque i std::list spełniają te wymagania i mogą być użyte jako kontener bazowy.

Standardowe kontenery adaptacyjne to:

Kontenery asocjacyjne

Kontenery asocjacyjne są stosowane do posortowanych struktur danych o czasie wyszukiwania według klucza rzędu $O(\log n)$.

Wśród kontenerów asocjacyjnych wyróżniamy takie, które wymagają unikalnych kluczy i takie, które dopuszczają więcej niż jeden element z danym kluczem

Dla każdego z tych kontenerów można podać komparator kluczy, np. dla std::set


template<
    class Key,
    class Compare = std::less<Key>,
    class Allocator = std::allocator<Key>
> class set;

Defaultową funkcją porównującą dla kontenerów asocjacyjnych jest std::less(). Może ona zostać zmieniona w czasie deklaracji kontenera

Nieuporządkowane kontenery asocjacyjne

Nieuporządkowane kontenery asocjacyjne implementują nieposortowaną strukturę danych, wykorzystującą technikę haszowania. W najgorszym przypadku czas dostępu jest $O(n)$, ale na ogół znacznie mniejszy.

Dla wszystkich kontenerów tego typu dostęp do danych zapewnia klucz. Podobnie do posortowanych kontenerów, kontenery nieposortowane możemy podzielić na te, które wymagają unikalnych kluczy i te, które dopuszczają duplikaty:

Również tutaj podczas tworzenia kontenera możemy nadpisać jego defaultowe ustawienia:


template<
    class Key,
    class Hash = std::hash<Key>,
    class KeyEqual = std::equal_to<Key>,
    class Allocator = std::allocator<Key>
> class unordered_set;

Dlaczego używać standardowych kontenerów?

Standardowe kontenery ułatwiają tworzenie nowych systemów bez potrzeby reimplementacji najczęściej używanych struktur danych i algorytmów. Do ich zalet należy:

  1. kontenery STL są bardzo dobrze przetestowane i raczej nie wymagają debugowania przez użytkownika
  2. kontenery STL są szybkie i optymalnie zaprojektowane; jest mała szansa, że będziemy w stanie napisać lepszą wersję
  3. kontenery STL współdzielą wspólny interfejs co ułatwia ich wykorzystanie bez konieczności odwoływania się do szczegółowej dokumentacji
  4. kontenery STL są bardzo dobrze udokumentowane i ich wykorzystanie jest proste

Systemy wykorzystujące standardowe kontenery STL są łatwiejsze do zrozumienia niż te, które używają własnych, nieznanych szerzej implementacji.

Przykładowy kontener: std::vector

std::vector jest klasą wzorcową reprezentującą tablicę dynamiczną, zwykle alokowaną na stercie (chyba, że w momencie deklaracji wektora zmienimy standardowy alokator). Rozmiar wektora zwiększa lub zmniejsza się automatycznie podczas dodawania / usuwania elementów. Należy jednak pamiętać, że podlega tym samym narzutom czasowym co inne dynamiczne alokowane obiekty.

std::vector ułatwia operacje, które mogą być skomplikowane dla zwykłych tablic, np.:

  1. W każdym momencie mamy dostęp do aktualnego rozmiaru wektora; przy wykorzystaniu wbudowanych tablic musimy ręcznie śledzić aktualny rozmiar.
  2. Podobnie mamy dostęp do rozmiaru zaalokowanej pamięci
  3. Zmiana rozmiaru wektora jest automatyczna
  4. Wstawianie / usuwanie elementów do środka tablicy sprowadza się do wywołania funkcji; wbudowane tablice wymagają ręcznego przesuwania elementów w momencie wstawiania nowego

Ponieważ std::vector jest obiektem, jego destruktor jest wołany automatycznie gdy kończy się jego zakres. Destruktor (między innymi) zwalnia zaalokowaną pamięć, ograniczając możliwość wycieków pamięci.

Tworzenie wektora

Jak inne kontenery std::vector jest klasą wzorcową, co oznacza, że należy explicite podać typ elementów wektora (od C++-17 kompilator zwykle jest w stanie wywnioskować ten typ na podstawie inicjalizatora).


//Declare an empty std::vector that will hold ints
std::vector<int> v;

Można od razu zainicjalizować wektor podając tzw. listę inicjalizacyjną:


//v will be sized to the length of the initializer list
std::vector<int> v1 {-1, 3, 5, -8, 0};
std::vector v2 {-1, 3, 5, -8, 0}; // since C++-17

Wektory mają przeładowany konstruktor kopiujący i operator przypisania, więc kopiowanie ich jest proste.


auto v3(v1); // copy ctor
v2 = v1; // assignment operator

Accessing Data

std::vectori posiada interfejs umożliwiający prosty dostęp do danych kontenera:


std::cout << "v1.front() is the first element: " << v1.front() << std::endl;
std::cout << "v1.back() is the last element: " << v1.back() << std::endl;
std::cout << "v1[0]: " << v1[0] << std::endl; // no bounds check
std::cout << "v1.at(4): " << v1.at(4) << std::endl; // bounds are checked

Wniosek: Używajmy operatora [] tylko w momencie, gdy jesteśmy pewni, że indeksacja jest poprawna. W pozostałych przypadkach bezpieczniejsze jest użycie funkcji at().

data()

Funkcja data() zwraca adres wewnętrznego bufora danych klasy std::vestor. Może to być użyteczne, jeżeli potrzebujemy interfejsu do tablicy w standardzie C, np.:


void carr_func(int* vec, size_t size) {
    std::cout << "carr_func - vec: " << vec << std::endl;
}

Powyższa funkcja przyjmuje argument typu int* (tablicę w stylu C). Wywołanie tej funkcji z obiektem klasy std::vector spowoduje błąd kompilacji. Możemy to umożliwić wykorzystując funkcję data()


//carr_func(v1, v1.size()); // Error:
carr_func(v1.data(), v1.size()); // OK:

Dodawanie i usuwanie elementów

Do dodawania / usuwania elementów wektora można wykorzystać jedną z funkcji:

w celu dodania nowego elementu na koniec wektora używamy funkcji push_back() lub emplace_back(). Te funkcje działają bardzo podobnie, z jedną zasadniczą różnicą: emplace_back() pozwala na przekazanie w wywołaniu argumentów konstruktora, czyli nowy element może być utworzony w miejscu. Jeżeli dodajemy istniejący obiekt (lub chcemy stworzyc obiekt tymczasowy) to używamy push_back().

Dla typów wbudowanych rozróżnienie jest trywialne:


int x = 0;
v2.push_back(x); // adds element to end
v2.emplace_back(10); // constructs an element in place at the end

Dla bardziej skomplikowanych obiektów emplace_back() bywa użyteczne


// Constructor: circular_buffer(size_t size)
std::vector<circular_buffer<int>> vb;
vb.emplace_back(10); // forwards the arg to the circular_buffer constructor to make a buffer of size 10

Aby dodać element w dowolnym miejscu wektora używamy funkcji insert() lub emplace (różnica jak powyżej). Dodatkowo przekazujemy funkcji iterator do miejsca, gdzie należy wstawić element


v2.insert(v2.begin(), -1); // insert an element at the beginning
v2.emplace(v2.end(), 1000); //construct and place an element at the iterator

Aby usunąć wyznaczony element wywołujemy erase() z iteratorem, wskazującym na ten element.


v2.erase(v2.begin()); // erase element - also needs an iterator

Ostatni element usuwamy wywołując pop_back()


v2.pop_back(); // removes last element

Uwaga: funkcja nie zwraca wartości! Jeżeli chcemy przeczytać wartość usuwanego elementu, można najpierw wywołać end().

Wywołanie clear() usuwa wszystkie elementy wektora i ustawia size() na 0 (ale capacity() zostaje niezmienione).

Zarządzanie pamięcią

Wektor często zajmuje więcej pamięci niż to wynika z jego rozmiaru (zwracanego przez size(). Wynika to z tego, że pamięć wektora alokowana jest blokami, aby uniknąć każdorazowej realokacji. Ponadto usunięcie elementu nie powoduje zwolnienia zajmowanej przez niego pamięci.

Są jednak funkcje, pomagające w zarządzaniu pamięcią wektora:

Realokacja pamięci jest kosztowna. Dlatego jeżeli znamy (przynajmniej w przybliżeniu) docelowy rozmiar wektora, lepiej zaalokować od razu cały obszar, używając np. funkcji reserve().


v2.reserve(10) // increase vector capacity to 10 elements

reserve() może tylko zwiększyć rozmiar wektora. Jeżeli żądana pojemność jest mniejsza od aktualnej, funkcja nic nie zmienia

Jeżeli chcemy zmniejszyć pojemność wektora do jego aktualnego rozmiaru, korzystamy z funkcji shrink_to_fit().


// If you have reserved space greater than your current needs, you can shrink the buffer
v2.shrink_to_fit();

Funkcja clear() może być użyta do usunięcia wszystkich elementów wektora bez zmiany jego pojemności.


v2.clear()

Funkcja resize() zmniejsza lub zwiększa rozmiar wektora. Przy zwiększeniu rozmiaru, nowe elementy zostaną wyzerowane (można też podać inną wartość początkową). Jeżeli podamy mniejszy rozmiar niż aktualny, elementy z końca wektora zostaną usunięte.


v2.resize(7); // resize to 7. The new elements will be 0-initialized
v2.resize(10, -1); // resize to 10. New elements initialized with -1
v2.resize(4); // shrink and strip off extra elements

Poruszanie się po kontenerze

Po wektorze, jak i po każdym innym kontenerze, możemy wygodnie poruszać się przy pomocy skróconej wersji pętli for. Aby na przykład wydrukować wszystkie elementy wektora możemy napisać:


std::cout << std::endl << "v2: " << std::endl;
for (const auto& t : v2) {
    std::cout << t << " ";
}
std::cout << std::endl;

Algorytmy

Standardowe algorytmy znajdują się w <algorithms>. STL Algorithms Library składa się z następujących części:

  1. Sortowanie
    • Quicksort
      
      template <typename RandomAccessIterator>
      void sort(RandomAccessIterator a, RandomAccessIterator b);
      
    • Stable sort
      
      template <typename RandomAccessIterator>
      void stable_sort(RandomAccessIterator a, RandomAccessIterator b);
      
  2. Algorytmy niezmiennicze (Algorytmy, które nie modyfikują zawartości kontenera)
    • find()
      
      template <typeame InputIterator, typename T>
      	InputIterator find(InputIterator first, InputIterator last, const T& value);
      

      Znajduje pierwszą pozycję w zakresie [first, last), w której znajduje się wartość value. Jeżeli nie ma takiej wartości funkcja zwraca last.

    • find_if()
      
      template <typeame InputIterator, typename Predicate>
      	InputIterator find_if(InputIterator first, InputIterator last, Predicate p);
      

      Znajduje pierwszą pozycję w zakresie [first, last), dla której spełniony jest predykat p. Jeżeli nie ma takiej pozycji funkcja zwraca last.

    • count()
      
      template<class InputIt, class T>
      	typename iterator_traits<InputIt>::difference_type
      	count(InputIt first, InputIt last, const T &value);
      

      Funkcja zlicza elementy równe value.

    • count_if()
      
      template<class InputIt, class UnaryPredicate>
      	typename iterator_traits<InputIt>::difference_type
      	count_if(InputIt first, InputIt last, UnaryPredicate p);
      

      Funkcja zlicza elementy dla których predykat p jest prawdziwy.

  3. Algorytmy modyfikujące
    • copy()
      
      template<class InputIterator, class OutputIterator>
      void copy(InputIterator first, InputIterator last, OutputIterator result);
      

      Algorytm kopiuje sekwencję obiektów [first, last) na sekwencję [result, result + (last - first)).

    • swap()
      
      template <class T> void swap(T &a, T &b);
      

      Algorytm zamienia wartościami obiekty przekazane przez referencję.

    • transform()
      
      template <class InputIterator, class OutputIterator, class UnaryOperation>
      	OutputIterator transform(InputIterator first, InputIterator last,
      	OutputIterator result, UnaryOperation op);
      

      transform wywołuje funkcję op dla każdego obiektu sekwencji [first, last) i wynik umieszcza odpowiednio na pozycji [result, result + (last - first)).

    • replace()
      
      template <class ForwardIterator, class T>
      	void replace(ForwardIterator first, ForwardIterator last,
      	const T& old_value, const T& new_value);
      

      replace modyfikuje obiekty sekwencji [first, last) tak, że obiekty o wartości old_value przyjmują wartość new_value podczas gdy pozostałe nie ulegają zmianie.

    • fill()
      
      template <class ForwardIterator, class T>
      void fill(ForwardIterator first, ForwardIterator last, const T& value);
      

      fill przypisuje wartość value do każdego obiektu sekwencji [first, last).

    • generate()
      
      template <class ForwardIterator, class Generator>
      void generate(ForwardIterator first, ForwardIterator last, Generator gen);
      

      generate przypisuje do każdego obiektu sekwencji [first, last) wartość generowaną przez last - first kolejnych wywołań funkcji gen.

  4. Algorytmy numeryczne
    • accumulate()
      
      template <class InputIt, class T>
      T accumulate(InputIt first, InputIt last, T init);
      

      accumulate oblicza sumę wartości init i elementów sekwencji [first, last).

    • inner_product()
      
      template <class InputIt1, class InputIt2, class T>
      T inner_product(InputIt1 first1, InputIt1 last1, InputIt2 first2, T init);
      

      inner_product oblicza sumę wartości init i iloczynu skalarnego elementów sekwencji [first1, last1) i [first2, first2 + (last1 - first1)).

    Przykład:

    
    #include <iostream>
    #include <numeric>
    using namespace std;
    
    int main() {
    	double u[3] = { 1.1, 2.2, 3.3 };
    	double v[3] = { 11.1, 22.2, 33.3 };
    
    	double sum = accumulate(u, u+3, 0.0);
    	double ip = inner_product(u, u+3, v, 0.0);
    
    	cout << "sum = " << sum << endl;
    	cout << "inner product = " << ip << endl;
    
    return 0;
    }
    

Wymienione wyżej algorytmy to tylko wybrane przykłady z bardzo obszernej listy. Po kompletny spis możliwości biblioteki należy sięgnąć do dokumentacji.

Więcej przykładów z użyciem algorytmów można znaleźć w źródłach na stronie przedmiotu