1. Podstawowa składnia języka

1.1. Struktura programu w C++

Struktura programu w C++

Język C++, jak już wiecie - jest dość elastyczny. Struktura programu jako takiego jest zupełnie swobodna, np. bloki instrukcji oraz definiowania zmiennych mogą się praktycznie dowolnie przeplatać ze sobą, funkcje mogą być definiowane w różnej kolejności i w różnych miejscach. Podobnie wygląda sytuacja z klasami

Schemat każdego programu w języku C++ można zapisać następująco:


/** Na samym początku zazwyczaj umieszcza się pliki dołączane (nagłówki bibliotek)
wraz z dodatkowymi dyrektywami kompilatora. Mówimy o zwyczajowym umieszczaniu -
bo z punktu widzenia składni języka załączenie bibliotek może być wszędzie */
/** dołączenie biblioteki standardowej języka C / C++ */
#include <cstdlib>

/** dołączenie strumieniowego wejścia / wyjścia (zalecanego dla języka C++ */
#include <iostream>

/** biblioteka string zawiera implementację łańcuchów tekstowych (napisów).
W C++ nie ma typu prostego w pełni implementującego napisy */
#include <string>

/** Wykorzystanie przestrzeni nazw biblioteki standardowej */
using namespace std;

/** Program zapisuje się w C++ w postaci funkcji. Każda funkcja zaczyna się
nagłówkiem, potem występuje treść zamknięta w nawiasy klamrowe. Więcej o funkcjach
będzie w dalszej treści podręcznika. Program może składać się z wielu funkcji. Zawsze
musi być co najmniej jedna - main (patrz niżej). Od niej zaczyna się tok wykonania
programu */
typ_zwracanej_wartosci nazwa_funkcji(lista_parametrow)
{
...
};

/** Przed lub pomiędzy funkcjami zamieszcza się definicje i deklaracje stałych,
zmiennych, typów i klas globalnych dla danego pliku, czyli takich, z których można
korzystać w każdej funkcji. */
const int...{definicja stalych}
...
typedef...{definicja typow}
...
double... {definicja zmiennych}
...

/** W każdym programie C++ jest jedna główna funkcja - nazywa się main. */
int main(int argc, char *argv[])
/** Nawiasy klamrowe służą do oznaczenia początku i końca funkcji */
{
    /** Kolejne instrukcje składające się na nasz algorytm */

    instrukcja;
    instrukcja;
    ...
    instrukcja;

    /** W C++ pomiędzy instrukcjami znów mogą się znaleźć definicje
    zmiennych, typów, stałych - lecz w takim wypadku będą one lokalne.
    O zasięgu widoczności zmiennych będzie zamieszczona w dalszej części podręcznika. */

    /** Funkcja main powinna zwrócić jakąś wartość. W przypadku prawidłowego zakończenia
    programu zwrócone powinno zostać 0 lub równoważna stała symboliczna EXIT_SUCCESS */
    return EXIT_SUCCESS;
} /// Zamykający nawias klamrowy na koniec funkcji main

Komentarze

