Podstawowe pojęcia

Pierwszy program

Kod programu

#include <iostream>

int main()
{
  std::cout << "Hello world!" << std::endl;
  return 0;
}

Kompilacja

Język C++ jest językiem kompilowanym. Kompilator przetwarza kod źródłowy do kodu obiektowego w języku assembler.

Pliki wynikowe zawierające kod obiektowy mają rozszerzenie .o lub .obj.

Łączenie

Program łączący (linker) łączy pliki z kodem obiektowym w program wykonywalny.

Podstawowe typy danych

Typ void

Typ z pustą listą wartości. Nie można tworzyć zmiennych typu void - jest to typem niekompletny. Zabronione jest tworzenie tablic typu void oraz referencji do typu void. Można tworzyć zmienne typu void* oraz funkcje zwracające typ void.

Typy liczbowe

Nazwa

Rozmiar

Zakres

char

1 bajt

signed: [-128; 127]
unsigned: [0; 255]

short int

2 bajty

signed: [-32768; 32767]
unsigned: [0; 65535]

int

4 bajty

signed: [-2147483648; 2147483647]
unsigned: [0; 4294967295]

long int

4 bajty

signed: [-2147483648; 2147483647]
unsigned: [0; 4294967295]

long long int

8 bajtów

signed: [-9223372036854775808; 9223372036854775807]
unsigned [0; 18446744073709551615]

bool

1 bajt

true lub false

float

4 bajty

+/- 3.4e +/- 38 (~7 cyfr)

double

8 bajty

+/- 1.7e +/- 308 (~15 cyfr)

long double

8 bajty

+/- 1.7e +/- 308 (~15 cyfr)

wchar_t

2 lub 4 bajty

Znak Unicode

Literały

Dla typów całkowitych można podawać wartości korzystając z następujących sufixów:

int d = 42;
int o = 052;      // octal
int x = 0x2a;     // hex
int X = 0X2A;     // hex
int b = 0b101010; // binary - C++14

Dla typów całkowitych obowiązują następujące rodzaje literałów:

suffix

typy

brak

int long int long long int

u lub U

unsigned int unsigned long int unsigned long long int

l lub L

long int long long int

l/L i u/U

unsigned long int unsigned long long int

ll/LL

long long int

ll/LL i u/U

unsigned long long int

Łańcuchy znaków

W C++ występują dwa typy łańcuchów znaków:

  1. Niskopoziomowy const char*

    std::cout << "Simple C-string!\n";
    
    const char* text = "C-style text";
    
  2. Wysokopoziomowy std::string

    #include <string>
    
    std::string text1("Szkolenie");
    std::string text2 = "C++";
    auto text3 = "C++14"s;
    std::cout << text1 << " " << text2 << " " << text3 <<  std::endl;
    

Zmienne

Definicja zmiennych:

int x = 39;
unsigned int number_of_steps = 100;

double result = 3.14;

char decimal_point = ',';

const char* name = "Anna Maria";
std::string text = "Ala ma kota";
std::string text2 = "text"s;

Dedukcja typu auto

Słowo kluczowe auto umożliwia dedukcję typu obiektu na podstawie wyrażenia inicjalizującego. Typ dedukowany może być typem zmiennej, stałej (const) lub stałego wyrażenia (constexpr).

auto a1 = 78; // a1 - int

auto text1 = "Text"; // const char*
auto text2 = "Text"s; // std::string

auto text3 = text2 + " " + text1; // std::string

Konwersje typów

Konwersje bezpieczne dla typów

W razie potrzeby można bezpiecznie przekonwertować char na int, a int na double:

char c = 'x';
int i1 = c;
int i2 = 'x';

Bezpieczne są następujące konwersje typów:

  • bool na char

  • bool na int

  • char na int

  • char na double

  • int na double

Konwersje niebezpieczne dla typów

Język C++ dopuszcza także niejawne konwersje zawężające, które mogą być niebezpieczne. Polegają one na wstawianiu wartości do zmiennych, których typ jest za mały (wąski), aby je pomieścić. Wszystkie poniższe konwersje kompilator zaakceptuje mimo, że są one niebezpieczne dla typów:

  • double na int

  • double na char

  • int na char

  • int na bool

  • char na bool

