Rafał Barszczewski

Podstawy C++ dla programistów ObjectPascal

  1. Wprowadzenie
  2. Typy wbudowane
  3. Deklarowanie zmiennych
  4. Operatory
  5. Instrukcje proste i złożone
  6. Konstrukcje sterujące
  7. Funkcje
  8. Konwersja typów
  9. Enumeracje
  10. Tablice
  11. Struktury
  12. Wskaźniki
  13. Napisy
  14. Programowanie obiektowe
  15. Standard Template Library
  16. Przestrzenie nazw

1. Wprowadzenie

Tekst ten ma za zadanie zapoznać osobę na co dzień posługującą się Paskalem z podstawami składni języka C++. Oczywiście, sama znajomość składni nie wystarczy do pisania programów w tym języku, ale pomoże w pewnym sensie ujednolicić sposób komunikacji i pozwoli lepiej rozumieć teksty przykładowych programów, prezentujących konkretne technologie.

Starałem się, żeby ten tekst był łatwo przyswajalny dla każdego, stąd niektóre aspekty składni są zaprezentowane w sposób dość nieformalny, ale to powinno raczej każdemu wystarczyć.

Na początek należy zwrócić uwagę na najważniejsze różnice między dwoma rozpatrywanymi językami. Przede wszystkim - C++ jest językiem rozróżniającym wielkość liter. Stąd while i While to dwa różne słowa - trzeba koniecznie o tym pamiętać. Generalnie - przynajmniej na początku - można odnieść wrażenie, że składnia C++ jest nieco łagodniejsza od paskalowej, dopuszcza więcej "luzu", różnych możliwości, wyjątków. Nie ma podziału na instrukcje i wyrażenia, jak w Paskalu. Można powiedzieć, że w C++ wszystko jest wyrażeniem - także instrukcja podstawienia (wartość takiego wyrażenia jest równa podstawianej wartości - szczegóły później). Umożliwia to wykonanie wielu różnych czynności za jednym zamachem - trzeba jednak uważać, żeby nie przesadzić (czytelność też się liczy).

Komentarze - w C++ można je pisać na dwa sposoby. Pierwszy sposób to komentarze jednowierszowe - rozpoczynają się od "//", a kończą razem z końcem wiersza. Natomiast komentarze wielowierszowe piszemy między parami znaków "/*" a "*/". Przykłady:

  [instrukcja]    // to jest komentarz jednowierszowy
  [instrukcja]
  /* a to
     jest komentarz
     wielowierszowy */
  [instrukcja]

2. Typy wbudowane

Nie będę tu wymieniał wszystkich typów standardowych w C++, a jedynie te najczęściej wykorzystywane. Uwaga: wszystko opisuję w kontekście standardu ANSI/ISO C++ - został on opracowany ledwie kilka lat temu, więc niektóre starsze kompilatory mogą mieć problemy np. z rozpoznaniem typu bool. Generalnie nie polecam nauki przy użyciu kompilatorów (a także książek czy kursów) sprzed roku 2001.

Najczęściej wykorzystywane typy wbudowane to:

3. Deklarowanie zmiennych

Deklaracja zmiennej w C++ ma postać:

  id_typu  id_zmiennej ;

Przykłady:

  int x ;
  int zmienna ;
  char znak ;
  bool wykonano ;

Można zadeklarować kilka zmiennych tego samego typu naraz (ich identyfikatory oddzielamy przecinkami):

  int x, y, z ;
  char znak1, znak2 ;

Pascal rozróżnia zmienne z wartością początkową i te "normalne" - bez wartości początkowej. W C++ takiego rozróżnienia nie ma - jedyna różnica między jednymi a drugimi jest taka, że te pierwsze są zainicjowane podaną wartością, a te drugie na początku mają wartość nieokreśloną. Wartość początkową w deklaracji nadajemy w następujący sposób:

  id_typu  id_zmiennej = wartosc ;

Przykładowo:

  int x = 5 ;
  int zmienna = 7 ;
  char znak = 'J' ;
  bool wykonano = false ;

Można też deklarować takie zmienne "hurtem", a także mieszać deklaracje. Np.:

  int x = 5, y, zmienna = 7 ;  // x i zmienna zainicjowane, y niezainicjowane
  char znak1 = 'A', znak2 = 'B' ;

W Paskalu zmienne wolno deklarować jedynie w części definicyjno-deklaracyjnej bloku. W C++ takie pojęcie nie istnieje. Zmienne możemy deklarować w dowolnym miejscu w kodzie programu czy funkcji, wewnątrz konstrukcji itp. Oczywiście zmienna jest widoczna jedynie dla instrukcji występujących po deklaracji tej zmiennej. Przykładowo:

  [jakaś instrukcja (nie może używać zmiennej x)]
  [jakaś instrukcja (nie może używać zmiennej x)]
  int x ;  // deklaracja zmiennej x
  [jakaś instrukcja (może używać x)]
  [jakaś instrukcja (może używać x)]

Stosują się tutaj też analogiczne w stosunku do Paskala zasady przesłaniania identyfikatorów, lokalności zmiennych, dynamicznej alokacji itp.

Deklaracja stałej ma składnię bardzo podobną do deklaracji zmiennej, jest jedynie poprzedzona słowem kluczowym const. Przykłady:

  const int x = 5 ;
  const float pi = 3.1415 ;
  const char c = 'z' ;

Co do nazewnictwa zmiennych, istnieje dość rozpowszechniona konwencja, zwana notacją węgierską. Polega ona na tym, że jako pierwszą literę nazwy zmiennej przyjmujemy pierwszą literę identyfikatora jej typu. Przykładowo zmienna typu bool może nazywać się bDone, bOK, bFinished. Zmienna typu int: iWidth, iLength, iNumCars itp. itd. Wbrew pozorom czasami poprawia do czytelność tekstu programu. Przy czym nie stosujemy oczywiście tego zapisu do krótkich, jednoliterowych identyfikatorów.

4. Operatory

Operatory arytmetyczne

Jak widać, C++ nie definiuje oddzielnego operatora dla dzielenia całkowitego. Wynik dzielenia jest całkowity (reszta jest gubiona), jeśli oba argumenty są typu całkowitego. Ponadto, wolno podstawić wartość typu rzeczywistego do zmiennej typu całkowitego - przeprowadzona zostanie automatyczna konwersja polegająca na obcięciu części ułamkowej.

Słowo wyjaśnienia co do ostatnich dwóch operatorów. Instrukcje:

  x++ ;
  ++y ;
  z-- ;
  --zmienna ;

odpowiadają paskalowym instrukcjom: Inc(x), Inc(y), Dec(z), Dec(zmienna). Czyli operator ++ powoduje zwiększenie wartości zmiennej o 1, a operator -- zmniejszenie o 1.

Czym natomiast różni się zapis ++x od x++ ? Jeśli chodzi o wartość zmiennej x po wykonaniu instrukcji, to niczym. Różnica jest jednak w wartości wyrażenia, które stanowi instrukcja. Najlepiej wytłumaczyć na przykładzie: załóżmy, że wartość zmiennej całkowitej x wynosi 5. Zatem warunek x < 6 jest prawdziwy. Rozpatrzmy teraz warunek postaci:

  ++x < 6