Tłumaczenie zaczniemy dość nietypowo – od zamieszczania komentarzy. W języku C++ mamy dwa możliwe tryby komentowania. Pierwszy z nich historycznie wywodzi się jeszcze z języka C. W tym wypadku komentarzem jest każdy fragment tekstu zaczynający się od znaków /* i kończący się na */. Taki komentarz może zawierać wiele linii tekstu. Jeśli wewnątrz komentarza wystąpi jeszcze raz para /* zostanie ona zignorowana.

Drugi tryb komentarzy jest uzupełnieniem pierwszego: wszystko co zaczyna się od znaku podwójnego ukośnika // aż do końca linii jest traktowane jako komentarz.

Samo stosowanie komentarzy również rządzi się pewnymi regułami. My będziemy (i Wam również zalecamy) stosowali następującą konwencję komentowania:

  • Komentarz zaczynający się od początku linii (bez instrukcji przed nim) będzie dotyczył tego co jest poniżej - czyli najpierw komentarz, potem kod.
  • Komentarz umieszczony za instrukcją dotyczy tej instrukcji

Pamiętajcie, że nadmiar trywialnych komentarzy stanowi mniejszy błąd niż ich zupełny brak. Podkreślmy przy okazji, że komentarze służą wyłącznie osobie czytającej treść programu, i nie wpływają w żaden sposób na jego wykonanie. Kompilator (a dokładniej: preprocesor) usuwa wszelkie komentarze. Autor programu w postaci komentarzy informuje odbiorców (innych programistów, a nie użytkowników) o tym, co umieścił w programie (w tym i siebie samego ...). Jak widzicie - nie uważam że samokomentujący się kod jest najlepszym rozwiązaniem ;)

W tym miejscu chciałbym także, byście rzucili okiem na komentarze nieco bardziej sformalizowane. Istnieje darmowy system generowania dokumentacji nazywający się doxygen. Na dzień dzisiejszy to najpopularniejszy standard komentowania kodu w różnych językach programowania, oraz generowania z takiego kodu gotowej dokumentacji – łatwej do czytania i przeglądania. Sama nazwa doxygen oznacza program parsujący kod źródłowy i generujący dokumentację do niego. Jeśli kod nie jest odpowiednio skomentowany – to jedyną informacją którą możemy z niego uzyskać jest informacja o strukturze kodu – klasach, ich wzajemnych związkach, dołączanych plikach, itp. W przypadku gdy w kodzie umieścicie odpowiednio przygotowane komentarze – możliwości programu rosną w sposób znaczący. Możecie uzyskać dokumentację API (Application Programming Interface) o jakości nie odbiegającej od dokumentacji dostarczanej do komercyjnych środowisk programistycznych. I to wszystko dla Waszego kodu, w dodatku zupełnie za darmo...

Jak więc komentować kod? Zasad jest kilka. Po pierwsze – istnieją specjalne znaki komentarza, które rozpoznaje i interpretuje Doxygen. W przypadku języka C++ jest to komentarz w następującej formie:


/**
* ... tekst (z opcjonalną gwiazdką * na początku) ...
*/

/*!
* tekst (z opcjonalną gwiazdką * na początku)
*/

///
/// ... text ...
///

//!
//! ... text ...
//!
Jak widzicie – możliwości komentowania jest wiele. Wystarczy wybrać jedną. Sam doxygen wykorzystuje dwa rodzaje opisu fragmentu kodu: krótki i szczegółowy. Zalecam zamieszczanie obu z nich – jest to możliwe bez specjalnej komplikacji:

/*! \brief Tu zamieszczamy opis krótki
* Dalszy ciąg krótkiego opisu
*
* Opis szczegółowy jest oddzielony od krótkiego pustą linią.
*/

/// Alternatywnie możecie zamieścić opis krótki po trzech ukośnikach.
/** A za nim zamieścić opis szczegółowy oznaczony jako blok */
To nie są wszystkie możliwości programu w sensie rozróżniania rodzajów opisu. Zainteresowanych odsyłam na strony projektu. Komentarz do fragmentu kodu można umieszczać w dwóch miejscach względem komentowanego kodu: przed komentowanym kodem, oraz za nim. Przedstawione wyżej sposoby odnoszą się do komentarzy umieszczonych przed komentowanym kodem. Aby Doxygen zrozumiał komentarz umieszczony za komentowanym kodem należy użyć znaku mniejszości przed komentarzem, tak jak w przykadzie poniżej (ale lepiej tej techniki nie stosować):

int zmienna; /*!< To jest krótki opis zmiennej */
W przypadku funkcji, oprócz komentarzy krótkich (ogólnych) i szczegółowych, warto również poświęcić kilka minut na udokumentowanie argumentów wejściowych i wyjściowych oraz zwracanych wartości. Poniżej przykład poprawnie udokumentowanej funkcji z wykorzystaniem komend specjalnych (@param oraz @return):

/**
* Funkcja sprawdza, czy z trzech odcinków da się zbudować trójkat.
* Pobiera trzy wartości typu int i zwraca wartość typu bool.
*
* @param[in] x długość pierwszego odcinka.
* @param[in] y długość drugiego odcinka.
* @param[in] z długość trzeciego odcinka.
* @return true jeśli da się zbudować trójkąt, false w przeciwnym wypadku
*/
bool triangle(int x, int y, int z) {
    return (x < y+z) && (y < x+z) && (z < x+y);
}
Atrybut [in] umieszczony po komendzie @param jest atrybutem opcjonalnym, wskazującym że komentowany argument jest argumentem wejściowym funkcji (dostarcza danych do funkcji). Jeśli na liście argumentów znajduje się argument przekazywany przez referencję lub za pomocą wskaźników (jest pobierany i zmieniany w trakcje działania funkcji), należy użyć atrybutu [in,out]. Jeśli argument nie wprowadza żadnych danych do funkcji, a jedynie funkcja zwraca wartość za pomocą argumentu, należy użyć atrybutu [out]. Doxygen posiada zdefiniowanych jeszcze wiele komend specjalnych (takich jak @param), można także oznaczać je na różne sposoby (np. \param też jest dopuszczalne). Więcej informacji znajdziecie na stronie projektu.
W tym podręczniku także będę wykorzystywał składnię doxygena do komentarzy.