Standardowe strumienie IN/OUT

  • std::cout – standardowy strumień wyjścia (konsola)

  • std::cin – standardowy strumień wejścia (klawiatura)

#include <iostream>

int main()
{
   int number;
   std::cout << "Podaj liczbe: ";
   std::cin >> number;
   std::cout << "wartosc w zapisie szesnastkowym = 0x"
             << std::hex << number << std::endl;
}

Operatory

Operator

Nazwa

Opis

f(a)

wywołanie funkcji

przekazuje a do f jako argument

++l-wartość

preinkrementacja

zwiększa wartość i potem jej używa

--l-wartość

predekrementacja

zmniejsza wartość i potem jej używa

l-wartość++

postinkrementacja

używa wartości, a potem ją zwiększa

l-wartość--

postdekrementacja

używa wartości, a potem ją zmniejsza

!a

negacja

wynikiem jest wartość logiczna

-a

jednoargumentowy minus

a * b

mnożenie

a / b

dzielenie

a % b

reszta z dzielenia (modulo)

dotyczy tylko typów całkowitoliczbowych

a + b

dodawanie

a - b

odejmowanie

out << b

wysyłanie b na wyjście

out jest strumieniem ostream

in >> b

wczytywanie z wejścia do b

in jest strumieniem istream

a < b

znak mniejszości

wynikiem jest wartość logiczna

a <= b

mniejszy lub równy

wynikiem jest wartość logiczna

a > b

znak większości

wynikiem jest wartość logiczna

a >= b

większy lub równy

wynikiem jest wartość logiczna

a == b

znak równości

nie mylić z =

a != b

różny od

wynikiem jest wartość logiczna

a && b

iloczyn logiczny

wynikiem jest wartość logiczna

a || b

suma logiczna

wynikiem jest wartość logiczna

l-wartość = a

przypisanie

nie mylić z ==

l-wartość*= a

przypisanie złożone

l-wartość = l-wartość * a dotyczy także operatorów /, %, + oraz -

Blok instrukcji

Instrukcje mogą zostać zgrupowane przez umieszczenie ich między nawiasami { i }

int foo()
{
  //...
  {
     instr_1;
     instr_2;
     // ...
     instr_n;
  }
  //...
}

Instrukcje sterujące przebiegiem wykonania programu

Instrukcja wyboru if

if ( traffic_light == green )
  go();

Instrukcja wyboru if else

if ( x % 2 == 0 )
{
  std::cout << "x jest parzysta" << std::endl;
}
else
  std::cout << "x jest nieparzysta" << std::endl;

Instrukcja wyboru switch

size_t day = 2;
//…
switch (day)
{
   case 1:
     std::cout << "poniedzialek" << std::endl;
     break;
  case 2:
  case 3:
     std::cout << "wtorek lub sroda" << std::endl;
     break;
  default:
     std::cout << "inny dzien tygodnia" << std::endl;
     break;
}

Reguły użycia instrukcji switch:

  • Wartość wykorzystywana do porównania w instrukcji switch musi być liczbą całkowitą typu char lub wyliczeniem. Nie można używać typu std::string.

  • Wartości w etykietach case muszą być wyrażeniami stałymi. Nie można w nich używać zmiennych.

  • Nie można zastosować takiej samej wartości w dwóch etykietach case.

  • Można użyć kilku etykiet case dla jednego przypadku.

  • Zestaw instrukcji każdej etykiety powinien kończyć się instrukcją break.

Pętla while i do… while

Pętla while:

int i = 0;
while (i < 10)
{
    std::cout << i << std::endl;
    ++i;
}

Pętla do… while:

int i = 0;
do
{
    std::cout << i << std::endl;
    ++i;
} while (i < 10);

Pętla for

Pętla for:

for(int i = 0; i < 10; ++i)
    std::cout << i << std::endl;

Zagnieżdżona pętla for:

for(int i = 0; i < 10; ++i)
   for(int j = 0; j < 10; ++j)
      std::cout << i << "*" << j << "=" << i*j << std::endl;