Możemy tak napisać, bo wyrażenie po lewej stronie ma wartość, którą można porównać z wartością 6. Jaka to wartość? Jest to wartość zmiennej x powiększona o 1. Przy obliczaniu wartości logicznej tego wyrażenia, najpierw zostanie obliczona wartość "podwyrażenia" ++x, która wynosi 5+1, czyli 6 (przy okazji zostanie zmieniona wartość zmiennej x - jak widać wyrażenie w C++ może wpływać na wartość zmiennych nawet jeśli nie występuje w nim wywołanie funkcji!). Otrzymujemy nierówność 6 < 6, czyli fałsz.

A co się stanie gdy napiszemy:

  x++ < 6

W tym przypadku warunek będzie prawdziwy. W miejsce podwyrażenia x++ wstawiana jest pierwotna wartość zmiennej x, czyli 5. Zatem 5 < 6 => prawda.

Jednym słowem: różnica między ++x a x++ jest taka, że w pierwszym przypadku jako wartość wyrażenia przyjmowana jest nowa wartość zmiennej x, a w drugim przypadku - stara wartość zmiennej x.

Operatory bitowe

Operatory porównania

Operatory przypisania

Uwaga! Bardzo częstym błędem początkujących programistów C++ jest mylenie operatora porównania == z operatorem przypisania =. Kompilator tego błędu nie wykrywa! Warunki należy pisać bardzo uważnie!

Dla programisty paskalowego zaskoczeniem może być obecność wielu operatorów przypisania. Pierwszy z nich dokładnie odpowiada paskalowemu operatorowi :=. Natomiast pozostałe można traktować jako skrótowe (i zoptymalizowane) wersje zwykłych podstawień. Przykładowo instrukcja: x += 5 powoduje dodanie 5 do wartości zmiennej x, w skutkach działania jest więc równoważna instrukcji x = x + 5. Podobnie instrukcje:

  x -= 3 ;
  y *= x ;
  z /= 2 ;
  zmienna %= 4 ;
  n <<= 2 ;

możnaby zapisać jako (odpowiednio):

  x = x - 3 ;
  y = y * x ;
  z = z / 2 ;
  zmienna = zmienna % 4 ;
  n = n << 2 ;

Generalnie instrukcja:

  zmienna op= wyrazenie
jest równoważna co do efektu instrukcji:
  zmienna = zmienna op wyrazenie

Priorytety i kojarzenie operatorów

Wszystkie operatory mają określone priorytety, z jakimi są traktowane przez kompilator. Oczywiście operator * ma priorytet wyższy niż operator +, dlatego wyrażenie 3+4*5 zostanie potraktowane (zgodnie z oczekiwaniami) jako 3+(4*5). Operatory arytmetyczne mają wyższe priorytety niż operatory porównania, te z kolei mają wyższe priorytety niż operatory negacji, koniunkcji i alternatywy logicznej (!, && i ||), dzięki czemu nie trzeba (jak w Paskalu) używać nawiasów aby napisać warunek:

  x > 0 && x*y <= 100

W Paskalu musielibyśmy napisać: (x > 0) and (x*y <= 100).

Jak wspominałem, każda instrukcja jest zarazem wyrażeniem. Instrukcja podstawienia jest wyrażeniem, którego wartość jest równa podstawianej wartości. Oznacza to, że np. wartość wyrażenia c = 5 wynosi 5. Ale skoro taka instrukcja "pozostawia" po sobie wartość, to czemu nie wykorzystać jej do kolejnego podstawienia? Jak najbardziej dopuszczalne jest napisanie instrukcji: b = c = 5. Operator przypisania jest kojarzony od prawej do lewej (dokładniej o tym za chwilę), więc najpierw obliczony zostanie człon "c = 5". Do zmiennej c podstawiona zostaje wartość 5 i ta sama wartość zostaje przyjęta za wartość wyrażenia, jest zatem podstawiana do zmiennej b. Idąc dalej możemy napisać:

  a = b = c = 0 ;

W ten sposób podstawiamy wartość 0 do trzech zmiennych a, b, c przy pomocy tylko jednej instrukcji. Należy jednak korzystać z takich ciekawostek ostrożnie, najlepiej tylko w najprostszych przypadkach (dlaczego? przy złożonych wyrażeniach kolejność ich obliczania przez kompilator w takich sytuacjach nie jest określona - dokładniejsze wytłumaczenie i przykłady w innych źródłach).

Wspomniałem o kojarzeniu operatorów. Każdy z operatorów ma określoną kolejność, zgodnie z którą jest brany pod uwagę przez kompilator. Operatory przypisania są kojarzone od prawej do lewej, dlatego instrukcja "wielokrotnego" podstawienia mogła zadziałać. Większość innych operatorów (w tym wszystkie operatory porównania i prawie wszystkie arytmetyczne) jest kojarzonych od lewej do prawej. Dlatego w wyrażeniu x-y-z najpierw zostanie obliczona różnica x-y, a dopiero potem od wyniku odjęte zostanie z (tak jak powinno być). Jedynymi operatorami arytmetycznymi kojarzonymi od prawej do lewej są ++x i --x (ale x++ i x-- są kojarzone od lewej do prawej!).

5. Instrukcje proste i złożone

W C++ każda instrukcja prosta (czyli podstawienie, wywołanie funkcji albo deklaracja zmiennej z inicjacją) musi być zakończona średnikiem. Z kolei po poszczególnych konstrukcjach (jak zobaczymy później) średnik nie jest wymagany. Jest to więc trochę inaczej niż w Paskalu, gdzie średnik pełnił funkcję spójnika - tutaj średnik jest elementem kończącym instrukcję. Co z tego wynika - zaraz zobaczymy.

Instrukcja złożona w C++ to ciąg instrukcji prostych (każda z nich musi być zakończona średnikiem) lub złożonych, zawarta pomiędzy nawiasami figurowymi: { i }. Nawiasy figurowe są więc odpowiednikami paskalowych słów kluczowych begin i end. Przy tym nie jest wymagane stawianie średnika po nawiasie zamykającym (nie trzeba kończyć konstrukcji średnikami jak w Paskalu). Trzeba jednak stawiać średnik po ostatniej instrukcji prostej wewnątrz bloku (inaczej niż w Paskalu, gdzie można było pominąć średnik przed endem).

Przykładowy fragment programu:

  int y = 3, z ;
  int x = 5 ;
  z = x % 2 ;
  {
      z += 3 + y*(x-2) ;
      int a = x + z ;
      x-- ;
      {
         int b = 10 ;
         b = a*y ;
      }
      y++ ;
  }
  x *= z ;

W Paskalu taki fragment programu wyglądałby następująco:

  var  x, y, z, a, b :Integer ;
begin
  y := 3 ;
  x := 5 ;
  z := x mod 2 ;
  begin
    z := z + 3 + y*(x-2) ;
    a := x + z ;
    Dec(x) ;
    begin
      b := 10 ;
      b := a*y
    end ;
    Inc(y)
  end ;
  x := x*z
end ;

(oczywiście ten program nie robi nic sensownego)

6. Konstrukcje sterujące