Składnia języka raz jeszcze

W języku C++ formalizm zapisu jest stosunkowo prosty i ograniczony. Jednakże elegancja obowiązuje zawsze – tym bardziej, że w większości współczesnych środowisk i edytorów programistycznych możecie swobodnie korzystać z autoformatowania. Przypominam zestaw dobrych rad odnośnie formatowania:

  • Każda instrukcja powinna być zapisana w oddzielnej linii,
  • Wszystko to, co znajduje się pomiędzy nawiasami klamrowymi { i } (blok programu), powinno zostać przesunięte względem nich o 2-3 spacje,
  • Zmienne deklarujemy kolejno, w oddzielnych linijkach umieszczając oddzielne deklaracje / definicje.
  • Każda zmienna powinna być opisana za pomocą komentarza. Podobnie podstawowe kroki algorytmu.
Wspomniane wyżej 4 zasady formatowania nie są ani standardem, ani kompletnym opisem sposobu formatowania. Właściwie każda grupa programistów wypracowuje własny styl formatowania, nazywania zmiennych i funkcji, nazywania plików, itp. Ważniejsze od konkretnych reguł w takim stylu jest jego konsekwentne stosowanie. Poniżej zamieszczam dla Was przykład formatowania którego trzymam się w podręczniku
namespace foospace
{
    class Bar
    {
    public:
        int foo();

    private:
        int foo_2();
    };

    int Bar::foo()
    {
        switch (x) {
        case 1:
            a++;
            break;
        default:
            break;
        }
        if (isBar) {
            bar();
            return m_foo+1;
        } else
            return 0;
    }
}

Budowa programu

Aby uruchomić program napisany w języku C++ wymagany jest etap kompilacji - przekształcenia kodu żródłowego w postać wykonywalną, charakterystyczną dla określonej platformy sprzętowej i systemu operacyjnego. Typowe (i zalecane) podejście do programowania w C++ zakłada, iż każdy program składa się z wielu plików:

Etapy kompilacji C++

Podział na wiele plików zdecydowanie ułatwia panowanie nad kodem, zwiększa możliwość jego ponownego wykorzystania, czy też pozwala na przyspieszenie etapu kompilacji dzięki wykorzystaniu tzw. kompilacji przyrostowej - kiedy to po wprowadzeniu zmian w jednym miejscu w kodzie kompilowany jest jedynie zmieniony plik, w przypadku pozostałych wykorzystywane są wcześniej już uzyskane pliki z kodem maszynowym (o / obj) - i prowadzony jest etap konsolidacji. 

Standard języka dzieli elementy z których budujemy aplikacje na dwie zasadnicze grupy: 

  • składniki rdzenne - nie wymagają dołączania plików z deklaracjami, są dostępne w kodzie zawsze. Tu zaliczymy typy wbudowane (int, double, itp...) czy też podstawowe konstrukcje językowe (instrukcje if, pętle for, while, itp...) 
  • składniki biblioteki standardowej - wymagają dołączenia zewnętrznych bibliotek. Tu zaliczymy zarówno biblioteki systemowe (np do obsługi plików), jak i kontenery, algorytmy czy podobne elementy definiowane w tej bibliotece. 

Sama biblioteka standardowa C++ została napisana przy wykorzystaniu ... C++ - co tylko dowodzi uniwersalności języka. 

Wyrażenia

W dużym skrócie przypomnę – że w języku C++ mamy do czynienia ze słowami kluczowymi (jest ich określona ilość i nie można ich zmieniać), oraz typami, zmiennymi, stałymi, itp – definiowanymi przez użytkownika. Zobaczcie na trywialny przykład:

x = y + f(2);

W C++ by to miało sens – x, y i f muszą być odpowiednio zadeklarowane (by stały się bytami o swoich nazwach). Z każdą nazwą (identyfikatorem) jest związany typ, który określa jakie operacje można wykonać na jego przedstawicielu. Każdy taki byt musi być identyfikowalny – vide posiadać identyfikator. Identyfikator w C++ definiuje się następująco:

Identyfikator
identyfikator:
        niecyfra
        identyfikator niecyfra
        identyfikator cyfra

W skrócie:

<nazwa>::=<niecyfra> {<niecyfra>|<cyfra>}

gdzie niecyfra: litera łacińska lub _

Słowa kluczowe są zastrzeżone, wielkość liter jest rozróżnialna (ma znaczenie). 

Przykłady poprawnych i niepoprawnych identyfikatorów:


// poprawne zmienne:
int Aaa, aAa, aaa;
double _kot, mi29, Moja1B_;

// niepoprawnie
char ala ma kota;
bool 39A;
double new;
float $inna_zmienna;

// definicja zmiennej
double zmienna;
// deklaracja
extern double inna;

Nie będę tutaj powtarzał znanych już Wam z poprzednich zajęć dodatkowych informacji o słowach kluczowych, znakach przestankowych, itp – zainteresowani niech sięgną do odpowiednich materiałów. Tu natomiast zatrzymamy się jeszcze przez chwilę przy deklaracjach i definicjach.

Deklaracje i definicje

C++ jest językiem ze statyczną kontrolą typów - co oznacza, że każdy jego element musi mieć typ. Co więcej - ten typ musi być znany kompilatorowi od pierwszego momentu jego użycia. By typ był znany - należy go najpierw kompilatorowi pokazać, czyli powiązać typ z identyfikatorem.  Tak więc z każdą nazwą (identyfikatorem) jest związany typ, który określa jakie operacje można wykonać na jego przedstawicielu. Typy są różne – i nie mówimy tu tylko o typach danych ale o każdym elemencie języka. Przykładowe operacje które można wykonać na:

  • stałych – można odczytać ich wartość
  • zmiennych – można odczytać i zapisać
  • funkcjach – można wykonać.
  • klasach, szablonach, przestrzeniach nazw – o tym powiemy w drugiej części podręcznika
Związanie nazwy (identyfikatora) z jej typem będziemy nazywali deklaracją

Deklaracja nie oznacza przyznania pamięci dla zmiennej, czy podania kodu dla funkcji – jest to jedynie informacja składniowa. W ten sposób programista może „obiecać” kompilatorowi, że gdzieś tam znajdzie się definicja zmiennej czy funkcji. Kompilator musi przyjąć deklarację programisty, i wg deklaracji sprawdzana jest poprawność składniowa kodu. 

Z pojęciem deklaracji ściśle powiązane jest pojęcie definicji:

Żądanie przyznania pamięci dla zmiennych, podanie kodu dla funkcji czy podanie opisu dla typów własnych będziemy nazywali definicją.

Innymi słowy – definicją są wszystkie informacje niezbędne do wygenerowania kodu wynikowego programu. Z tego wynika zależność pomiędzy deklaracją a definicją: każda definicja jest jednocześnie deklaracją (każdy program który można prawidłowo skompilować i uruchomić jest poprawny składniowo), natomiast nie każda deklaracja jest definicją (nie każdy program poprawny składniowo można skompilować i uruchomić).

W języku C++ wszystko co ma nadaną nazwę (stałe, zmienne, funkcje, itp) musi mieć typ, przy czym dla każdej nazwy musi istnieć tylko jedna definicja, natomiast może istnieć wiele takich samych deklaracji. Dlatego też działa mechanizm dołączania plików nagłówkowych (ale o tym za chwilę). Kilka przykładów deklaracji i definicji:


char znak; // definicja zmiennej typu podstawowego
string s; // definicja zmiennej typu definiowanego w bibl. standardowych
int licznik{1}; // definicja wraz z inicjacją
const double pi = 3.14; // definicja stałej (musi być z inicjacją, stara składnia)
extern long pid; // deklaracja zmiennej

char *imie = "Alicja"; // definicja tablicy znakow

// definicja tablicy tablic.
char *pora[] = {"wiosna",
"lato",
"jesien",
"zima"}

// wiele deklaracji – jedna definicja
extern int licznik;
int licznik;
extern int licznik;

// poniższy kod jest błędny
// licznik był już zadeklarowany / zdefiniowany jako int
extern double licznik;

// podobnie tutaj: licznik był już zadeklarowany / zdefiniowany jako int
double licznik;

// mimo że dwie definicje są identyczne – definicji nie można powtarzać.
int licznik;

// definicja dwóch zmiennych tego samego typu
int x, y;

// definicja mieszana – z inicjacją i bez niej.
int a1{2}, b1;

// definicja wskaźnika
long int *pole{nullptr};

// definicja mieszana – jedna zmienna to wskaźnik, druga zmienna jest statyczna.
int* p2, p3;