Instrukcja break

Instrukcja break jest używana w instrukcjach switch, while, do oraz for:

#include <iostream>

int main()
{
    for (int i = 1; i < 10; i++)
    {
        std::cout << i << std::endl;

        if (i == 4) break;
    }
}
1
2
3
4

Instrukcja continue

Instrukcja continue powoduje natychmiastowy przeskok do kontrolnego wyrażenia instrukcji iteracyjnych do, while oraz for.

#include <iostream>

int main()
{
   int i = 0;
   do
   {
      i++;
      std::cout << "before the continue\n";

      continue;

      std::cout << "after the continue, should never print\n";
   } while (i < 3);

  std::cout << "after the do loop\n";
}
before the continue
before the continue
before the continue
after the do loop

Deklaracja a definicja

Deklaracja – zapowiada istnienie w programie obiektu określonego typu lub określonej funkcji

Definicja – tworzy (rezerwuje miejsce w pamięci) określony obiekt (zmienną lub funkcję)

extern int var;             // deklaracja zmiennej
int func1(int, int);        // deklaracja funkcji
extern float func2(float);  // deklaracja funkcji

int i;                      // definicja zmiennej

bool negat(bool b)          // definicja funkcji
{
   return !b;
}

Typ wyliczeniowy

Typ wyliczeniowy (enum) to prosty definiowany przez użytkownika typ danych, który zawiera zestaw wartości (elementy wyliczenia) w postaci stałych symbolicznych.

enum Month
{
  jan=1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec
};

void f(Month m)
{
   switch(m)
   {
      case jan:
         // ...
         break;
      case feb:
         // ...
         break;
   }
}

Dla typów wyliczeniowych można w C++11 specyfikować typ całkowity na którym definiowane jest wyliczenie.

Wartości podawane w wyliczeniu muszą mieścić się w dozwolonym zakresie dla typu.

enum Coffee : std::uint_8_t { espresso, cappucino, latte };

enum State : unsigned char { opened, closed, unknown = 999 }; // error!

Typ na bazie którego definiowane jest wyliczenie jest dostępny za pomocą metafunkcji std::underlying_type<EnumType>:

#include <type_traits>

using namespace std;

enum Coffee : uint8_t { espresso, cappucino, latte };

int main()
{
    Coffee coffee = espresso;

    bool enum_type_is_uint8
        = std::is_same<uint8_t, typename std::underlying_type<Coffee>::type>::value;

    if (enum_type_is_uint8)
        cout << "Underlying type is uint8_t" << endl;
}

Wyliczenia silnie typizowane - Scoped Enumerations

Dla wyliczeń silnie typizowanych (scoped enums):

  • Można specyfikować na bazie jakiego typu definiowane jest wyliczenie

    • Domyślnym typem na którym definiowane jest wyliczenie jest int

  • Niejawna konwersja do i z typu całkowitego nie jest dozwolona

  • Nie ma możliwości porównania obiektów różnych typów wyliczeniowych

  • Wartości wyliczenia są umieszczone w zakresie typu

  • Istnieje możliwość użycia deklaracji zapowiadającej (forward declaration). Gdy używany jest tylko typ nie ma potrzeby rekompilacji przy dodaniu nowej wartości dla wyliczenia.

enum class Engine : char;  // forward declaration

Engine e;

enum class Engine : char { petrol, diesel, wankel };

e = Engine::petrol; // OK

e = diesel;  // error - no "diesel" in scope

e = 3; // error

e = static_cast<Engine>(1); // OK: e == Engine::diesel

int index = Engine::wankel; // error

Zamiast enum class może być użyte enum struct - nie ma żadnej semantycznej różnicy między tymi formami.

Stałe

C++ oferuje koncepcję zdefiniowanej przez użytkownika wartości stałej. Aby zadeklarować obiekt jako stały należy poprzedzić jego deklarację słowem kluczowym const. Stała musi zostać zainicjowana w momencie deklaracji.

const double g = 9.807;   // g jest stałą
const int x;              // błąd – brak inicjalizacji
g = 10.788;               // błąd