Język C++ definiuje 6 konstrukcji sterujących: selekcję if, selekcję if-else, pętlę while, pętlę do-while, pętlę for oraz instrukcję wyboru switch.

Selekcja if

Postać:

if (warunek)
  instrukcja

Równoważnik paskalowy: if warunek then instrukcja

Uwaga: nawiasy obejmujące warunek są obowiązkowe. W C++ nie ma odpowiednika słowa kluczowego then - nawiasy są jedynym elementem wskazującym, w którym miejscu kończy się warunek a zaczyna instrukcja.

Przykłady:

  if (x == 5)
      x = 0 ;

  if (x > 0 && x < 10)   // czyli: jesli 0 < x < 10
  {
      int y = x - 5 ;
      x *= y ;
      x-- ;
  }

Zwróć uwagę na zapis warunku! Operator && działa jako koniunkcja logiczna (spójnik AND). Ponadto nie są wymagane nawiasy dla poszczególnych części warunku, bo && ma niższy priorytet niż operatory porównania (odwrotnie niż w Paskalu!).

Selekcja if-else

Postać:

if (warunek)
  instr1
else
  instr2

Równoważnik paskalowy: if warunek then instr1 else instr2

Przykłady:

  if (x == 5)
      x = 0 ;
  else
      x++ ;

  if (x > 0)
  {
      x-- ;
      y += x ;
  }
  else    // czyli x <= 0
  {
      x = 1 ;
      y = 0 ;
  }

  if (znak == 'A')
  {
     cout << znak ;   // wyświetla wartość zmiennej znak na konsoli (działa jak Write(znak) w Paskalu)
     znak = 'X' ;
  }
  else if (znak == 'B')
  {
     cout << "Litera B" ;
     znak++ ;
  }
  else
     cout << "Inny znak" ;

Pętla while

Postać:

while (warunek)
  instrukcja

Równoważnik paskalowy: while warunek do instrukcja

Tak jak w przypadku selekcji, tak i tutaj nawiasy obejmujące warunek są obowiązkowe.

Przykłady:

  while (x > 0)
      x-- ;

  while (x != 5 && (x <= 10 || !wykonano))    // wykonano jest zmienną typu bool
  {
      x++ ;
      wykonano = (x - y)%2 <> 0 ;
  }

Pętla do-while

Postać:

do
  instrukcja
while warunek

Równoważnik paskalowy: repeat instrukcja until not warunek

Refren takiej pętli wykonuje się co najmniej jeden raz. Raczej rzadko wykorzystywana konstrukcja.

Przykłady:

  /* pętla wyświetla liczby od 1 do 10 */
  x = 0 ;
  do
  {
      x++ ;
      cout << x << endl ;    // endl oznacza koniec wiersza (instrukcja odpowiada WriteLn(x))
  } while (x < 10)

Pętla for

Postać:

for (instr1 ; warunek ; instr2)
  instr3

Równoważnik paskalowy:

  instr1 ;
  while
    warunek
  do
    begin
      instr3 ;
      instr2
    end

Bardzo często wykorzystywana pętla, nie należy jednak jej mylić z paskalową pętlą for, która działa nieco inaczej.

W C++ pętla for wykonywana jest następująco: najpierw wykonywana jest instrukcja instr1, która zazwyczaj ma postać deklaracji z inicjacją zmiennej sterującej (ale nie musi tak być - może to nawet być instrukcja pusta). Warunek - to wyrażenie, którego wartość będzie badana przed każdym obrotem pętli (także przed pierwszym). Jego wartość logiczna będzie obliczana za każdym razem, a nie tylko raz. W ramach wykonania refrenu pętli wykonywana jest instrukcja instr3 (standardowy refren), a następnie instr2, która zazwyczaj ma postać instrukcji zmieniającej wartość zmiennej sterującej (również może być pusta).

Przykłady typowych zastosowań pętli for:

  /* pętla wyświetla liczby od 1 do 10 */
  for (int x = 1 ; x <= 10 ; x++)
      cout << x << endl ;

  bool wykonano ;
  [jakieś instrukcje]
  for (wykonano = false ; !wykonano ; )    // instr1 jest podstawieniem, a instr2 jest pusta
  {
      [... jakieś instrukcje]
      wykonano = [...jakieś wyrażenie logiczne] ;
  }  // w tym przypadku lepiej było użyć pętli while (czytelniej)

  for (int x = 5, y = 10 ; x*y+1 < z ; x++, y++)    // można wpisać kilka instrukcji po przecinku
  {
      x += y ;
      y-- ;
  }

Taka pętla działa jak automat, pozwala na zwięzły zapis i załatwienie kilku spraw za jednym zamachem. Zmienna zadeklarowana w instrukcji instr1 jest lokalna dla pętli, czyli poza pętlą jest niedostępna! Ma to swoje zalety: możemy deklarować w różnych pętlach zmienną sterującą o takim samym identyfikatorze, np. x, jeśli tak nam wygodnie (starsze kompilatory często interpretowały to odwrotnie - do czasu wprowadzenia ujednoliconego standardu ANSI/ISO).

Konstrukcja switch

Postać:

switch (wyrażenie)
{
case wart1:
    instrukcja
    instrukcja
    [...]
    instrukcja
    break ;
case wart2:
case wart3:
    instrukcja
    instrukcja
    [...]
    instrukcja
    break ;
[...]
case wartn:
    instrukcja
    instrukcja
    [...]
    instrukcja
    break ;
default:
    instrukcja
    instrukcja
    [...]
    instrukcja
    break ;   
}

Równoważnik paskalowy:

  case wyrażenie of
    wart1:  begin instrukcja ; instrukcja ; [...] instrukcja end ;
    wart2, wart3:  begin instrukcja ; instrukcja ; [...] instrukcja end ;
    [...]
    wartn:  begin instrukcja ; instrukcja ; [...] instrukcja end
    else    begin instrukcja ; instrukcja ; [...] instrukcja end
  end

Wartości po etykietach case muszą być stałymi. Instrukcja switch wykonywana jest następująco: najpierw zostaje obliczona wartość wyrażenia. Następnie odnaleziona zostaje etykieta case, przy której wyszczególniono tę wartość. Od tego miejsca rozpoczyna się wykonywanie instrukcji, przy czym jest ono przerywane dopiero przy natrafieniu na instrukcję break. Instrukcję break można (teoretycznie) pominąć - wtedy wykonywanie kodu nie zatrzyma się aż do następnego break. Można w ten sposób coś uzyskać, ale nie zaleca się takiego podejścia poza przypadkami trywialnymi (gdyż jest to podejście niestrukturalne). Jeżeli nie znaleziono etykiety case z odpowiednią wartością, to wykonywane są instrukcje po słowie default (o ile ono jest, bo można tą część pominąć, podobnie jak else w Paskalu).

Przykłady:

  char znak ;
  cout << "Podaj znak: " ;
  cin >> znak ;    // pobranie znaku od użytkownika (odpowiednik Read(znak))
  switch (znak)
  {
  case 'a':
  case 'A':
      cout << "Litera A." ;
      break ;
  case 'B':
      cout << "Duża litera B." ;
      break ;
  case 'F':
      cout << "Duża litera F. " ;    // uwaga: niestrukturalne podejście - brak break
  default:
      cout << "Litera późniejsza od B" ;
      break ;    // to break już nie jest konieczne
  }
  /* jeśli znak == 'F', to instrukcja wyświetli "Duża litera F. Litera późniejsza od B" */

