Podstawy programowania 2

Szablon programu w języku C++

Widzieliśmy już proste programy w C++. Wszystkie one miały tę samą strukturę:

#include <iostream>

using namespace std;

int main() {
  //
  //Instrukcje
  //
  return 0;
}

Teraz przyjrzymy się szybko poszczególnym elementom.

Preprocesor

#include <iostream>

Instrukcja ta nie jest instrukcją kompilatora a tzw. dyrektywą preprocesora. Jest to program działający na tekście pisanego programu, wyjście z preprocesora stanowi dopiero wejście do właściwego kompilatora. Dyrektywa #include jest zastępowana przez preprocesor zawartością odpowiedniego pliku (znajdujące się w nim ewentualne kolejne dyrektywy są znowu rozwijane). W tym przypadku zażyczyliśmy sobie, aby preprocesor dołączył plik iostream. Pliki te nazywa się nagłówkami i z reguły mają rozszerzenie .h, ale biblioteka standardowa nie używa rozszerzeń.

Pisząc go w nawiasach kątowych dodatkowo zaznaczamy, by preprocesor przeszukiwał tylko katalogi systemowe. Można też używać cudzysłowów:

#include "iostream"

Wtedy jednak najpierw będzie przeszukiwany bieżący katalog. Jest to więc odpowiednie dla własnych plików nagłówkowych.

Dołączenie nagłówka systemowego powoduje dołączenie zaskakująco dużych ilości kodu (który jednak z reguły nie powoduje sam z siebie generowania kodu maszynowego). Dla przykładu nasz kilkulinjkowy program, który nic nie robi, „puchnie” do 18821 linijek kodu. Wynik pracy preprocesora można obejrzeć tutaj (uwaga, duży plik).

Preprocesor ma także inne możliwości, które jednak poznamy później.

Instrukcja using

using namespace std;

Instrukcja using jest tylko informacją dla kompilatora, że wszystkie nazwy używanych przez nas typów, zmiennych i funkcji mają być wyszukiwane także wewnątrz określonej przestrzeni nazw (w tym przypadku std). Bez tego należałoby pisać przed większością używanych nazw std::. Celem wprowadzenia przestrzeni nazw jest umożliwienie programistom pracy bez przejmowania się możliwymi konfliktami z nazwami w innych miejscach dużego programu.

Zamiast importować wszystkie nazwy z przestrzeni std można też importować pojedyncze elementy, na przykład:

using std::cout;

Funkcja main

int main() {


  return 0;
}

Funkcja main jest punktem startowym każdego programu zarówno w C jak i w C++. Uruchomienie programu jest równoznaczne z uruchomieniem funkcji main W całym programie może znajdować się tylko jedna taka funkcja, inaczej dostaniemy błąd konsolidacji.

Zgodnie ze standardem języka funkcja main zwraca wartość typu int!

Jeżeli chcemy w programie obsługiwać tzw. argumenty linii poleceń (jest to czasem bardzo przydatne), to funkcja main przyjmuje te argumenty jako swoje argumenty. Co prawda ich nazwy są dowolne, to argumenty te zgodnie z konwencją nazywa się argc i argv:

int main(int argc, char* argv[]) {
}

Komentarze

Komentarze nie stanowią istotnej części programu z punktu widzenia kompilatora. Jak sama nazwa wskazuje, przy kompilacji są one całkowicie pomijane. Nie znaczy to jednak, że komentarze są nieistotne. Kod programu jest bardzo często odczytywany przez programistów – czy to przez innych czy to przez autora po kilku latach. Komentarze są wtedy nieocenioną pomocą przy trudniejszych fragmentach programu. Z drugiej strony najlepiej byłoby pisać czytelny kod, który byłby sam swoim komentarzem...

Komentarzami są fragmenty kody umieszczone między /* i */ (tzw. komentarze blokowe) i od // do końca linii (komentarze wierszowe).

Spójrzmy na przykład poprawnych komentarzy:

/*
   Dear maintainer:

   Once you are done trying to 'optimize' this routine,
   and have realized what a terrible mistake that was,
   please increment the following counter as a warning
   to the next guy:

   total_hours_wasted_here = 42
*/

// Magic. Do not touch.

Instrukcje sterujące wykonaniem programu

W programie Suma instrukcje naszego programu wykonywały się kolejno tak jak je napisaliśmy w pliku źródłowym. Jak sobie można wyobrazić, w ten sposób nie da się zaimplementować żadnego sensownego algorytmu. Z tego powodu każdy język programowania pozwala na powtarzanie fragmentów kodu (pętle, skoki) i wykonywanie fragmentów kodu pod określonymi warunkami (instrukcje warunkowe).

Wyrażenia logiczne

Wyrażenia proste

Wewnątrz instrukcji sterujących często znajdziemy wyrażenia logiczne, a więc wyrażenia o wartości true lub false. Słowa kluczowe true i false oznaczają odpowiednio logiczną prawdę i fałsz. Ponadto możemy oczywiście tworzyć własne wyrażenia zawierające zmienne naszego programu, dla przykładu poniższe napisy to proste wyrażenia logiczne:

5 < 1
i < 10
i <= j
i == 5
i != 5

Wiele typów pozalogicznych również ma w C++ wartość logiczną, co pozwala na pisanie krótszych (lecz niekoniecznie bardziej przejrzystych) programów. W szczególności, dla typów całkowitoliczbowych, wartość 0 jest w odpowiednim kontekście traktowana jako false, podczas gdy każda inna wartość jest traktowana jako true. Podobnie jest dla typów zmiennoprzecinkowych, ale poleganie na tym jest wysoce niewskazane. Możliwe operatory relacji (takie jak <, > itp.) omówimy później.

Wyrażenia złożone

Świat jest złożony i czasem tak proste wyrażenia logiczne nie starczają. Wtedy trzeba sięgnąć po operatory logiczne koniunkcji, alternatywy oraz negacji, pozwalające na budowanie bardziej złożonych wyrażeń.

Koniunkcja

Jeżeli całe wyrażenie ma być prawdziwe tylko wtedy, jeśli prawdziwe są dwa warunki prostsze, to używamy operatora koniunkcji (ang. AND):

(0 < k) && (k < 5) // To wyrażenie jest prawdziwe,
                   // tylko gdy k jest w przedziale (0, 5).
Alternatywa

Jeżeli całe wyrażenie ma być prawdziwe wtedy, jeśli prawdziwy jest któryś z dwóch warunków prostszych, to używamy operatora koniunkcji (ang. OR):

(k < 0) || (k > 5) // To wyrażenie jest prawdziwe,
                   // tylko gdy k jest w przedziale (-∞, 0)
                   // lub w przedziale (5, ∞).
Negacja

Jeżeli warunek ma być prawdziwy wtedy gdy warunek prostszy nie jest prawdziwy, używamy operatora negacji (ang. NOT):

!(k < 0) // To wyrażenie jest prawdziwe wtedy,
         // tylko gdy k nie jest mniejsze od zera,
         // a więc wtedy gdy k jest w przedziale [0, ∞).
         //
         // W praktyce napisalibyśmy raczej k >= 0
Razem

Czasem i pojedyncze spójniki logiczne nie starczają i potrzebne warunki są jeszcze bardziej skomplikowane. Wtedy oczywiście możemy stosować wszystkie powyższe operatory razem, jednak mając na uwadze przejrzystość kodu, nie należy zbytnio przesadzać. Dla przykładu:

!(k < 1) || (k % 2 == 0)
!(!(k % 3 == 0) && (k % 2 == 0))
!((k % 4) && !(!(k % 3) || (k % 2)))

Nie we wszystkich powyższych wyrażeniach nawiasy są potrzebne – kolejność wykonywania operacji pozwala je czasem pominąć. Jednak dla czytelności programu dobrze jest je wpisywać, nawet gdy są one w istocie niekonieczne.

Język C++ nie prowadzi niepotrzebnych obliczeń gdy znana jest już ostateczna wartość wyrażenia logicznego. Na przykład, gdy oblicza wartość koniunkcji i jej pierwszy człon okaże się nieprawdziwy, drugi człon w ogóle nie jest obliczany.

Bloki kodu i zasięg zmiennych

Każdy zbiór wyrażeń bądź poleceń języka C++ otoczony parą nawiasów klamrowych tworzy blok. Blok może zawierać deklaracje lokalnych zmiennych, które są dostępne od miejsca zadeklarowania oraz we wszystkich zagnieżdżonych blokach kodu. Zmienne lokalne przestają być dostępne w momencie zakończenia bloku zawierającego ich deklarację. Zmienne te mogą mieć nazwy takie same jak zmienne z bloku nadrzędnego. Zmienne z bloku nadrzędnego są wtedy przesłaniane. Dla przykładu:

#include <iostream>
using namespace std;

int i = 0; //zmienna globalna

int main() {
  cout << i << endl;
  int i = 1; //nowa zmienna lokalna
  cout << i << endl;
  {
     int i = 2;
     cout << i << endl;
  } // zmienne zadeklarowane w tym bloku przestają być dostępne
  cout << i << endl;
  {
     int i = 2;
     ::i += i + 1; //odwołanie do zasięgu globalnego
     cout << i << endl;
  }
  cout << i << endl;
  cout << ::i << endl; //odwołanie do zasięgu globalnego
  return 0;
}

Czasami jest to bardzo przydatne – przy używaniu cudzego kodu, nie musimy uważać, czy przypadkiem nie używamy tej samej nazwy zmiennej. Oczywiście nie należy używać tego w pisanym samemu kodzie, gdyż prowadzi to do szybkiej utraty czytelności.

Pętle

Pętla while

Pętla while pozwala na powtarzanie instrukcji lub bloku instrukcji dopóki dany warunek jest spełniony. Składnia jest następująca:

while ( warunek ) {
  // kod do powtarzania
}

Przykładowy kod wygląda następująco:

#include <iostream>

using namespace std;

int main() {
  int i = 0;
  while (i < 10) {
    cout << i << endl;
    i = i + 1;
  }
  return 0;
}

Pętla do-while