C-łańcuchy

Literały znakowe reprezentowane są jako tablice znaków zakończone wartością «0». Typem literałów znakowych jest const char*.

const char* s = "hello";

for(const char* p=s; *p != '\0'; ++p)
  std::cout << *p << std::endl;

for(size_t i = 0; i < strlen(s); ++i)
  std::cout << s[i] << std::endl;

Problemy z C-łańcuchami.

const char* s = "hello";
const char* t = "C++";
s = t;        // Uwaga! przypisanie wskaźników

Operacje na łańcuchach są kłopotliwe. Wymagają użycia funkcji znanych z języka C.

size_t strlen(const char* s);                   // Liczy znaki
char* strcat(char* s1, const char* s2);         // Kopiuje s2 na koniec s1
int strcmp(const char* s1, const char* s2);     // Porównanie leksykograficzne
char* strcpy(char* s1, const char* s2);         // Kopiuje s2 do s1

char* strchr(const char* s, int c);             // Znajduje c w s
char* strstr(const char* s1, const char* s2);   // Znajduje s2 w s1

char* strncpy(char*, const char*, size_t n);    // strcpy, maks. n znaków
char* strncat(char*, const char*, size_t n);    // strcat, maks. n znaków
int strncmp(const char*, const char*, size_t n); // strcmp, maks. n znaków

Typ string

Typ string hermetyzuje szczegóły niskopoziomowej reprezentacji łańcuchów za pomocą typów podstawowych.

#include <string>

std::string firstname = "bjarne";
//powoduje powstanie obiektu klasy std::string zainicjowanego wartością
//typu const char*
std::string lastname("stroustrup");

firstname[0] = 'B';
lastname[0] = 'S';

Łańcuchy możemy używać w taki sam sposób, co typów podstawowych

  • Za pomocą operatorów == i != możemy je porównywać

  • Za pomocą operatorów + możemy je konkatenować

  • Za pomocą operatora = możemy przypisywać jeden łańcuch do drugiego

std::string name = firstname + " " + lastname;

if (name != "")
{
  std::cout << name;
}

std::string text = "hello" + " " + "world"; // źle
text = static_cast<string>("hello") + " " + "world";

Operacje łańcuchowe

Operacja

Opis

s = s2

Przypisuje s2 do s. s2 może być zwykłym łańcuchem lub łańcuchem w stylu języka C.

s += x

Dodaje x na końcu s. x może być znakiem, zwykłym łańcuchem lub łańcuchem w stylu języka C.

s[i]

Indeksowanie

s + s2

Konkatenacja. Wynikiem jest nowy łańcuch zawierający znaki z s i znaki z s2.

s == s2

Porównywanie łańcuchów. s lub s2, ale nie obydwa jednocześnie. Mogą być łańcuchami w stylu języka C.

s !=s 2

Porównywanie łańcuchów. s lub s2, ale nie obydwa jednocześnie. Mogą być łańcuchami w stylu języka C.

s < s2

Leksykograficzne porównywanie łańcuchów. s lub s2, ale nie obydwa jednocześnie. Mogą być łańcuchami w stylu języka C.

s <= s2

Leksykograficzne porównywanie łańcuchów. s lub s2, ale nie obydwa jednocześnie. Mogą być łańcuchami w stylu języka C.

s > s2

Leksykograficzne porównywanie łańcuchów. s lub s2, ale nie obydwa jednocześnie. Mogą być łańcuchami w stylu języka C.

s >=s 2

Leksykograficzne porównywanie łańcuchów. s lub s2, ale nie obydwa jednocześnie. Mogą być łańcuchami w stylu języka C.

s.size()

Liczba znaków w s.

s.length()

Liczba znaków w s.

s.c_str()

Wersja w postaci łańcucha w stylu języka C (zakończona zerem) znaków w s.

s.begin()

Iterator wskazujący pierwszy znak

s.end()

Iterator wskazujący miejsce za ostatnim znakiem

s.insert(pos, x)

Wstawia x przed s[pos]. x może być znakiem, zwykłym łańcuchem lub łańcuchem w stylu języka C.