Instrukcje niestrukturalne

Do instrukcji niestrukturalnych zaliczamy między innymi wspomnianą break oraz continue. Instrukcja break służy do natychmiastowego wyjścia poza konstrukcję. Poza typowym zastosowaniem w konstrukcji switch, można ją wykorzystać także w pętli. Wykonanie break zrywa najlbiższą (w sensie zagnieżdzenia) pętlę w trakcie jej wykonywania, niezależnie od warunku. Przykład:

  /* obie poniższe pętle wyświetlają liczby od 1 do 10 */
  for (int x = 1 ; x <= 10 ; x++)
      cout << x << endl ;

  int x = 1 ;
  while (true)
  {
      if (x = 11)
          break ;              // zerwanie pętli
      cout << x++ << endl ;    // wyświetlenie x i zwiększenie x o 1
  }

Z kolei instrukcja continue jest wykorzystywana tylko w pętlach i powoduje natychmiastowe zakończenie wykonywania aktualnego obrotu pętli i przejście do następnego obrotu. Przykład:

  /* pętla wyświetla liczby od 1 do 10, ale z pominięciem 5 */
  for (int x = 1 ; x <= 10 ; x++)
  {
      if (x = 5)
          continue ;    // ignoruj pozostałe instrukcje refrenu i przejdź do kolejnego obrotu pętli
      cout << x << endl ;
  }

Używanie instrukcji break i continue należy ograniczyć do przypadków trywialnych.

Wyrażenie warunkowe

Wyrażenie warunkowe to wyrażenie postaci:

  warunek ? wyr1 : wyr2

Jeżeli warunek jest prawdziwy, to wartość wyrażenia warunkowego jest równa wartości wyr1, a w przeciwnym razie - wartości wyr2. W szczególności wyrażeniami wyr1 i wyr2 mogą być instrukcje.

Wyrażenie warunkowe jest bardzo pomocne w sytuacjach, w których potrzebujemy dwóch bardzo podobnych wersji jednego wzoru w zależności od pewnego warunku. Przykładowo:

  x = a * b * (podwojnyIloczyn ? 2 : 1) ;
  a = a > 0 ? a : -a ;    // wygodniej: a = abs(a) ;

7. Funkcje

Deklarowanie funkcji

W C++ nie ma rozróżnienia na procedury i funkcje. Wszystkie podprogramy nazywają się funkcjami, z tym że niektóre z nich zwracają wartość pustą (oznaczaną słowem kluczowym void). Deklaracja funkcji w C++ ma postać:

id_typu_zwracanego  id_funkcji (lista_parametrów)
{
    [...instrukcje]
}

Jeśli jako id_typu_zwracanego podany jest void, to funkcja nie zwraca wartości (tak jak procedury w Paskalu). Listę parametrów tworzy się wyszczególniając poszczególne parametry w postaci: id_typu id_parametru, przy czym poszczególne takie pary są rozdzielone przecinkami (jeśli oczywiście parametrów jest więcej niż jeden). Nawiasy obejmujące listę parametrów są obowiązkowe, nawet jeśli ta lista jest pusta (funkcja bezparametrowa) - to samo tyczy się wywoływania funkcji. Przykłady deklaracji funkcji (na razie bez definiowania ich ciał):

  int Suma (int a, int b)
  {
      [...]
  }

  void WyswietlSume (double x, double y, bool naSrodku)
  {
      [...]
  }
 
  bool JestNiedziela ()
  {
      [...]
  }

W Paskalu równoważne deklaracje wyglądałyby następująco:

  function Suma (a, b :Integer) : Integer ;
  begin
    [...]
  end ;

  procedure WyswietlSume (x, y :Double ; naSrodku :Boolean) ;
  begin
    [...]
  end ;

  function JestNiedziela : Boolean ;
  begin
    [...]
  end ;

Deklarowanie funkcji wewnątrz innych funkcji w C++ nie jest dozwolone.

Funkcje wywołujemy w taki sam sposób jak w Paskalu, ze wspomnianym już zastrzeżeniem o obowiązkowych nawiasach:

  cout << Suma(7, 15) << endl ;    // wyświetla 22

  WyswietlSume(a, b/8, JestNiedziela()) ;

  if (JestNiedziela())
      cout << "**********" ;

Tryby przekazywania parametrów

W C++ istnieje trochę więcej trybów przekazywania parametrów do funkcji niż w Paskalu. Oczywiście podstawowym trybem jest przekazywanie przez wartość (działa tak samo jak w Paskalu) - z nim mieliśmy do czynienia w powyższych przykładach. Istnieje jeszcze m.in. przekazywanie przez referencję (działa jak var w Paskalu), przez stałą referencję, przez adres i przez stały adres. Pierwsze dwa z nich omówię teraz, dwa ostatnie (częściej wykorzystywane) - opiszę przy okazji omawiania typów wskaźnikowych.

Przekazywanie przez referencję działa tak, jak przekazywanie przez zmienną w Paskalu. Nie jest tworzona zmienna robocza funkcji, poprzez parametr mamy dostęp bezpośrednio do zmiennej podanej jako argument i możemy zmieniać jej wartość. Parametr przekazywany przez referencję deklaruje się następująco:

id_typu & id_parametru

Jak widać, kluczowy jest tutaj znak &. Przekazywanie przez referencję wykorzystuje się najczęściej do wydania wyników działania funkcji, jeśli jest ich więcej niż jeden.

Przekazywanie przez stałą referencję jest bardzo podobne, z tą tylko różnicą, że nie wolno nam zmieniać wartości parametru. Jest on stały w obrębie tej funkcji, zmiana jego wartości jest blokowana przez kompilator, ale nie jest tworzona dla niego zmienna robocza - operujemy na zmiennej podanej jako argument. Jest to przydatne do przekazywania do funkcji większych danych strukturalnych, które normalnie przekazywalibyśmy przez wartość, ale ze względu na ich rozmiar chcemy ten proces zoptymalizować, jednocześnie zachowując pewność że wartość danej nie zostanie zmieniona (jednak jak zobaczymy później, w takich sytuacjach częściej wykorzystuje się przekazywanie przez stały adres). Parametr przekazywany przez stałą referencję deklaruje się podobnie, jak przez zmienną referencję, jedynie z dodanym słowem const na początku:

const id_typu & id_parametru

Zwracanie wartości

Zwrócenie wartości funkcji następuje poprzez wykonanie instrukcji return z argumentem będącym wyrażeniem typu zgodnego z typem wartości funkcji. Instrukcji return może być wiele w jednej funkcji, ale co najmniej jedna z nich musi być wykonana (z wyjątkiem funkcji void, o czym za chwilę). Przy czym wykonanie instrukcji return powoduje natychmiastowy powrót do punktu wywołania i zignorowanie pozostałych instrukcji (jeśli jeszcze jakieś są).

W przypadku funkcji niezwracających wartości są dwie możliwości: albo instrukcja return może zostać podana bez argumentu, albo może jej w ogóle nie być (wtedy powrót do punktu wywołania nastąpi w momencie napotkania końca bloku funkcji - nawiasu "}").