Pętla do-while pozwala na powtarzanie instrukcji lub bloku instrukcji dopóki dany warunek jest spełniony. Składnia jest następująca:

do {
  // kod do powtarzania
} while ( warunek )

Przykładowy kod wygląda następująco:

#include <iostream>

using namespace std;

int main() {
  int i = 0;
  do {
    cout << i << endl;
    i = i + 1;
  } while (i < 10)
  return 0;
}

Różnica w stosunku do pętli while jest taka, że kod wykonuje się zanim sprawdzany jest warunek. W szczególności pętla do-wile wykona się przynajmniej raz.

Pętla for

Pętla for również pozwala na powtarzanie instrukcji lub bloku instrukcji dopóki dany warunek jest spełniony. Nie różni się zbytnio od pętli while, ale stosowana w typowych sytuacjach pozwala pisać czytelniejszy kod. Składnia jest następująca:

for ( inicjalizacja zmiennej; warunek; modyfikacja zmiennej) {
  // kod do powtarzania
}

Każdą pętlę while można zamienić na pętle for i na odwrót (w przybliżeniu, takie modyfikacje mogą zmusić do zmiany kodu także poza tymi pętlami). Co więcej, każdą pętlę for można tak przerobić, żeby blok kodu do powtarzania był pusty! To jednak są przypadki patologiczne. W praktyce najczęściej w kodzie inicjalizującym pętli for deklarujemy licznik od którego zależy wykonanie algorytmu, w warunku sprawdzamy, czy powinniśmy dalej powtarzać pętlę, w kodzie modyfikującym modyfikujemy zmienną licznika. Pętla for jest mniej więcej równoważna następującemu fragmentowi:

inicjalizacja zmiennej
while ( warunek ) {
  // kod do powtarzania
  modyfikacja zmiennej
}

Przykładowy kod wygląda następująco:

#include <iostream>

using namespace std;

int main() {
  for (int i = 0; i < 10; ++i) {
    cout << i << endl;
  }
  return 0;
}

Dla doświadczonych programistów jest to znacznie czytelniejszy zapis niż z użyciem pętli while.

Wraz z nowym standardem języka C++ wprowadzono możliwość używania pętli for do wykonywania zadanych instrukcji dla wszystkich obiektów z jakiejś kolekcji (np. tablicy). Zainteresowanych odsyłam do literatury.

break i continue

Czasem zachodzi potrzeba nagłego przerwania wykonywania pętli (bo np. znaleźliśmy to czego szukaliśmy i dalsze obliczenia są zbędne). Służy do tego instrukcja

break;

Wywołanie break powoduje natychmiastowe przerwanie wykonywania pętli, w pętli for pomija też modyfikację licznika.

Podobna do niej jest instrukcja

continue;

Ta instrukcja przerywa wykonanie powtarzanego kodu i sterowanie skacze do końca bloku instrukcji. W pętli for po continue następuje modyfikacja licznika.

Przykład:

#include <iostream>

using namespace std;

int main() {
  for (int i = 0; i < 10; ++i) {
    if (i % 3 == 0)
      continue;
    cout << i << endl;
    if (i == 7)
      break;
  }
  return 0;
}

Instrukcja warunkowa

Kolejną ważną konstrukcją jest instrukcja warunkowa if. Pozwala ona na wykonanie kodu jeśli spełniony jest odpowiedni warunek.

Składnia jest następująca:

if ( warunek ) {
  // kod wykonywany, jeśli warunek jest spełniony
} else {
  // kod wykonywany, jeśli warunek nie jest spełniony
}

Instrukcja wyboru

Instrukcja switch pozwala wykonać odpowiedni kod w zależności od wartości wyrażenia. Wygląda ona następująco:

switch( wyrazenie ) {
  case a:  instrukcja;
           instrukcja;
           instrukcja;
           break;
  case b:  instrukcja;
           instrukcja;
           instrukcja;
  default: instrukcja;
           instrukcja;
}

w powyższym kodzie wyrazenie musi mieć określoną wartość stałoprzecinkową (np. int), a wartości a i b muszą być możliwe do wyliczenia w czasie kompilacji. W czasie wykonywania kodu, najpierw wyliczana jest wartość wyrażenia i zgodnie z tą wartością wykonywany jest skok do jednego z przypadków, jeżeli żadna z opcji nie pasuje, wykonywany jest skok do (opcjonalnej) części default.

Zaskakujące może być, że od miejsca skoku domyślnie kod jest wykonywany do końca całej instrukcji switch, aby skończyć wykonywanie instrukcji, musimy użyć instrukcji break.

Dla przykładu poniższy kod wypisuje odpowiedni komentarz do oceny:

#include <iostream>
using namespace std;

int main() {
  cout << "Wpisz ocenę (1-6): "
  int ocena;
  cin >> ocena;
  switch(ocena) {
    case 6:
    case 5:
      cout << "Gratulacje";
      break;
    case 4:
    case 3:
      cout << "Przeciętnie";
      break
    case 2:
      cout << "Ledwo, ledwo";
      break;
    case 1:
      cout << "Poprawka";
      break;
    default:
      cout << "Nie ma takiej oceny!";
  }

  return 0;
}