s.append(pos, x)

Wstawia x za s[pos]. x może być znakiem, zwykłym łańcuchem lub łańcuchem w stylu języka C.

s.erase(pos)

Usuwa znak s[pos].

s.push_back(c)

Dodaje na końcu znak c.

pos=s.find(x)

Znajduje x w s. x może być znakiem, zwykłym łańcuchem lub łańcuchem w stylu języka C. pos jest indeksem pierwszego znalezionego znaku lub npos (miejscem za końcem łańcucha s).

in >> s

Wczytuje do s słowo ze strumienia in.

out << s

Zapisuje s słowo do strumienia out.

Raw String Literals

W C++11 możemy uniknąć specjalnego traktowania „znaków specjalnych” w literałach znakowych stosując tzw. „Raw String Literals”.

„Raw string” zaczyna się od R"(, a kończy się ").

  • np. n i podwójny cudzysłów

    std::string no_newlines = R"(\n\n)";
    std::string cmd(R"(cd "C:\new folder\text")");
    
  • łamanych lini w tekście

    std::string with_newlines(R"(Line 1 of text...
    Line 2...
    Line 3)");
    

Aby mieć możliwość umieszczenia sekwencji ") w literale „raw string”, należy użyć sekwencji przestankowej delim. W rezultacie kompletna składnia literału „raw string” to: R"delim()delim".

string str1 = R"nc(a\
b\nc()"
)nc";

string str2 = "a\\\n    b\\nc()\"\n    ";

Literały „raw string” są szczególnie przydatne do definiowania wyrażeń regularnych.

std::regex re1(R"!("operator\(\)"|"operator->"!"));  // "operator()"|"operator->"

Sekwencja przestankowa może mieć długośc do 16 znaków i nie może zawierać białych znaków (spacji itp.).

Odniesienia

Referencja (odniesienie) jest inną nazwą dla danego obiektu. Jest wykorzystywana przy przesyłaniu argumentów do funkcji lub jako sposób zwracania wartości przez funkcję (np. przeładowywanie operatorów). Referencja musi zostać zainicjalizowana w momencie tworzenia.

Wartość referencji nie może zostać zmieniona po inicjalizacji (referencja zawsze odnosi się do zainicjalizowanego obiektu).

int i = 1;
int& r = i;   // r oraz i odnoszą się teraz do tej samej zmiennej typu int
int x = r;    // x = 1
r = 2;        // i = 2

Inicjalizacja referencji dla „zwykłego” typu T& wymaga l-wartości (lvalue) – obiektu, dla którego możliwe jest pobranie adresu. Inicjalizacja dla const T& nie wymaga l-wartości. Obiekt tymczasowy utworzony aby zainicjalizować referencję istnieje do końca istnienia referencji.

double& dr = 1;   // błąd! wymagana l-wartość
const double& cdr = 1;  // ok

Aliasy typów

Słowo kluczowe typedef umożliwia tworzenie aliasów typów.

typedef unsigned long ulong;  // alias
ulong i;                      // definicja zmiennej typu unsigned long
typedef int* PtrInt;
PtrInt px;

Aliasy typów using

W C++11 deklaracja using może zostać użyta do tworzenia bardziej czytelnych aliasów dla typów - zamiennik dla typedef.

using CarID = int;

using Func = int(*)(double, double);

using DictionaryDesc = std::map<std::string, std::string, std::greater<std::string>>;

Aliasy typów mogą być parametryzowane typem. Można je wykorzystać do tworzenia częściowo związanych typów szablonowych.

template <typename T>
using StrKeyMap = std::map<std::string, T>;

StrKeyMap<int> my_map; // std::map<std::string, int>

Struktury

Słowo kluczowe struct umożliwia łączenie w grupę kilku zmiennych. W połączeniu z typedef (C) umożliwia zdefiniowanie przez użytkownika nowego typu.

typedef struct Structure
{
  char c;
  int f;
  double d;
};

int main()
{
  Structure s1, s2;
  s1.c = 'a';
  s2.d = 3.14;
}