Poza przypadkami trywialnymi odradza się umieszczanie w funkcji wielu instrukcji return, gdyż jest to podejście niestrukturalne.

Przykłady pełnych deklaracji funkcji:

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

  void ObliczSume (int a, int b, int & wynik)    // w złym stylu - lepiej wynik przyjmować jako wartość funkcji
  {
      wynik = a + b ;
  }
  
  void WyswietlSume (double a, double b)
  {
      cout << a + b ;
      return ;           // tej instrukcji mogłoby nie być
  }

  bool JestNiedziela ()
  {
      if (JestWeekend())
      {
          int nr = NumerDniaWTygodniu() ;
          return nr % 2 ;    // jeśli numer dnia nieparzysty => niedziela
      }
      return false ;
  }

W Paskalu powyższe deklaracje wyglądałyby następująco:

  function Suma (a, b :Integer) :Boolean ;
  begin
    Suma := a + b
  end ;

  procedure ObliczSume (a, b :Integer ; var wynik :Integer) ;
  begin
    wynik := a + b
  end ;

  procedure WyswietlSume (a, b :Double) ;
  begin
    WriteLn(a + b)
  end ;

  function JestNiedziela : Boolean ;
    var  nr :Integer ;
  begin
    JestNiedziela := false ;
    if
      JestWeekend
    then
      begin
        nr := NumerDniaWTygodniu ;
        JestNiedziela := nr mod 2 <> 0    // albo: JestNiedziela := Odd(nr)
      end
  end ;

Deklarowanie nagłówków

Jeżeli potrzebujemy użyć funkcji, której definicja znajduje się w dalszej części tekstu programu (później niż wywołanie), to musimy taką funkcję "zapowiedzieć", tj. zadeklarować jej nagłówek (w Paskalu robimy to wykorzystując dyrektywę forward). Deklaracja nagłówka funkcji to po prostu jej nagłówek zakończony średnikiem i, opcjonalnie, z usuniętymi identyfikatorami parametrów. Przykładowe nagłówki (dla funkcji z poprzedniego przykładu):

  int Suma (int a, int b) ;
  void ObliczSume (int, int, int &) ;         // identyfikatory parametrów można pominąć (ale nie tryb przekazywania!)
  void WyswietlSume (double x, double y) ;    // jednak dla wygody i czytelności lepiej je zostawić
  bool JestNiedziela () ;

Wtedy możemy wywoływać te funkcje nawet przed podaniem ich pełnej definicji (oczywiście przed w sensie przestrzennym, nie czasowym...).

Argumenty domyślne

Zaprezentuję teraz dwie przydatne cechy C++, które nie mają odpowiedników w Paskalu. Pierwszą z nich jest możliwość deklarowania argumentów domyślnych dla funkcji. Argument domyślny to taki argument, który w wywołaniu funkcji można pominąć (ale nie trzeba) i wówczas przyjmowana jest jego domyślna wartość. Przy czym może być to tylko ostatni, albo kilka ostatnich argumentów. Przykład:

  double Pierwiastek (double x, int stopien = 2)
  {   // (stopien jest argumentem domyślnie równym 2 i w wywołaniu można go pominąć)
      [...]
  }

Teraz funkcję Pierwiastek() możemy wywołać na kilka sposobów:

  a = Pierwiastek(b) ;       // domyślnie pierwiastek kwadratowy - krótko i wygodnie
  x = Pierwiastek(y, 3) ;    // ale funkcja jest uniwersalna - obsługuje też np. pierwiastek sześcienny

Inne przykłady:

  void Funkcja1 (int x, int y = 0, int z = 0)    // dwa argumenty domyślne
  {
      [...]
  }

  void Funkcja2 (int a = 5, int b = 10, int c = 15 )    // wszystkie argumenty domyślne
  {
      [...]
  }

  /* przykładowe wywołania */
  Funkcja1(10, 20, 30) ;
  Funkcja1(40, 50) ;
  Funkcja1(60) ;
  // Funkcja1() ;  -> niedozwolone!
  Funkcja2(10, 20, 30) ;
  Funkcja2(40, 50) ;
  Funkcja2(60) ;
  Funkcja2() ;

Argumenty domyślne nie mają wpływu na ciało funkcji - dla takich argumentów również tworzone są zmienne lokalne, więc algorytm (treść funkcji) w żaden sposób się nie zmienia.

Nie wolno zadeklarować np. funkcji trzyargumentowej, której tylko drugi argument byłby domyślny. Argumenty domyślne mogą być tylko na końcu listy parametrów.

W deklaracji nagłówka funkcji z parametrami domyślnymi, nie można pominąć domyślnych wartości. Przykład takich deklaracji dla funkcji z ostatniego przykładu:

  double Pierwiastek (double x, int stopien = 2) ;
  void Funkcja1 (int, int = 0, int = 0) ;    // wolno pominąć identyfikatory parametrów, ale nie domyślne wartości
  void Funkcja2 (int = 5, int = 10, int = 15) ;

Przeciążanie funkcji

Kolejnym udogodnieniem w C++ jest możliwość przeciążania funkcji (w ObjectPascalu wprawdzie też jest taka opcja, ale raczej mało znana i trochę niewygodna). Przeciążanie funkcji polega na deklarowaniu kilku różnych funkcji o tym samym identyfikatorze, ale różniących się listami parametrów (dodatkowo mogą też różnić się typem zwracanej wartości). Dwie listy parametrów uznajemy za różne, jeśli - w skrócie - różna jest liczba parametrów, albo co najmniej jeden z ich typów. Przykłady:

  int Potega (int x, int wykladnik)
  {
      [...]
  }

  double Potega (double x, int wykladnik)    // dzięki temu możemy używać tej samej funkcji do potęgowania
  {                                          // liczb całkowitych i rzeczywistych, bez ich konwertowania
      [...]
  }

Zwracany typ może być identyczny, ale listy parametrów takich funkcji muszą się różnić. Przeciążanie funkcji stosuje się bardzo często i jest to naprawdę przydatna możliwość, ale nie będę tutaj dokładniej tego opisywał - wszystko wyjdzie w praktyce...

8. Konwersja typów

W C++ jawna konwersja typów (zwana w Paskalu przez niektórych obsadą) możliwa jest na kilka sposobów. Dla uproszczenia omówię tylko niektóre z nich. Najprostsze rzutowanie typu wygląda następująco:

  (id_typu) wyr

Wartość powyższego wyrażenia jest typu określonego przez id_typu. Ten rodzaj konwersji nie jest jednak wspierany przez standard ANSI/ISO C++. W jego miejsce proponuje się stosować składnię:

  static_cast<id_typu>(wyr)

Jest to tak zwane rzutowanie statyczne. Przykłady:

  int x = 3, y = 2 ;
  float a = x / y ;                      // po wykonaniu a == 1.0 ! to niedobrze
  float b = (float) x / y ;              // po wykonaniu b == 1.5
  float c = static_cast<float>(x)/y ;    // po wykonaniu: c == 1.5

Podobnie jak w Paskalu, w C++ wartości całkowite można podstawiać bezpośrednio do zmiennych rzeczywistych. W C++ istnieje również automatyczna konwersja w drugą stronę (której brak w Paskalu). Przy podstawianiu wartości rzeczywistej do zmiennej całkowitej część ułamkowa jest obcinana.

W C++ możliwa jest także automatyczna konwersja wartości logicznych do całkowitych i w drugą stronę. W pierwszym przypadku wartości false przyporządkowywane jest 0, a wartości true - 1. Przy konwersji w drugą stronę wartość całkowita równa zero interpretowana jest jako false, a wartość całkowita różna od zera (niekoniecznie równa 1!) - jako true. Przykład:

  /* pętla wyświetla liczby od 1 do 10, ale od końca */
  int x = 10 ;
  while (x)                    // czyli: while (x != 0)
      cout << x-- << endl ;

Podobnie jak w Paskalu, także w C++ istnieje sposób na rozpoznanie liczby bajtów zajmowanych przez zmienną danego typu. W Paskalu była to procedura SizeOf, natomiast C++ definiuje operator sizeof (różnica między operatorem a procedurą objawia się np. tym, że argumentu operatora nie trzeba ujmować w nawiasy). Przykłady:

  cout << sizeof(bool) ;    // wyświetla 1
  cout << sizeof(int) ;    // wyświetla 4
  int x ;
  cout << sizeof(x) ;      // wyświetla 4

9. Enumeracje

W C++ nie ma dokładnego odpowiednika paskalowych typów wyliczeniowych. Istnieje jednak sposób na zadeklarowanie serii stałych - identyfikatorów, którym odpowiadają kolejne liczby całkowite. Tym sposobem są enumeracje. Enumerację deklarujemy następująco:

enum ID_TYPU { ID1, ID2, [...] } ;

ID_TYPU to identyfikator nowego typu, jakim będzie enumeracja. Można potem deklarować zmienne tego typu, ale zazwyczaj się tego nie robi. Enumeracja służy do zadeklarowania serii stałych (żeby nie trzeba było używać numerków), numerowanych od 0 do n-1, gdzie n oznacza liczbę wyszczególnionych identyfikatorów. Do przechowywania takich wartości zazwyczaj używa się zmiennych typu int (wartości odpowiadające identyfikatorom są typu całkowitego). Zachodzi automatyczna konwersja typu enumeracji do typu całkowitego (w drugą stronę już nie, więc w razie potrzeby (bardzo rzadkiej) trzeba użyć jawnego rzutowania typów). Przykłady enumeracji:

  enum DNITYGODNIA { PON, WTO, SRO, CZW, PIA, SOB, NIE } ;
  enum BOOL { FALSE, TRUE } ;    // zazwyczaj definiowany przez kompilatory dla zgodności ze starszymi

Przykłady użycia:

  DNITYGODNIA dz = PON ;    // nowa zmienna typu "enumeracyjnego" zainicjowana
                            // wartością zero (czyli PON)
  int dzien = PON ;         // wychodzi na to samo, ale zazwyczaj właśnie tak się to robi
  [...]
  if (dzien == WTO || dzien >= 5)
      cout << "Jest wtorek albo weekend." << endl ;
  [...]
  dzien = 3 ;                           // dozwolone
  //  dz = 3 ;                          // niedozwolone!
  dz = static_cast<DNITYGODNIA>(3) ;    // dozwolone

Ważne jest, aby pamiętać, że głównym celem deklarowania enumeracji nie jest tworzenie nowego typu, ale zadeklarowanie kilku stałych (które są ze sobą w jakiś sposób powiązane). Do tego celu pomocne są często jeszcze dwie opcje: enumeracje anonimowe (takie, w których pominięto ID_TYPU) oraz przypisywanie identyfikatorom wartości (innych niż domyślne, czyli od 0 do n-1). Przykłady:

  enum { PN, WT, SR, CZ, PI, SO, NI } ;  // enumeracja anonimowa

  enum { PON = 1, WTO, SRO, CZW, PIA,    // zmiana numerowania: PON ma numer 1, a kolejne identyfikatory
         SOB, NIE } ;                    // otrzymują następne numery, czyli 2, 3, 4 itd. w ten sposób
                                         // uzyskaliśmy tradycyjną numerację dni - od 1 do 7

  enum { ID1, ID2, ID3 = 17, ID4 = 0,    // identyfikatory będą mieć kolejno wartości:
         ID5, ID6 = 3, ID7, ID8 } ;      // 0, 1, 17, 0, 1, 3, 4, 5

W szczególności można wykorzystać enumerację do deklaracji pojedynczych stałych (jak zobaczymy później, jest to alternatywa dla stałych statycznych w klasach):

  /* poniższe deklaracje dają nam to samo */
  const int MAX = 100 ;
  enum { MAX = 100 } ;    // (oczywiście nie mogą one wystąpić naraz w jednym programie)

10. Tablice

Tablice jednowymiarowe

Jedną z zalet składni C++ jest z pewnością łatwość i wygoda posługiwania się tablicami. Deklaracja tablicy jednowymiarowej wygląda następująco:

typ_składowy  identyfikator[rozmiar] ;

W Paskalu wyglądałoby to tak: var identyfikator :array[0..rozmiar-1] of typ_składowy ;

Jak widać, nie mamy możliwości wyboru zakresu indeksowania. Tablice w C++ są indeksowane zawsze 0 do n-1 (gdzie n oznacza podany rozmiar). Kilka przykładów:

  int tab1[20] ;      // deklaruje 20-elementową tablicę liczb całkowitych (o indeksach od 0 do 19)
  char tab2[5] ;      // 5-elementowa tablica znaków
  bool tab3[100] ;    // 100-elementowa tablica wartości logicznych

Typem składowym tablicy może być dowolny typ (dotyczy to także struktur i klas).

Deklaracja tablicy może zawierać także inicjację składowych. Wtedy ma ona postać:

typ_składowy  identyfikator[rozmiar] = { wart0, wart1, wart2, [...] } ;

Wyszczególnione wartości zostaną przypisane kolejnym indeksom tablicy.

Lista inicjacyjna (ta w nawiasach figurowych) może zawierać mniej wartości, niż podany rozmiar. Wówczas "brakujące" elementy zostaną domyślnie wyzerowane (ale jeśli w ogóle nie podajemy listy inicjacyjnej, to wszystkie elementy bedą nieokreślone!). Ponadto, przy takiej deklaracji, można nie podawać rozmiaru - wtedy tablica będzie składać się z tylu elementów, ile wyszczególniono wartości. Kilka przykładów:

  char znaki[3] = { 'a', 'b', 'c' } ;
  int liczby1[] = { 10, 20, 30 } ;          // tablica 3-elementowa
  int liczby2[7] = { 10, 20, 30, 40 } ;    // tablica 10-elementowa, elementy od 4 do 6 - wyzerowane

Wyświetlmy wartości elementów tych trzech tablic:

  for (int i = 0 ; i < 3 ; i++)
      cout << znaki[i] << " " ;
  cout << endl ;
  for (int i = 0 ; i < 3 ; i++)
      cout << liczby1[i] << " " ;
  cout << endl ;
  for (int i = 0 ; i < 7 ; i++)
      cout << liczby2[i] << " " ;

Otrzymujemy wyniki:

  a b c
  10 20 30
  10 20 30 40 0 0 0

Przykład pętli wczytującej wartości do tablicy:

  const int rozmiar = 10 ;
  int tab[rozmiar] ;
  for (int i = 0 ; i < rozmiar ; i++)
  {
      cout << "Podaj liczbę numer " << i << ": " ;
      cin >> tab[i] ;
      cout << endl ;
  }

Tablice wielowymiarowe

Deklaracja tablicy wielowymiarowej ma postać:

typ_składowy  identyfikator[rozmiar1][rozmiar2]...[rozmiarn] ;

Maksymalna liczba wymiarów tablicy to 16. W przypadku tablic dwuwymiarowych podobnie jak w Paskalu elementy są rozmieszczane wierszami. Więc pierwszy wymiar powinien odpowiadać za wiersze, a drugi za kolumny. Przykład:

  int tab[4][3] = { { 5, 10, 15 }, { 1, 2, 3 }, { 100, 200, 300 }, { -10, -20, -30 } } ;
      // (4 wiersze po 3 kolumny)

Wyświetlmy zawartość tej tablicy:

  for (int y = 0 ; y < 4 ; y++)
  {
      for (int x = 0 ; x < 3 ; x++)
          cout << tab[y][x] << " " ;
      cout << endl ;
  }

Otrzymujemy wyniki:

  5 10 15
  1 2 3
  100 200 300
  -10 -20 -30

Przekazywanie tablic do funkcji

Najczęściej wykorzystywanym sposobem jest przekazywanie adresów, ale o tym dokładniej napiszę w rozdziale o wskaźnikach.

W przypadku tablic jednowymiarowych najprościej zadeklarować parametr postaci:

typ_składowy  identyfikator[rozmiar]

Możemy pominąć rozmiar - wtedy funkcja będzie przyjmować tablice dowolnego rozmiaru. Przykłady:

  int Suma10Liczb (int tab[10])
  {
      int suma = 0 ;
      for (int i = 0 ; i < 10 ; suma+=tab[i], i++) ;  // sprytne, prawda? refren tylko pozornie jest pusty
      return suma ;
  }

  int SumaNLiczb (int tab[], int rozmiar = 10)  // wersja uniwersalna - można podać rozmiar inny niż domyślny
  {
      int suma = 0 ;
      for (int i = 0 ; i < rozmiar ; suma+=tab[i], i++) ;
      return suma ;
  }

W przypadku tablic n-wymiarowych w nagłówku funkcji trzeba podać co najmniej n-1 rozmiarów (rozmiar tylko jednej "podtablicy" - pierwszej od lewej - może zostać pominięty).

11. Struktury

Struktura w C++ to odpowiednik paskalowego rekordu. Deklaracja struktury ma postać:

struct identyfikator
{
    składowa
    składowa
    [...]
    składowa
} ;

Składowe struktury mogą być dowolnego typu. Przykłady definicji struktur:

  struct Data
  {
      int dzien, miesiac, rok ;
  } ;

  struct Pracownik
  {
      enum { MAX = 25 } ;
      char    imie[MAX] ;
      char    nazwisko[MAX] ;
      Data    dataUr ;           // zagnieżdżona struktura
      bool    pelnyEtat ;
      int     pensja ;
      int     godziny[12] ;      // liczba godzin przepracowanych w każdym miesiącu
  } ;

Przykładowa funkcja korzystająca z zadeklarowanych powyżej typów:

  void WyswietlDanePracownika (const Pracownik & prac)
      // parametr przekazywany jest przez stałą referencję - dla optymalizacji
  {
      cout << "Pracownik: " << prac.imie << prac.nazwisko << endl
           << " Urodzony: " << prac.dataUr.dzien << "/" << prac.dataUr.miesiac
                            << "/" << prac.dataUr.rok << endl
           << "    Umowa: " << ( prac.pelnyEtat ? "pełna, " : "pół etatu, " )
                            << prac.pensja/100.0 << "PLN" << endl ;
      cout << "Przepracowane godziny: " ;
      for (int mies = 0 ; mies < 12 ; mies++)
          cout << prac.godziny[mies] << " " ;
      cout << endl ;
  }

Jak widać, struktury można w sobie zagnieżdżać. Dostęp do konkretnej składowej uzyskujemy - tak jak w Paskalu - poprzez notację z kropką. W C++ nie ma odpowiednika paskalowej instrukcji with.

Kopiowanie struktur - jest możliwe poprzez zwykłe podstawienie (przy zgodnych typach). Wykonywane jest wtedy automatyczne kopiowanie składowych (dość szybkie - kopiowany jest cały blok bajtów). Nie należy jednak tego stosować, jeśli pola struktury mogą być typu wskaźnikowego.

Struktura poza polami może także zawierać metody. Wynika to z faktu, że w C++ struktura jest równoważna klasie (jedyna różnica to inny domyślny tryb dostępu do składowych). Więcej o tym w rozdziale o klasach.

12. Wskaźniki

Deklarowanie wskaźników

Zmienną typu wskaźnikowego w C++ deklarujemy:

typ_wskazywany * identyfikator ;

Ta deklaracja odpowiada paskalowej: var identyfikator :^typ_wskazywany. Można więc powiedzieć, że paskalowemu operatorowi ^ odpowiada w C++ operator *.

Zmiennej wskaźnikowej można przypisać albo adres zmiennej typu wskazywanego, albo 0. W tym drugim przypadku zazwyczaj - dla czytelności - przypisuje się stałą NULL (NULL i 0 są sobie równe i można ich używać zamiennie). Aby uzyskać adres zmiennej typu wskazywanego, poprzedzamy jej identyfikator operatorem & (w Paskalu używamy @). Przykłady:

  int x = 10, y = 0 ;
  int * p1 = &x ;                        // p1 wskazuje na x
  int * p2 = &y, p3 = &y, p4 = NULL ;    // p2 i p3 wskazują na y, a p4 jest pusty

Dereferencja wskaźników

Dereferencja wskaźnika (czyli uzyskanie wartości przez niego wskazywanej) następuje poprzez poprzedzenie identyfikatora tego wskaźnika operatorem * (w Paskalu piszemy operator ^ po identyfikatorze). Przykłady (do poprzednich deklaracji):

  y = *p1 ;       // równoważne y = 10
  p1 = NULL ;     // wyzerowanie wskaźnika
  // y = *p1 ;    // niedozwolone! *(NULL) jest nieokreślone!
  y = *p2 + *p3 ;    // y = y + y, czyli y *= 2, czyli y = 20

Jeśli chodzi o typy strukturalne, to w celu uzyskania dostępu do składowej zmiennej wskazywanej mamy do wyboru dwie notacje:

(*wskaźnik).składowa
wskaźnik->składowa

Ta druga notacja wydaje się wygodniejsza, bo nie trzeba pisać dodatkowych nawiasów. Poniższy przykład wykorzystuje typy strukturalne zadeklarowane w poprzednim rozdziale:

  Pracownik prac = { "Jan", "Kowalski", { 1, 4, 1965 }, true, 300000 } ;  // uwaga: inicjator struktury!
  Pracownik * wsk = &prac ;
  [...]
  cout << (*wsk).imie << " " << (*wsk).nazwisko << endl      // pierwszy sposób
       << wsk->pelnyEtat << ", " << wsk->pensja << endl      // drugi sposób
  cout << wsk->dataUr.dzien << "/" << wsk->dataUr.miesiac
       << "/" << wsk->dataUr.rok ;

Przekazywanie parametrów wskaźnikowych do funkcji

Teraz możemy wprowadzić nowe tryby przekazywania parametrów do funkcji: przekazywanie przez adres i przez stały adres. Pierwszy tryb - to zwykłe przekazanie wskaźnika do zmiennej. Zmienna robocza jest tworzona tylko dla wskaźnika, a zmienna wskazywana jest ta sama - możemy zmieniać jej wartość, w ten sposób uzyskując nowy sposób zwracania wyników z funkcji. Drugi tryb - przekazywanie przez stały adres - różni się tym, że zablokowana jest możliwość zmiany wartości zmiennej wskazywanej (jest ona stała w ciele funkcji). Wykorzystuje się to zamiast przekazywania przez wartość większych struktur danych (adres to tylko 4 bajty). Przykład (inna wersja funkcji z poprzedniego rozdziału):

  void WyswietlDanePracownika (const Pracownik * pPrac)
      // parametr przekazywany jest przez stały adres
  {
      cout << "Pracownik: " << pPrac->imie << pPrac->nazwisko << endl
           << " Urodzony: " << pPrac->dataUr.dzien << "/" << pPrac->dataUr.miesiac
                            << "/" << pPrac->dataUr.rok << endl
           << "    Umowa: " << ( pPrac->pelnyEtat ? "pełna, " : "pół etatu, " )
                            << pPrac->pensja/100.0 << "PLN" << endl ;
      cout << "Przepracowane godziny: " ;
      for (int mies = 0 ; mies < 12 ; mies++)
          cout << pPrac->godziny[mies] << " " ;
      cout << endl ;
  }

Przykład wywołania tej funkcji:

  Pracownik prac1 = { "Adam", "Nowak", { 13, 12, 1981 }, false, 250000 } ;
  [...]
  WyswietlDanePracownika(&prac1) ;    // mamy pewność, że funkcja nie zmieni danych pracownika

Operatory new i delete

Operatory te są odpowiednikami procedur New i Dispose z Paskala. Używamy ich w następujący sposób:

  int * wsk = NULL ;
  [...]
  wsk = new int ;           // tworzymy nową zmienną typu int
  *wsk = 10 ;
  cout << *wsk << endl ;    // wyświetla 10
  delete wsk ;              // zwolnij pamięć

Operator new jako argument przyjmuje identyfikator typu wskazywanego (a właściwie jego konstruktor, ale o tym - w rozdziale o klasach) oraz zwraca adres do nowo utworzonej zmiennej. Ten adres możemy przypisać do zmiennej wskaźnikowej. Każdemu użyciu operatora new powinno odpowiadać późniejsze wystąpienie operatora delete, który zwraca przydzielony dynamicznie obszar pamięci do puli systemu.

Jeszcze mała uwaga - zamiast:

  int * wsk = new int ;
  *wsk = 10 ;

można od razu napisać:

  int * wsk = new int(10) ;    // przekazujemy do konstruktora wartość inicjującą

Istnieją oddzielne operatory new[] i delete[] służące do dynamicznego tworzenia tablic. Przykład:

  int * wsk = NULL ;
  int rozmiar ;
  cout << "Podaj liczbę elementów: " ;
  cin >> rozmiar ;
  wsk = new int[rozmiar] ;    // dynamicznie tworzymy tablicę o podanym rozmiarze
  cout << "Podaj wartości elementów: " ;
  for (int i = 0 ; i < rozmiar ; i++)
      cin >> wsk[i] ;
  [...]
  delete [] wsk ;

Uwaga: nie wolno niszczyć zmiennych utworzonych przez operator new [] operatorem delete, trzeba użyć delete []! Podobny zakaz zachodzi w drugą stronę, jednym słowem - nie wolno mieszać tych par operatorów.

Wskaźniki a tablice

W poprzednim przykładzie utworzyliśmy dynamicznie tablicę 20-elementową i jej adres zapisaliśmy w zmiennej wskaźnikowej. Ale dlaczego do uzyskania danego elementu użyliśmy zapisu wsk[i], a nie np. (*wsk)[i]?

Wszystko staje się jasne, jeśli uświadomimy sobie sposób reprezentacji zmiennych tablicowych. Otóż jeżeli deklarujemy:

  int tab[20] ;

to oznacza to, że zmienna tab jest stałym wskaźnikiem (typu int *), przechowującym adres początku obszaru pamięci (w tym przypadku: 80-bajtowego), w którym znajdują się faktyczne wartości elementów tablicy. Zatem zapis *tab jest równoważny zapisowi tab[0]!

Ta równoważność zachodzi też w drugą stronę. Jeśli zadeklarowaliśmy wskaźnik wsk (typu int *), to zapis wsk[0] jest równoważny zapisowi *wsk (ale wsk[1] jest już nieokreślone!).

Dlatego nie musieliśmy używać dodatkowej dereferencji przy odwoływaniu się do elementu tablicy utworzonej dynamicznie.

13. Napisy

O napisach będzie bardzo krótko i podstawowo. Nie będę na razie opisywał typu string - o nim wspomnę w rozdziale o STL. Teraz tylko o najprostszych sposobach przechowywania i operowania na łańcuchach.

Łańcuch w C++ to ciąg znaków w cudzysłowie, np. "abc", "abc def ghi 123", "x". Zapis: "x" oznacza łańcuch jednoznakowy, a zapis: 'x' oznacza znak (to jest co innego!).

Zmienną przechowującą napis deklarujemy np. tak:

  char napis1[20] ;
  char napis2[] = "Napis" ;
  char * napis3 ;

Jak widać, zmienne łańcuchowe możemy traktować jako tablice znaków.

Do funkcji parametry napisowe przekazujemy jako const char *, np.:

  void WyswietlNapis (const char * napis)
  {
      cout << napis << endl ;
  }

Przykład wywołania:

  WyswietlNapis("abc") ;
  WyswietlNapis(napis2) ;

Znaki specjalne: np. '\n' oznacza nowy wiersz (odpowiednik paskalowego #13), '\t' oznacza tabulator. Żeby w łańcuchu umieścić backslash, trzeba wpisać '\\'. Przykład:

  cout << "Zdanie 1\nZdanie 2\nZdanie 3" ;
  /* Wyświetla:
  Zdanie 1
  Zdanie 2
  Zdanie 3 */

Podstawowe funkcje operujące na łańcuchach:

14. Programowanie obiektowe

Uznałem, że nie ma sensu żebym się tu tyle produkował, skoro mam tekst, który opisuje temat lepiej i jaśniej. Poniżej znajdziecie linka. Tekst jest troszkę bardziej zaawansowany, ale każdy powinien znać i rozumieć przynajmniej połowę. Oczywiście na wszelkie pytania odpowiadam. Oto link:

Programowanie obiektowe (skrypt prof. Bieleckiego)